diff --git a/libraries/session/src/main/java/androidx/media3/session/ConnectedControllersManager.java b/libraries/session/src/main/java/androidx/media3/session/ConnectedControllersManager.java index 71efbf6d8f..f81c6ee9b4 100644 --- a/libraries/session/src/main/java/androidx/media3/session/ConnectedControllersManager.java +++ b/libraries/session/src/main/java/androidx/media3/session/ConnectedControllersManager.java @@ -37,7 +37,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; * *

The generic {@code T} denotes a key of connected {@link MediaController controllers}, and it * can be either {@link android.os.IBinder} or {@link - * androidx.media.MediaSessionManager.RemoteUserInfo}. + * androidx.media3.session.legacy.MediaSessionManager.RemoteUserInfo}. * *

This class is thread-safe. */ diff --git a/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java b/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java index 106cacef79..f5550cded4 100644 --- a/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java +++ b/libraries/session/src/main/java/androidx/media3/session/LegacyConversions.java @@ -15,8 +15,6 @@ */ package androidx.media3.session; -import static android.support.v4.media.session.MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS; -import static androidx.media.utils.MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS; import static androidx.media3.common.Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS; import static androidx.media3.common.Player.COMMAND_CHANGE_MEDIA_ITEMS; import static androidx.media3.common.Player.COMMAND_GET_AUDIO_ATTRIBUTES; @@ -44,6 +42,8 @@ import static androidx.media3.common.Player.COMMAND_STOP; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Util.constrainValue; import static androidx.media3.session.MediaConstants.EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY; +import static androidx.media3.session.legacy.MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS; +import static androidx.media3.session.legacy.MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS; import static java.lang.Math.max; import static java.util.concurrent.TimeUnit.MILLISECONDS; @@ -54,19 +54,8 @@ import android.media.AudioManager; import android.net.Uri; import android.os.Bundle; import android.os.SystemClock; -import android.support.v4.media.MediaBrowserCompat; -import android.support.v4.media.MediaDescriptionCompat; -import android.support.v4.media.MediaMetadataCompat; -import android.support.v4.media.RatingCompat; -import android.support.v4.media.session.MediaControllerCompat; -import android.support.v4.media.session.MediaSessionCompat.QueueItem; -import android.support.v4.media.session.PlaybackStateCompat; -import android.support.v4.media.session.PlaybackStateCompat.CustomAction; import android.text.TextUtils; import androidx.annotation.Nullable; -import androidx.media.AudioAttributesCompat; -import androidx.media.MediaBrowserServiceCompat.BrowserRoot; -import androidx.media.VolumeProviderCompat; import androidx.media3.common.AdPlaybackState; import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; @@ -87,6 +76,17 @@ import androidx.media3.common.Timeline.Window; import androidx.media3.common.util.Log; import androidx.media3.common.util.Util; import androidx.media3.session.MediaLibraryService.LibraryParams; +import androidx.media3.session.legacy.AudioAttributesCompat; +import androidx.media3.session.legacy.MediaBrowserCompat; +import androidx.media3.session.legacy.MediaBrowserServiceCompat.BrowserRoot; +import androidx.media3.session.legacy.MediaControllerCompat; +import androidx.media3.session.legacy.MediaDescriptionCompat; +import androidx.media3.session.legacy.MediaMetadataCompat; +import androidx.media3.session.legacy.MediaSessionCompat.QueueItem; +import androidx.media3.session.legacy.PlaybackStateCompat; +import androidx.media3.session.legacy.PlaybackStateCompat.CustomAction; +import androidx.media3.session.legacy.RatingCompat; +import androidx.media3.session.legacy.VolumeProviderCompat; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import java.io.ByteArrayOutputStream; @@ -1231,12 +1231,15 @@ import java.util.concurrent.TimeoutException; sessionCommandsBuilder.remove(SessionCommand.COMMAND_CODE_SESSION_SET_RATING); } - if (state != null && state.getCustomActions() != null) { - for (CustomAction customAction : state.getCustomActions()) { - String action = customAction.getAction(); - @Nullable Bundle extras = customAction.getExtras(); - sessionCommandsBuilder.add( - new SessionCommand(action, extras == null ? Bundle.EMPTY : extras)); + if (state != null) { + List customActions = state.getCustomActions(); + if (customActions != null) { + for (CustomAction customAction : customActions) { + String action = customAction.getAction(); + @Nullable Bundle extras = customAction.getExtras(); + sessionCommandsBuilder.add( + new SessionCommand(action, extras == null ? Bundle.EMPTY : extras)); + } } } return sessionCommandsBuilder.build(); @@ -1254,8 +1257,12 @@ import java.util.concurrent.TimeoutException; if (state == null) { return ImmutableList.of(); } + List customActions = state.getCustomActions(); + if (customActions == null) { + return ImmutableList.of(); + } ImmutableList.Builder layout = new ImmutableList.Builder<>(); - for (CustomAction customAction : state.getCustomActions()) { + for (CustomAction customAction : customActions) { String action = customAction.getAction(); @Nullable Bundle extras = customAction.getExtras(); @CommandButton.Icon diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplLegacy.java b/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplLegacy.java index 68301d2d4f..5cc064a270 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplLegacy.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaBrowserImplLegacy.java @@ -23,9 +23,6 @@ import static androidx.media3.session.LibraryResult.RESULT_ERROR_UNKNOWN; import android.content.Context; import android.os.Bundle; import android.os.Looper; -import android.support.v4.media.MediaBrowserCompat; -import android.support.v4.media.MediaBrowserCompat.ItemCallback; -import android.support.v4.media.MediaBrowserCompat.SubscriptionCallback; import android.text.TextUtils; import androidx.annotation.Nullable; import androidx.media3.common.MediaItem; @@ -33,6 +30,9 @@ import androidx.media3.common.MediaMetadata; import androidx.media3.common.util.BitmapLoader; import androidx.media3.common.util.Log; import androidx.media3.session.MediaLibraryService.LibraryParams; +import androidx.media3.session.legacy.MediaBrowserCompat; +import androidx.media3.session.legacy.MediaBrowserCompat.ItemCallback; +import androidx.media3.session.legacy.MediaBrowserCompat.SubscriptionCallback; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -224,7 +224,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; new MediaBrowserCompat.SearchCallback() { @Override public void onSearchResult( - String query, Bundle extras, List items) { + String query, @Nullable Bundle extras, List items) { getInstance() .notifyBrowserListener( listener -> { @@ -238,7 +238,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; } @Override - public void onError(String query, Bundle extras) { + public void onError(String query, @Nullable Bundle extras) { getInstance() .notifyBrowserListener( listener -> { @@ -276,7 +276,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; new MediaBrowserCompat.SearchCallback() { @Override public void onSearchResult( - String query, Bundle extrasSent, List items) { + String query, @Nullable Bundle extrasSent, List items) { future.set( LibraryResult.ofItemList( LegacyConversions.convertBrowserItemListToMediaItemList(items), @@ -284,7 +284,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; } @Override - public void onError(String query, Bundle extrasSent) { + public void onError(String query, @Nullable Bundle extrasSent) { future.set(LibraryResult.ofError(RESULT_ERROR_UNKNOWN)); } }); @@ -370,23 +370,26 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; } @Override - public void onError(String parentId) { + public void onError(@Nullable String parentId) { onErrorInternal(); } @Override - public void onError(String parentId, Bundle options) { + public void onError(@Nullable String parentId, @Nullable Bundle options) { onErrorInternal(); } @Override - public void onChildrenLoaded(String parentId, List children) { + public void onChildrenLoaded( + @Nullable String parentId, @Nullable List children) { onChildrenLoadedInternal(parentId, children); } @Override public void onChildrenLoaded( - String parentId, List children, Bundle options) { + @Nullable String parentId, + @Nullable List children, + @Nullable Bundle options) { onChildrenLoadedInternal(parentId, children); } @@ -397,7 +400,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; } private void onChildrenLoadedInternal( - String parentId, @Nullable List children) { + @Nullable String parentId, @Nullable List children) { if (TextUtils.isEmpty(parentId)) { Log.w(TAG, "SubscribeCallback.onChildrenLoaded(): Ignoring empty parentId"); return; @@ -441,23 +444,26 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; } @Override - public void onError(String parentId) { + public void onError(@Nullable String parentId) { onErrorInternal(); } @Override - public void onError(String parentId, Bundle options) { + public void onError(@Nullable String parentId, @Nullable Bundle options) { onErrorInternal(); } @Override - public void onChildrenLoaded(String parentId, List children) { + public void onChildrenLoaded( + @Nullable String parentId, @Nullable List children) { onChildrenLoadedInternal(parentId, children); } @Override public void onChildrenLoaded( - String parentId, List children, Bundle options) { + @Nullable String parentId, + @Nullable List children, + Bundle options) { onChildrenLoadedInternal(parentId, children); } @@ -466,7 +472,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; } private void onChildrenLoadedInternal( - String parentId, List children) { + @Nullable String parentId, @Nullable List children) { if (TextUtils.isEmpty(parentId)) { Log.w(TAG, "GetChildrenCallback.onChildrenLoaded(): Ignoring empty parentId"); return; diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java b/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java index b15dc34abd..5ce4f8027d 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java @@ -19,12 +19,12 @@ import android.app.PendingIntent; import android.content.Intent; import android.os.Bundle; import android.os.Parcelable; -import android.support.v4.media.session.PlaybackStateCompat; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaMetadata; import androidx.media3.common.util.UnstableApi; import androidx.media3.session.MediaLibraryService.LibraryParams; import androidx.media3.session.MediaLibraryService.MediaLibrarySession; +import androidx.media3.session.legacy.PlaybackStateCompat; /** Constants that can be shared between media session and controller. */ public final class MediaConstants { @@ -54,7 +54,7 @@ public final class MediaConstants { */ @UnstableApi public static final String EXTRAS_KEY_MEDIA_ID_COMPAT = - androidx.media.utils.MediaConstants.PLAYBACK_STATE_EXTRAS_KEY_MEDIA_ID; + androidx.media3.session.legacy.MediaConstants.PLAYBACK_STATE_EXTRAS_KEY_MEDIA_ID; /** * {@link Bundle} key used for a localized error resolution string. @@ -64,7 +64,8 @@ public final class MediaConstants { * service call. */ public static final String EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL_COMPAT = - androidx.media.utils.MediaConstants.PLAYBACK_STATE_EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL; + androidx.media3.session.legacy.MediaConstants + .PLAYBACK_STATE_EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL; /** * {@link Bundle} key used for an error resolution intent. @@ -74,7 +75,8 @@ public final class MediaConstants { * service call. */ public static final String EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT = - androidx.media.utils.MediaConstants.PLAYBACK_STATE_EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT; + androidx.media3.session.legacy.MediaConstants + .PLAYBACK_STATE_EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT; /** * {@link Bundle} key used to store a {@link PendingIntent}. When launched, the {@link @@ -93,7 +95,7 @@ public final class MediaConstants { */ @UnstableApi public static final String EXTRAS_KEY_ERROR_RESOLUTION_USING_CAR_APP_LIBRARY_INTENT_COMPAT = - androidx.media.utils.MediaConstants + androidx.media3.session.legacy.MediaConstants .PLAYBACK_STATE_EXTRAS_KEY_ERROR_RESOLUTION_USING_CAR_APP_LIBRARY_INTENT; /** @@ -110,7 +112,8 @@ public final class MediaConstants { * @see androidx.media3.common.Player#COMMAND_SEEK_TO_NEXT_MEDIA_ITEM */ public static final String EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT = - androidx.media.utils.MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT; + androidx.media3.session.legacy.MediaConstants + .SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT; /** * {@link Bundle} key to indicate a preference that a region of space for the skip to previous @@ -126,7 +129,8 @@ public final class MediaConstants { * @see androidx.media3.common.Player#COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM */ public static final String EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV = - androidx.media.utils.MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV; + androidx.media3.session.legacy.MediaConstants + .SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV; /** * {@link Bundle} key used in {@link MediaMetadata#extras} to indicate the playback completion @@ -142,7 +146,7 @@ public final class MediaConstants { */ @UnstableApi public static final String EXTRAS_KEY_COMPLETION_STATUS = - androidx.media.utils.MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS; + androidx.media3.session.legacy.MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS; /** * {@link Bundle} value used in {@link MediaMetadata#extras} to indicate that the corresponding @@ -154,7 +158,8 @@ public final class MediaConstants { */ @UnstableApi public static final int EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED = - androidx.media.utils.MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED; + androidx.media3.session.legacy.MediaConstants + .DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED; /** * {@link Bundle} value used in {@link MediaMetadata#extras} to indicate that the corresponding @@ -166,7 +171,7 @@ public final class MediaConstants { */ @UnstableApi public static final int EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED = - androidx.media.utils.MediaConstants + androidx.media3.session.legacy.MediaConstants .DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED; /** @@ -179,7 +184,8 @@ public final class MediaConstants { */ @UnstableApi public static final int EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED = - androidx.media.utils.MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED; + androidx.media3.session.legacy.MediaConstants + .DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED; /** * {@link Bundle} key used in {@link MediaMetadata#extras} to indicate an amount of completion @@ -196,7 +202,7 @@ public final class MediaConstants { */ @UnstableApi public static final String EXTRAS_KEY_COMPLETION_PERCENTAGE = - androidx.media.utils.MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE; + androidx.media3.session.legacy.MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE; /** * {@link Bundle} key used to indicate a preference about how playable instances of {@link @@ -221,7 +227,7 @@ public final class MediaConstants { */ @UnstableApi public static final String EXTRAS_KEY_CONTENT_STYLE_PLAYABLE = - androidx.media.utils.MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE; + androidx.media3.session.legacy.MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE; /** * {@link Bundle} key used to indicate a preference about how browsable instances of {@link @@ -249,7 +255,7 @@ public final class MediaConstants { */ @UnstableApi public static final String EXTRAS_KEY_CONTENT_STYLE_BROWSABLE = - androidx.media.utils.MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE; + androidx.media3.session.legacy.MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE; /** * {@link Bundle} key used in {@link MediaMetadata#extras} to indicate a preference about how the @@ -269,7 +275,8 @@ public final class MediaConstants { */ @UnstableApi public static final String EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM = - androidx.media.utils.MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM; + androidx.media3.session.legacy.MediaConstants + .DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM; /** * {@link Bundle} value used in {@link MediaMetadata#extras} to indicate a preference that certain @@ -282,7 +289,8 @@ public final class MediaConstants { */ @UnstableApi public static final int EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM = - androidx.media.utils.MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM; + androidx.media3.session.legacy.MediaConstants + .DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM; /** * {@link Bundle} value used in {@link MediaMetadata#extras} to indicate a preference that certain @@ -295,7 +303,8 @@ public final class MediaConstants { */ @UnstableApi public static final int EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM = - androidx.media.utils.MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM; + androidx.media3.session.legacy.MediaConstants + .DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM; /** * {@link Bundle} value used in {@link MediaMetadata#extras} to indicate a preference that @@ -309,7 +318,8 @@ public final class MediaConstants { */ @UnstableApi public static final int EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_LIST_ITEM = - androidx.media.utils.MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_LIST_ITEM; + androidx.media3.session.legacy.MediaConstants + .DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_LIST_ITEM; /** * {@link Bundle} value used in {@link MediaMetadata#extras} to indicate a preference that @@ -323,7 +333,8 @@ public final class MediaConstants { */ @UnstableApi public static final int EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_GRID_ITEM = - androidx.media.utils.MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_GRID_ITEM; + androidx.media3.session.legacy.MediaConstants + .DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_GRID_ITEM; /** * {@link Bundle} key used in {@link MediaMetadata#extras} to indicate that certain instances of @@ -339,7 +350,8 @@ public final class MediaConstants { */ @UnstableApi public static final String EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE = - androidx.media.utils.MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE; + androidx.media3.session.legacy.MediaConstants + .DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE; /** * {@link Bundle} key used in {@link MediaMetadata#extras} to indicate that the corresponding @@ -364,7 +376,7 @@ public final class MediaConstants { */ @UnstableApi public static final String EXTRAS_KEY_IS_ADVERTISEMENT = - androidx.media.utils.MediaConstants.METADATA_KEY_IS_ADVERTISEMENT; + androidx.media3.session.legacy.MediaConstants.METADATA_KEY_IS_ADVERTISEMENT; /** * {@link Bundle} value used to indicate the presence of an attribute described by its @@ -388,7 +400,7 @@ public final class MediaConstants { */ @UnstableApi public static final String EXTRAS_KEY_ROOT_CHILDREN_LIMIT = - androidx.media.utils.MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT; + androidx.media3.session.legacy.MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT; /** * {@link Bundle} key used in {@link LibraryParams#extras} passed to {@link @@ -422,7 +434,7 @@ public final class MediaConstants { */ @UnstableApi public static final String EXTRAS_KEY_MEDIA_ART_SIZE_PIXELS = - androidx.media.utils.MediaConstants.BROWSER_ROOT_HINTS_KEY_MEDIA_ART_SIZE_PIXELS; + androidx.media3.session.legacy.MediaConstants.BROWSER_ROOT_HINTS_KEY_MEDIA_ART_SIZE_PIXELS; /** * {@link Bundle} key used to indicate that the media app that provides the service supports @@ -446,7 +458,7 @@ public final class MediaConstants { */ @UnstableApi public static final String EXTRAS_KEY_APPLICATION_PREFERENCES_USING_CAR_APP_LIBRARY_INTENT = - androidx.media.utils.MediaConstants + androidx.media3.session.legacy.MediaConstants .BROWSER_SERVICE_EXTRAS_KEY_APPLICATION_PREFERENCES_USING_CAR_APP_LIBRARY_INTENT; /** diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaController.java b/libraries/session/src/main/java/androidx/media3/session/MediaController.java index c31f3fdc09..db63c231ec 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaController.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaController.java @@ -27,7 +27,6 @@ import android.content.Context; import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.support.v4.media.MediaBrowserCompat; import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; @@ -57,6 +56,7 @@ import androidx.media3.common.util.Size; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSourceBitmapLoader; +import androidx.media3.session.legacy.MediaBrowserCompat; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java index a19de8d34e..9a24401c59 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java @@ -41,7 +41,6 @@ import android.os.Message; import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; -import android.support.v4.media.MediaBrowserCompat; import android.util.Pair; import android.view.Surface; import android.view.SurfaceHolder; @@ -81,6 +80,7 @@ import androidx.media3.common.util.Size; import androidx.media3.common.util.Util; import androidx.media3.session.MediaController.MediaControllerImpl; import androidx.media3.session.PlayerInfo.BundlingExclusions; +import androidx.media3.session.legacy.MediaBrowserCompat; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; 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 c855edb1dd..d4cc1fe019 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java @@ -33,13 +33,6 @@ import android.os.Handler; import android.os.Looper; import android.os.ResultReceiver; import android.os.SystemClock; -import android.support.v4.media.MediaBrowserCompat; -import android.support.v4.media.MediaMetadataCompat; -import android.support.v4.media.RatingCompat; -import android.support.v4.media.session.MediaControllerCompat; -import android.support.v4.media.session.MediaSessionCompat; -import android.support.v4.media.session.MediaSessionCompat.QueueItem; -import android.support.v4.media.session.PlaybackStateCompat; import android.util.Pair; import android.view.Surface; import android.view.SurfaceHolder; @@ -47,7 +40,6 @@ import android.view.SurfaceView; import android.view.TextureView; import androidx.annotation.CheckResult; import androidx.annotation.Nullable; -import androidx.media.VolumeProviderCompat; import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; import androidx.media3.common.DeviceInfo; @@ -75,6 +67,14 @@ import androidx.media3.common.util.NullableType; import androidx.media3.common.util.Size; import androidx.media3.common.util.Util; import androidx.media3.session.LegacyConversions.ConversionException; +import androidx.media3.session.legacy.MediaBrowserCompat; +import androidx.media3.session.legacy.MediaControllerCompat; +import androidx.media3.session.legacy.MediaMetadataCompat; +import androidx.media3.session.legacy.MediaSessionCompat; +import androidx.media3.session.legacy.MediaSessionCompat.QueueItem; +import androidx.media3.session.legacy.PlaybackStateCompat; +import androidx.media3.session.legacy.RatingCompat; +import androidx.media3.session.legacy.VolumeProviderCompat; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -1824,7 +1824,10 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; } @Override - public void onSessionEvent(String event, Bundle extras) { + public void onSessionEvent(@Nullable String event, @Nullable Bundle extras) { + if (event == null) { + return; + } getInstance() .notifyControllerListener( listener -> @@ -1832,11 +1835,11 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; listener.onCustomCommand( getInstance(), new SessionCommand(event, /* extras= */ Bundle.EMPTY), - extras))); + extras == null ? Bundle.EMPTY : extras))); } @Override - public void onPlaybackStateChanged(PlaybackStateCompat state) { + public void onPlaybackStateChanged(@Nullable PlaybackStateCompat state) { pendingLegacyPlayerInfo = pendingLegacyPlayerInfo.copyWithPlaybackStateCompat( convertToSafePlaybackStateCompat(state)); @@ -1844,26 +1847,26 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; } @Override - public void onMetadataChanged(MediaMetadataCompat metadata) { + public void onMetadataChanged(@Nullable MediaMetadataCompat metadata) { pendingLegacyPlayerInfo = pendingLegacyPlayerInfo.copyWithMediaMetadataCompat(metadata); startWaitingForPendingChanges(); } @Override - public void onQueueChanged(@Nullable List<@NullableType QueueItem> queue) { + public void onQueueChanged(@Nullable List queue) { pendingLegacyPlayerInfo = pendingLegacyPlayerInfo.copyWithQueue(convertToNonNullQueueItemList(queue)); startWaitingForPendingChanges(); } @Override - public void onQueueTitleChanged(CharSequence title) { + public void onQueueTitleChanged(@Nullable CharSequence title) { pendingLegacyPlayerInfo = pendingLegacyPlayerInfo.copyWithQueueTitle(title); startWaitingForPendingChanges(); } @Override - public void onExtrasChanged(Bundle extras) { + public void onExtrasChanged(@Nullable Bundle extras) { controllerInfo = new ControllerInfo( controllerInfo.playerInfo, @@ -1876,7 +1879,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; } @Override - public void onAudioInfoChanged(MediaControllerCompat.PlaybackInfo newPlaybackInfo) { + public void onAudioInfoChanged(@Nullable MediaControllerCompat.PlaybackInfo newPlaybackInfo) { pendingLegacyPlayerInfo = pendingLegacyPlayerInfo.copyWithPlaybackInfoCompat(newPlaybackInfo); startWaitingForPendingChanges(); } @@ -1928,7 +1931,7 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; LegacyPlayerInfo oldLegacyPlayerInfo, ControllerInfo oldControllerInfo, LegacyPlayerInfo newLegacyPlayerInfo, - String sessionPackageName, + @Nullable String sessionPackageName, long sessionFlags, boolean isSessionReady, @RatingCompat.Style int ratingType, @@ -2580,12 +2583,12 @@ import org.checkerframework.checker.initialization.qual.UnderInitialization; SessionCommands availableSessionCommands, Commands availablePlayerCommands, ImmutableList customLayout, - Bundle sessionExtras) { + @Nullable Bundle sessionExtras) { this.playerInfo = playerInfo; this.availableSessionCommands = availableSessionCommands; this.availablePlayerCommands = availablePlayerCommands; this.customLayout = customLayout; - this.sessionExtras = sessionExtras; + this.sessionExtras = sessionExtras == null ? Bundle.EMPTY : sessionExtras; } } } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaLibraryServiceLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaLibraryServiceLegacyStub.java index 77e26df420..1b2e608a39 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaLibraryServiceLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaLibraryServiceLegacyStub.java @@ -15,28 +15,25 @@ */ package androidx.media3.session; -import static android.support.v4.media.MediaBrowserCompat.EXTRA_PAGE; -import static android.support.v4.media.MediaBrowserCompat.EXTRA_PAGE_SIZE; -import static androidx.media.utils.MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Util.castNonNull; import static androidx.media3.common.util.Util.postOrRun; import static androidx.media3.session.LibraryResult.RESULT_SUCCESS; import static androidx.media3.session.MediaUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES; +import static androidx.media3.session.legacy.MediaBrowserCompat.EXTRA_PAGE; +import static androidx.media3.session.legacy.MediaBrowserCompat.EXTRA_PAGE_SIZE; +import static androidx.media3.session.legacy.MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED; import android.annotation.SuppressLint; import android.graphics.Bitmap; import android.os.BadParcelableException; import android.os.Bundle; import android.os.RemoteException; -import android.support.v4.media.MediaBrowserCompat; import android.text.TextUtils; import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; import androidx.core.util.ObjectsCompat; -import androidx.media.MediaBrowserServiceCompat; -import androidx.media.MediaSessionManager.RemoteUserInfo; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaMetadata; import androidx.media3.common.util.ConditionVariable; @@ -46,6 +43,9 @@ import androidx.media3.common.util.Util; import androidx.media3.session.MediaLibraryService.LibraryParams; import androidx.media3.session.MediaSession.ControllerCb; import androidx.media3.session.MediaSession.ControllerInfo; +import androidx.media3.session.legacy.MediaBrowserCompat; +import androidx.media3.session.legacy.MediaBrowserServiceCompat; +import androidx.media3.session.legacy.MediaSessionManager.RemoteUserInfo; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.AsyncFunction; import com.google.common.util.concurrent.Futures; @@ -82,7 +82,7 @@ import java.util.concurrent.atomic.AtomicReference; @Override @Nullable public BrowserRoot onGetRoot( - String clientPackageName, int clientUid, @Nullable Bundle rootHints) { + @Nullable String clientPackageName, int clientUid, @Nullable Bundle rootHints) { @Nullable BrowserRoot browserRoot = super.onGetRoot(clientPackageName, clientUid, rootHints); if (browserRoot == null) { return null; @@ -140,7 +140,7 @@ import java.util.concurrent.atomic.AtomicReference; // content. @SuppressLint("RestrictedApi") @Override - public void onSubscribe(String id, Bundle option) { + public void onSubscribe(@Nullable String id, @Nullable Bundle option) { @Nullable ControllerInfo controller = getCurrentController(); if (controller == null) { return; @@ -166,7 +166,7 @@ import java.util.concurrent.atomic.AtomicReference; @SuppressLint("RestrictedApi") @Override - public void onUnsubscribe(String id) { + public void onUnsubscribe(@Nullable String id) { @Nullable ControllerInfo controller = getCurrentController(); if (controller == null) { return; @@ -188,13 +188,14 @@ import java.util.concurrent.atomic.AtomicReference; } @Override - public void onLoadChildren(String parentId, Result> result) { + public void onLoadChildren( + @Nullable String parentId, Result> result) { onLoadChildren(parentId, result, /* options= */ null); } @Override public void onLoadChildren( - String parentId, + @Nullable String parentId, Result> result, @Nullable Bundle options) { @Nullable ControllerInfo controller = getCurrentController(); diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java index c6c8aa7e32..fc2fd7e4e3 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java @@ -29,7 +29,6 @@ import android.app.PendingIntent; import android.content.Context; import android.os.Bundle; import android.os.RemoteException; -import android.support.v4.media.session.MediaSessionCompat; import androidx.annotation.Nullable; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaMetadata; @@ -41,6 +40,7 @@ import androidx.media3.session.MediaLibraryService.LibraryParams; import androidx.media3.session.MediaLibraryService.MediaLibrarySession; import androidx.media3.session.MediaSession.ControllerCb; import androidx.media3.session.MediaSession.ControllerInfo; +import androidx.media3.session.legacy.MediaSessionCompat; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java index 015fbaa463..a8edd4e66d 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -31,8 +31,6 @@ import android.os.Bundle; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; -import android.support.v4.media.session.MediaControllerCompat; -import android.support.v4.media.session.MediaSessionCompat; import android.view.KeyEvent; import androidx.annotation.DoNotInline; import androidx.annotation.GuardedBy; @@ -40,7 +38,6 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; import androidx.core.app.NotificationCompat; -import androidx.media.MediaSessionManager.RemoteUserInfo; import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; import androidx.media3.common.DeviceInfo; @@ -64,6 +61,10 @@ import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSourceBitmapLoader; import androidx.media3.session.MediaLibraryService.LibraryParams; import androidx.media3.session.MediaLibraryService.MediaLibrarySession; +import androidx.media3.session.legacy.LegacyParcelableUtil; +import androidx.media3.session.legacy.MediaControllerCompat; +import androidx.media3.session.legacy.MediaSessionCompat; +import androidx.media3.session.legacy.MediaSessionManager.RemoteUserInfo; import com.google.common.base.Objects; import com.google.common.collect.ImmutableList; import com.google.common.primitives.Longs; @@ -628,9 +629,10 @@ public class MediaSession { * Bundle)} instead. */ @VisibleForTesting(otherwise = PRIVATE) + @SuppressWarnings("UnnecessarilyFullyQualified") // Avoiding clash with Media3 RemoteUserInfo. @Deprecated public static ControllerInfo createTestOnlyControllerInfo( - RemoteUserInfo remoteUserInfo, + androidx.media.MediaSessionManager.RemoteUserInfo remoteUserInfo, int libraryVersion, int interfaceVersion, boolean trusted, @@ -1144,10 +1146,27 @@ public class MediaSession { /** * Returns the legacy {@code android.support.v4.media.session.MediaSessionCompat.Token} of the * {@code android.support.v4.media.session.MediaSessionCompat} created internally by this session. + * + * @deprecated Use {@link #getPlatformToken()} instead. */ + @Deprecated @UnstableApi - public final MediaSessionCompat.Token getSessionCompatToken() { - return impl.getSessionCompat().getSessionToken(); + public final android.support.v4.media.session.MediaSessionCompat.Token getSessionCompatToken() { + return LegacyParcelableUtil.convert( + impl.getSessionCompat().getSessionToken(), + android.support.v4.media.session.MediaSessionCompat.Token.CREATOR); + } + + /** + * Returns the platform {@link android.media.session.MediaSession.Token} of the {@link + * android.media.session.MediaSession} created internally by this session. + */ + @SuppressWarnings("UnnecessarilyFullyQualified") // Avoiding clash with Media3 token. + @RequiresApi(21) + @UnstableApi + public final android.media.session.MediaSession.Token getPlatformToken() { + return (android.media.session.MediaSession.Token) + impl.getSessionCompat().getSessionToken().getToken(); } /** @@ -1165,6 +1184,7 @@ public class MediaSession { impl.connectFromService(controller, controllerInfo); } + @Nullable /* package */ final IBinder getLegacyBrowserServiceBinder() { return impl.getLegacyBrowserServiceBinder(); } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java index 22e4a8101c..1de512a824 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -50,7 +50,6 @@ import android.os.Message; import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; -import android.support.v4.media.session.MediaSessionCompat; import android.view.KeyEvent; import android.view.ViewConfiguration; import androidx.annotation.CheckResult; @@ -59,7 +58,6 @@ import androidx.annotation.FloatRange; import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; -import androidx.media.MediaBrowserServiceCompat; import androidx.media3.common.AudioAttributes; import androidx.media3.common.DeviceInfo; import androidx.media3.common.MediaItem; @@ -85,6 +83,8 @@ import androidx.media3.session.MediaSession.ControllerCb; import androidx.media3.session.MediaSession.ControllerInfo; import androidx.media3.session.MediaSession.MediaItemsWithStartPosition; import androidx.media3.session.SequencedFutureManager.SequencedFuture; +import androidx.media3.session.legacy.MediaBrowserServiceCompat; +import androidx.media3.session.legacy.MediaSessionCompat; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; @@ -759,6 +759,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * Gets the service binder from the MediaBrowserServiceCompat. Should be only called by the thread * with a Looper. */ + @Nullable protected IBinder getLegacyBrowserServiceBinder() { MediaSessionServiceLegacyStub legacyStub; synchronized (lock) { diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java index e1d90c77d1..449aef9c60 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -57,21 +57,12 @@ import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.os.ResultReceiver; -import android.support.v4.media.MediaDescriptionCompat; -import android.support.v4.media.MediaMetadataCompat; -import android.support.v4.media.RatingCompat; -import android.support.v4.media.session.MediaSessionCompat; -import android.support.v4.media.session.MediaSessionCompat.QueueItem; -import android.support.v4.media.session.PlaybackStateCompat; import android.text.TextUtils; import android.view.KeyEvent; import androidx.annotation.DoNotInline; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.core.util.ObjectsCompat; -import androidx.media.MediaSessionManager; -import androidx.media.MediaSessionManager.RemoteUserInfo; -import androidx.media.VolumeProviderCompat; import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; import androidx.media3.common.DeviceInfo; @@ -92,6 +83,15 @@ import androidx.media3.session.MediaSession.ControllerCb; import androidx.media3.session.MediaSession.ControllerInfo; import androidx.media3.session.MediaSession.MediaItemsWithStartPosition; import androidx.media3.session.SessionCommand.CommandCode; +import androidx.media3.session.legacy.MediaDescriptionCompat; +import androidx.media3.session.legacy.MediaMetadataCompat; +import androidx.media3.session.legacy.MediaSessionCompat; +import androidx.media3.session.legacy.MediaSessionCompat.QueueItem; +import androidx.media3.session.legacy.MediaSessionManager; +import androidx.media3.session.legacy.MediaSessionManager.RemoteUserInfo; +import androidx.media3.session.legacy.PlaybackStateCompat; +import androidx.media3.session.legacy.RatingCompat; +import androidx.media3.session.legacy.VolumeProviderCompat; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; @@ -315,7 +315,7 @@ import org.checkerframework.checker.initialization.qual.Initialized; public boolean onMediaButtonEvent(Intent intent) { return sessionImpl.onMediaButtonEvent( new ControllerInfo( - sessionCompat.getCurrentControllerInfo(), + checkNotNull(sessionCompat.getCurrentControllerInfo()), ControllerInfo.LEGACY_CONTROLLER_VERSION, ControllerInfo.LEGACY_CONTROLLER_INTERFACE_VERSION, /* trusted= */ false, @@ -353,7 +353,7 @@ import org.checkerframework.checker.initialization.qual.Initialized; } @Override - public void onPrepareFromMediaId(String mediaId, @Nullable Bundle extras) { + public void onPrepareFromMediaId(@Nullable String mediaId, @Nullable Bundle extras) { handleMediaRequest( createMediaItemForMediaRequest( mediaId, /* mediaUri= */ null, /* searchQuery= */ null, extras), @@ -361,14 +361,14 @@ import org.checkerframework.checker.initialization.qual.Initialized; } @Override - public void onPrepareFromSearch(String query, @Nullable Bundle extras) { + public void onPrepareFromSearch(@Nullable String query, @Nullable Bundle extras) { handleMediaRequest( createMediaItemForMediaRequest(/* mediaId= */ null, /* mediaUri= */ null, query, extras), /* play= */ false); } @Override - public void onPrepareFromUri(Uri mediaUri, @Nullable Bundle extras) { + public void onPrepareFromUri(@Nullable Uri mediaUri, @Nullable Bundle extras) { handleMediaRequest( createMediaItemForMediaRequest( /* mediaId= */ null, mediaUri, /* searchQuery= */ null, extras), @@ -384,7 +384,7 @@ import org.checkerframework.checker.initialization.qual.Initialized; } @Override - public void onPlayFromMediaId(String mediaId, @Nullable Bundle extras) { + public void onPlayFromMediaId(@Nullable String mediaId, @Nullable Bundle extras) { handleMediaRequest( createMediaItemForMediaRequest( mediaId, /* mediaUri= */ null, /* searchQuery= */ null, extras), @@ -392,14 +392,14 @@ import org.checkerframework.checker.initialization.qual.Initialized; } @Override - public void onPlayFromSearch(String query, @Nullable Bundle extras) { + public void onPlayFromSearch(@Nullable String query, @Nullable Bundle extras) { handleMediaRequest( createMediaItemForMediaRequest(/* mediaId= */ null, /* mediaUri= */ null, query, extras), /* play= */ true); } @Override - public void onPlayFromUri(Uri mediaUri, @Nullable Bundle extras) { + public void onPlayFromUri(@Nullable Uri mediaUri, @Nullable Bundle extras) { handleMediaRequest( createMediaItemForMediaRequest( /* mediaId= */ null, mediaUri, /* searchQuery= */ null, extras), @@ -504,12 +504,12 @@ import org.checkerframework.checker.initialization.qual.Initialized; } @Override - public void onSetRating(RatingCompat ratingCompat) { + public void onSetRating(@Nullable RatingCompat ratingCompat) { onSetRating(ratingCompat, null); } @Override - public void onSetRating(RatingCompat ratingCompat, @Nullable Bundle unusedExtras) { + public void onSetRating(@Nullable RatingCompat ratingCompat, @Nullable Bundle unusedExtras) { @Nullable Rating rating = LegacyConversions.convertToRating(ratingCompat); if (rating == null) { Log.w(TAG, "Ignoring invalid RatingCompat " + ratingCompat); @@ -1442,7 +1442,7 @@ import org.checkerframework.checker.initialization.qual.Initialized; @DoNotInline public static void setMediaButtonBroadcastReceiver( MediaSessionCompat mediaSessionCompat, ComponentName broadcastReceiver) { - ((android.media.session.MediaSession) mediaSessionCompat.getMediaSession()) + ((android.media.session.MediaSession) checkNotNull(mediaSessionCompat.getMediaSession())) .setMediaButtonBroadcastReceiver(broadcastReceiver); } } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java index 32b93afb0e..346a134aeb 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionService.java @@ -39,13 +39,13 @@ import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.collection.ArrayMap; -import androidx.media.MediaBrowserServiceCompat; -import androidx.media.MediaSessionManager; import androidx.media3.common.MediaLibraryInfo; import androidx.media3.common.util.Log; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.session.MediaSession.ControllerInfo; +import androidx.media3.session.legacy.MediaBrowserServiceCompat; +import androidx.media3.session.legacy.MediaSessionManager; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionServiceLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionServiceLegacyStub.java index f44fc9ee46..03c1fb25a1 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionServiceLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionServiceLegacyStub.java @@ -18,15 +18,15 @@ package androidx.media3.session; import static androidx.media3.common.util.Util.postOrRun; import android.os.Bundle; -import android.support.v4.media.MediaBrowserCompat.MediaItem; -import android.support.v4.media.session.MediaSessionCompat; import androidx.annotation.Nullable; -import androidx.media.MediaBrowserServiceCompat; -import androidx.media.MediaSessionManager; -import androidx.media.MediaSessionManager.RemoteUserInfo; import androidx.media3.common.util.ConditionVariable; import androidx.media3.common.util.Log; import androidx.media3.session.MediaSession.ControllerInfo; +import androidx.media3.session.legacy.MediaBrowserCompat.MediaItem; +import androidx.media3.session.legacy.MediaBrowserServiceCompat; +import androidx.media3.session.legacy.MediaSessionCompat; +import androidx.media3.session.legacy.MediaSessionManager; +import androidx.media3.session.legacy.MediaSessionManager.RemoteUserInfo; import java.util.List; import java.util.concurrent.atomic.AtomicReference; @@ -59,7 +59,7 @@ import java.util.concurrent.atomic.AtomicReference; @Override @Nullable public BrowserRoot onGetRoot( - String clientPackageName, int clientUid, @Nullable Bundle rootHints) { + @Nullable String clientPackageName, int clientUid, @Nullable Bundle rootHints) { RemoteUserInfo info = getCurrentBrowserInfo(); MediaSession.ControllerInfo controller = createControllerInfo(info, rootHints != null ? rootHints : Bundle.EMPTY); @@ -89,7 +89,7 @@ import java.util.concurrent.atomic.AtomicReference; } @Override - public void onLoadChildren(String parentId, Result> result) { + public void onLoadChildren(@Nullable String parentId, Result> result) { result.sendResult(/* result= */ null); } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java index 5576ea61a3..98744faf06 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionStub.java @@ -63,7 +63,6 @@ import android.text.TextUtils; import android.view.Surface; import androidx.annotation.Nullable; import androidx.core.util.ObjectsCompat; -import androidx.media.MediaSessionManager; import androidx.media3.common.AudioAttributes; import androidx.media3.common.BundleListRetriever; import androidx.media3.common.C; @@ -88,6 +87,7 @@ import androidx.media3.session.MediaSession.ControllerCb; import androidx.media3.session.MediaSession.ControllerInfo; import androidx.media3.session.MediaSession.MediaItemsWithStartPosition; import androidx.media3.session.SessionCommand.CommandCode; +import androidx.media3.session.legacy.MediaSessionManager; import com.google.common.collect.ImmutableBiMap; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.Futures; 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 cf4f907072..faadf5da8d 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaStyleNotificationHelper.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaStyleNotificationHelper.java @@ -25,7 +25,6 @@ import android.app.Notification; import android.app.PendingIntent; import android.os.Build; import android.os.Bundle; -import android.support.v4.media.session.MediaSessionCompat; import android.view.View; import android.widget.RemoteViews; import androidx.annotation.DoNotInline; @@ -38,6 +37,7 @@ import androidx.core.graphics.drawable.IconCompat; import androidx.media3.common.util.NullableType; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; +import androidx.media3.session.legacy.MediaSessionCompat; import com.google.errorprone.annotations.CanIgnoreReturnValue; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -262,7 +262,7 @@ public class MediaStyleNotificationHelper { if (actionsToShowInCompact != null) { int[] actions = actionsToShowInCompact; final int numActionsInCompact = Math.min(actions.length, MAX_MEDIA_BUTTONS_IN_COMPACT); - view.removeAllViews(androidx.media.R.id.media_actions); + view.removeAllViews(androidx.media3.session.R.id.media_actions); if (numActionsInCompact > 0) { for (int i = 0; i < numActionsInCompact; i++) { if (i >= numActions) { @@ -275,24 +275,25 @@ public class MediaStyleNotificationHelper { final androidx.core.app.NotificationCompat.Action action = mBuilder.mActions.get(actions[i]); final RemoteViews button = generateMediaActionButton(action); - view.addView(androidx.media.R.id.media_actions, button); + view.addView(androidx.media3.session.R.id.media_actions, button); } } } if (showCancelButton) { - view.setViewVisibility(androidx.media.R.id.end_padder, View.GONE); - view.setViewVisibility(androidx.media.R.id.cancel_action, View.VISIBLE); - view.setOnClickPendingIntent(androidx.media.R.id.cancel_action, cancelButtonIntent); + view.setViewVisibility(androidx.media3.session.R.id.end_padder, View.GONE); + view.setViewVisibility(androidx.media3.session.R.id.cancel_action, View.VISIBLE); + view.setOnClickPendingIntent( + androidx.media3.session.R.id.cancel_action, cancelButtonIntent); view.setInt( - androidx.media.R.id.cancel_action, + androidx.media3.session.R.id.cancel_action, "setAlpha", mBuilder .mContext .getResources() - .getInteger(androidx.media.R.integer.cancel_button_image_alpha)); + .getInteger(androidx.media3.session.R.integer.cancel_button_image_alpha)); } else { - view.setViewVisibility(androidx.media.R.id.end_padder, View.VISIBLE); - view.setViewVisibility(androidx.media.R.id.cancel_action, View.GONE); + view.setViewVisibility(androidx.media3.session.R.id.end_padder, View.VISIBLE); + view.setViewVisibility(androidx.media3.session.R.id.cancel_action, View.GONE); } return view; } @@ -303,20 +304,21 @@ public class MediaStyleNotificationHelper { RemoteViews button = new RemoteViews( mBuilder.mContext.getPackageName(), - androidx.media.R.layout.notification_media_action); + androidx.media3.session.R.layout.media3_notification_media_action); IconCompat iconCompat = action.getIconCompat(); if (iconCompat != null) { - button.setImageViewResource(androidx.media.R.id.action0, iconCompat.getResId()); + button.setImageViewResource(androidx.media3.session.R.id.action0, iconCompat.getResId()); } if (!tombstone) { - button.setOnClickPendingIntent(androidx.media.R.id.action0, action.getActionIntent()); + button.setOnClickPendingIntent( + androidx.media3.session.R.id.action0, action.getActionIntent()); } - button.setContentDescription(androidx.media.R.id.action0, action.getTitle()); + button.setContentDescription(androidx.media3.session.R.id.action0, action.getTitle()); return button; } /* package */ int getContentViewLayoutResource() { - return androidx.media.R.layout.notification_template_media; + return androidx.media3.session.R.layout.media3_notification_template_media; } @Override @@ -338,33 +340,33 @@ public class MediaStyleNotificationHelper { getBigContentViewLayoutResource(actionCount), /* fitIn1U= */ false); - big.removeAllViews(androidx.media.R.id.media_actions); + big.removeAllViews(androidx.media3.session.R.id.media_actions); if (actionCount > 0) { for (int i = 0; i < actionCount; i++) { final RemoteViews button = generateMediaActionButton(mBuilder.mActions.get(i)); - big.addView(androidx.media.R.id.media_actions, button); + big.addView(androidx.media3.session.R.id.media_actions, button); } } if (showCancelButton) { - big.setViewVisibility(androidx.media.R.id.cancel_action, View.VISIBLE); + big.setViewVisibility(androidx.media3.session.R.id.cancel_action, View.VISIBLE); big.setInt( - androidx.media.R.id.cancel_action, + androidx.media3.session.R.id.cancel_action, "setAlpha", mBuilder .mContext .getResources() - .getInteger(androidx.media.R.integer.cancel_button_image_alpha)); - big.setOnClickPendingIntent(androidx.media.R.id.cancel_action, cancelButtonIntent); + .getInteger(androidx.media3.session.R.integer.cancel_button_image_alpha)); + big.setOnClickPendingIntent(androidx.media3.session.R.id.cancel_action, cancelButtonIntent); } else { - big.setViewVisibility(androidx.media.R.id.cancel_action, View.GONE); + big.setViewVisibility(androidx.media3.session.R.id.cancel_action, View.GONE); } return big; } /* package */ int getBigContentViewLayoutResource(int actionCount) { return actionCount <= 3 - ? androidx.media.R.layout.notification_template_big_media_narrow - : androidx.media.R.layout.notification_template_big_media; + ? androidx.media3.session.R.layout.media3_notification_template_big_media_narrow + : androidx.media3.session.R.layout.media3_notification_template_big_media; } } @@ -395,9 +397,9 @@ public class MediaStyleNotificationHelper { * * *

If you are using this style, consider using the corresponding styles like {@link - * androidx.media.R.style#TextAppearance_Compat_Notification_Media} or {@link - * androidx.media.R.style#TextAppearance_Compat_Notification_Title_Media} in your custom views in - * order to get the correct styling on each platform version. + * androidx.media3.session.R.style#TextAppearance_Compat_Notification_Media} or {@link + * androidx.media3.session.R.style#TextAppearance_Compat_Notification_Title_Media} in your custom + * views in order to get the correct styling on each platform version. * * @see androidx.core.app.NotificationCompat.DecoratedCustomViewStyle * @see MediaStyle @@ -469,7 +471,7 @@ public class MediaStyleNotificationHelper { @Override /* package */ int getContentViewLayoutResource() { return mBuilder.getContentView() != null - ? androidx.media.R.layout.notification_template_media_custom + ? androidx.media3.session.R.layout.media3_notification_template_media_custom : super.getContentViewLayoutResource(); } @@ -500,8 +502,8 @@ public class MediaStyleNotificationHelper { @Override /* package */ int getBigContentViewLayoutResource(int actionCount) { return actionCount <= 3 - ? androidx.media.R.layout.notification_template_big_media_narrow_custom - : androidx.media.R.layout.notification_template_big_media_custom; + ? androidx.media3.session.R.layout.media3_notification_template_big_media_narrow_custom + : androidx.media3.session.R.layout.media3_notification_template_big_media_custom; } @Override @@ -536,9 +538,12 @@ public class MediaStyleNotificationHelper { .mContext .getResources() .getColor( - androidx.media.R.color.notification_material_background_media_default_color); + androidx.media3.session.R.color + .notification_material_background_media_default_color); views.setInt( - androidx.media.R.id.status_bar_latest_event_content, "setBackgroundColor", color); + androidx.media3.session.R.id.status_bar_latest_event_content, + "setBackgroundColor", + color); } } 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 bae4127da8..1286135862 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java @@ -22,11 +22,9 @@ import static java.lang.Math.min; import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; -import android.support.v4.media.session.PlaybackStateCompat; import android.text.TextUtils; import android.util.Pair; import androidx.annotation.Nullable; -import androidx.media.MediaBrowserServiceCompat.BrowserRoot; import androidx.media3.common.C; import androidx.media3.common.Player; import androidx.media3.common.Player.Command; @@ -34,6 +32,8 @@ import androidx.media3.common.Player.Commands; import androidx.media3.common.util.NullableType; import androidx.media3.common.util.Util; import androidx.media3.session.PlayerInfo.BundlingExclusions; +import androidx.media3.session.legacy.MediaBrowserServiceCompat.BrowserRoot; +import androidx.media3.session.legacy.PlaybackStateCompat; import java.util.ArrayList; import java.util.List; 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 a0a3faf294..5f1b3f09e5 100644 --- a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java +++ b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java @@ -28,14 +28,11 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; -import android.support.v4.media.session.MediaSessionCompat; -import android.support.v4.media.session.PlaybackStateCompat; import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.TextureView; import androidx.annotation.Nullable; -import androidx.media.VolumeProviderCompat; import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; import androidx.media3.common.DeviceInfo; @@ -53,6 +50,9 @@ import androidx.media3.common.text.CueGroup; import androidx.media3.common.util.Log; import androidx.media3.common.util.Size; import androidx.media3.common.util.Util; +import androidx.media3.session.legacy.MediaSessionCompat; +import androidx.media3.session.legacy.PlaybackStateCompat; +import androidx.media3.session.legacy.VolumeProviderCompat; import com.google.common.collect.ImmutableList; import java.util.List; diff --git a/libraries/session/src/main/java/androidx/media3/session/QueueTimeline.java b/libraries/session/src/main/java/androidx/media3/session/QueueTimeline.java index b0913b9ddd..2477b386b0 100644 --- a/libraries/session/src/main/java/androidx/media3/session/QueueTimeline.java +++ b/libraries/session/src/main/java/androidx/media3/session/QueueTimeline.java @@ -18,13 +18,13 @@ package androidx.media3.session; import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Util.msToUs; -import android.support.v4.media.MediaMetadataCompat; -import android.support.v4.media.session.MediaSessionCompat.QueueItem; import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.MediaItem; import androidx.media3.common.Timeline; import androidx.media3.common.util.Util; +import androidx.media3.session.legacy.MediaMetadataCompat; +import androidx.media3.session.legacy.MediaSessionCompat.QueueItem; import com.google.common.base.Objects; import com.google.common.collect.ImmutableList; import java.util.ArrayList; diff --git a/libraries/session/src/main/java/androidx/media3/session/SessionToken.java b/libraries/session/src/main/java/androidx/media3/session/SessionToken.java index 5fcd12a5fe..aa580e9ed5 100644 --- a/libraries/session/src/main/java/androidx/media3/session/SessionToken.java +++ b/libraries/session/src/main/java/androidx/media3/session/SessionToken.java @@ -29,19 +29,20 @@ import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; +import android.os.Parcelable; import android.os.ResultReceiver; -import android.support.v4.media.session.MediaControllerCompat; -import android.support.v4.media.session.MediaSessionCompat; import android.text.TextUtils; import androidx.annotation.IntDef; import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; -import androidx.media.MediaBrowserServiceCompat; import androidx.media3.common.Bundleable; import androidx.media3.common.C; import androidx.media3.common.MediaLibraryInfo; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; +import androidx.media3.session.legacy.LegacyParcelableUtil; +import androidx.media3.session.legacy.MediaBrowserServiceCompat; +import androidx.media3.session.legacy.MediaControllerCompat; +import androidx.media3.session.legacy.MediaSessionCompat; import com.google.common.collect.ImmutableSet; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; @@ -262,49 +263,51 @@ public final class SessionToken implements Bundleable { } /** - * Creates a token from a {@link android.media.session.MediaSession.Token}. + * Creates a token from a {@link android.media.session.MediaSession.Token} or {@code + * android.support.v4.media.session.MediaSessionCompat.Token}. * * @param context A {@link Context}. - * @param token The {@link android.media.session.MediaSession.Token}. + * @param token The {@link android.media.session.MediaSession.Token} or {@code + * android.support.v4.media.session.MediaSessionCompat.Token}. * @return A {@link ListenableFuture} for the {@link SessionToken}. */ - @SuppressWarnings("UnnecessarilyFullyQualified") // Avoiding clash with Media3 MediaSession. @UnstableApi - @RequiresApi(21) public static ListenableFuture createSessionToken( - Context context, android.media.session.MediaSession.Token token) { - return createSessionToken(context, MediaSessionCompat.Token.fromToken(token)); + Context context, Parcelable token) { + return createSessionToken(context, createCompatToken(token)); } /** - * Creates a token from a {@link android.media.session.MediaSession.Token}. + * Creates a token from a {@link android.media.session.MediaSession.Token} or {@code + * android.support.v4.media.session.MediaSessionCompat.Token}. * * @param context A {@link Context}. - * @param token The {@link android.media.session.MediaSession.Token}. + * @param token The {@link android.media.session.MediaSession.Token} or {@code + * android.support.v4.media.session.MediaSessionCompat.Token}.. * @param completionLooper The {@link Looper} on which the returned {@link ListenableFuture} * completes. This {@link Looper} can't be used to call {@code future.get()} on the returned * {@link ListenableFuture}. * @return A {@link ListenableFuture} for the {@link SessionToken}. */ - @SuppressWarnings("UnnecessarilyFullyQualified") // Avoiding clash with Media3 MediaSession. @UnstableApi - @RequiresApi(21) public static ListenableFuture createSessionToken( - Context context, android.media.session.MediaSession.Token token, Looper completionLooper) { - return createSessionToken(context, MediaSessionCompat.Token.fromToken(token), completionLooper); + Context context, Parcelable token, Looper completionLooper) { + return createSessionToken(context, createCompatToken(token), completionLooper); } - /** - * Creates a token from a {@link android.support.v4.media.session.MediaSessionCompat.Token}. - * - * @param context A {@link Context}. - * @param compatToken The {@code android.support.v4.media.session.MediaSessionCompat.Token}. - * @return A {@link ListenableFuture} for the {@link SessionToken}. - */ - @SuppressWarnings("UnnecessarilyFullyQualified") // Avoiding clash with Media3 MediaSession. - @UnstableApi - public static ListenableFuture createSessionToken( - Context context, android.support.v4.media.session.MediaSessionCompat.Token compatToken) { + private static MediaSessionCompat.Token createCompatToken( + Parcelable platformOrLegacyCompatToken) { + if (Util.SDK_INT >= 21 + && platformOrLegacyCompatToken instanceof android.media.session.MediaSession.Token) { + return MediaSessionCompat.Token.fromToken(platformOrLegacyCompatToken); + } + // Assume this is an android.support.v4.media.session.MediaSessionCompat.Token. + return LegacyParcelableUtil.convert( + platformOrLegacyCompatToken, MediaSessionCompat.Token.CREATOR); + } + + private static ListenableFuture createSessionToken( + Context context, MediaSessionCompat.Token compatToken) { HandlerThread thread = new HandlerThread("SessionTokenThread"); thread.start(); ListenableFuture tokenFuture = @@ -313,29 +316,15 @@ public final class SessionToken implements Bundleable { return tokenFuture; } - /** - * Creates a token from a {@link android.support.v4.media.session.MediaSessionCompat.Token}. - * - * @param context A {@link Context}. - * @param compatToken The {@link android.support.v4.media.session.MediaSessionCompat.Token}. - * @param completionLooper The {@link Looper} on which the returned {@link ListenableFuture} - * completes. This {@link Looper} can't be used to call {@code future.get()} on the returned - * {@link ListenableFuture}. - * @return A {@link ListenableFuture} for the {@link SessionToken}. - */ - @SuppressWarnings("UnnecessarilyFullyQualified") // Avoiding clash with Media3 MediaSession. - @UnstableApi - public static ListenableFuture createSessionToken( - Context context, - android.support.v4.media.session.MediaSessionCompat.Token compatToken, - Looper completionLooper) { + private static ListenableFuture createSessionToken( + Context context, MediaSessionCompat.Token compatToken, Looper completionLooper) { checkNotNull(context, "context must not be null"); checkNotNull(compatToken, "compatToken must not be null"); SettableFuture future = SettableFuture.create(); // Try retrieving media3 token by connecting to the session. MediaControllerCompat controller = new MediaControllerCompat(context, compatToken); - String packageName = controller.getPackageName(); + String packageName = checkNotNull(controller.getPackageName()); Handler handler = new Handler(completionLooper); Runnable createFallbackLegacyToken = () -> { diff --git a/libraries/session/src/main/java/androidx/media3/session/SessionTokenImplLegacy.java b/libraries/session/src/main/java/androidx/media3/session/SessionTokenImplLegacy.java index c0fd1f84aa..373e3a796d 100644 --- a/libraries/session/src/main/java/androidx/media3/session/SessionTokenImplLegacy.java +++ b/libraries/session/src/main/java/androidx/media3/session/SessionTokenImplLegacy.java @@ -25,10 +25,10 @@ import static androidx.media3.session.SessionToken.TYPE_SESSION_LEGACY; import android.content.ComponentName; import android.os.Bundle; -import android.support.v4.media.session.MediaSessionCompat; import androidx.annotation.Nullable; import androidx.media3.common.util.Util; import androidx.media3.session.SessionToken.SessionTokenImpl; +import androidx.media3.session.legacy.MediaSessionCompat; import com.google.common.base.Objects; /* package */ final class SessionTokenImplLegacy implements SessionTokenImpl { diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/AudioAttributesCompat.java b/libraries/session/src/main/java/androidx/media3/session/legacy/AudioAttributesCompat.java new file mode 100644 index 0000000000..41f2cf5a43 --- /dev/null +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/AudioAttributesCompat.java @@ -0,0 +1,1107 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.session.legacy; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY; +import static androidx.media3.common.util.Assertions.checkNotNull; + +import android.annotation.SuppressLint; +import android.media.AudioAttributes; +import android.media.AudioManager; +import android.os.Build; +import android.util.SparseIntArray; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.RestrictTo; +import androidx.media3.common.util.Log; +import androidx.media3.common.util.UnstableApi; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.Locale; +import java.util.Objects; + +/** + * A class to encapsulate a collection of attributes describing information about an audio stream. + * + *

AudioAttributesCompat supersede the notion of stream types (see for instance + * {@link AudioManager#STREAM_MUSIC} or {@link AudioManager#STREAM_ALARM}) for defining the behavior + * of audio playback. Attributes allow an application to specify more information than is conveyed + * in a stream type by allowing the application to define: + * + *

+ * + *

AudioAttributesCompat instance is built through its builder, {@link + * AudioAttributesCompat.Builder}. Also see {@link android.media.AudioAttributes} for the framework + * implementation of this class. + */ +@UnstableApi +@RestrictTo(LIBRARY) +public class AudioAttributesCompat { + static final String TAG = "AudioAttributesCompat"; + + /** Content type value to use when the content type is unknown, or other than the ones defined. */ + public static final int CONTENT_TYPE_UNKNOWN = AudioAttributes.CONTENT_TYPE_UNKNOWN; + + /** Content type value to use when the content type is speech. */ + public static final int CONTENT_TYPE_SPEECH = AudioAttributes.CONTENT_TYPE_SPEECH; + + /** Content type value to use when the content type is music. */ + public static final int CONTENT_TYPE_MUSIC = AudioAttributes.CONTENT_TYPE_MUSIC; + + /** + * Content type value to use when the content type is a soundtrack, typically accompanying a movie + * or TV program. + */ + public static final int CONTENT_TYPE_MOVIE = AudioAttributes.CONTENT_TYPE_MOVIE; + + /** + * Content type value to use when the content type is a sound used to accompany a user action, + * such as a beep or sound effect expressing a key click, or event, such as the type of a sound + * for a bonus being received in a game. These sounds are mostly synthesized or short Foley + * sounds. + */ + public static final int CONTENT_TYPE_SONIFICATION = AudioAttributes.CONTENT_TYPE_SONIFICATION; + + /** Usage value to use when the usage is unknown. */ + public static final int USAGE_UNKNOWN = AudioAttributes.USAGE_UNKNOWN; + + /** Usage value to use when the usage is media, such as music, or movie soundtracks. */ + public static final int USAGE_MEDIA = AudioAttributes.USAGE_MEDIA; + + /** Usage value to use when the usage is voice communications, such as telephony or VoIP. */ + public static final int USAGE_VOICE_COMMUNICATION = AudioAttributes.USAGE_VOICE_COMMUNICATION; + + /** + * Usage value to use when the usage is in-call signalling, such as with a "busy" beep, or DTMF + * tones. + */ + public static final int USAGE_VOICE_COMMUNICATION_SIGNALLING = + AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING; + + /** Usage value to use when the usage is an alarm (e.g. wake-up alarm). */ + public static final int USAGE_ALARM = AudioAttributes.USAGE_ALARM; + + /** + * Usage value to use when the usage is notification. See other notification usages for more + * specialized uses. + */ + public static final int USAGE_NOTIFICATION = AudioAttributes.USAGE_NOTIFICATION; + + /** Usage value to use when the usage is telephony ringtone. */ + public static final int USAGE_NOTIFICATION_RINGTONE = AudioAttributes.USAGE_NOTIFICATION_RINGTONE; + + /** + * Usage value to use when the usage is a request to enter/end a communication, such as a VoIP + * communication or video-conference. + */ + public static final int USAGE_NOTIFICATION_COMMUNICATION_REQUEST = + AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST; + + /** + * Usage value to use when the usage is notification for an "instant" communication such as a + * chat, or SMS. + */ + public static final int USAGE_NOTIFICATION_COMMUNICATION_INSTANT = + AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT; + + /** + * Usage value to use when the usage is notification for a non-immediate type of communication + * such as e-mail. + */ + public static final int USAGE_NOTIFICATION_COMMUNICATION_DELAYED = + AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED; + + /** + * Usage value to use when the usage is to attract the user's attention, such as a reminder or low + * battery warning. + */ + public static final int USAGE_NOTIFICATION_EVENT = AudioAttributes.USAGE_NOTIFICATION_EVENT; + + /** Usage value to use when the usage is for accessibility, such as with a screen reader. */ + public static final int USAGE_ASSISTANCE_ACCESSIBILITY = + AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY; + + /** Usage value to use when the usage is driving or navigation directions. */ + public static final int USAGE_ASSISTANCE_NAVIGATION_GUIDANCE = + AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE; + + /** Usage value to use when the usage is sonification, such as with user interface sounds. */ + public static final int USAGE_ASSISTANCE_SONIFICATION = + AudioAttributes.USAGE_ASSISTANCE_SONIFICATION; + + /** Usage value to use when the usage is for game audio. */ + public static final int USAGE_GAME = AudioAttributes.USAGE_GAME; + + // usage not available to clients + public static final int USAGE_VIRTUAL_SOURCE = 15; // AudioAttributes.USAGE_VIRTUAL_SOURCE; + + /** + * Usage value to use for audio responses to user queries, audio instructions or help utterances. + */ + @SuppressLint("InlinedApi") // save to access compile time constant + public static final int USAGE_ASSISTANT = AudioAttributes.USAGE_ASSISTANT; + + /** + * IMPORTANT: when adding new usage types, add them to SDK_USAGES and update SUPPRESSIBLE_USAGES + * if applicable. + */ + + // private API + private static final int SUPPRESSIBLE_NOTIFICATION = 1; + + private static final int SUPPRESSIBLE_CALL = 2; + private static final SparseIntArray SUPPRESSIBLE_USAGES; + + // used by tests + @SuppressWarnings("WeakerAccess") /* synthetic access */ + static boolean sForceLegacyBehavior; + + static { + SUPPRESSIBLE_USAGES = new SparseIntArray(); + SUPPRESSIBLE_USAGES.put(USAGE_NOTIFICATION, SUPPRESSIBLE_NOTIFICATION); + SUPPRESSIBLE_USAGES.put(USAGE_NOTIFICATION_RINGTONE, SUPPRESSIBLE_CALL); + SUPPRESSIBLE_USAGES.put(USAGE_NOTIFICATION_COMMUNICATION_REQUEST, SUPPRESSIBLE_CALL); + SUPPRESSIBLE_USAGES.put(USAGE_NOTIFICATION_COMMUNICATION_INSTANT, SUPPRESSIBLE_NOTIFICATION); + SUPPRESSIBLE_USAGES.put(USAGE_NOTIFICATION_COMMUNICATION_DELAYED, SUPPRESSIBLE_NOTIFICATION); + SUPPRESSIBLE_USAGES.put(USAGE_NOTIFICATION_EVENT, SUPPRESSIBLE_NOTIFICATION); + } + + @SuppressWarnings("unused") + private static final int[] SDK_USAGES = { + USAGE_UNKNOWN, + USAGE_MEDIA, + USAGE_VOICE_COMMUNICATION, + USAGE_VOICE_COMMUNICATION_SIGNALLING, + USAGE_ALARM, + USAGE_NOTIFICATION, + USAGE_NOTIFICATION_RINGTONE, + USAGE_NOTIFICATION_COMMUNICATION_REQUEST, + USAGE_NOTIFICATION_COMMUNICATION_INSTANT, + USAGE_NOTIFICATION_COMMUNICATION_DELAYED, + USAGE_NOTIFICATION_EVENT, + USAGE_ASSISTANCE_ACCESSIBILITY, + USAGE_ASSISTANCE_NAVIGATION_GUIDANCE, + USAGE_ASSISTANCE_SONIFICATION, + USAGE_GAME, + USAGE_ASSISTANT, + }; + + /** Flag defining a behavior where the audibility of the sound will be ensured by the system. */ + public static final int FLAG_AUDIBILITY_ENFORCED = 0x1 << 0; + + static final int FLAG_SECURE = 0x1 << 1; + static final int FLAG_SCO = 0x1 << 2; + static final int FLAG_BEACON = 0x1 << 3; + + /** Flag requesting the use of an output stream supporting hardware A/V synchronization. */ + public static final int FLAG_HW_AV_SYNC = 0x1 << 4; + + static final int FLAG_HW_HOTWORD = 0x1 << 5; + static final int FLAG_BYPASS_INTERRUPTION_POLICY = 0x1 << 6; + static final int FLAG_BYPASS_MUTE = 0x1 << 7; + static final int FLAG_LOW_LATENCY = 0x1 << 8; + static final int FLAG_DEEP_BUFFER = 0x1 << 9; + + static final int FLAG_ALL = + (FLAG_AUDIBILITY_ENFORCED + | FLAG_SECURE + | FLAG_SCO + | FLAG_BEACON + | FLAG_HW_AV_SYNC + | FLAG_HW_HOTWORD + | FLAG_BYPASS_INTERRUPTION_POLICY + | FLAG_BYPASS_MUTE + | FLAG_LOW_LATENCY + | FLAG_DEEP_BUFFER); + static final int FLAG_ALL_PUBLIC = + (FLAG_AUDIBILITY_ENFORCED | FLAG_HW_AV_SYNC | FLAG_LOW_LATENCY); + + static final int INVALID_STREAM_TYPE = -1; // AudioSystem.STREAM_DEFAULT + + public final AudioAttributesImpl mImpl; + + AudioAttributesCompat(AudioAttributesImpl impl) { + mImpl = impl; + } + + /** + * Returns the stream type matching the given attributes for volume control. Use this method to + * derive the stream type needed to configure the volume control slider in an {@link + * android.app.Activity} with {@link android.app.Activity#setVolumeControlStream(int)}.
+ * Do not use this method to set the stream type on an audio player object (e.g. {@link + * android.media.AudioTrack}, {@link android.media.MediaPlayer}) as this is deprecated; use + * AudioAttributes instead. + * + * @return a valid stream type for Activity or stream volume control that matches the + * attributes, or {@link AudioManager#USE_DEFAULT_STREAM_TYPE} if there isn't a direct match. + * Note that USE_DEFAULT_STREAM_TYPE is not a valid value for {@link + * AudioManager#setStreamVolume(int, int, int)}. + */ + public int getVolumeControlStream() { + return mImpl.getVolumeControlStream(); + } + + // public API unique to AudioAttributesCompat + + /** + * If the current SDK level is 21 or higher, return the {@link AudioAttributes} object inside this + * {@link AudioAttributesCompat}. Otherwise null. + * + * @return the underlying {@link AudioAttributes} object or null + */ + @Nullable + public Object unwrap() { + return mImpl.getAudioAttributes(); + } + + /** + * Returns a stream type passed to {@link Builder#setLegacyStreamType(int)}, or best guessing from + * flags and usage, or -1 if there is no converting logic in framework side (API 21+). + * + * @return the stream type {@see AudioManager} + */ + public int getLegacyStreamType() { + return mImpl.getLegacyStreamType(); + } + + /** + * Creates an {@link AudioAttributesCompat} given an API 21 {@link AudioAttributes} object. + * + * @param aa an instance of {@link AudioAttributes}. + * @return the new AudioAttributesCompat, or null on API < 21 + */ + @Nullable + public static AudioAttributesCompat wrap(Object aa) { + if (sForceLegacyBehavior) { + return null; + } + if (Build.VERSION.SDK_INT >= 26) { + return new AudioAttributesCompat(new AudioAttributesImplApi26((AudioAttributes) aa)); + } else if (Build.VERSION.SDK_INT >= 21) { + return new AudioAttributesCompat(new AudioAttributesImplApi21((AudioAttributes) aa)); + } + return null; + } + + // The rest of this file implements an approximation to AudioAttributes using old stream types + + /** + * Returns the content type. + * + * @return one of the values that can be set in {@link Builder#setContentType(int)} + */ + public int getContentType() { + return mImpl.getContentType(); + } + + /** + * Returns the usage. + * + * @return one of the values that can be set in {@link Builder#setUsage(int)} + */ + public @AttributeUsage int getUsage() { + return mImpl.getUsage(); + } + + /** + * Returns the flags. + * + * @return a combined mask of all flags + */ + public int getFlags() { + return mImpl.getFlags(); + } + + /** + * Builder class for {@link AudioAttributesCompat} objects. + * + *

example: + * + *

+   * new AudioAttributes.Builder()
+   * .setUsage(AudioAttributes.USAGE_MEDIA)
+   * .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+   * .build();
+   * 
+ * + *

By default all types of information (usage, content type, flags) conveyed by an + * AudioAttributesCompat instance are set to "unknown". Unknown information will be + * interpreted as a default value that is dependent on the context of use, for instance a {@link + * android.media.MediaPlayer} will use a default usage of {@link + * AudioAttributesCompat#USAGE_MEDIA}. See also {@link AudioAttributes.Builder}. + */ + public static class Builder { + final AudioAttributesImpl.Builder mBuilderImpl; + + /** + * Constructs a new Builder with the defaults. By default, usage and content type are + * respectively {@link AudioAttributesCompat#USAGE_UNKNOWN} and {@link + * AudioAttributesCompat#CONTENT_TYPE_UNKNOWN}, and flags are 0. It is recommended to configure + * the usage (with {@link #setUsage(int)}) or deriving attributes from a legacy stream type + * (with {@link #setLegacyStreamType(int)}) before calling {@link #build()} to override any + * default playback behavior in terms of routing and volume management. + */ + public Builder() { + if (sForceLegacyBehavior) { + mBuilderImpl = new AudioAttributesImplBase.Builder(); + } else if (Build.VERSION.SDK_INT >= 26) { + mBuilderImpl = new AudioAttributesImplApi26.Builder(); + } else if (Build.VERSION.SDK_INT >= 21) { + mBuilderImpl = new AudioAttributesImplApi21.Builder(); + } else { + mBuilderImpl = new AudioAttributesImplBase.Builder(); + } + } + + /** + * Constructs a new Builder from a given AudioAttributes + * + * @param aa the AudioAttributesCompat object whose data will be reused in the new Builder. + */ + public Builder(AudioAttributesCompat aa) { + if (sForceLegacyBehavior) { + mBuilderImpl = new AudioAttributesImplBase.Builder(aa); + } else if (Build.VERSION.SDK_INT >= 26) { + mBuilderImpl = new AudioAttributesImplApi26.Builder(checkNotNull(aa.unwrap())); + } else if (Build.VERSION.SDK_INT >= 21) { + mBuilderImpl = new AudioAttributesImplApi21.Builder(checkNotNull(aa.unwrap())); + } else { + mBuilderImpl = new AudioAttributesImplBase.Builder(aa); + } + } + + /** + * Combines all of the attributes that have been set and return a new {@link + * AudioAttributesCompat} object. + * + * @return a new {@link AudioAttributesCompat} object + */ + public AudioAttributesCompat build() { + return new AudioAttributesCompat(mBuilderImpl.build()); + } + + /** + * Sets the attribute describing what is the intended use of the audio signal, such as alarm or + * ringtone. + * + * @param usage one of {@link AudioAttributesCompat#USAGE_UNKNOWN}, {@link + * AudioAttributesCompat#USAGE_MEDIA}, {@link + * AudioAttributesCompat#USAGE_VOICE_COMMUNICATION}, {@link + * AudioAttributesCompat#USAGE_VOICE_COMMUNICATION_SIGNALLING}, {@link + * AudioAttributesCompat#USAGE_ALARM}, {@link AudioAttributesCompat#USAGE_NOTIFICATION}, + * {@link AudioAttributesCompat#USAGE_NOTIFICATION_RINGTONE}, {@link + * AudioAttributesCompat#USAGE_NOTIFICATION_COMMUNICATION_REQUEST}, {@link + * AudioAttributesCompat#USAGE_NOTIFICATION_COMMUNICATION_INSTANT}, {@link + * AudioAttributesCompat#USAGE_NOTIFICATION_COMMUNICATION_DELAYED}, {@link + * AudioAttributesCompat#USAGE_NOTIFICATION_EVENT}, {@link + * AudioAttributesCompat#USAGE_ASSISTANT}, {@link + * AudioAttributesCompat#USAGE_ASSISTANCE_ACCESSIBILITY}, {@link + * AudioAttributesCompat#USAGE_ASSISTANCE_NAVIGATION_GUIDANCE}, {@link + * AudioAttributesCompat#USAGE_ASSISTANCE_SONIFICATION}, {@link + * AudioAttributesCompat#USAGE_GAME}. + * @return the same Builder instance. + */ + public Builder setUsage(@AttributeUsage int usage) { + mBuilderImpl.setUsage(usage); + return this; + } + + /** + * Sets the attribute describing the content type of the audio signal, such as speech, or music. + * + * @param contentType the content type values, one of {@link + * AudioAttributesCompat#CONTENT_TYPE_MOVIE}, {@link + * AudioAttributesCompat#CONTENT_TYPE_MUSIC}, {@link + * AudioAttributesCompat#CONTENT_TYPE_SONIFICATION}, {@link + * AudioAttributesCompat#CONTENT_TYPE_SPEECH}, {@link + * AudioAttributesCompat#CONTENT_TYPE_UNKNOWN}. + * @return the same Builder instance. + */ + public Builder setContentType(@AttributeContentType int contentType) { + mBuilderImpl.setContentType(contentType); + return this; + } + + /** + * Sets the combination of flags. + * + *

This is a bitwise OR with the existing flags. + * + * @param flags a combination of {@link AudioAttributesCompat#FLAG_AUDIBILITY_ENFORCED}, {@link + * AudioAttributesCompat#FLAG_HW_AV_SYNC}. + * @return the same Builder instance. + */ + public Builder setFlags(int flags) { + mBuilderImpl.setFlags(flags); + return this; + } + + /** + * Sets attributes as inferred from the legacy stream types. + * + *

Warning: do not use this method in combination with setting any other attributes such as + * usage, content type, or flags, as this method will overwrite (the more accurate) information + * describing the use case previously set in the Builder. In general, avoid using it and prefer + * setting usage and content type directly with {@link #setUsage(int)} and {@link + * #setContentType(int)}. + * + *

Use this method when building an {@link AudioAttributes} instance to initialize some of + * the attributes by information derived from a legacy stream type. + * + * @param streamType one of AudioManager.STREAM_* + * @return this same Builder instance. + */ + public Builder setLegacyStreamType(int streamType) { + mBuilderImpl.setLegacyStreamType(streamType); + return this; + } + } + + @Override + public int hashCode() { + return mImpl.hashCode(); + } + + @Override + public String toString() { + return mImpl.toString(); + } + + static String usageToString(int usage) { + switch (usage) { + case USAGE_UNKNOWN: + return "USAGE_UNKNOWN"; + case USAGE_MEDIA: + return "USAGE_MEDIA"; + case USAGE_VOICE_COMMUNICATION: + return "USAGE_VOICE_COMMUNICATION"; + case USAGE_VOICE_COMMUNICATION_SIGNALLING: + return "USAGE_VOICE_COMMUNICATION_SIGNALLING"; + case USAGE_ALARM: + return "USAGE_ALARM"; + case USAGE_NOTIFICATION: + return "USAGE_NOTIFICATION"; + case USAGE_NOTIFICATION_RINGTONE: + return "USAGE_NOTIFICATION_RINGTONE"; + case USAGE_NOTIFICATION_COMMUNICATION_REQUEST: + return "USAGE_NOTIFICATION_COMMUNICATION_REQUEST"; + case USAGE_NOTIFICATION_COMMUNICATION_INSTANT: + return "USAGE_NOTIFICATION_COMMUNICATION_INSTANT"; + case USAGE_NOTIFICATION_COMMUNICATION_DELAYED: + return "USAGE_NOTIFICATION_COMMUNICATION_DELAYED"; + case USAGE_NOTIFICATION_EVENT: + return "USAGE_NOTIFICATION_EVENT"; + case USAGE_ASSISTANCE_ACCESSIBILITY: + return "USAGE_ASSISTANCE_ACCESSIBILITY"; + case USAGE_ASSISTANCE_NAVIGATION_GUIDANCE: + return "USAGE_ASSISTANCE_NAVIGATION_GUIDANCE"; + case USAGE_ASSISTANCE_SONIFICATION: + return "USAGE_ASSISTANCE_SONIFICATION"; + case USAGE_GAME: + return "USAGE_GAME"; + case USAGE_ASSISTANT: + return "USAGE_ASSISTANT"; + default: + return "unknown usage " + usage; + } + } + + abstract static class AudioManagerHidden { + public static final int STREAM_BLUETOOTH_SCO = 6; + public static final int STREAM_SYSTEM_ENFORCED = 7; + public static final int STREAM_TTS = 9; + public static final int STREAM_ACCESSIBILITY = 10; + + private AudioManagerHidden() {} + } + + /** Prevent AudioAttributes from being used even on platforms that support it. */ + public static void setForceLegacyBehavior(boolean force) { + sForceLegacyBehavior = force; + } + + int getRawLegacyStreamType() { + return mImpl.getRawLegacyStreamType(); + } + + static int toVolumeStreamType( + boolean fromGetVolumeControlStream, int flags, @AttributeUsage int usage) { + // flags to stream type mapping + if ((flags & FLAG_AUDIBILITY_ENFORCED) == FLAG_AUDIBILITY_ENFORCED) { + return fromGetVolumeControlStream + ? AudioManager.STREAM_SYSTEM + : AudioManagerHidden.STREAM_SYSTEM_ENFORCED; + } + if ((flags & FLAG_SCO) == FLAG_SCO) { + return fromGetVolumeControlStream + ? AudioManager.STREAM_VOICE_CALL + : AudioManagerHidden.STREAM_BLUETOOTH_SCO; + } + + // usage to stream type mapping + switch (usage) { + case USAGE_MEDIA: + case USAGE_GAME: + case USAGE_ASSISTANCE_NAVIGATION_GUIDANCE: + case USAGE_ASSISTANT: + return AudioManager.STREAM_MUSIC; + case USAGE_ASSISTANCE_SONIFICATION: + return AudioManager.STREAM_SYSTEM; + case USAGE_VOICE_COMMUNICATION: + return AudioManager.STREAM_VOICE_CALL; + case USAGE_VOICE_COMMUNICATION_SIGNALLING: + return fromGetVolumeControlStream + ? AudioManager.STREAM_VOICE_CALL + : AudioManager.STREAM_DTMF; + case USAGE_ALARM: + return AudioManager.STREAM_ALARM; + case USAGE_NOTIFICATION_RINGTONE: + return AudioManager.STREAM_RING; + case USAGE_NOTIFICATION: + case USAGE_NOTIFICATION_COMMUNICATION_REQUEST: + case USAGE_NOTIFICATION_COMMUNICATION_INSTANT: + case USAGE_NOTIFICATION_COMMUNICATION_DELAYED: + case USAGE_NOTIFICATION_EVENT: + return AudioManager.STREAM_NOTIFICATION; + case USAGE_ASSISTANCE_ACCESSIBILITY: + return AudioManagerHidden.STREAM_ACCESSIBILITY; + case USAGE_UNKNOWN: + return AudioManager.STREAM_MUSIC; + default: + if (fromGetVolumeControlStream) { + throw new IllegalArgumentException( + "Unknown usage value " + usage + " in audio attributes"); + } else { + return AudioManager.STREAM_MUSIC; + } + } + } + + @Override + public boolean equals(@Nullable Object o) { + if (!(o instanceof AudioAttributesCompat)) { + return false; + } + final AudioAttributesCompat that = (AudioAttributesCompat) o; + if (this.mImpl == null) { + return that.mImpl == null; + } + return this.mImpl.equals(that.mImpl); + } + + @IntDef({ + USAGE_UNKNOWN, + USAGE_MEDIA, + USAGE_VOICE_COMMUNICATION, + USAGE_VOICE_COMMUNICATION_SIGNALLING, + USAGE_ALARM, + USAGE_NOTIFICATION, + USAGE_NOTIFICATION_RINGTONE, + USAGE_NOTIFICATION_COMMUNICATION_REQUEST, + USAGE_NOTIFICATION_COMMUNICATION_INSTANT, + USAGE_NOTIFICATION_COMMUNICATION_DELAYED, + USAGE_NOTIFICATION_EVENT, + USAGE_ASSISTANCE_ACCESSIBILITY, + USAGE_ASSISTANCE_NAVIGATION_GUIDANCE, + USAGE_ASSISTANCE_SONIFICATION, + USAGE_GAME, + USAGE_ASSISTANT, + USAGE_VIRTUAL_SOURCE + }) + @Retention(RetentionPolicy.SOURCE) + public @interface AttributeUsage {} + + @IntDef({ + CONTENT_TYPE_UNKNOWN, + CONTENT_TYPE_SPEECH, + CONTENT_TYPE_MUSIC, + CONTENT_TYPE_MOVIE, + CONTENT_TYPE_SONIFICATION + }) + @Retention(RetentionPolicy.SOURCE) + public @interface AttributeContentType {} + + public interface AudioAttributesImpl { + /** Gets framework {@link android.media.AudioAttributes}. */ + @Nullable + Object getAudioAttributes(); + + int getVolumeControlStream(); + + int getLegacyStreamType(); + + // Returns explicitly set legacy stream type. + int getRawLegacyStreamType(); + + int getContentType(); + + @AudioAttributesCompat.AttributeUsage + int getUsage(); + + int getFlags(); + + interface Builder { + + AudioAttributesImpl build(); + + Builder setUsage(@AudioAttributesCompat.AttributeUsage int usage); + + Builder setContentType(@AudioAttributesCompat.AttributeContentType int contentType); + + Builder setFlags(int flags); + + Builder setLegacyStreamType(int streamType); + } + } + + public static class AudioAttributesImplBase implements AudioAttributesImpl { + public int mUsage = USAGE_UNKNOWN; + + public int mContentType = CONTENT_TYPE_UNKNOWN; + + public int mFlags = 0x0; + + public int mLegacyStream = INVALID_STREAM_TYPE; + + public AudioAttributesImplBase() {} + + AudioAttributesImplBase(int contentType, int flags, int usage, int legacyStream) { + mContentType = contentType; + mFlags = flags; + mUsage = usage; + mLegacyStream = legacyStream; + } + + @Override + @Nullable + public Object getAudioAttributes() { + return null; + } + + @Override + public int getVolumeControlStream() { + return AudioAttributesCompat.toVolumeStreamType(true, mFlags, mUsage); + } + + @Override + public int getLegacyStreamType() { + if (mLegacyStream != INVALID_STREAM_TYPE) { + return mLegacyStream; + } + return AudioAttributesCompat.toVolumeStreamType(false, mFlags, mUsage); + } + + @Override + public int getRawLegacyStreamType() { + return mLegacyStream; + } + + @Override + public int getContentType() { + return mContentType; + } + + @Override + public @AudioAttributesCompat.AttributeUsage int getUsage() { + return mUsage; + } + + @Override + public int getFlags() { + int flags = mFlags; + int legacyStream = getLegacyStreamType(); + if (legacyStream == AudioManagerHidden.STREAM_BLUETOOTH_SCO) { + flags |= AudioAttributesCompat.FLAG_SCO; + } else if (legacyStream == AudioManagerHidden.STREAM_SYSTEM_ENFORCED) { + flags |= AudioAttributesCompat.FLAG_AUDIBILITY_ENFORCED; + } + return flags & AudioAttributesCompat.FLAG_ALL_PUBLIC; + } + + ////////////////////////////////////////////////////////////////////// + // Override Object methods + + @Override + public int hashCode() { + return Arrays.hashCode(new Object[] {mContentType, mFlags, mUsage, mLegacyStream}); + } + + @Override + public boolean equals(@Nullable Object o) { + if (!(o instanceof AudioAttributesImplBase)) { + return false; + } + final AudioAttributesImplBase that = (AudioAttributesImplBase) o; + return ((mContentType == that.getContentType()) + && (mFlags == that.getFlags()) + && (mUsage == that.getUsage()) + && (mLegacyStream == that.mLegacyStream)); // query the slot directly, don't guess + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("AudioAttributesCompat:"); + if (mLegacyStream != INVALID_STREAM_TYPE) { + sb.append(" stream=").append(mLegacyStream); + sb.append(" derived"); + } + sb.append(" usage=") + .append(AudioAttributesCompat.usageToString(mUsage)) + .append(" content=") + .append(mContentType) + .append(" flags=0x") + .append(Integer.toHexString(mFlags).toUpperCase(Locale.ROOT)); + return sb.toString(); + } + + static class Builder implements AudioAttributesImpl.Builder { + private int mUsage = USAGE_UNKNOWN; + private int mContentType = CONTENT_TYPE_UNKNOWN; + private int mFlags = 0x0; + private int mLegacyStream = INVALID_STREAM_TYPE; + + Builder() {} + + Builder(AudioAttributesCompat aa) { + mUsage = aa.getUsage(); + mContentType = aa.getContentType(); + mFlags = aa.getFlags(); + mLegacyStream = aa.getRawLegacyStreamType(); + } + + @Override + public AudioAttributesImpl build() { + return new AudioAttributesImplBase(mContentType, mFlags, mUsage, mLegacyStream); + } + + @Override + public Builder setUsage(@AudioAttributesCompat.AttributeUsage int usage) { + switch (usage) { + case USAGE_UNKNOWN: + case USAGE_MEDIA: + case USAGE_VOICE_COMMUNICATION: + case USAGE_VOICE_COMMUNICATION_SIGNALLING: + case USAGE_ALARM: + case USAGE_NOTIFICATION: + case USAGE_NOTIFICATION_RINGTONE: + case USAGE_NOTIFICATION_COMMUNICATION_REQUEST: + case USAGE_NOTIFICATION_COMMUNICATION_INSTANT: + case USAGE_NOTIFICATION_COMMUNICATION_DELAYED: + case USAGE_NOTIFICATION_EVENT: + case USAGE_ASSISTANCE_ACCESSIBILITY: + case USAGE_ASSISTANCE_NAVIGATION_GUIDANCE: + case USAGE_ASSISTANCE_SONIFICATION: + case USAGE_GAME: + case USAGE_VIRTUAL_SOURCE: + mUsage = usage; + break; + // TODO: shouldn't it be USAGE_ASSISTANT? + case USAGE_ASSISTANT: + mUsage = USAGE_ASSISTANCE_NAVIGATION_GUIDANCE; + break; + default: + mUsage = USAGE_UNKNOWN; + } + return this; + } + + @Override + public Builder setContentType(@AudioAttributesCompat.AttributeContentType int contentType) { + switch (contentType) { + case CONTENT_TYPE_UNKNOWN: + case CONTENT_TYPE_MOVIE: + case CONTENT_TYPE_MUSIC: + case CONTENT_TYPE_SONIFICATION: + case CONTENT_TYPE_SPEECH: + mContentType = contentType; + break; + default: + mContentType = CONTENT_TYPE_UNKNOWN; + } + return this; + } + + @Override + public Builder setFlags(int flags) { + flags &= AudioAttributesCompat.FLAG_ALL; + mFlags |= flags; + return this; + } + + @Override + public Builder setLegacyStreamType(int streamType) { + if (streamType == AudioManagerHidden.STREAM_ACCESSIBILITY) { + throw new IllegalArgumentException( + "STREAM_ACCESSIBILITY is not a legacy stream " + + "type that was used for audio playback"); + } + mLegacyStream = streamType; + return setInternalLegacyStreamType(streamType); + } + + private Builder setInternalLegacyStreamType(int streamType) { + switch (streamType) { + case AudioManager.STREAM_VOICE_CALL: + mContentType = CONTENT_TYPE_SPEECH; + break; + case AudioManagerHidden.STREAM_SYSTEM_ENFORCED: + mFlags |= AudioAttributesCompat.FLAG_AUDIBILITY_ENFORCED; + // intended fall through, attributes in common with STREAM_SYSTEM + case AudioManager.STREAM_SYSTEM: + mContentType = CONTENT_TYPE_SONIFICATION; + break; + case AudioManager.STREAM_RING: + mContentType = CONTENT_TYPE_SONIFICATION; + break; + case AudioManager.STREAM_MUSIC: + mContentType = CONTENT_TYPE_MUSIC; + break; + case AudioManager.STREAM_ALARM: + mContentType = CONTENT_TYPE_SONIFICATION; + break; + case AudioManager.STREAM_NOTIFICATION: + mContentType = CONTENT_TYPE_SONIFICATION; + break; + case AudioManagerHidden.STREAM_BLUETOOTH_SCO: + mContentType = CONTENT_TYPE_SPEECH; + mFlags |= AudioAttributesCompat.FLAG_SCO; + break; + case AudioManager.STREAM_DTMF: + mContentType = CONTENT_TYPE_SONIFICATION; + break; + case AudioManagerHidden.STREAM_TTS: + mContentType = CONTENT_TYPE_SONIFICATION; + break; + case AudioManager.STREAM_ACCESSIBILITY: + mContentType = CONTENT_TYPE_SPEECH; + break; + default: + Log.e(TAG, "Invalid stream type " + streamType + " for AudioAttributesCompat"); + } + mUsage = usageForStreamType(streamType); + return this; + } + } + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + static int usageForStreamType(int streamType) { + switch (streamType) { + case AudioManager.STREAM_VOICE_CALL: + return USAGE_VOICE_COMMUNICATION; + case AudioManagerHidden.STREAM_SYSTEM_ENFORCED: + case AudioManager.STREAM_SYSTEM: + return USAGE_ASSISTANCE_SONIFICATION; + case AudioManager.STREAM_RING: + return USAGE_NOTIFICATION_RINGTONE; + case AudioManager.STREAM_MUSIC: + return USAGE_MEDIA; + case AudioManager.STREAM_ALARM: + return USAGE_ALARM; + case AudioManager.STREAM_NOTIFICATION: + return USAGE_NOTIFICATION; + case AudioManagerHidden.STREAM_BLUETOOTH_SCO: + return USAGE_VOICE_COMMUNICATION; + case AudioManager.STREAM_DTMF: + return USAGE_VOICE_COMMUNICATION_SIGNALLING; + case AudioManager.STREAM_ACCESSIBILITY: + return USAGE_ASSISTANCE_ACCESSIBILITY; + case AudioManagerHidden.STREAM_TTS: + default: + return USAGE_UNKNOWN; + } + } + } + + @RequiresApi(21) + public static class AudioAttributesImplApi21 implements AudioAttributesImpl { + + @Nullable public AudioAttributes mAudioAttributes; + + public int mLegacyStreamType = INVALID_STREAM_TYPE; + + public AudioAttributesImplApi21() {} + + AudioAttributesImplApi21(AudioAttributes audioAttributes) { + this(audioAttributes, INVALID_STREAM_TYPE); + } + + AudioAttributesImplApi21(AudioAttributes audioAttributes, int explicitLegacyStream) { + mAudioAttributes = audioAttributes; + mLegacyStreamType = explicitLegacyStream; + } + + @Override + @Nullable + public Object getAudioAttributes() { + return mAudioAttributes; + } + + @Override + public int getVolumeControlStream() { + // TODO: address the framework change ag/4995785. + return AudioAttributesCompat.toVolumeStreamType(true, getFlags(), getUsage()); + } + + @Override + public int getLegacyStreamType() { + if (mLegacyStreamType != INVALID_STREAM_TYPE) { + return mLegacyStreamType; + } + return AudioAttributesCompat.toVolumeStreamType(false, getFlags(), getUsage()); + } + + @Override + public int getRawLegacyStreamType() { + return mLegacyStreamType; + } + + @Override + public int getContentType() { + return checkNotNull(mAudioAttributes).getContentType(); + } + + @Override + public @AudioAttributesCompat.AttributeUsage int getUsage() { + return checkNotNull(mAudioAttributes).getUsage(); + } + + @Override + public int getFlags() { + return checkNotNull(mAudioAttributes).getFlags(); + } + + @Override + public int hashCode() { + return checkNotNull(mAudioAttributes).hashCode(); + } + + @Override + public boolean equals(@Nullable Object o) { + if (!(o instanceof AudioAttributesImplApi21)) { + return false; + } + final AudioAttributesImplApi21 that = (AudioAttributesImplApi21) o; + return Objects.equals(mAudioAttributes, that.mAudioAttributes); + } + + @Override + public String toString() { + return "AudioAttributesCompat: audioattributes=" + mAudioAttributes; + } + + @RequiresApi(21) + static class Builder implements AudioAttributesImpl.Builder { + final AudioAttributes.Builder mFwkBuilder; + + Builder() { + mFwkBuilder = new AudioAttributes.Builder(); + } + + Builder(Object aa) { + mFwkBuilder = new AudioAttributes.Builder((AudioAttributes) aa); + } + + @Override + public AudioAttributesImpl build() { + return new AudioAttributesImplApi21(mFwkBuilder.build()); + } + + @Override + @SuppressLint("WrongConstant") + public Builder setUsage(int usage) { + if (usage == AudioAttributes.USAGE_ASSISTANT) { + // TODO: shouldn't we keep the origin usage? + usage = AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE; + } + mFwkBuilder.setUsage(usage); + return this; + } + + @Override + public Builder setContentType(int contentType) { + mFwkBuilder.setContentType(contentType); + return this; + } + + @Override + public Builder setFlags(int flags) { + mFwkBuilder.setFlags(flags); + return this; + } + + @Override + public Builder setLegacyStreamType(int streamType) { + mFwkBuilder.setLegacyStreamType(streamType); + return this; + } + } + } + + @RequiresApi(26) + public static class AudioAttributesImplApi26 extends AudioAttributesImplApi21 { + + public AudioAttributesImplApi26() {} + + AudioAttributesImplApi26(AudioAttributes audioAttributes) { + super(audioAttributes, INVALID_STREAM_TYPE); + } + + @Override + public int getVolumeControlStream() { + return checkNotNull(mAudioAttributes).getVolumeControlStream(); + } + + @RequiresApi(26) + static class Builder extends AudioAttributesImplApi21.Builder { + Builder() { + super(); + } + + Builder(Object aa) { + super(aa); + } + + @Override + public AudioAttributesImpl build() { + return new AudioAttributesImplApi26(mFwkBuilder.build()); + } + + @Override + public Builder setUsage(int usage) { + mFwkBuilder.setUsage(usage); + return this; + } + } + } +} diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/IMediaControllerCallback.java b/libraries/session/src/main/java/androidx/media3/session/legacy/IMediaControllerCallback.java new file mode 100644 index 0000000000..04ea6817f2 --- /dev/null +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/IMediaControllerCallback.java @@ -0,0 +1,596 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.session.legacy; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY; +import static androidx.media3.common.util.Assertions.checkNotNull; + +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.media3.common.util.UnstableApi; + +/** + * Callback interface for a MediaSessionCompat to send updates to a MediaControllerCompat. This is + * only used on pre-Lollipop systems. + */ +@UnstableApi +@RestrictTo(LIBRARY) +public interface IMediaControllerCallback extends android.os.IInterface { + /** Local-side IPC implementation stub class. */ + public abstract static class Stub extends android.os.Binder implements IMediaControllerCallback { + private static final String DESCRIPTOR = + "android.support.v4.media.session.IMediaControllerCallback"; + + /** Construct the stub at attach it to the interface. */ + // Using this in constructor + @SuppressWarnings({"method.invocation.invalid", "argument.type.incompatible"}) + public Stub() { + this.attachInterface(this, DESCRIPTOR); + } + + /** + * Cast an IBinder object into an androidx.media3.session.legacy.IMediaControllerCallback + * interface, generating a proxy if needed. + */ + @Nullable + public static IMediaControllerCallback asInterface(@Nullable android.os.IBinder obj) { + if ((obj == null)) { + return null; + } + android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR); + if (((iin != null) && (iin instanceof IMediaControllerCallback))) { + return ((IMediaControllerCallback) iin); + } + return new Proxy(obj); + } + + @Override + public android.os.IBinder asBinder() { + return this; + } + + @Override + public boolean onTransact( + int code, android.os.Parcel data, @Nullable android.os.Parcel reply, int flags) + throws android.os.RemoteException { + String descriptor = DESCRIPTOR; + switch (code) { + case INTERFACE_TRANSACTION: + { + checkNotNull(reply).writeString(descriptor); + return true; + } + case TRANSACTION_onEvent: + { + data.enforceInterface(descriptor); + String _arg0; + _arg0 = data.readString(); + android.os.Bundle _arg1; + if ((0 != data.readInt())) { + _arg1 = android.os.Bundle.CREATOR.createFromParcel(data); + } else { + _arg1 = null; + } + this.onEvent(_arg0, _arg1); + return true; + } + case TRANSACTION_onSessionDestroyed: + { + data.enforceInterface(descriptor); + this.onSessionDestroyed(); + return true; + } + case TRANSACTION_onPlaybackStateChanged: + { + data.enforceInterface(descriptor); + PlaybackStateCompat _arg0; + if ((0 != data.readInt())) { + _arg0 = PlaybackStateCompat.CREATOR.createFromParcel(data); + } else { + _arg0 = null; + } + this.onPlaybackStateChanged(_arg0); + return true; + } + case TRANSACTION_onMetadataChanged: + { + data.enforceInterface(descriptor); + MediaMetadataCompat _arg0; + if ((0 != data.readInt())) { + _arg0 = MediaMetadataCompat.CREATOR.createFromParcel(data); + } else { + _arg0 = null; + } + this.onMetadataChanged(_arg0); + return true; + } + case TRANSACTION_onQueueChanged: + { + data.enforceInterface(descriptor); + java.util.List _arg0; + _arg0 = data.createTypedArrayList(MediaSessionCompat.QueueItem.CREATOR); + this.onQueueChanged(_arg0); + return true; + } + case TRANSACTION_onQueueTitleChanged: + { + data.enforceInterface(descriptor); + CharSequence _arg0; + if (0 != data.readInt()) { + _arg0 = android.text.TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(data); + } else { + _arg0 = null; + } + this.onQueueTitleChanged(_arg0); + return true; + } + case TRANSACTION_onExtrasChanged: + { + data.enforceInterface(descriptor); + android.os.Bundle _arg0; + if ((0 != data.readInt())) { + _arg0 = android.os.Bundle.CREATOR.createFromParcel(data); + } else { + _arg0 = null; + } + this.onExtrasChanged(_arg0); + return true; + } + case TRANSACTION_onVolumeInfoChanged: + { + data.enforceInterface(descriptor); + ParcelableVolumeInfo _arg0; + if ((0 != data.readInt())) { + _arg0 = ParcelableVolumeInfo.CREATOR.createFromParcel(data); + } else { + _arg0 = null; + } + this.onVolumeInfoChanged(_arg0); + return true; + } + case TRANSACTION_onRepeatModeChanged: + { + data.enforceInterface(descriptor); + int _arg0; + _arg0 = data.readInt(); + this.onRepeatModeChanged(_arg0); + return true; + } + case TRANSACTION_onShuffleModeChangedRemoved: + { + data.enforceInterface(descriptor); + boolean _arg0; + _arg0 = (0 != data.readInt()); + this.onShuffleModeChangedRemoved(_arg0); + return true; + } + case TRANSACTION_onCaptioningEnabledChanged: + { + data.enforceInterface(descriptor); + boolean _arg0; + _arg0 = (0 != data.readInt()); + this.onCaptioningEnabledChanged(_arg0); + return true; + } + case TRANSACTION_onShuffleModeChanged: + { + data.enforceInterface(descriptor); + int _arg0; + _arg0 = data.readInt(); + this.onShuffleModeChanged(_arg0); + return true; + } + case TRANSACTION_onSessionReady: + { + data.enforceInterface(descriptor); + this.onSessionReady(); + return true; + } + default: + { + return super.onTransact(code, data, reply, flags); + } + } + } + + private static class Proxy implements IMediaControllerCallback { + private android.os.IBinder mRemote; + + Proxy(android.os.IBinder remote) { + mRemote = remote; + } + + @Override + public android.os.IBinder asBinder() { + return mRemote; + } + + public String getInterfaceDescriptor() { + return DESCRIPTOR; + } + + @Override + public void onEvent(@Nullable String event, @Nullable android.os.Bundle extras) + throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(event); + if ((extras != null)) { + _data.writeInt(1); + extras.writeToParcel(_data, 0); + } else { + _data.writeInt(0); + } + boolean _status = + mRemote.transact( + Stub.TRANSACTION_onEvent, _data, null, android.os.IBinder.FLAG_ONEWAY); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).onEvent(event, extras); + return; + } + } finally { + _data.recycle(); + } + } + + @Override + public void onSessionDestroyed() throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = + mRemote.transact( + Stub.TRANSACTION_onSessionDestroyed, _data, null, android.os.IBinder.FLAG_ONEWAY); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).onSessionDestroyed(); + return; + } + } finally { + _data.recycle(); + } + } + + // These callbacks are for the TransportController + + @Override + public void onPlaybackStateChanged(@Nullable PlaybackStateCompat state) + throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + if ((state != null)) { + _data.writeInt(1); + state.writeToParcel(_data, 0); + } else { + _data.writeInt(0); + } + boolean _status = + mRemote.transact( + Stub.TRANSACTION_onPlaybackStateChanged, + _data, + null, + android.os.IBinder.FLAG_ONEWAY); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).onPlaybackStateChanged(state); + return; + } + } finally { + _data.recycle(); + } + } + + @Override + public void onMetadataChanged(@Nullable MediaMetadataCompat metadata) + throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + if ((metadata != null)) { + _data.writeInt(1); + metadata.writeToParcel(_data, 0); + } else { + _data.writeInt(0); + } + boolean _status = + mRemote.transact( + Stub.TRANSACTION_onMetadataChanged, _data, null, android.os.IBinder.FLAG_ONEWAY); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).onMetadataChanged(metadata); + return; + } + } finally { + _data.recycle(); + } + } + + @Override + public void onQueueChanged(@Nullable java.util.List queue) + throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeTypedList(queue); + boolean _status = + mRemote.transact( + Stub.TRANSACTION_onQueueChanged, _data, null, android.os.IBinder.FLAG_ONEWAY); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).onQueueChanged(queue); + return; + } + } finally { + _data.recycle(); + } + } + + @Override + public void onQueueTitleChanged(@Nullable CharSequence title) + throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + if (title != null) { + _data.writeInt(1); + android.text.TextUtils.writeToParcel(title, _data, 0); + } else { + _data.writeInt(0); + } + boolean _status = + mRemote.transact( + Stub.TRANSACTION_onQueueTitleChanged, + _data, + null, + android.os.IBinder.FLAG_ONEWAY); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).onQueueTitleChanged(title); + return; + } + } finally { + _data.recycle(); + } + } + + @Override + public void onExtrasChanged(@Nullable android.os.Bundle extras) + throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + if ((extras != null)) { + _data.writeInt(1); + extras.writeToParcel(_data, 0); + } else { + _data.writeInt(0); + } + boolean _status = + mRemote.transact( + Stub.TRANSACTION_onExtrasChanged, _data, null, android.os.IBinder.FLAG_ONEWAY); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).onExtrasChanged(extras); + return; + } + } finally { + _data.recycle(); + } + } + + @Override + public void onVolumeInfoChanged(@Nullable ParcelableVolumeInfo info) + throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + if ((info != null)) { + _data.writeInt(1); + info.writeToParcel(_data, 0); + } else { + _data.writeInt(0); + } + boolean _status = + mRemote.transact( + Stub.TRANSACTION_onVolumeInfoChanged, + _data, + null, + android.os.IBinder.FLAG_ONEWAY); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).onVolumeInfoChanged(info); + return; + } + } finally { + _data.recycle(); + } + } + + @Override + public void onRepeatModeChanged(int repeatMode) throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeInt(repeatMode); + boolean _status = + mRemote.transact( + Stub.TRANSACTION_onRepeatModeChanged, + _data, + null, + android.os.IBinder.FLAG_ONEWAY); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).onRepeatModeChanged(repeatMode); + return; + } + } finally { + _data.recycle(); + } + } + + @Override + public void onShuffleModeChangedRemoved(boolean enabled) throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeInt(((enabled) ? (1) : (0))); + boolean _status = + mRemote.transact( + Stub.TRANSACTION_onShuffleModeChangedRemoved, + _data, + null, + android.os.IBinder.FLAG_ONEWAY); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).onShuffleModeChangedRemoved(enabled); + return; + } + } finally { + _data.recycle(); + } + } + + @Override + public void onCaptioningEnabledChanged(boolean enabled) throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeInt(((enabled) ? (1) : (0))); + boolean _status = + mRemote.transact( + Stub.TRANSACTION_onCaptioningEnabledChanged, + _data, + null, + android.os.IBinder.FLAG_ONEWAY); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).onCaptioningEnabledChanged(enabled); + return; + } + } finally { + _data.recycle(); + } + } + + @Override + public void onShuffleModeChanged(int shuffleMode) throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeInt(shuffleMode); + boolean _status = + mRemote.transact( + Stub.TRANSACTION_onShuffleModeChanged, + _data, + null, + android.os.IBinder.FLAG_ONEWAY); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).onShuffleModeChanged(shuffleMode); + return; + } + } finally { + _data.recycle(); + } + } + + @Override + public void onSessionReady() throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = + mRemote.transact( + Stub.TRANSACTION_onSessionReady, _data, null, android.os.IBinder.FLAG_ONEWAY); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).onSessionReady(); + return; + } + } finally { + _data.recycle(); + } + } + + @Nullable public static IMediaControllerCallback sDefaultImpl; + } + + static final int TRANSACTION_onEvent = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0); + static final int TRANSACTION_onSessionDestroyed = + (android.os.IBinder.FIRST_CALL_TRANSACTION + 1); + static final int TRANSACTION_onPlaybackStateChanged = + (android.os.IBinder.FIRST_CALL_TRANSACTION + 2); + static final int TRANSACTION_onMetadataChanged = + (android.os.IBinder.FIRST_CALL_TRANSACTION + 3); + static final int TRANSACTION_onQueueChanged = (android.os.IBinder.FIRST_CALL_TRANSACTION + 4); + static final int TRANSACTION_onQueueTitleChanged = + (android.os.IBinder.FIRST_CALL_TRANSACTION + 5); + static final int TRANSACTION_onExtrasChanged = (android.os.IBinder.FIRST_CALL_TRANSACTION + 6); + static final int TRANSACTION_onVolumeInfoChanged = + (android.os.IBinder.FIRST_CALL_TRANSACTION + 7); + static final int TRANSACTION_onRepeatModeChanged = + (android.os.IBinder.FIRST_CALL_TRANSACTION + 8); + static final int TRANSACTION_onShuffleModeChangedRemoved = + (android.os.IBinder.FIRST_CALL_TRANSACTION + 9); + static final int TRANSACTION_onCaptioningEnabledChanged = + (android.os.IBinder.FIRST_CALL_TRANSACTION + 10); + static final int TRANSACTION_onShuffleModeChanged = + (android.os.IBinder.FIRST_CALL_TRANSACTION + 11); + static final int TRANSACTION_onSessionReady = (android.os.IBinder.FIRST_CALL_TRANSACTION + 12); + + public static boolean setDefaultImpl(IMediaControllerCallback impl) { + // Only one user of this interface can use this function + // at a time. This is a heuristic to detect if two different + // users in the same process use this function. + if (Proxy.sDefaultImpl != null) { + throw new IllegalStateException("setDefaultImpl() called twice"); + } + if (impl != null) { + Proxy.sDefaultImpl = impl; + return true; + } + return false; + } + + @Nullable + public static IMediaControllerCallback getDefaultImpl() { + return Proxy.sDefaultImpl; + } + } + + public void onEvent(@Nullable String event, @Nullable android.os.Bundle extras) + throws android.os.RemoteException; + + public void onSessionDestroyed() throws android.os.RemoteException; + + // These callbacks are for the TransportController + + public void onPlaybackStateChanged(@Nullable PlaybackStateCompat state) + throws android.os.RemoteException; + + public void onMetadataChanged(@Nullable MediaMetadataCompat metadata) + throws android.os.RemoteException; + + public void onQueueChanged(@Nullable java.util.List queue) + throws android.os.RemoteException; + + public void onQueueTitleChanged(@Nullable CharSequence title) throws android.os.RemoteException; + + public void onExtrasChanged(@Nullable android.os.Bundle extras) throws android.os.RemoteException; + + public void onVolumeInfoChanged(@Nullable ParcelableVolumeInfo info) + throws android.os.RemoteException; + + public void onRepeatModeChanged(int repeatMode) throws android.os.RemoteException; + + public void onShuffleModeChangedRemoved(boolean enabled) throws android.os.RemoteException; + + public void onCaptioningEnabledChanged(boolean enabled) throws android.os.RemoteException; + + public void onShuffleModeChanged(int shuffleMode) throws android.os.RemoteException; + + public void onSessionReady() throws android.os.RemoteException; +} diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/IMediaSession.java b/libraries/session/src/main/java/androidx/media3/session/legacy/IMediaSession.java new file mode 100644 index 0000000000..482df5aaeb --- /dev/null +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/IMediaSession.java @@ -0,0 +1,2045 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.session.legacy; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY; +import static androidx.media3.common.util.Assertions.checkNotNull; + +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.media3.common.util.UnstableApi; +import org.checkerframework.checker.nullness.qual.PolyNull; + +/** Interface to a MediaSessionCompat. */ +@UnstableApi +@RestrictTo(LIBRARY) +public interface IMediaSession extends android.os.IInterface { + /** Local-side IPC implementation stub class. */ + public abstract static class Stub extends android.os.Binder implements IMediaSession { + private static final String DESCRIPTOR = "android.support.v4.media.session.IMediaSession"; + + /** Construct the stub at attach it to the interface. */ + // Using this in constructor + @SuppressWarnings({"method.invocation.invalid", "argument.type.incompatible"}) + public Stub() { + this.attachInterface(this, DESCRIPTOR); + } + + /** + * Cast an IBinder object into an androidx.media3.session.legacy.IMediaSession interface, + * generating a proxy if needed. + */ + public static @PolyNull IMediaSession asInterface(android.os.@PolyNull IBinder obj) { + if ((obj == null)) { + return null; + } + android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR); + if (((iin != null) && (iin instanceof IMediaSession))) { + return ((IMediaSession) iin); + } + return new Proxy(obj); + } + + @Override + public android.os.IBinder asBinder() { + return this; + } + + @Override + public boolean onTransact( + int code, android.os.Parcel data, @Nullable android.os.Parcel reply, int flags) + throws android.os.RemoteException { + String descriptor = DESCRIPTOR; + switch (code) { + case INTERFACE_TRANSACTION: + { + checkNotNull(reply).writeString(descriptor); + return true; + } + case TRANSACTION_sendCommand: + { + data.enforceInterface(descriptor); + String _arg0; + _arg0 = data.readString(); + android.os.Bundle _arg1; + if ((0 != data.readInt())) { + _arg1 = android.os.Bundle.CREATOR.createFromParcel(data); + } else { + _arg1 = null; + } + MediaSessionCompat.ResultReceiverWrapper _arg2; + if ((0 != data.readInt())) { + _arg2 = MediaSessionCompat.ResultReceiverWrapper.CREATOR.createFromParcel(data); + } else { + _arg2 = null; + } + this.sendCommand(_arg0, _arg1, _arg2); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_sendMediaButton: + { + data.enforceInterface(descriptor); + android.view.KeyEvent _arg0; + if ((0 != data.readInt())) { + _arg0 = android.view.KeyEvent.CREATOR.createFromParcel(data); + } else { + _arg0 = null; + } + boolean _result = this.sendMediaButton(_arg0); + checkNotNull(reply).writeNoException(); + checkNotNull(reply).writeInt(((_result) ? (1) : (0))); + return true; + } + case TRANSACTION_registerCallbackListener: + { + data.enforceInterface(descriptor); + IMediaControllerCallback _arg0; + _arg0 = IMediaControllerCallback.Stub.asInterface(data.readStrongBinder()); + this.registerCallbackListener(_arg0); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_unregisterCallbackListener: + { + data.enforceInterface(descriptor); + IMediaControllerCallback _arg0; + _arg0 = IMediaControllerCallback.Stub.asInterface(data.readStrongBinder()); + this.unregisterCallbackListener(_arg0); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_isTransportControlEnabled: + { + data.enforceInterface(descriptor); + boolean _result = this.isTransportControlEnabled(); + checkNotNull(reply).writeNoException(); + checkNotNull(reply).writeInt(((_result) ? (1) : (0))); + return true; + } + case TRANSACTION_getPackageName: + { + data.enforceInterface(descriptor); + String _result = this.getPackageName(); + checkNotNull(reply).writeNoException(); + checkNotNull(reply).writeString(_result); + return true; + } + case TRANSACTION_getTag: + { + data.enforceInterface(descriptor); + String _result = this.getTag(); + checkNotNull(reply).writeNoException(); + checkNotNull(reply).writeString(_result); + return true; + } + case TRANSACTION_getLaunchPendingIntent: + { + data.enforceInterface(descriptor); + android.app.PendingIntent _result = this.getLaunchPendingIntent(); + checkNotNull(reply).writeNoException(); + if ((_result != null)) { + checkNotNull(reply).writeInt(1); + _result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE); + } else { + checkNotNull(reply).writeInt(0); + } + return true; + } + case TRANSACTION_getFlags: + { + data.enforceInterface(descriptor); + long _result = this.getFlags(); + checkNotNull(reply).writeNoException(); + checkNotNull(reply).writeLong(_result); + return true; + } + case TRANSACTION_getVolumeAttributes: + { + data.enforceInterface(descriptor); + ParcelableVolumeInfo _result = this.getVolumeAttributes(); + checkNotNull(reply).writeNoException(); + if ((_result != null)) { + checkNotNull(reply).writeInt(1); + _result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE); + } else { + checkNotNull(reply).writeInt(0); + } + return true; + } + case TRANSACTION_adjustVolume: + { + data.enforceInterface(descriptor); + int _arg0; + _arg0 = data.readInt(); + int _arg1; + _arg1 = data.readInt(); + String _arg2; + _arg2 = data.readString(); + this.adjustVolume(_arg0, _arg1, _arg2); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_setVolumeTo: + { + data.enforceInterface(descriptor); + int _arg0; + _arg0 = data.readInt(); + int _arg1; + _arg1 = data.readInt(); + String _arg2; + _arg2 = data.readString(); + this.setVolumeTo(_arg0, _arg1, _arg2); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_getMetadata: + { + data.enforceInterface(descriptor); + MediaMetadataCompat _result = this.getMetadata(); + checkNotNull(reply).writeNoException(); + if ((_result != null)) { + checkNotNull(reply).writeInt(1); + _result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE); + } else { + checkNotNull(reply).writeInt(0); + } + return true; + } + case TRANSACTION_getPlaybackState: + { + data.enforceInterface(descriptor); + PlaybackStateCompat _result = this.getPlaybackState(); + checkNotNull(reply).writeNoException(); + if ((_result != null)) { + checkNotNull(reply).writeInt(1); + _result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE); + } else { + checkNotNull(reply).writeInt(0); + } + return true; + } + case TRANSACTION_getQueue: + { + data.enforceInterface(descriptor); + java.util.List _result = this.getQueue(); + checkNotNull(reply).writeNoException(); + checkNotNull(reply).writeTypedList(_result); + return true; + } + case TRANSACTION_getQueueTitle: + { + data.enforceInterface(descriptor); + CharSequence _result = this.getQueueTitle(); + checkNotNull(reply).writeNoException(); + if (_result != null) { + checkNotNull(reply).writeInt(1); + android.text.TextUtils.writeToParcel( + _result, reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE); + } else { + checkNotNull(reply).writeInt(0); + } + return true; + } + case TRANSACTION_getExtras: + { + data.enforceInterface(descriptor); + android.os.Bundle _result = this.getExtras(); + checkNotNull(reply).writeNoException(); + if ((_result != null)) { + checkNotNull(reply).writeInt(1); + _result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE); + } else { + checkNotNull(reply).writeInt(0); + } + return true; + } + case TRANSACTION_getRatingType: + { + data.enforceInterface(descriptor); + int _result = this.getRatingType(); + checkNotNull(reply).writeNoException(); + checkNotNull(reply).writeInt(_result); + return true; + } + case TRANSACTION_isCaptioningEnabled: + { + data.enforceInterface(descriptor); + boolean _result = this.isCaptioningEnabled(); + checkNotNull(reply).writeNoException(); + checkNotNull(reply).writeInt(((_result) ? (1) : (0))); + return true; + } + case TRANSACTION_getRepeatMode: + { + data.enforceInterface(descriptor); + int _result = this.getRepeatMode(); + checkNotNull(reply).writeNoException(); + checkNotNull(reply).writeInt(_result); + return true; + } + case TRANSACTION_isShuffleModeEnabledRemoved: + { + data.enforceInterface(descriptor); + boolean _result = this.isShuffleModeEnabledRemoved(); + checkNotNull(reply).writeNoException(); + checkNotNull(reply).writeInt(((_result) ? (1) : (0))); + return true; + } + case TRANSACTION_getShuffleMode: + { + data.enforceInterface(descriptor); + int _result = this.getShuffleMode(); + checkNotNull(reply).writeNoException(); + checkNotNull(reply).writeInt(_result); + return true; + } + case TRANSACTION_addQueueItem: + { + data.enforceInterface(descriptor); + MediaDescriptionCompat _arg0; + if ((0 != data.readInt())) { + _arg0 = MediaDescriptionCompat.CREATOR.createFromParcel(data); + } else { + _arg0 = null; + } + this.addQueueItem(_arg0); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_addQueueItemAt: + { + data.enforceInterface(descriptor); + MediaDescriptionCompat _arg0; + if ((0 != data.readInt())) { + _arg0 = MediaDescriptionCompat.CREATOR.createFromParcel(data); + } else { + _arg0 = null; + } + int _arg1; + _arg1 = data.readInt(); + this.addQueueItemAt(_arg0, _arg1); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_removeQueueItem: + { + data.enforceInterface(descriptor); + MediaDescriptionCompat _arg0; + if ((0 != data.readInt())) { + _arg0 = MediaDescriptionCompat.CREATOR.createFromParcel(data); + } else { + _arg0 = null; + } + this.removeQueueItem(_arg0); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_removeQueueItemAt: + { + data.enforceInterface(descriptor); + int _arg0; + _arg0 = data.readInt(); + this.removeQueueItemAt(_arg0); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_getSessionInfo: + { + data.enforceInterface(descriptor); + android.os.Bundle _result = this.getSessionInfo(); + checkNotNull(reply).writeNoException(); + if ((_result != null)) { + checkNotNull(reply).writeInt(1); + _result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE); + } else { + checkNotNull(reply).writeInt(0); + } + return true; + } + case TRANSACTION_prepare: + { + data.enforceInterface(descriptor); + this.prepare(); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_prepareFromMediaId: + { + data.enforceInterface(descriptor); + String _arg0; + _arg0 = data.readString(); + android.os.Bundle _arg1; + if ((0 != data.readInt())) { + _arg1 = android.os.Bundle.CREATOR.createFromParcel(data); + } else { + _arg1 = null; + } + this.prepareFromMediaId(_arg0, _arg1); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_prepareFromSearch: + { + data.enforceInterface(descriptor); + String _arg0; + _arg0 = data.readString(); + android.os.Bundle _arg1; + if ((0 != data.readInt())) { + _arg1 = android.os.Bundle.CREATOR.createFromParcel(data); + } else { + _arg1 = null; + } + this.prepareFromSearch(_arg0, _arg1); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_prepareFromUri: + { + data.enforceInterface(descriptor); + android.net.Uri _arg0; + if ((0 != data.readInt())) { + _arg0 = android.net.Uri.CREATOR.createFromParcel(data); + } else { + _arg0 = null; + } + android.os.Bundle _arg1; + if ((0 != data.readInt())) { + _arg1 = android.os.Bundle.CREATOR.createFromParcel(data); + } else { + _arg1 = null; + } + this.prepareFromUri(_arg0, _arg1); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_play: + { + data.enforceInterface(descriptor); + this.play(); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_playFromMediaId: + { + data.enforceInterface(descriptor); + String _arg0; + _arg0 = data.readString(); + android.os.Bundle _arg1; + if ((0 != data.readInt())) { + _arg1 = android.os.Bundle.CREATOR.createFromParcel(data); + } else { + _arg1 = null; + } + this.playFromMediaId(_arg0, _arg1); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_playFromSearch: + { + data.enforceInterface(descriptor); + String _arg0; + _arg0 = data.readString(); + android.os.Bundle _arg1; + if ((0 != data.readInt())) { + _arg1 = android.os.Bundle.CREATOR.createFromParcel(data); + } else { + _arg1 = null; + } + this.playFromSearch(_arg0, _arg1); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_playFromUri: + { + data.enforceInterface(descriptor); + android.net.Uri _arg0; + if ((0 != data.readInt())) { + _arg0 = android.net.Uri.CREATOR.createFromParcel(data); + } else { + _arg0 = null; + } + android.os.Bundle _arg1; + if ((0 != data.readInt())) { + _arg1 = android.os.Bundle.CREATOR.createFromParcel(data); + } else { + _arg1 = null; + } + this.playFromUri(_arg0, _arg1); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_skipToQueueItem: + { + data.enforceInterface(descriptor); + long _arg0; + _arg0 = data.readLong(); + this.skipToQueueItem(_arg0); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_pause: + { + data.enforceInterface(descriptor); + this.pause(); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_stop: + { + data.enforceInterface(descriptor); + this.stop(); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_next: + { + data.enforceInterface(descriptor); + this.next(); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_previous: + { + data.enforceInterface(descriptor); + this.previous(); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_fastForward: + { + data.enforceInterface(descriptor); + this.fastForward(); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_rewind: + { + data.enforceInterface(descriptor); + this.rewind(); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_seekTo: + { + data.enforceInterface(descriptor); + long _arg0; + _arg0 = data.readLong(); + this.seekTo(_arg0); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_rate: + { + data.enforceInterface(descriptor); + RatingCompat _arg0; + if ((0 != data.readInt())) { + _arg0 = RatingCompat.CREATOR.createFromParcel(data); + } else { + _arg0 = null; + } + this.rate(_arg0); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_rateWithExtras: + { + data.enforceInterface(descriptor); + RatingCompat _arg0; + if ((0 != data.readInt())) { + _arg0 = RatingCompat.CREATOR.createFromParcel(data); + } else { + _arg0 = null; + } + android.os.Bundle _arg1; + if ((0 != data.readInt())) { + _arg1 = android.os.Bundle.CREATOR.createFromParcel(data); + } else { + _arg1 = null; + } + this.rateWithExtras(_arg0, _arg1); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_setPlaybackSpeed: + { + data.enforceInterface(descriptor); + float _arg0; + _arg0 = data.readFloat(); + this.setPlaybackSpeed(_arg0); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_setCaptioningEnabled: + { + data.enforceInterface(descriptor); + boolean _arg0; + _arg0 = (0 != data.readInt()); + this.setCaptioningEnabled(_arg0); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_setRepeatMode: + { + data.enforceInterface(descriptor); + int _arg0; + _arg0 = data.readInt(); + this.setRepeatMode(_arg0); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_setShuffleModeEnabledRemoved: + { + data.enforceInterface(descriptor); + boolean _arg0; + _arg0 = (0 != data.readInt()); + this.setShuffleModeEnabledRemoved(_arg0); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_setShuffleMode: + { + data.enforceInterface(descriptor); + int _arg0; + _arg0 = data.readInt(); + this.setShuffleMode(_arg0); + checkNotNull(reply).writeNoException(); + return true; + } + case TRANSACTION_sendCustomAction: + { + data.enforceInterface(descriptor); + String _arg0; + _arg0 = data.readString(); + android.os.Bundle _arg1; + if ((0 != data.readInt())) { + _arg1 = android.os.Bundle.CREATOR.createFromParcel(data); + } else { + _arg1 = null; + } + this.sendCustomAction(_arg0, _arg1); + checkNotNull(reply).writeNoException(); + return true; + } + default: + { + return super.onTransact(code, data, reply, flags); + } + } + } + + private static class Proxy implements IMediaSession { + private android.os.IBinder mRemote; + + Proxy(android.os.IBinder remote) { + mRemote = remote; + } + + @Override + public android.os.IBinder asBinder() { + return mRemote; + } + + public String getInterfaceDescriptor() { + return DESCRIPTOR; + } + + // Next ID: 50 + + @Override + public void sendCommand( + @Nullable String command, + @Nullable android.os.Bundle args, + @Nullable MediaSessionCompat.ResultReceiverWrapper cb) + throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(command); + if ((args != null)) { + _data.writeInt(1); + args.writeToParcel(_data, 0); + } else { + _data.writeInt(0); + } + if ((cb != null)) { + _data.writeInt(1); + cb.writeToParcel(_data, 0); + } else { + _data.writeInt(0); + } + boolean _status = mRemote.transact(Stub.TRANSACTION_sendCommand, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).sendCommand(command, args, cb); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Override + public boolean sendMediaButton(@Nullable android.view.KeyEvent mediaButton) + throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + boolean _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + if ((mediaButton != null)) { + _data.writeInt(1); + mediaButton.writeToParcel(_data, 0); + } else { + _data.writeInt(0); + } + boolean _status = mRemote.transact(Stub.TRANSACTION_sendMediaButton, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + return checkNotNull(getDefaultImpl()).sendMediaButton(mediaButton); + } + _reply.readException(); + _result = (0 != _reply.readInt()); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + @SuppressWarnings("argument.type.incompatible") // writeStrongBinder not annotated correctly + @Override + public void registerCallbackListener(@Nullable IMediaControllerCallback cb) + throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeStrongBinder((((cb != null)) ? (cb.asBinder()) : (null))); + boolean _status = + mRemote.transact(Stub.TRANSACTION_registerCallbackListener, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).registerCallbackListener(cb); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @SuppressWarnings("argument.type.incompatible") // writeStrongBinder not annotated correctly + @Override + public void unregisterCallbackListener(@Nullable IMediaControllerCallback cb) + throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeStrongBinder((((cb != null)) ? (cb.asBinder()) : (null))); + boolean _status = + mRemote.transact(Stub.TRANSACTION_unregisterCallbackListener, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).unregisterCallbackListener(cb); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Override + public boolean isTransportControlEnabled() throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + boolean _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = + mRemote.transact(Stub.TRANSACTION_isTransportControlEnabled, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + return checkNotNull(getDefaultImpl()).isTransportControlEnabled(); + } + _reply.readException(); + _result = (0 != _reply.readInt()); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + @Nullable + @Override + public String getPackageName() throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + String _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_getPackageName, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + return checkNotNull(getDefaultImpl()).getPackageName(); + } + _reply.readException(); + _result = _reply.readString(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + @Nullable + @Override + public String getTag() throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + String _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_getTag, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + return checkNotNull(getDefaultImpl()).getTag(); + } + _reply.readException(); + _result = _reply.readString(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + @Nullable + @Override + public android.app.PendingIntent getLaunchPendingIntent() throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + android.app.PendingIntent _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = + mRemote.transact(Stub.TRANSACTION_getLaunchPendingIntent, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + return checkNotNull(getDefaultImpl()).getLaunchPendingIntent(); + } + _reply.readException(); + if ((0 != _reply.readInt())) { + _result = android.app.PendingIntent.CREATOR.createFromParcel(_reply); + } else { + _result = null; + } + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + @Override + public long getFlags() throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + long _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_getFlags, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + return checkNotNull(getDefaultImpl()).getFlags(); + } + _reply.readException(); + _result = _reply.readLong(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + @Nullable + @Override + public ParcelableVolumeInfo getVolumeAttributes() throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + ParcelableVolumeInfo _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = + mRemote.transact(Stub.TRANSACTION_getVolumeAttributes, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + return checkNotNull(getDefaultImpl()).getVolumeAttributes(); + } + _reply.readException(); + if ((0 != _reply.readInt())) { + _result = ParcelableVolumeInfo.CREATOR.createFromParcel(_reply); + } else { + _result = null; + } + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + @Override + public void adjustVolume(int direction, int flags, @Nullable String packageName) + throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeInt(direction); + _data.writeInt(flags); + _data.writeString(packageName); + boolean _status = mRemote.transact(Stub.TRANSACTION_adjustVolume, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).adjustVolume(direction, flags, packageName); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Override + public void setVolumeTo(int value, int flags, @Nullable String packageName) + throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeInt(value); + _data.writeInt(flags); + _data.writeString(packageName); + boolean _status = mRemote.transact(Stub.TRANSACTION_setVolumeTo, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).setVolumeTo(value, flags, packageName); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Nullable + @Override + public MediaMetadataCompat getMetadata() throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + MediaMetadataCompat _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_getMetadata, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + return checkNotNull(getDefaultImpl()).getMetadata(); + } + _reply.readException(); + if ((0 != _reply.readInt())) { + _result = MediaMetadataCompat.CREATOR.createFromParcel(_reply); + } else { + _result = null; + } + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + @Nullable + @Override + public PlaybackStateCompat getPlaybackState() throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + PlaybackStateCompat _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_getPlaybackState, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + return checkNotNull(getDefaultImpl()).getPlaybackState(); + } + _reply.readException(); + if ((0 != _reply.readInt())) { + _result = PlaybackStateCompat.CREATOR.createFromParcel(_reply); + } else { + _result = null; + } + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + @Nullable + @Override + public java.util.List getQueue() + throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + java.util.List _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_getQueue, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + return checkNotNull(getDefaultImpl()).getQueue(); + } + _reply.readException(); + _result = _reply.createTypedArrayList(MediaSessionCompat.QueueItem.CREATOR); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + @Nullable + @Override + public CharSequence getQueueTitle() throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + CharSequence _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_getQueueTitle, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + return checkNotNull(getDefaultImpl()).getQueueTitle(); + } + _reply.readException(); + if (0 != _reply.readInt()) { + _result = android.text.TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(_reply); + } else { + _result = null; + } + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + @Nullable + @Override + public android.os.Bundle getExtras() throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + android.os.Bundle _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_getExtras, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + return checkNotNull(getDefaultImpl()).getExtras(); + } + _reply.readException(); + if ((0 != _reply.readInt())) { + _result = android.os.Bundle.CREATOR.createFromParcel(_reply); + } else { + _result = null; + } + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + @Override + public int getRatingType() throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + int _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_getRatingType, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + return checkNotNull(getDefaultImpl()).getRatingType(); + } + _reply.readException(); + _result = _reply.readInt(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + @Override + public boolean isCaptioningEnabled() throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + boolean _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = + mRemote.transact(Stub.TRANSACTION_isCaptioningEnabled, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + return checkNotNull(getDefaultImpl()).isCaptioningEnabled(); + } + _reply.readException(); + _result = (0 != _reply.readInt()); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + @Override + public int getRepeatMode() throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + int _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_getRepeatMode, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + return checkNotNull(getDefaultImpl()).getRepeatMode(); + } + _reply.readException(); + _result = _reply.readInt(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + @Override + public boolean isShuffleModeEnabledRemoved() throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + boolean _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = + mRemote.transact(Stub.TRANSACTION_isShuffleModeEnabledRemoved, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + return checkNotNull(getDefaultImpl()).isShuffleModeEnabledRemoved(); + } + _reply.readException(); + _result = (0 != _reply.readInt()); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + @Override + public int getShuffleMode() throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + int _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_getShuffleMode, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + return checkNotNull(getDefaultImpl()).getShuffleMode(); + } + _reply.readException(); + _result = _reply.readInt(); + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + @Override + public void addQueueItem(@Nullable MediaDescriptionCompat description) + throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + if ((description != null)) { + _data.writeInt(1); + description.writeToParcel(_data, 0); + } else { + _data.writeInt(0); + } + boolean _status = mRemote.transact(Stub.TRANSACTION_addQueueItem, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).addQueueItem(description); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Override + public void addQueueItemAt(@Nullable MediaDescriptionCompat description, int index) + throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + if ((description != null)) { + _data.writeInt(1); + description.writeToParcel(_data, 0); + } else { + _data.writeInt(0); + } + _data.writeInt(index); + boolean _status = mRemote.transact(Stub.TRANSACTION_addQueueItemAt, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).addQueueItemAt(description, index); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Override + public void removeQueueItem(@Nullable MediaDescriptionCompat description) + throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + if ((description != null)) { + _data.writeInt(1); + description.writeToParcel(_data, 0); + } else { + _data.writeInt(0); + } + boolean _status = mRemote.transact(Stub.TRANSACTION_removeQueueItem, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).removeQueueItem(description); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Override + public void removeQueueItemAt(int index) throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeInt(index); + boolean _status = mRemote.transact(Stub.TRANSACTION_removeQueueItemAt, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).removeQueueItemAt(index); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Nullable + @Override + public android.os.Bundle getSessionInfo() throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + android.os.Bundle _result; + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_getSessionInfo, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + return checkNotNull(getDefaultImpl()).getSessionInfo(); + } + _reply.readException(); + if ((0 != _reply.readInt())) { + _result = android.os.Bundle.CREATOR.createFromParcel(_reply); + } else { + _result = null; + } + } finally { + _reply.recycle(); + _data.recycle(); + } + return _result; + } + + // These commands are for the TransportControls + + @Override + public void prepare() throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_prepare, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).prepare(); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Override + public void prepareFromMediaId(@Nullable String uri, @Nullable android.os.Bundle extras) + throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(uri); + if ((extras != null)) { + _data.writeInt(1); + extras.writeToParcel(_data, 0); + } else { + _data.writeInt(0); + } + boolean _status = mRemote.transact(Stub.TRANSACTION_prepareFromMediaId, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).prepareFromMediaId(uri, extras); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Override + public void prepareFromSearch(@Nullable String string, @Nullable android.os.Bundle extras) + throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(string); + if ((extras != null)) { + _data.writeInt(1); + extras.writeToParcel(_data, 0); + } else { + _data.writeInt(0); + } + boolean _status = mRemote.transact(Stub.TRANSACTION_prepareFromSearch, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).prepareFromSearch(string, extras); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Override + public void prepareFromUri(@Nullable android.net.Uri uri, @Nullable android.os.Bundle extras) + throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + if ((uri != null)) { + _data.writeInt(1); + uri.writeToParcel(_data, 0); + } else { + _data.writeInt(0); + } + if ((extras != null)) { + _data.writeInt(1); + extras.writeToParcel(_data, 0); + } else { + _data.writeInt(0); + } + boolean _status = mRemote.transact(Stub.TRANSACTION_prepareFromUri, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).prepareFromUri(uri, extras); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Override + public void play() throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_play, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).play(); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Override + public void playFromMediaId(@Nullable String uri, @Nullable android.os.Bundle extras) + throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(uri); + if ((extras != null)) { + _data.writeInt(1); + extras.writeToParcel(_data, 0); + } else { + _data.writeInt(0); + } + boolean _status = mRemote.transact(Stub.TRANSACTION_playFromMediaId, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).playFromMediaId(uri, extras); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Override + public void playFromSearch(@Nullable String string, @Nullable android.os.Bundle extras) + throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(string); + if ((extras != null)) { + _data.writeInt(1); + extras.writeToParcel(_data, 0); + } else { + _data.writeInt(0); + } + boolean _status = mRemote.transact(Stub.TRANSACTION_playFromSearch, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).playFromSearch(string, extras); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Override + public void playFromUri(@Nullable android.net.Uri uri, @Nullable android.os.Bundle extras) + throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + if ((uri != null)) { + _data.writeInt(1); + uri.writeToParcel(_data, 0); + } else { + _data.writeInt(0); + } + if ((extras != null)) { + _data.writeInt(1); + extras.writeToParcel(_data, 0); + } else { + _data.writeInt(0); + } + boolean _status = mRemote.transact(Stub.TRANSACTION_playFromUri, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).playFromUri(uri, extras); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Override + public void skipToQueueItem(long id) throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeLong(id); + boolean _status = mRemote.transact(Stub.TRANSACTION_skipToQueueItem, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).skipToQueueItem(id); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Override + public void pause() throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_pause, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).pause(); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Override + public void stop() throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_stop, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).stop(); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Override + public void next() throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_next, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).next(); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Override + public void previous() throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_previous, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).previous(); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Override + public void fastForward() throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_fastForward, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).fastForward(); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Override + public void rewind() throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + boolean _status = mRemote.transact(Stub.TRANSACTION_rewind, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).rewind(); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Override + public void seekTo(long pos) throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeLong(pos); + boolean _status = mRemote.transact(Stub.TRANSACTION_seekTo, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).seekTo(pos); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Override + public void rate(@Nullable RatingCompat rating) throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + if ((rating != null)) { + _data.writeInt(1); + rating.writeToParcel(_data, 0); + } else { + _data.writeInt(0); + } + boolean _status = mRemote.transact(Stub.TRANSACTION_rate, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).rate(rating); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Override + public void rateWithExtras(@Nullable RatingCompat rating, @Nullable android.os.Bundle extras) + throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + if ((rating != null)) { + _data.writeInt(1); + rating.writeToParcel(_data, 0); + } else { + _data.writeInt(0); + } + if ((extras != null)) { + _data.writeInt(1); + extras.writeToParcel(_data, 0); + } else { + _data.writeInt(0); + } + boolean _status = mRemote.transact(Stub.TRANSACTION_rateWithExtras, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).rateWithExtras(rating, extras); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Override + public void setPlaybackSpeed(float speed) throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeFloat(speed); + boolean _status = mRemote.transact(Stub.TRANSACTION_setPlaybackSpeed, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).setPlaybackSpeed(speed); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Override + public void setCaptioningEnabled(boolean enabled) throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeInt(((enabled) ? (1) : (0))); + boolean _status = + mRemote.transact(Stub.TRANSACTION_setCaptioningEnabled, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).setCaptioningEnabled(enabled); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Override + public void setRepeatMode(int repeatMode) throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeInt(repeatMode); + boolean _status = mRemote.transact(Stub.TRANSACTION_setRepeatMode, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).setRepeatMode(repeatMode); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Override + public void setShuffleModeEnabledRemoved(boolean shuffleMode) + throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeInt(((shuffleMode) ? (1) : (0))); + boolean _status = + mRemote.transact(Stub.TRANSACTION_setShuffleModeEnabledRemoved, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).setShuffleModeEnabledRemoved(shuffleMode); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Override + public void setShuffleMode(int shuffleMode) throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeInt(shuffleMode); + boolean _status = mRemote.transact(Stub.TRANSACTION_setShuffleMode, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).setShuffleMode(shuffleMode); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Override + public void sendCustomAction(@Nullable String action, @Nullable android.os.Bundle args) + throws android.os.RemoteException { + android.os.Parcel _data = android.os.Parcel.obtain(); + android.os.Parcel _reply = android.os.Parcel.obtain(); + try { + _data.writeInterfaceToken(DESCRIPTOR); + _data.writeString(action); + if ((args != null)) { + _data.writeInt(1); + args.writeToParcel(_data, 0); + } else { + _data.writeInt(0); + } + boolean _status = mRemote.transact(Stub.TRANSACTION_sendCustomAction, _data, _reply, 0); + if (!_status && getDefaultImpl() != null) { + checkNotNull(getDefaultImpl()).sendCustomAction(action, args); + return; + } + _reply.readException(); + } finally { + _reply.recycle(); + _data.recycle(); + } + } + + @Nullable public static IMediaSession sDefaultImpl; + } + + static final int TRANSACTION_sendCommand = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0); + static final int TRANSACTION_sendMediaButton = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1); + static final int TRANSACTION_registerCallbackListener = + (android.os.IBinder.FIRST_CALL_TRANSACTION + 2); + static final int TRANSACTION_unregisterCallbackListener = + (android.os.IBinder.FIRST_CALL_TRANSACTION + 3); + static final int TRANSACTION_isTransportControlEnabled = + (android.os.IBinder.FIRST_CALL_TRANSACTION + 4); + static final int TRANSACTION_getPackageName = (android.os.IBinder.FIRST_CALL_TRANSACTION + 5); + static final int TRANSACTION_getTag = (android.os.IBinder.FIRST_CALL_TRANSACTION + 6); + static final int TRANSACTION_getLaunchPendingIntent = + (android.os.IBinder.FIRST_CALL_TRANSACTION + 7); + static final int TRANSACTION_getFlags = (android.os.IBinder.FIRST_CALL_TRANSACTION + 8); + static final int TRANSACTION_getVolumeAttributes = + (android.os.IBinder.FIRST_CALL_TRANSACTION + 9); + static final int TRANSACTION_adjustVolume = (android.os.IBinder.FIRST_CALL_TRANSACTION + 10); + static final int TRANSACTION_setVolumeTo = (android.os.IBinder.FIRST_CALL_TRANSACTION + 11); + static final int TRANSACTION_getMetadata = (android.os.IBinder.FIRST_CALL_TRANSACTION + 26); + static final int TRANSACTION_getPlaybackState = + (android.os.IBinder.FIRST_CALL_TRANSACTION + 27); + static final int TRANSACTION_getQueue = (android.os.IBinder.FIRST_CALL_TRANSACTION + 28); + static final int TRANSACTION_getQueueTitle = (android.os.IBinder.FIRST_CALL_TRANSACTION + 29); + static final int TRANSACTION_getExtras = (android.os.IBinder.FIRST_CALL_TRANSACTION + 30); + static final int TRANSACTION_getRatingType = (android.os.IBinder.FIRST_CALL_TRANSACTION + 31); + static final int TRANSACTION_isCaptioningEnabled = + (android.os.IBinder.FIRST_CALL_TRANSACTION + 44); + static final int TRANSACTION_getRepeatMode = (android.os.IBinder.FIRST_CALL_TRANSACTION + 36); + static final int TRANSACTION_isShuffleModeEnabledRemoved = + (android.os.IBinder.FIRST_CALL_TRANSACTION + 37); + static final int TRANSACTION_getShuffleMode = (android.os.IBinder.FIRST_CALL_TRANSACTION + 46); + static final int TRANSACTION_addQueueItem = (android.os.IBinder.FIRST_CALL_TRANSACTION + 40); + static final int TRANSACTION_addQueueItemAt = (android.os.IBinder.FIRST_CALL_TRANSACTION + 41); + static final int TRANSACTION_removeQueueItem = (android.os.IBinder.FIRST_CALL_TRANSACTION + 42); + static final int TRANSACTION_removeQueueItemAt = + (android.os.IBinder.FIRST_CALL_TRANSACTION + 43); + static final int TRANSACTION_getSessionInfo = (android.os.IBinder.FIRST_CALL_TRANSACTION + 49); + static final int TRANSACTION_prepare = (android.os.IBinder.FIRST_CALL_TRANSACTION + 32); + static final int TRANSACTION_prepareFromMediaId = + (android.os.IBinder.FIRST_CALL_TRANSACTION + 33); + static final int TRANSACTION_prepareFromSearch = + (android.os.IBinder.FIRST_CALL_TRANSACTION + 34); + static final int TRANSACTION_prepareFromUri = (android.os.IBinder.FIRST_CALL_TRANSACTION + 35); + static final int TRANSACTION_play = (android.os.IBinder.FIRST_CALL_TRANSACTION + 12); + static final int TRANSACTION_playFromMediaId = (android.os.IBinder.FIRST_CALL_TRANSACTION + 13); + static final int TRANSACTION_playFromSearch = (android.os.IBinder.FIRST_CALL_TRANSACTION + 14); + static final int TRANSACTION_playFromUri = (android.os.IBinder.FIRST_CALL_TRANSACTION + 15); + static final int TRANSACTION_skipToQueueItem = (android.os.IBinder.FIRST_CALL_TRANSACTION + 16); + static final int TRANSACTION_pause = (android.os.IBinder.FIRST_CALL_TRANSACTION + 17); + static final int TRANSACTION_stop = (android.os.IBinder.FIRST_CALL_TRANSACTION + 18); + static final int TRANSACTION_next = (android.os.IBinder.FIRST_CALL_TRANSACTION + 19); + static final int TRANSACTION_previous = (android.os.IBinder.FIRST_CALL_TRANSACTION + 20); + static final int TRANSACTION_fastForward = (android.os.IBinder.FIRST_CALL_TRANSACTION + 21); + static final int TRANSACTION_rewind = (android.os.IBinder.FIRST_CALL_TRANSACTION + 22); + static final int TRANSACTION_seekTo = (android.os.IBinder.FIRST_CALL_TRANSACTION + 23); + static final int TRANSACTION_rate = (android.os.IBinder.FIRST_CALL_TRANSACTION + 24); + static final int TRANSACTION_rateWithExtras = (android.os.IBinder.FIRST_CALL_TRANSACTION + 50); + static final int TRANSACTION_setPlaybackSpeed = + (android.os.IBinder.FIRST_CALL_TRANSACTION + 48); + static final int TRANSACTION_setCaptioningEnabled = + (android.os.IBinder.FIRST_CALL_TRANSACTION + 45); + static final int TRANSACTION_setRepeatMode = (android.os.IBinder.FIRST_CALL_TRANSACTION + 38); + static final int TRANSACTION_setShuffleModeEnabledRemoved = + (android.os.IBinder.FIRST_CALL_TRANSACTION + 39); + static final int TRANSACTION_setShuffleMode = (android.os.IBinder.FIRST_CALL_TRANSACTION + 47); + static final int TRANSACTION_sendCustomAction = + (android.os.IBinder.FIRST_CALL_TRANSACTION + 25); + + public static boolean setDefaultImpl(IMediaSession impl) { + // Only one user of this interface can use this function + // at a time. This is a heuristic to detect if two different + // users in the same process use this function. + if (Proxy.sDefaultImpl != null) { + throw new IllegalStateException("setDefaultImpl() called twice"); + } + if (impl != null) { + Proxy.sDefaultImpl = impl; + return true; + } + return false; + } + + @Nullable + public static IMediaSession getDefaultImpl() { + return Proxy.sDefaultImpl; + } + } + + // Next ID: 50 + + public void sendCommand( + @Nullable String command, + @Nullable android.os.Bundle args, + @Nullable MediaSessionCompat.ResultReceiverWrapper cb) + throws android.os.RemoteException; + + public boolean sendMediaButton(@Nullable android.view.KeyEvent mediaButton) + throws android.os.RemoteException; + + public void registerCallbackListener(@Nullable IMediaControllerCallback cb) + throws android.os.RemoteException; + + public void unregisterCallbackListener(@Nullable IMediaControllerCallback cb) + throws android.os.RemoteException; + + public boolean isTransportControlEnabled() throws android.os.RemoteException; + + @Nullable + public String getPackageName() throws android.os.RemoteException; + + @Nullable + public String getTag() throws android.os.RemoteException; + + @Nullable + public android.app.PendingIntent getLaunchPendingIntent() throws android.os.RemoteException; + + public long getFlags() throws android.os.RemoteException; + + @Nullable + public ParcelableVolumeInfo getVolumeAttributes() throws android.os.RemoteException; + + public void adjustVolume(int direction, int flags, @Nullable String packageName) + throws android.os.RemoteException; + + public void setVolumeTo(int value, int flags, @Nullable String packageName) + throws android.os.RemoteException; + + @Nullable + public MediaMetadataCompat getMetadata() throws android.os.RemoteException; + + @Nullable + public PlaybackStateCompat getPlaybackState() throws android.os.RemoteException; + + @Nullable + public java.util.List getQueue() throws android.os.RemoteException; + + @Nullable + public CharSequence getQueueTitle() throws android.os.RemoteException; + + @Nullable + public android.os.Bundle getExtras() throws android.os.RemoteException; + + public int getRatingType() throws android.os.RemoteException; + + public boolean isCaptioningEnabled() throws android.os.RemoteException; + + public int getRepeatMode() throws android.os.RemoteException; + + public boolean isShuffleModeEnabledRemoved() throws android.os.RemoteException; + + public int getShuffleMode() throws android.os.RemoteException; + + public void addQueueItem(@Nullable MediaDescriptionCompat description) + throws android.os.RemoteException; + + public void addQueueItemAt(@Nullable MediaDescriptionCompat description, int index) + throws android.os.RemoteException; + + public void removeQueueItem(@Nullable MediaDescriptionCompat description) + throws android.os.RemoteException; + + public void removeQueueItemAt(int index) throws android.os.RemoteException; + + @Nullable + public android.os.Bundle getSessionInfo() throws android.os.RemoteException; + + // These commands are for the TransportControls + + public void prepare() throws android.os.RemoteException; + + public void prepareFromMediaId(@Nullable String uri, @Nullable android.os.Bundle extras) + throws android.os.RemoteException; + + public void prepareFromSearch(@Nullable String string, @Nullable android.os.Bundle extras) + throws android.os.RemoteException; + + public void prepareFromUri(@Nullable android.net.Uri uri, @Nullable android.os.Bundle extras) + throws android.os.RemoteException; + + public void play() throws android.os.RemoteException; + + public void playFromMediaId(@Nullable String uri, @Nullable android.os.Bundle extras) + throws android.os.RemoteException; + + public void playFromSearch(@Nullable String string, @Nullable android.os.Bundle extras) + throws android.os.RemoteException; + + public void playFromUri(@Nullable android.net.Uri uri, @Nullable android.os.Bundle extras) + throws android.os.RemoteException; + + public void skipToQueueItem(long id) throws android.os.RemoteException; + + public void pause() throws android.os.RemoteException; + + public void stop() throws android.os.RemoteException; + + public void next() throws android.os.RemoteException; + + public void previous() throws android.os.RemoteException; + + public void fastForward() throws android.os.RemoteException; + + public void rewind() throws android.os.RemoteException; + + public void seekTo(long pos) throws android.os.RemoteException; + + public void rate(@Nullable RatingCompat rating) throws android.os.RemoteException; + + public void rateWithExtras(@Nullable RatingCompat rating, @Nullable android.os.Bundle extras) + throws android.os.RemoteException; + + public void setPlaybackSpeed(float speed) throws android.os.RemoteException; + + public void setCaptioningEnabled(boolean enabled) throws android.os.RemoteException; + + public void setRepeatMode(int repeatMode) throws android.os.RemoteException; + + public void setShuffleModeEnabledRemoved(boolean shuffleMode) throws android.os.RemoteException; + + public void setShuffleMode(int shuffleMode) throws android.os.RemoteException; + + public void sendCustomAction(@Nullable String action, @Nullable android.os.Bundle args) + throws android.os.RemoteException; +} diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/LegacyParcelableUtil.java b/libraries/session/src/main/java/androidx/media3/session/legacy/LegacyParcelableUtil.java new file mode 100644 index 0000000000..ebe6eb6daa --- /dev/null +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/LegacyParcelableUtil.java @@ -0,0 +1,122 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.session.legacy; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY; + +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.RestrictTo; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; +import java.util.ArrayList; +import java.util.List; +import org.checkerframework.checker.nullness.qual.PolyNull; + +/** + * Utilities to convert {@link android.os.Parcelable} instances to and from legacy package names + * when writing to or reading them from a {@link android.os.Bundle}. + */ +@UnstableApi +@RestrictTo(LIBRARY) +public final class LegacyParcelableUtil { + + private LegacyParcelableUtil() {} + + /** + * Converts one {@link Parcelable} to another assuming they both share the same parcel structure. + * + * @param value The input {@link Parcelable}. + * @param creator The {@link Parcelable.Creator} of the output type. + * @return The output {@link Parcelable}. + * @param The output type. + * @param The input type. + */ + public static @PolyNull T convert( + @PolyNull U value, Parcelable.Creator creator) { + if (value == null) { + return null; + } + value = maybeApplyMediaDescriptionParcelableBugWorkaround(value); + Parcel parcel = Parcel.obtain(); + try { + value.writeToParcel(parcel, /* flags= */ 0); + parcel.setDataPosition(0); + T result = creator.createFromParcel(parcel); + result = maybeApplyMediaDescriptionParcelableBugWorkaround(result); + return result; + } finally { + parcel.recycle(); + } + } + + /** + * Converts one {@link Parcelable} {@link List} to another assuming they both share the same + * parcel structure. + * + * @param value The input {@link Parcelable} {@link List}. + * @param creator The {@link Parcelable.Creator} of the output type. + * @return The output {@link Parcelable} {@link ArrayList}. + * @param The output type. + * @param The input type. + */ + public static @PolyNull ArrayList convertList( + @PolyNull List value, Parcelable.Creator creator) { + if (value == null) { + return null; + } + ArrayList output = new ArrayList<>(); + for (int i = 0; i < value.size(); i++) { + output.add(convert(value.get(i), creator)); + } + return output; + } + + // TODO: b/335804969 - Remove this workaround once the bug fix is in the androidx.media dependency + @SuppressWarnings("unchecked") + private static T maybeApplyMediaDescriptionParcelableBugWorkaround(T value) { + if (Util.SDK_INT < 21 || Util.SDK_INT >= 23) { + return value; + } + if (value instanceof android.support.v4.media.MediaBrowserCompat.MediaItem) { + android.support.v4.media.MediaBrowserCompat.MediaItem mediaItem = + (android.support.v4.media.MediaBrowserCompat.MediaItem) value; + return (T) + new android.support.v4.media.MediaBrowserCompat.MediaItem( + rebuildMediaDescriptionCompat(mediaItem.getDescription()), mediaItem.getFlags()); + } else if (value instanceof android.support.v4.media.MediaDescriptionCompat) { + android.support.v4.media.MediaDescriptionCompat description = + (android.support.v4.media.MediaDescriptionCompat) value; + return (T) rebuildMediaDescriptionCompat(description); + } else { + return value; + } + } + + private static android.support.v4.media.MediaDescriptionCompat rebuildMediaDescriptionCompat( + android.support.v4.media.MediaDescriptionCompat value) { + return new android.support.v4.media.MediaDescriptionCompat.Builder() + .setMediaId(value.getMediaId()) + .setTitle(value.getTitle()) + .setSubtitle(value.getSubtitle()) + .setDescription(value.getDescription()) + .setIconBitmap(value.getIconBitmap()) + .setIconUri(value.getIconUri()) + .setExtras(value.getExtras()) + .setMediaUri(value.getMediaUri()) + .build(); + } +} diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserCompat.java b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserCompat.java new file mode 100644 index 0000000000..d46d437001 --- /dev/null +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserCompat.java @@ -0,0 +1,2484 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.session.legacy; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY; +import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.session.legacy.MediaBrowserProtocol.CLIENT_MSG_ADD_SUBSCRIPTION; +import static androidx.media3.session.legacy.MediaBrowserProtocol.CLIENT_MSG_CONNECT; +import static androidx.media3.session.legacy.MediaBrowserProtocol.CLIENT_MSG_DISCONNECT; +import static androidx.media3.session.legacy.MediaBrowserProtocol.CLIENT_MSG_GET_MEDIA_ITEM; +import static androidx.media3.session.legacy.MediaBrowserProtocol.CLIENT_MSG_REGISTER_CALLBACK_MESSENGER; +import static androidx.media3.session.legacy.MediaBrowserProtocol.CLIENT_MSG_REMOVE_SUBSCRIPTION; +import static androidx.media3.session.legacy.MediaBrowserProtocol.CLIENT_MSG_SEARCH; +import static androidx.media3.session.legacy.MediaBrowserProtocol.CLIENT_MSG_SEND_CUSTOM_ACTION; +import static androidx.media3.session.legacy.MediaBrowserProtocol.CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER; +import static androidx.media3.session.legacy.MediaBrowserProtocol.CLIENT_VERSION_CURRENT; +import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_CALLBACK_TOKEN; +import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_CALLING_PID; +import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_CUSTOM_ACTION; +import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_CUSTOM_ACTION_EXTRAS; +import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_MEDIA_ITEM_ID; +import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_MEDIA_ITEM_LIST; +import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_MEDIA_SESSION_TOKEN; +import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_NOTIFY_CHILDREN_CHANGED_OPTIONS; +import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_OPTIONS; +import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_PACKAGE_NAME; +import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_RESULT_RECEIVER; +import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_ROOT_HINTS; +import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_SEARCH_EXTRAS; +import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_SEARCH_QUERY; +import static androidx.media3.session.legacy.MediaBrowserProtocol.EXTRA_CALLING_PID; +import static androidx.media3.session.legacy.MediaBrowserProtocol.EXTRA_CLIENT_VERSION; +import static androidx.media3.session.legacy.MediaBrowserProtocol.EXTRA_MESSENGER_BINDER; +import static androidx.media3.session.legacy.MediaBrowserProtocol.EXTRA_SERVICE_VERSION; +import static androidx.media3.session.legacy.MediaBrowserProtocol.EXTRA_SESSION_BINDER; +import static androidx.media3.session.legacy.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT; +import static androidx.media3.session.legacy.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT_FAILED; +import static androidx.media3.session.legacy.MediaBrowserProtocol.SERVICE_MSG_ON_LOAD_CHILDREN; +import static androidx.media3.session.legacy.MediaBrowserProtocol.SERVICE_VERSION_2; + +import android.annotation.SuppressLint; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.media.MediaDescription; +import android.media.browse.MediaBrowser; +import android.os.BadParcelableException; +import android.os.Binder; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.Process; +import android.os.RemoteException; +import android.support.v4.os.ResultReceiver; +import android.text.TextUtils; +import android.util.Log; +import androidx.annotation.DoNotInline; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.RestrictTo; +import androidx.collection.ArrayMap; +import androidx.media3.common.util.NullableType; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.legacy.MediaControllerCompat.TransportControls; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Browses media content offered by a {@link MediaBrowserServiceCompat}. + * + *

The app targeting API level 30 or higher must include a {@code } element in their + * manifest to connect to a media browser service in another app. See the following example and this guide for more information. + * + *

{@code
+ * 
+ * 
+ *   
+ * 
+ * 
+ * 
+ * }
+ * + *

This object is not thread-safe. All calls should happen on the thread on which the browser was + * constructed. All callback methods will be called from the thread on which the browser was + * constructed.

+ * + *

Developer Guides

+ * + *

For information about building your media application, read the Media Apps developer guide.

+ */ +@UnstableApi +@RestrictTo(LIBRARY) +public final class MediaBrowserCompat { + static final String TAG = "MediaBrowserCompat"; + static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + /** + * Used as an int extra field to denote the page number to subscribe. The value of {@code + * EXTRA_PAGE} should be greater than or equal to 0. + * + * @see android.service.media.MediaBrowserService.BrowserRoot + * @see #EXTRA_PAGE_SIZE + */ + public static final String EXTRA_PAGE = "android.media.browse.extra.PAGE"; + + /** + * Used as an int extra field to denote the number of media items in a page. The value of {@code + * EXTRA_PAGE_SIZE} should be greater than or equal to 1. + * + * @see android.service.media.MediaBrowserService.BrowserRoot + * @see #EXTRA_PAGE + */ + public static final String EXTRA_PAGE_SIZE = "android.media.browse.extra.PAGE_SIZE"; + + /** + * Used as a string extra field to denote the target {@link MediaItem}. + * + * @see #CUSTOM_ACTION_DOWNLOAD + * @see #CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE + */ + public static final String EXTRA_MEDIA_ID = "android.media.browse.extra.MEDIA_ID"; + + /** + * Used as a float extra field to denote the current progress during download. The value of this + * field must be a float number within [0.0, 1.0]. + * + * @see #CUSTOM_ACTION_DOWNLOAD + * @see CustomActionCallback#onProgressUpdate + */ + public static final String EXTRA_DOWNLOAD_PROGRESS = + "android.media.browse.extra.DOWNLOAD_PROGRESS"; + + /** + * Predefined custom action to ask the connected service to download a specific {@link MediaItem} + * for offline playback. The id of the media item must be passed in an extra bundle. The download + * progress might be delivered to the browser via {@link CustomActionCallback#onProgressUpdate}. + * + * @see #EXTRA_MEDIA_ID + * @see #EXTRA_DOWNLOAD_PROGRESS + * @see #CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE + */ + public static final String CUSTOM_ACTION_DOWNLOAD = "android.support.v4.media.action.DOWNLOAD"; + + /** + * Predefined custom action to ask the connected service to remove the downloaded file of {@link + * MediaItem} by the {@link #CUSTOM_ACTION_DOWNLOAD download} action. The id of the media item + * must be passed in an extra bundle. + * + * @see #EXTRA_MEDIA_ID + * @see #CUSTOM_ACTION_DOWNLOAD + */ + public static final String CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE = + "android.support.v4.media.action.REMOVE_DOWNLOADED_FILE"; + + private final MediaBrowserImpl mImpl; + + /** + * Creates a media browser for the specified media browse service. + * + * @param context The context. + * @param serviceComponent The component name of the media browse service. + * @param callback The connection callback. + * @param rootHints An optional bundle of service-specific arguments to send to the media browse + * service when connecting and retrieving the root id for browsing, or null if none. The + * contents of this bundle may affect the information returned when browsing. + * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_RECENT + * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_OFFLINE + * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_SUGGESTED + */ + public MediaBrowserCompat( + Context context, + ComponentName serviceComponent, + ConnectionCallback callback, + @Nullable Bundle rootHints) { + // To workaround an issue of {@link #unsubscribe(String, SubscriptionCallback)} on API 24 + // and 25 devices, use the support library version of implementation on those devices. + if (Build.VERSION.SDK_INT >= 26) { + mImpl = new MediaBrowserImplApi26(context, serviceComponent, callback, rootHints); + } else if (Build.VERSION.SDK_INT >= 23) { + mImpl = new MediaBrowserImplApi23(context, serviceComponent, callback, rootHints); + } else if (Build.VERSION.SDK_INT >= 21) { + mImpl = new MediaBrowserImplApi21(context, serviceComponent, callback, rootHints); + } else { + mImpl = new MediaBrowserImplBase(context, serviceComponent, callback, rootHints); + } + } + + /** + * Connects to the media browse service. Internally, it binds to the service. + * + *

The connection callback specified in the constructor will be invoked when the connection + * completes or fails. + */ + public void connect() { + Log.d(TAG, "Connecting to a MediaBrowserService."); + mImpl.connect(); + } + + /** Disconnects from the media browse service. After this, no more callbacks will be received. */ + public void disconnect() { + mImpl.disconnect(); + } + + /** Returns whether the browser is connected to the service. */ + public boolean isConnected() { + return mImpl.isConnected(); + } + + /** Gets the service component that the media browser is connected to. */ + public ComponentName getServiceComponent() { + return mImpl.getServiceComponent(); + } + + /** + * Gets the root id. + * + *

Note that the root id may become invalid or change when when the browser is disconnected. + * + * @throws IllegalStateException if not connected. + */ + public String getRoot() { + return mImpl.getRoot(); + } + + /** + * Gets any extras for the media service. + * + * @return The extra bundle if it is connected and set, and {@code null} otherwise. + * @throws IllegalStateException if not connected. + */ + @Nullable + public Bundle getExtras() { + return mImpl.getExtras(); + } + + /** + * Gets the media session token associated with the media browser. + * + *

Note that the session token may become invalid or change when when the browser is + * disconnected. + * + * @return The session token for the browser, never null. + * @throws IllegalStateException if not connected. + */ + public MediaSessionCompat.Token getSessionToken() { + return mImpl.getSessionToken(); + } + + /** + * Queries for information about the media items that are contained within the specified id and + * subscribes to receive updates when they change. + * + *

The list of subscriptions is maintained even when not connected and is restored after the + * reconnection. It is ok to subscribe while not connected but the results will not be returned + * until the connection completes. + * + *

If the id is already subscribed with a different callback then the new callback will replace + * the previous one and the child data will be reloaded. + * + * @param parentId The id of the parent media item whose list of children will be subscribed. + * @param callback The callback to receive the list of children. + */ + public void subscribe(String parentId, SubscriptionCallback callback) { + // Check arguments. + if (TextUtils.isEmpty(parentId)) { + throw new IllegalArgumentException("parentId is empty"); + } + if (callback == null) { + throw new IllegalArgumentException("callback is null"); + } + mImpl.subscribe(parentId, null, callback); + } + + /** + * Queries with service-specific arguments for information about the media items that are + * contained within the specified id and subscribes to receive updates when they change. + * + *

The list of subscriptions is maintained even when not connected and is restored after the + * reconnection. It is ok to subscribe while not connected but the results will not be returned + * until the connection completes. + * + *

If the id is already subscribed with a different callback then the new callback will replace + * the previous one and the child data will be reloaded. + * + * @param parentId The id of the parent media item whose list of children will be subscribed. + * @param options A bundle of service-specific arguments to send to the media browse service. The + * contents of this bundle may affect the information returned when browsing. + * @param callback The callback to receive the list of children. + */ + public void subscribe(String parentId, Bundle options, SubscriptionCallback callback) { + // Check arguments. + if (TextUtils.isEmpty(parentId)) { + throw new IllegalArgumentException("parentId is empty"); + } + if (callback == null) { + throw new IllegalArgumentException("callback is null"); + } + if (options == null) { + throw new IllegalArgumentException("options are null"); + } + mImpl.subscribe(parentId, options, callback); + } + + /** + * Unsubscribes for changes to the children of the specified media id. + * + *

The query callback will no longer be invoked for results associated with this id once this + * method returns. + * + * @param parentId The id of the parent media item whose list of children will be unsubscribed. + */ + public void unsubscribe(String parentId) { + // Check arguments. + if (TextUtils.isEmpty(parentId)) { + throw new IllegalArgumentException("parentId is empty"); + } + mImpl.unsubscribe(parentId, null); + } + + /** + * Unsubscribes for changes to the children of the specified media id. + * + *

The query callback will no longer be invoked for results associated with this id once this + * method returns. + * + * @param parentId The id of the parent media item whose list of children will be unsubscribed. + * @param callback A callback sent to the media browse service to subscribe. + */ + public void unsubscribe(String parentId, SubscriptionCallback callback) { + // Check arguments. + if (TextUtils.isEmpty(parentId)) { + throw new IllegalArgumentException("parentId is empty"); + } + if (callback == null) { + throw new IllegalArgumentException("callback is null"); + } + mImpl.unsubscribe(parentId, callback); + } + + /** + * Retrieves a specific {@link MediaItem} from the connected service. Not all services may support + * this, so falling back to subscribing to the parent's id should be used when unavailable. + * + * @param mediaId The id of the item to retrieve. + * @param cb The callback to receive the result on. + */ + public void getItem(final String mediaId, final ItemCallback cb) { + mImpl.getItem(mediaId, cb); + } + + /** + * Searches {@link MediaItem media items} from the connected service. Not all services may support + * this, and {@link SearchCallback#onError} will be called if not implemented. + * + * @param query The search query that contains keywords separated by space. Should not be an empty + * string. + * @param extras The bundle of service-specific arguments to send to the media browser service. + * The contents of this bundle may affect the search result. + * @param callback The callback to receive the search result. Must be non-null. + * @throws IllegalStateException if the browser is not connected to the media browser service. + */ + public void search(final String query, @Nullable Bundle extras, SearchCallback callback) { + if (TextUtils.isEmpty(query)) { + throw new IllegalArgumentException("query cannot be empty"); + } + if (callback == null) { + throw new IllegalArgumentException("callback cannot be null"); + } + mImpl.search(query, extras, callback); + } + + /** + * Sends a custom action to the connected service. If the service doesn't support the given + * action, {@link CustomActionCallback#onError} will be called. + * + * @param action The custom action that will be sent to the connected service. Should not be an + * empty string. + * @param extras The bundle of service-specific arguments to send to the media browser service. + * @param callback The callback to receive the result of the custom action. + * @see #CUSTOM_ACTION_DOWNLOAD + * @see #CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE + */ + public void sendCustomAction( + String action, @Nullable Bundle extras, @Nullable CustomActionCallback callback) { + if (TextUtils.isEmpty(action)) { + throw new IllegalArgumentException("action cannot be empty"); + } + mImpl.sendCustomAction(action, extras, callback); + } + + /** + * Gets the options which is passed to {@link MediaBrowserServiceCompat#notifyChildrenChanged( + * String, Bundle)} call that triggered {@link SubscriptionCallback#onChildrenLoaded}. This should + * be called inside of {@link SubscriptionCallback#onChildrenLoaded}. + * + * @return A bundle which is passed to {@link MediaBrowserServiceCompat#notifyChildrenChanged( + * String, Bundle)} + */ + @Nullable + public Bundle getNotifyChildrenChangedOptions() { + return mImpl.getNotifyChildrenChangedOptions(); + } + + /** + * A class with information on a single media item for use in browsing/searching media. MediaItems + * are application dependent so we cannot guarantee that they contain the right values. + */ + @SuppressLint("BanParcelableUsage") + public static class MediaItem implements Parcelable { + private final int mFlags; + private final MediaDescriptionCompat mDescription; + + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = {FLAG_BROWSABLE, FLAG_PLAYABLE}) + private @interface Flags {} + + /** Flag: Indicates that the item has children of its own. */ + public static final int FLAG_BROWSABLE = 1 << 0; + + /** + * Flag: Indicates that the item is playable. + * + *

The id of this item may be passed to {@link TransportControls#playFromMediaId(String, + * Bundle)} to start playing it. + */ + public static final int FLAG_PLAYABLE = 1 << 1; + + /** + * Creates an instance from a framework {@link android.media.browse.MediaBrowser.MediaItem} + * object. + * + *

This method is only supported on API 21+. On API 20 and below, it returns null. + * + * @param itemObj A {@link android.media.browse.MediaBrowser.MediaItem} object. + * @return An equivalent {@link MediaItem} object, or null if none. + */ + @Nullable + public static MediaItem fromMediaItem(@Nullable Object itemObj) { + if (itemObj == null || Build.VERSION.SDK_INT < 21) { + return null; + } + MediaBrowser.MediaItem itemFwk = (MediaBrowser.MediaItem) itemObj; + int flags = Api21Impl.getFlags(itemFwk); + MediaDescriptionCompat descriptionCompat = + MediaDescriptionCompat.fromMediaDescription(Api21Impl.getDescription(itemFwk)); + return new MediaItem(descriptionCompat, flags); + } + + /** + * Creates a list of {@link MediaItem} objects from a framework {@link + * android.media.browse.MediaBrowser.MediaItem} object list. + * + *

This method is only supported on API 21+. On API 20 and below, it returns null. + * + * @param itemList A list of {@link android.media.browse.MediaBrowser.MediaItem} objects. + * @return An equivalent list of {@link MediaItem} objects, or null if none. + */ + @Nullable + public static List fromMediaItemList(@Nullable List itemList) { + if (itemList == null || Build.VERSION.SDK_INT < 21) { + return null; + } + List items = new ArrayList<>(itemList.size()); + for (Object itemObj : itemList) { + MediaItem item = fromMediaItem(itemObj); + if (item != null) { + items.add(item); + } + } + return items; + } + + /** + * Create a new MediaItem for use in browsing media. + * + * @param description The description of the media, which must include a media id. + * @param flags The flags for this item. + */ + public MediaItem(@Nullable MediaDescriptionCompat description, @Flags int flags) { + if (description == null) { + throw new IllegalArgumentException("description cannot be null"); + } + if (TextUtils.isEmpty(description.getMediaId())) { + throw new IllegalArgumentException("description must have a non-empty media id"); + } + mFlags = flags; + mDescription = description; + } + + /** Private constructor. */ + MediaItem(Parcel in) { + mFlags = in.readInt(); + mDescription = MediaDescriptionCompat.CREATOR.createFromParcel(in); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(mFlags); + mDescription.writeToParcel(out, flags); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("MediaItem{"); + sb.append("mFlags=").append(mFlags); + sb.append(", mDescription=").append(mDescription); + sb.append('}'); + return sb.toString(); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public MediaItem createFromParcel(Parcel in) { + return new MediaItem(in); + } + + @Override + public MediaItem[] newArray(int size) { + return new MediaItem[size]; + } + }; + + /** Gets the flags of the item. */ + @Flags + public int getFlags() { + return mFlags; + } + + /** + * Returns whether this item is browsable. + * + * @see #FLAG_BROWSABLE + */ + public boolean isBrowsable() { + return (mFlags & FLAG_BROWSABLE) != 0; + } + + /** + * Returns whether this item is playable. + * + * @see #FLAG_PLAYABLE + */ + public boolean isPlayable() { + return (mFlags & FLAG_PLAYABLE) != 0; + } + + /** Returns the description of the media. */ + public MediaDescriptionCompat getDescription() { + return mDescription; + } + + /** + * Returns the media id in the {@link MediaDescriptionCompat} for this item. + * + * @see MediaMetadataCompat#METADATA_KEY_MEDIA_ID + */ + @Nullable + public String getMediaId() { + return mDescription.getMediaId(); + } + } + + /** Callbacks for connection related events. */ + public static class ConnectionCallback { + @Nullable final MediaBrowser.ConnectionCallback mConnectionCallbackFwk; + @Nullable ConnectionCallbackInternal mConnectionCallbackInternal; + + public ConnectionCallback() { + if (Build.VERSION.SDK_INT >= 21) { + mConnectionCallbackFwk = new ConnectionCallbackApi21(); + } else { + mConnectionCallbackFwk = null; + } + } + + /** + * Invoked after {@link MediaBrowserCompat#connect()} when the request has successfully + * completed. This can also be called when the service is next running after it crashed or has + * been killed. + * + * @see ServiceConnection#onServiceConnected(ComponentName, IBinder) + * @see ServiceConnection#onServiceDisconnected(ComponentName) + */ + public void onConnected() {} + + /** + * Invoked when a connection to the browser service has been lost. This typically happens when + * the process hosting the service has crashed or been killed. This does not remove the + * connection itself -- this binding to the service will remain active, and {@link + * #onConnected()} will be called when the service is next running. + * + * @see ServiceConnection#onServiceDisconnected(ComponentName) + */ + public void onConnectionSuspended() {} + + /** + * Invoked when the connection to the media browser service failed. Connection failures can + * happen when the browser failed to bind to the service, or when it is rejected from the + * service. + */ + public void onConnectionFailed() {} + + void setInternalConnectionCallback(ConnectionCallbackInternal connectionCallbackInternal) { + mConnectionCallbackInternal = connectionCallbackInternal; + } + + interface ConnectionCallbackInternal { + void onConnected(); + + void onConnectionSuspended(); + + void onConnectionFailed(); + } + + @RequiresApi(21) + private class ConnectionCallbackApi21 extends MediaBrowser.ConnectionCallback { + ConnectionCallbackApi21() {} + + @Override + public void onConnected() { + if (mConnectionCallbackInternal != null) { + mConnectionCallbackInternal.onConnected(); + } + ConnectionCallback.this.onConnected(); + } + + @Override + public void onConnectionSuspended() { + if (mConnectionCallbackInternal != null) { + mConnectionCallbackInternal.onConnectionSuspended(); + } + ConnectionCallback.this.onConnectionSuspended(); + } + + @Override + public void onConnectionFailed() { + if (mConnectionCallbackInternal != null) { + mConnectionCallbackInternal.onConnectionFailed(); + } + ConnectionCallback.this.onConnectionFailed(); + } + } + } + + /** Callbacks for subscription related events. */ + public abstract static class SubscriptionCallback { + @Nullable final MediaBrowser.SubscriptionCallback mSubscriptionCallbackFwk; + final IBinder mToken; + @Nullable WeakReference mSubscriptionRef; + + public SubscriptionCallback() { + mToken = new Binder(); + if (Build.VERSION.SDK_INT >= 26) { + mSubscriptionCallbackFwk = new SubscriptionCallbackApi26(); + } else if (Build.VERSION.SDK_INT >= 21) { + mSubscriptionCallbackFwk = new SubscriptionCallbackApi21(); + } else { + mSubscriptionCallbackFwk = null; + } + } + + /** + * Called when the list of children is loaded or updated. + * + * @param parentId The media id of the parent media item. + * @param children The children which were loaded. + */ + public void onChildrenLoaded(@Nullable String parentId, @Nullable List children) {} + + /** + * Called when the list of children is loaded or updated. + * + * @param parentId The media id of the parent media item. + * @param children The children which were loaded. + * @param options A bundle of service-specific arguments to send to the media browse service. + * The contents of this bundle may affect the information returned when browsing. + */ + public void onChildrenLoaded( + @Nullable String parentId, @Nullable List children, @Nullable Bundle options) {} + + /** + * Called when the id doesn't exist or other errors in subscribing. + * + *

If this is called, the subscription remains until {@link MediaBrowserCompat#unsubscribe} + * called, because some errors may heal themselves. + * + * @param parentId The media id of the parent media item whose children could not be loaded. + */ + public void onError(@Nullable String parentId) {} + + /** + * Called when the id doesn't exist or other errors in subscribing. + * + *

If this is called, the subscription remains until {@link MediaBrowserCompat#unsubscribe} + * called, because some errors may heal themselves. + * + * @param parentId The media id of the parent media item whose children could not be loaded. + * @param options A bundle of service-specific arguments sent to the media browse service. + */ + public void onError(@Nullable String parentId, @Nullable Bundle options) {} + + void setSubscription(Subscription subscription) { + mSubscriptionRef = new WeakReference<>(subscription); + } + + @RequiresApi(21) + private class SubscriptionCallbackApi21 extends MediaBrowser.SubscriptionCallback { + SubscriptionCallbackApi21() {} + + @Override + public void onChildrenLoaded(String parentId, List children) { + Subscription sub = mSubscriptionRef == null ? null : mSubscriptionRef.get(); + if (sub == null) { + SubscriptionCallback.this.onChildrenLoaded( + parentId, MediaItem.fromMediaItemList(children)); + } else { + List itemList = + checkNotNull(MediaItem.fromMediaItemList(children)); + final List callbacks = sub.getCallbacks(); + final List<@NullableType Bundle> optionsList = sub.getOptionsList(); + for (int i = 0; i < callbacks.size(); ++i) { + Bundle options = optionsList.get(i); + if (options == null) { + SubscriptionCallback.this.onChildrenLoaded(parentId, itemList); + } else { + SubscriptionCallback.this.onChildrenLoaded( + parentId, applyOptions(itemList, options), options); + } + } + } + } + + @Override + public void onError(String parentId) { + SubscriptionCallback.this.onError(parentId); + } + + @Nullable + List applyOptions( + List list, final Bundle options) { + if (list == null) { + return null; + } + int page = options.getInt(MediaBrowserCompat.EXTRA_PAGE, -1); + int pageSize = options.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, -1); + if (page == -1 && pageSize == -1) { + return list; + } + int fromIndex = pageSize * page; + int toIndex = fromIndex + pageSize; + if (page < 0 || pageSize < 1 || fromIndex >= list.size()) { + return Collections.emptyList(); + } + if (toIndex > list.size()) { + toIndex = list.size(); + } + return list.subList(fromIndex, toIndex); + } + } + + @RequiresApi(26) + private class SubscriptionCallbackApi26 extends SubscriptionCallbackApi21 { + SubscriptionCallbackApi26() {} + + @Override + public void onChildrenLoaded( + String parentId, List children, Bundle options) { + MediaSessionCompat.ensureClassLoader(options); + SubscriptionCallback.this.onChildrenLoaded( + parentId, MediaItem.fromMediaItemList(children), options); + } + + @Override + public void onError(String parentId, Bundle options) { + MediaSessionCompat.ensureClassLoader(options); + SubscriptionCallback.this.onError(parentId, options); + } + } + } + + /** Callback for receiving the result of {@link #getItem}. */ + public abstract static class ItemCallback { + @Nullable final MediaBrowser.ItemCallback mItemCallbackFwk; + + public ItemCallback() { + if (Build.VERSION.SDK_INT >= 23) { + mItemCallbackFwk = new ItemCallbackApi23(); + } else { + mItemCallbackFwk = null; + } + } + + /** + * Called when the item has been returned by the browser service. + * + * @param item The item that was returned or null if it doesn't exist. + */ + public void onItemLoaded(@Nullable MediaItem item) {} + + /** + * Called when the item doesn't exist or there was an error retrieving it. + * + * @param itemId The media id of the media item which could not be loaded. + */ + public void onError(String itemId) {} + + @RequiresApi(23) + private class ItemCallbackApi23 extends MediaBrowser.ItemCallback { + ItemCallbackApi23() {} + + @Override + public void onItemLoaded(MediaBrowser.MediaItem item) { + ItemCallback.this.onItemLoaded(MediaItem.fromMediaItem(item)); + } + + @Override + public void onError(String itemId) { + ItemCallback.this.onError(itemId); + } + } + } + + /** Callback for receiving the result of {@link #search}. */ + public abstract static class SearchCallback { + /** + * Called when the {@link #search} finished successfully. + * + * @param query The search query sent for the search request to the connected service. + * @param extras The bundle of service-specific arguments sent to the connected service. + * @param items The list of media items which contains the search result. + */ + public void onSearchResult(String query, @Nullable Bundle extras, List items) {} + + /** + * Called when an error happens while {@link #search} or the connected service doesn't support + * {@link #search}. + * + * @param query The search query sent for the search request to the connected service. + * @param extras The bundle of service-specific arguments sent to the connected service. + */ + public void onError(String query, @Nullable Bundle extras) {} + } + + /** Callback for receiving the result of {@link #sendCustomAction}. */ + public abstract static class CustomActionCallback { + /** + * Called when an interim update was delivered from the connected service while performing the + * custom action. + * + * @param action The custom action sent to the connected service. + * @param extras The bundle of service-specific arguments sent to the connected service. + * @param data The additional data delivered from the connected service. + */ + public void onProgressUpdate(String action, @Nullable Bundle extras, @Nullable Bundle data) {} + + /** + * Called when the custom action finished successfully. + * + * @param action The custom action sent to the connected service. + * @param extras The bundle of service-specific arguments sent to the connected service. + * @param resultData The additional data delivered from the connected service. + */ + public void onResult(String action, @Nullable Bundle extras, @Nullable Bundle resultData) {} + + /** + * Called when an error happens while performing the custom action or the connected service + * doesn't support the requested custom action. + * + * @param action The custom action sent to the connected service. + * @param extras The bundle of service-specific arguments sent to the connected service. + * @param data The additional data delivered from the connected service. + */ + public void onError(String action, @Nullable Bundle extras, @Nullable Bundle data) {} + } + + interface MediaBrowserImpl { + void connect(); + + void disconnect(); + + boolean isConnected(); + + ComponentName getServiceComponent(); + + String getRoot(); + + @Nullable + Bundle getExtras(); + + MediaSessionCompat.Token getSessionToken(); + + void subscribe(String parentId, @Nullable Bundle options, SubscriptionCallback callback); + + void unsubscribe(String parentId, @Nullable SubscriptionCallback callback); + + void getItem(String mediaId, ItemCallback cb); + + void search(String query, @Nullable Bundle extras, SearchCallback callback); + + void sendCustomAction( + String action, @Nullable Bundle extras, @Nullable CustomActionCallback callback); + + @Nullable + Bundle getNotifyChildrenChangedOptions(); + } + + interface MediaBrowserServiceCallbackImpl { + void onServiceConnected( + Messenger callback, + @Nullable String root, + @Nullable MediaSessionCompat.Token session, + @Nullable Bundle extra); + + void onConnectionFailed(Messenger callback); + + void onLoadChildren( + Messenger callback, + @Nullable String parentId, + @Nullable List list, + @Nullable Bundle options, + @Nullable Bundle notifyChildrenChangedOptions); + } + + static class MediaBrowserImplBase implements MediaBrowserImpl, MediaBrowserServiceCallbackImpl { + static final int CONNECT_STATE_DISCONNECTING = 0; + static final int CONNECT_STATE_DISCONNECTED = 1; + static final int CONNECT_STATE_CONNECTING = 2; + static final int CONNECT_STATE_CONNECTED = 3; + static final int CONNECT_STATE_SUSPENDED = 4; + + final Context mContext; + final ComponentName mServiceComponent; + final ConnectionCallback mCallback; + @Nullable final Bundle mRootHints; + + @SuppressWarnings({ + "argument.type.incompatible", + "assignment.type.incompatible" + }) // Using this before constructor finishes + final CallbackHandler mHandler = new CallbackHandler(this); + + private final ArrayMap mSubscriptions = new ArrayMap<>(); + + int mState = CONNECT_STATE_DISCONNECTED; + @Nullable MediaServiceConnection mServiceConnection; + @Nullable ServiceBinderWrapper mServiceBinderWrapper; + @Nullable Messenger mCallbacksMessenger; + @Nullable private String mRootId; + @Nullable private MediaSessionCompat.Token mMediaSessionToken; + @Nullable private Bundle mExtras; + @Nullable private Bundle mNotifyChildrenChangedOptions; + + public MediaBrowserImplBase( + Context context, + ComponentName serviceComponent, + ConnectionCallback callback, + @Nullable Bundle rootHints) { + if (context == null) { + throw new IllegalArgumentException("context must not be null"); + } + if (serviceComponent == null) { + throw new IllegalArgumentException("service component must not be null"); + } + if (callback == null) { + throw new IllegalArgumentException("connection callback must not be null"); + } + mContext = context; + mServiceComponent = serviceComponent; + mCallback = callback; + mRootHints = rootHints == null ? null : new Bundle(rootHints); + } + + @Override + @SuppressWarnings("ObjectToString") + public void connect() { + if (mState != CONNECT_STATE_DISCONNECTING && mState != CONNECT_STATE_DISCONNECTED) { + throw new IllegalStateException( + "connect() called while neigther disconnecting nor " + + "disconnected (state=" + + getStateLabel(mState) + + ")"); + } + + mState = CONNECT_STATE_CONNECTING; + mHandler.post( + new Runnable() { + @Override + public void run() { + // mState could be changed by the Runnable of disconnect() + if (mState == CONNECT_STATE_DISCONNECTING) { + return; + } + mState = CONNECT_STATE_CONNECTING; + // TODO: remove this extra check. + if (DEBUG) { + if (mServiceConnection != null) { + throw new RuntimeException( + "mServiceConnection should be null. Instead " + + "it is " + + mServiceConnection); + } + } + if (mServiceBinderWrapper != null) { + throw new RuntimeException( + "mServiceBinderWrapper should be null. Instead " + + "it is " + + mServiceBinderWrapper); + } + if (mCallbacksMessenger != null) { + throw new RuntimeException( + "mCallbacksMessenger should be null. Instead " + + "it is " + + mCallbacksMessenger); + } + + final Intent intent = new Intent(MediaBrowserServiceCompat.SERVICE_INTERFACE); + intent.setComponent(mServiceComponent); + + mServiceConnection = new MediaServiceConnection(); + boolean bound = false; + try { + bound = mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE); + } catch (Exception ex) { + Log.e(TAG, "Failed binding to service " + mServiceComponent); + } + + if (!bound) { + // Tell them that it didn't work. + forceCloseConnection(); + mCallback.onConnectionFailed(); + } + + if (DEBUG) { + Log.d(TAG, "connect..."); + dump(); + } + } + }); + } + + @Override + public void disconnect() { + // It's ok to call this any state, because allowing this lets apps not have + // to check isConnected() unnecessarily. They won't appreciate the extra + // assertions for this. We do everything we can here to go back to a valid state. + mState = CONNECT_STATE_DISCONNECTING; + mHandler.post( + new Runnable() { + @Override + public void run() { + // connect() could be called before this. Then we will disconnect and reconnect. + if (mCallbacksMessenger != null) { + try { + checkNotNull(mServiceBinderWrapper).disconnect(mCallbacksMessenger); + } catch (RemoteException ex) { + // We are disconnecting anyway. Log, just for posterity but it's not + // a big problem. + Log.w(TAG, "RemoteException during connect for " + mServiceComponent); + } + } + int state = mState; + forceCloseConnection(); + // If the state was not CONNECT_STATE_DISCONNECTING, keep the state so that + // the operation came after disconnect() can be handled properly. + if (state != CONNECT_STATE_DISCONNECTING) { + mState = state; + } + if (DEBUG) { + Log.d(TAG, "disconnect..."); + dump(); + } + } + }); + } + + /** + * Null out the variables and unbind from the service. This doesn't include calling disconnect + * on the service, because we only try to do that in the clean shutdown cases. + * + *

Everywhere that calls this EXCEPT for disconnect() should follow it with a call to + * mCallback.onConnectionFailed(). Disconnect doesn't do that callback for a clean shutdown, but + * everywhere else is a dirty shutdown and should notify the app. + */ + void forceCloseConnection() { + if (mServiceConnection != null) { + mContext.unbindService(mServiceConnection); + } + mState = CONNECT_STATE_DISCONNECTED; + mServiceConnection = null; + mServiceBinderWrapper = null; + mCallbacksMessenger = null; + mHandler.setCallbacksMessenger(null); + mRootId = null; + mMediaSessionToken = null; + } + + @Override + public boolean isConnected() { + return mState == CONNECT_STATE_CONNECTED; + } + + @Override + public ComponentName getServiceComponent() { + if (!isConnected()) { + throw new IllegalStateException( + "getServiceComponent() called while not connected" + " (state=" + mState + ")"); + } + return mServiceComponent; + } + + @Override + public String getRoot() { + if (!isConnected()) { + throw new IllegalStateException( + "getRoot() called while not connected" + "(state=" + getStateLabel(mState) + ")"); + } + return checkNotNull(mRootId); + } + + @Override + @Nullable + public Bundle getExtras() { + if (!isConnected()) { + throw new IllegalStateException( + "getExtras() called while not connected (state=" + getStateLabel(mState) + ")"); + } + return mExtras; + } + + @Override + public MediaSessionCompat.Token getSessionToken() { + if (!isConnected()) { + throw new IllegalStateException( + "getSessionToken() called while not connected" + "(state=" + mState + ")"); + } + return checkNotNull(mMediaSessionToken); + } + + @Override + public void subscribe( + String parentId, @Nullable Bundle options, SubscriptionCallback callback) { + // Update or create the subscription. + Subscription sub = mSubscriptions.get(parentId); + if (sub == null) { + sub = new Subscription(); + mSubscriptions.put(parentId, sub); + } + Bundle copiedOptions = options == null ? null : new Bundle(options); + sub.putCallback(copiedOptions, callback); + + // If we are connected, tell the service that we are watching. If we aren't + // connected, the service will be told when we connect. + if (isConnected()) { + try { + checkNotNull(mServiceBinderWrapper) + .addSubscription( + parentId, callback.mToken, copiedOptions, checkNotNull(mCallbacksMessenger)); + } catch (RemoteException e) { + // Process is crashing. We will disconnect, and upon reconnect we will + // automatically reregister. So nothing to do here. + Log.d(TAG, "addSubscription failed with RemoteException parentId=" + parentId); + } + } + } + + @Override + public void unsubscribe(String parentId, @Nullable SubscriptionCallback callback) { + Subscription sub = mSubscriptions.get(parentId); + if (sub == null) { + return; + } + + // Tell the service if necessary. + try { + if (callback == null) { + if (isConnected()) { + checkNotNull(mServiceBinderWrapper) + .removeSubscription(parentId, null, checkNotNull(mCallbacksMessenger)); + } + } else { + final List callbacks = sub.getCallbacks(); + final List<@NullableType Bundle> optionsList = sub.getOptionsList(); + for (int i = callbacks.size() - 1; i >= 0; --i) { + if (callbacks.get(i) == callback) { + if (isConnected()) { + checkNotNull(mServiceBinderWrapper) + .removeSubscription( + parentId, callback.mToken, checkNotNull(mCallbacksMessenger)); + } + callbacks.remove(i); + optionsList.remove(i); + } + } + } + } catch (RemoteException ex) { + // Process is crashing. We will disconnect, and upon reconnect we will + // automatically reregister. So nothing to do here. + Log.d(TAG, "removeSubscription failed with RemoteException parentId=" + parentId); + } + + if (sub.isEmpty() || callback == null) { + mSubscriptions.remove(parentId); + } + } + + @Override + public void getItem(final String mediaId, final ItemCallback cb) { + if (TextUtils.isEmpty(mediaId)) { + throw new IllegalArgumentException("mediaId is empty"); + } + if (cb == null) { + throw new IllegalArgumentException("cb is null"); + } + if (!isConnected()) { + Log.i(TAG, "Not connected, unable to retrieve the MediaItem."); + mHandler.post( + new Runnable() { + @Override + public void run() { + cb.onError(mediaId); + } + }); + return; + } + ResultReceiver receiver = new ItemReceiver(mediaId, cb, mHandler); + try { + checkNotNull(mServiceBinderWrapper) + .getMediaItem(mediaId, receiver, checkNotNull(mCallbacksMessenger)); + } catch (RemoteException e) { + Log.i(TAG, "Remote error getting media item: " + mediaId); + mHandler.post( + new Runnable() { + @Override + public void run() { + cb.onError(mediaId); + } + }); + } + } + + @Override + public void search(final String query, @Nullable Bundle extras, final SearchCallback callback) { + if (!isConnected()) { + throw new IllegalStateException( + "search() called while not connected" + " (state=" + getStateLabel(mState) + ")"); + } + + ResultReceiver receiver = new SearchResultReceiver(query, extras, callback, mHandler); + try { + checkNotNull(mServiceBinderWrapper) + .search(query, extras, receiver, checkNotNull(mCallbacksMessenger)); + } catch (RemoteException e) { + Log.i(TAG, "Remote error searching items with query: " + query, e); + mHandler.post( + new Runnable() { + @Override + public void run() { + callback.onError(query, extras); + } + }); + } + } + + @Override + public void sendCustomAction( + final String action, + @Nullable Bundle extras, + @Nullable final CustomActionCallback callback) { + if (!isConnected()) { + throw new IllegalStateException( + "Cannot send a custom action (" + + action + + ") with " + + "extras " + + extras + + " because the browser is not connected to the " + + "service."); + } + + ResultReceiver receiver = new CustomActionResultReceiver(action, extras, callback, mHandler); + try { + checkNotNull(mServiceBinderWrapper) + .sendCustomAction(action, extras, receiver, checkNotNull(mCallbacksMessenger)); + } catch (RemoteException e) { + Log.i( + TAG, + "Remote error sending a custom action: action=" + action + ", extras=" + extras, + e); + if (callback != null) { + mHandler.post( + new Runnable() { + @Override + public void run() { + callback.onError(action, extras, null); + } + }); + } + } + } + + @Override + public void onServiceConnected( + final Messenger callback, + @Nullable String root, + @Nullable MediaSessionCompat.Token session, + @Nullable Bundle extra) { + // Check to make sure there hasn't been a disconnect or a different ServiceConnection. + if (!isCurrent(callback, "onConnect")) { + return; + } + // Don't allow them to call us twice. + if (mState != CONNECT_STATE_CONNECTING) { + Log.w(TAG, "onConnect from service while mState=" + getStateLabel(mState) + "... ignoring"); + return; + } + mRootId = root; + mMediaSessionToken = session; + mExtras = extra; + mState = CONNECT_STATE_CONNECTED; + + if (DEBUG) { + Log.d(TAG, "ServiceCallbacks.onConnect..."); + dump(); + } + mCallback.onConnected(); + + // we may receive some subscriptions before we are connected, so re-subscribe + // everything now + try { + for (Map.Entry subscriptionEntry : mSubscriptions.entrySet()) { + String id = subscriptionEntry.getKey(); + Subscription sub = subscriptionEntry.getValue(); + List callbackList = sub.getCallbacks(); + List<@NullableType Bundle> optionsList = sub.getOptionsList(); + for (int i = 0; i < callbackList.size(); ++i) { + checkNotNull(mServiceBinderWrapper) + .addSubscription( + id, + callbackList.get(i).mToken, + optionsList.get(i), + checkNotNull(mCallbacksMessenger)); + } + } + } catch (RemoteException ex) { + // Process is crashing. We will disconnect, and upon reconnect we will + // automatically reregister. So nothing to do here. + Log.d(TAG, "addSubscription failed with RemoteException."); + } + } + + @Override + public void onConnectionFailed(final Messenger callback) { + Log.e(TAG, "onConnectFailed for " + mServiceComponent); + + // Check to make sure there hasn't been a disconnect or a different ServiceConnection. + if (!isCurrent(callback, "onConnectFailed")) { + return; + } + // Don't allow them to call us twice. + if (mState != CONNECT_STATE_CONNECTING) { + Log.w(TAG, "onConnect from service while mState=" + getStateLabel(mState) + "... ignoring"); + return; + } + + // Clean up + forceCloseConnection(); + + // Tell the app. + mCallback.onConnectionFailed(); + } + + @Override + @SuppressWarnings("unchecked") + public void onLoadChildren( + final Messenger callback, + @Nullable String parentId, + @Nullable List list, + @Nullable Bundle options, + @Nullable Bundle notifyChildrenChangedOptions) { + // Check that there hasn't been a disconnect or a different ServiceConnection. + if (!isCurrent(callback, "onLoadChildren")) { + return; + } + + if (DEBUG) { + Log.d(TAG, "onLoadChildren for " + mServiceComponent + " id=" + parentId); + } + + // Check that the subscription is still subscribed. + Subscription subscription = parentId == null ? null : mSubscriptions.get(parentId); + if (subscription == null) { + if (DEBUG) { + Log.d(TAG, "onLoadChildren for id that isn't subscribed id=" + parentId); + } + return; + } + + // Tell the app. + SubscriptionCallback subscriptionCallback = subscription.getCallback(options); + if (subscriptionCallback != null) { + if (options == null) { + if (list == null) { + subscriptionCallback.onError(parentId); + } else { + mNotifyChildrenChangedOptions = notifyChildrenChangedOptions; + subscriptionCallback.onChildrenLoaded(parentId, list); + mNotifyChildrenChangedOptions = null; + } + } else { + if (list == null) { + subscriptionCallback.onError(parentId, options); + } else { + mNotifyChildrenChangedOptions = notifyChildrenChangedOptions; + subscriptionCallback.onChildrenLoaded(parentId, list, options); + mNotifyChildrenChangedOptions = null; + } + } + } + } + + @Nullable + @Override + public Bundle getNotifyChildrenChangedOptions() { + return mNotifyChildrenChangedOptions; + } + + /** For debugging. */ + private static String getStateLabel(int state) { + switch (state) { + case CONNECT_STATE_DISCONNECTING: + return "CONNECT_STATE_DISCONNECTING"; + case CONNECT_STATE_DISCONNECTED: + return "CONNECT_STATE_DISCONNECTED"; + case CONNECT_STATE_CONNECTING: + return "CONNECT_STATE_CONNECTING"; + case CONNECT_STATE_CONNECTED: + return "CONNECT_STATE_CONNECTED"; + case CONNECT_STATE_SUSPENDED: + return "CONNECT_STATE_SUSPENDED"; + default: + return "UNKNOWN/" + state; + } + } + + /** Return true if {@code callback} is the current ServiceCallbacks. Also logs if it's not. */ + @SuppressWarnings({"ReferenceEquality", "ObjectToString"}) + private boolean isCurrent(Messenger callback, String funcName) { + if (mCallbacksMessenger != callback + || mState == CONNECT_STATE_DISCONNECTING + || mState == CONNECT_STATE_DISCONNECTED) { + if (mState != CONNECT_STATE_DISCONNECTING && mState != CONNECT_STATE_DISCONNECTED) { + Log.i( + TAG, + funcName + + " for " + + mServiceComponent + + " with mCallbacksMessenger=" + + mCallbacksMessenger + + " this=" + + this); + } + return false; + } + return true; + } + + /** Log internal state. */ + @SuppressWarnings("ObjectToString") + void dump() { + Log.d(TAG, "MediaBrowserCompat..."); + Log.d(TAG, " mServiceComponent=" + mServiceComponent); + Log.d(TAG, " mCallback=" + mCallback); + Log.d(TAG, " mRootHints=" + mRootHints); + Log.d(TAG, " mState=" + getStateLabel(mState)); + Log.d(TAG, " mServiceConnection=" + mServiceConnection); + Log.d(TAG, " mServiceBinderWrapper=" + mServiceBinderWrapper); + Log.d(TAG, " mCallbacksMessenger=" + mCallbacksMessenger); + Log.d(TAG, " mRootId=" + mRootId); + Log.d(TAG, " mMediaSessionToken=" + mMediaSessionToken); + } + + /** ServiceConnection to the other app. */ + private class MediaServiceConnection implements ServiceConnection { + MediaServiceConnection() {} + + @Override + public void onServiceConnected(final ComponentName name, final IBinder binder) { + postOrRun( + new Runnable() { + @Override + public void run() { + if (DEBUG) { + Log.d( + TAG, + "MediaServiceConnection.onServiceConnected name=" + + name + + " binder=" + + binder); + dump(); + } + + // Make sure we are still the current connection, and that they haven't + // called disconnect(). + if (!isCurrent("onServiceConnected")) { + return; + } + + // Save their binder + ServiceBinderWrapper wrapper = new ServiceBinderWrapper(binder, mRootHints); + mServiceBinderWrapper = wrapper; + + // We make a new mServiceCallbacks each time we connect so that we can drop + // responses from previous connections. + Messenger messenger = new Messenger(mHandler); + mCallbacksMessenger = messenger; + mHandler.setCallbacksMessenger(mCallbacksMessenger); + + mState = CONNECT_STATE_CONNECTING; + + // Call connect, which is async. When we get a response from that we will + // say that we're connected. + try { + if (DEBUG) { + Log.d(TAG, "ServiceCallbacks.onConnect..."); + dump(); + } + wrapper.connect(mContext, messenger); + } catch (RemoteException ex) { + // Connect failed, which isn't good. But the auto-reconnect on the + // service will take over and we will come back. We will also get the + // onServiceDisconnected, which has all the cleanup code. So let that + // do it. + Log.w(TAG, "RemoteException during connect for " + mServiceComponent); + if (DEBUG) { + Log.d(TAG, "ServiceCallbacks.onConnect..."); + dump(); + } + } + } + }); + } + + @Override + public void onServiceDisconnected(final ComponentName name) { + postOrRun( + new Runnable() { + @Override + public void run() { + if (DEBUG) { + Log.d( + TAG, + "MediaServiceConnection.onServiceDisconnected name=" + + name + + " this=" + + this + + " mServiceConnection=" + + mServiceConnection); + dump(); + } + + // Make sure we are still the current connection, and that they haven't + // called disconnect(). + if (!isCurrent("onServiceDisconnected")) { + return; + } + + // Clear out what we set in onServiceConnected + mServiceBinderWrapper = null; + mCallbacksMessenger = null; + mHandler.setCallbacksMessenger(null); + + // And tell the app that it's suspended. + mState = CONNECT_STATE_SUSPENDED; + mCallback.onConnectionSuspended(); + } + }); + } + + private void postOrRun(Runnable r) { + if (Thread.currentThread() == mHandler.getLooper().getThread()) { + r.run(); + } else { + mHandler.post(r); + } + } + + /** Return true if this is the current ServiceConnection. Also logs if it's not. */ + boolean isCurrent(String funcName) { + if (mServiceConnection != this + || mState == CONNECT_STATE_DISCONNECTING + || mState == CONNECT_STATE_DISCONNECTED) { + if (mState != CONNECT_STATE_DISCONNECTING && mState != CONNECT_STATE_DISCONNECTED) { + // Check mState, because otherwise this log is noisy. + Log.i( + TAG, + funcName + + " for " + + mServiceComponent + + " with mServiceConnection=" + + mServiceConnection + + " this=" + + this); + } + return false; + } + return true; + } + } + } + + @RequiresApi(21) + static class MediaBrowserImplApi21 + implements MediaBrowserImpl, + MediaBrowserServiceCallbackImpl, + ConnectionCallback.ConnectionCallbackInternal { + final Context mContext; + protected final MediaBrowser mBrowserFwk; + protected final Bundle mRootHints; + + @SuppressWarnings({ + "argument.type.incompatible", + "assignment.type.incompatible" + }) // Using this before constructor finishes + protected final CallbackHandler mHandler = new CallbackHandler(this); + + private final ArrayMap mSubscriptions = new ArrayMap<>(); + + protected int mServiceVersion; + @Nullable protected ServiceBinderWrapper mServiceBinderWrapper; + @Nullable protected Messenger mCallbacksMessenger; + @Nullable private MediaSessionCompat.Token mMediaSessionToken; + @Nullable private Bundle mNotifyChildrenChangedOptions; + + @SuppressWarnings("argument.type.incompatible") // Using this before constructor finishes + MediaBrowserImplApi21( + Context context, + ComponentName serviceComponent, + ConnectionCallback callback, + @Nullable Bundle rootHints) { + mContext = context; + mRootHints = (rootHints != null ? new Bundle(rootHints) : new Bundle()); + mRootHints.putInt(EXTRA_CLIENT_VERSION, CLIENT_VERSION_CURRENT); + mRootHints.putInt(EXTRA_CALLING_PID, Process.myPid()); + callback.setInternalConnectionCallback(this); + mBrowserFwk = + new MediaBrowser( + context, serviceComponent, checkNotNull(callback.mConnectionCallbackFwk), mRootHints); + } + + @Override + public void connect() { + mBrowserFwk.connect(); + } + + @Override + public void disconnect() { + if (mServiceBinderWrapper != null && mCallbacksMessenger != null) { + try { + mServiceBinderWrapper.unregisterCallbackMessenger(mCallbacksMessenger); + } catch (RemoteException e) { + Log.i(TAG, "Remote error unregistering client messenger."); + } + } + mBrowserFwk.disconnect(); + } + + @Override + public boolean isConnected() { + return mBrowserFwk.isConnected(); + } + + @Override + public ComponentName getServiceComponent() { + return mBrowserFwk.getServiceComponent(); + } + + @Override + public String getRoot() { + return mBrowserFwk.getRoot(); + } + + @Override + @Nullable + public Bundle getExtras() { + return mBrowserFwk.getExtras(); + } + + @Override + public MediaSessionCompat.Token getSessionToken() { + if (mMediaSessionToken == null) { + mMediaSessionToken = MediaSessionCompat.Token.fromToken(mBrowserFwk.getSessionToken()); + } + return mMediaSessionToken; + } + + @Override + public void subscribe( + final String parentId, @Nullable Bundle options, final SubscriptionCallback callback) { + // Update or create the subscription. + Subscription sub = mSubscriptions.get(parentId); + if (sub == null) { + sub = new Subscription(); + mSubscriptions.put(parentId, sub); + } + callback.setSubscription(sub); + Bundle copiedOptions = options == null ? null : new Bundle(options); + sub.putCallback(copiedOptions, callback); + + if (mServiceBinderWrapper == null) { + // TODO: When MediaBrowser is connected to framework's MediaBrowserService, + // subscribe with options won't work properly. + mBrowserFwk.subscribe(parentId, checkNotNull(callback.mSubscriptionCallbackFwk)); + } else { + try { + mServiceBinderWrapper.addSubscription( + parentId, callback.mToken, copiedOptions, checkNotNull(mCallbacksMessenger)); + } catch (RemoteException e) { + // Process is crashing. We will disconnect, and upon reconnect we will + // automatically reregister. So nothing to do here. + Log.i(TAG, "Remote error subscribing media item: " + parentId); + } + } + } + + @Override + public void unsubscribe(String parentId, @Nullable SubscriptionCallback callback) { + Subscription sub = mSubscriptions.get(parentId); + if (sub == null) { + return; + } + + ServiceBinderWrapper serviceBinderWrapper = this.mServiceBinderWrapper; + if (serviceBinderWrapper == null) { + if (callback == null) { + mBrowserFwk.unsubscribe(parentId); + } else { + final List callbacks = sub.getCallbacks(); + final List<@NullableType Bundle> optionsList = sub.getOptionsList(); + for (int i = callbacks.size() - 1; i >= 0; --i) { + if (callbacks.get(i) == callback) { + callbacks.remove(i); + optionsList.remove(i); + } + } + if (callbacks.size() == 0) { + mBrowserFwk.unsubscribe(parentId); + } + } + } else { + // Tell the service if necessary. + try { + if (callback == null) { + serviceBinderWrapper.removeSubscription( + parentId, null, checkNotNull(mCallbacksMessenger)); + } else { + final List callbacks = sub.getCallbacks(); + final List<@NullableType Bundle> optionsList = sub.getOptionsList(); + for (int i = callbacks.size() - 1; i >= 0; --i) { + if (callbacks.get(i) == callback) { + serviceBinderWrapper.removeSubscription( + parentId, callback.mToken, checkNotNull(mCallbacksMessenger)); + callbacks.remove(i); + optionsList.remove(i); + } + } + } + } catch (RemoteException ex) { + // Process is crashing. We will disconnect, and upon reconnect we will + // automatically reregister. So nothing to do here. + Log.d(TAG, "removeSubscription failed with RemoteException parentId=" + parentId); + } + } + + if (sub.isEmpty() || callback == null) { + mSubscriptions.remove(parentId); + } + } + + @Override + public void getItem(final String mediaId, final ItemCallback cb) { + if (TextUtils.isEmpty(mediaId)) { + throw new IllegalArgumentException("mediaId is empty"); + } + if (cb == null) { + throw new IllegalArgumentException("cb is null"); + } + if (!mBrowserFwk.isConnected()) { + Log.i(TAG, "Not connected, unable to retrieve the MediaItem."); + mHandler.post( + new Runnable() { + @Override + public void run() { + cb.onError(mediaId); + } + }); + return; + } + if (mServiceBinderWrapper == null) { + mHandler.post( + new Runnable() { + @Override + public void run() { + // Default framework implementation. + cb.onError(mediaId); + } + }); + return; + } + ResultReceiver receiver = new ItemReceiver(mediaId, cb, mHandler); + try { + mServiceBinderWrapper.getMediaItem(mediaId, receiver, checkNotNull(mCallbacksMessenger)); + } catch (RemoteException e) { + Log.i(TAG, "Remote error getting media item: " + mediaId); + mHandler.post( + new Runnable() { + @Override + public void run() { + cb.onError(mediaId); + } + }); + } + } + + @Override + public void search(final String query, @Nullable Bundle extras, final SearchCallback callback) { + if (!isConnected()) { + throw new IllegalStateException("search() called while not connected"); + } + if (mServiceBinderWrapper == null) { + Log.i(TAG, "The connected service doesn't support search."); + mHandler.post( + new Runnable() { + @Override + public void run() { + // Default framework implementation. + callback.onError(query, extras); + } + }); + return; + } + + ResultReceiver receiver = new SearchResultReceiver(query, extras, callback, mHandler); + try { + mServiceBinderWrapper.search(query, extras, receiver, checkNotNull(mCallbacksMessenger)); + } catch (RemoteException e) { + Log.i(TAG, "Remote error searching items with query: " + query, e); + mHandler.post( + new Runnable() { + @Override + public void run() { + callback.onError(query, extras); + } + }); + } + } + + @Override + public void sendCustomAction( + final String action, + @Nullable Bundle extras, + @Nullable final CustomActionCallback callback) { + if (!isConnected()) { + throw new IllegalStateException( + "Cannot send a custom action (" + + action + + ") with " + + "extras " + + extras + + " because the browser is not connected to the " + + "service."); + } + ServiceBinderWrapper serviceBinderWrapper = this.mServiceBinderWrapper; + if (serviceBinderWrapper == null) { + Log.i(TAG, "The connected service doesn't support sendCustomAction."); + if (callback != null) { + mHandler.post( + new Runnable() { + @Override + public void run() { + callback.onError(action, extras, null); + } + }); + } + return; + } + + ResultReceiver receiver = new CustomActionResultReceiver(action, extras, callback, mHandler); + try { + serviceBinderWrapper.sendCustomAction( + action, extras, receiver, checkNotNull(mCallbacksMessenger)); + } catch (RemoteException e) { + Log.i( + TAG, + "Remote error sending a custom action: action=" + action + ", extras=" + extras, + e); + if (callback != null) { + mHandler.post( + new Runnable() { + @Override + public void run() { + callback.onError(action, extras, null); + } + }); + } + } + } + + @Override + public void onConnected() { + Bundle extras; + try { + extras = mBrowserFwk.getExtras(); + } catch (IllegalStateException e) { + // Should not be here since onConnected() will be called in a connected state. + Log.e(TAG, "Unexpected IllegalStateException", e); + return; + } + if (extras == null) { + return; + } + mServiceVersion = extras.getInt(EXTRA_SERVICE_VERSION, 0); + IBinder serviceBinder = extras.getBinder(EXTRA_MESSENGER_BINDER); + if (serviceBinder != null) { + ServiceBinderWrapper serviceBinderWrapper = + new ServiceBinderWrapper(serviceBinder, mRootHints); + this.mServiceBinderWrapper = serviceBinderWrapper; + Messenger messenger = new Messenger(mHandler); + ; + this.mCallbacksMessenger = messenger; + mHandler.setCallbacksMessenger(messenger); + try { + serviceBinderWrapper.registerCallbackMessenger(mContext, messenger); + } catch (RemoteException e) { + Log.i(TAG, "Remote error registering client messenger."); + } + } + IMediaSession sessionToken = + IMediaSession.Stub.asInterface(extras.getBinder(EXTRA_SESSION_BINDER)); + if (sessionToken != null) { + mMediaSessionToken = + MediaSessionCompat.Token.fromToken(mBrowserFwk.getSessionToken(), sessionToken); + } + } + + @Override + public void onConnectionSuspended() { + mServiceBinderWrapper = null; + mCallbacksMessenger = null; + mMediaSessionToken = null; + mHandler.setCallbacksMessenger(null); + } + + @Override + public void onConnectionFailed() { + // Do noting + } + + @Override + public void onServiceConnected( + final Messenger callback, + @Nullable final String root, + @Nullable final MediaSessionCompat.Token session, + @Nullable Bundle extra) { + // This method will not be called. + } + + @Override + public void onConnectionFailed(Messenger callback) { + // This method will not be called. + } + + @Override + @SuppressWarnings({"ReferenceEquality", "unchecked"}) + public void onLoadChildren( + Messenger callback, + @Nullable String parentId, + @Nullable List list, + @Nullable Bundle options, + @Nullable Bundle notifyChildrenChangedOptions) { + if (mCallbacksMessenger != callback) { + return; + } + + // Check that the subscription is still subscribed. + Subscription subscription = parentId == null ? null : mSubscriptions.get(parentId); + if (subscription == null) { + if (DEBUG) { + Log.d(TAG, "onLoadChildren for id that isn't subscribed id=" + parentId); + } + return; + } + + // Tell the app. + SubscriptionCallback subscriptionCallback = subscription.getCallback(options); + if (subscriptionCallback != null) { + if (options == null) { + if (list == null) { + subscriptionCallback.onError(parentId); + } else { + mNotifyChildrenChangedOptions = notifyChildrenChangedOptions; + subscriptionCallback.onChildrenLoaded(parentId, list); + mNotifyChildrenChangedOptions = null; + } + } else { + if (list == null) { + subscriptionCallback.onError(parentId, options); + } else { + mNotifyChildrenChangedOptions = notifyChildrenChangedOptions; + subscriptionCallback.onChildrenLoaded(parentId, list, options); + mNotifyChildrenChangedOptions = null; + } + } + } + } + + @Nullable + @Override + public Bundle getNotifyChildrenChangedOptions() { + return mNotifyChildrenChangedOptions; + } + } + + @RequiresApi(23) + static class MediaBrowserImplApi23 extends MediaBrowserImplApi21 { + MediaBrowserImplApi23( + Context context, + ComponentName serviceComponent, + ConnectionCallback callback, + @Nullable Bundle rootHints) { + super(context, serviceComponent, callback, rootHints); + } + + @Override + public void getItem(final String mediaId, final ItemCallback cb) { + if (mServiceBinderWrapper == null) { + mBrowserFwk.getItem(mediaId, checkNotNull(cb.mItemCallbackFwk)); + } else { + super.getItem(mediaId, cb); + } + } + } + + @RequiresApi(26) + static class MediaBrowserImplApi26 extends MediaBrowserImplApi23 { + MediaBrowserImplApi26( + Context context, + ComponentName serviceComponent, + ConnectionCallback callback, + @Nullable Bundle rootHints) { + super(context, serviceComponent, callback, rootHints); + } + + @Override + public void subscribe( + String parentId, @Nullable Bundle options, SubscriptionCallback callback) { + // From service v2, we use compat code when subscribing. + // This is to prevent ClassNotFoundException when options has Parcelable in it. + if (mServiceBinderWrapper == null || mServiceVersion < SERVICE_VERSION_2) { + if (options == null) { + mBrowserFwk.subscribe(parentId, checkNotNull(callback.mSubscriptionCallbackFwk)); + } else { + mBrowserFwk.subscribe(parentId, options, checkNotNull(callback.mSubscriptionCallbackFwk)); + } + } else { + super.subscribe(parentId, options, callback); + } + } + + @Override + public void unsubscribe(String parentId, @Nullable SubscriptionCallback callback) { + // From service v2, we use compat code when subscribing. + // This is to prevent ClassNotFoundException when options has Parcelable in it. + if (mServiceBinderWrapper == null || mServiceVersion < SERVICE_VERSION_2) { + if (callback == null) { + mBrowserFwk.unsubscribe(parentId); + } else { + mBrowserFwk.unsubscribe(parentId, checkNotNull(callback.mSubscriptionCallbackFwk)); + } + } else { + super.unsubscribe(parentId, callback); + } + } + } + + private static class Subscription { + private final List mCallbacks; + private final List<@NullableType Bundle> mOptionsList; + + public Subscription() { + mCallbacks = new ArrayList<>(); + mOptionsList = new ArrayList<>(); + } + + public boolean isEmpty() { + return mCallbacks.isEmpty(); + } + + public List<@NullableType Bundle> getOptionsList() { + return mOptionsList; + } + + public List getCallbacks() { + return mCallbacks; + } + + @Nullable + public SubscriptionCallback getCallback(@Nullable Bundle options) { + for (int i = 0; i < mOptionsList.size(); ++i) { + if (MediaBrowserCompatUtils.areSameOptions(mOptionsList.get(i), options)) { + return mCallbacks.get(i); + } + } + return null; + } + + public void putCallback(@Nullable Bundle options, SubscriptionCallback callback) { + for (int i = 0; i < mOptionsList.size(); ++i) { + if (MediaBrowserCompatUtils.areSameOptions(mOptionsList.get(i), options)) { + mCallbacks.set(i, callback); + return; + } + } + mCallbacks.add(callback); + mOptionsList.add(options); + } + } + + private static class CallbackHandler extends Handler { + private final WeakReference mCallbackImplRef; + @Nullable private WeakReference mCallbacksMessengerRef; + + CallbackHandler(MediaBrowserServiceCallbackImpl callbackImpl) { + super(); + mCallbackImplRef = new WeakReference<>(callbackImpl); + } + + @Override + @SuppressWarnings("deprecation") + public void handleMessage(Message msg) { + if (mCallbacksMessengerRef == null) { + return; + } + Messenger callbacksMessenger = mCallbacksMessengerRef.get(); + MediaBrowserServiceCallbackImpl serviceCallback = mCallbackImplRef.get(); + if (callbacksMessenger == null || serviceCallback == null) { + return; + } + Bundle data = msg.getData(); + MediaSessionCompat.ensureClassLoader(data); + + try { + switch (msg.what) { + case SERVICE_MSG_ON_CONNECT: + { + Bundle rootHints = data.getBundle(DATA_ROOT_HINTS); + MediaSessionCompat.ensureClassLoader(rootHints); + + serviceCallback.onServiceConnected( + callbacksMessenger, + data.getString(DATA_MEDIA_ITEM_ID), + LegacyParcelableUtil.convert( + data.getParcelable(DATA_MEDIA_SESSION_TOKEN), + MediaSessionCompat.Token.CREATOR), + rootHints); + break; + } + case SERVICE_MSG_ON_CONNECT_FAILED: + serviceCallback.onConnectionFailed(callbacksMessenger); + break; + case SERVICE_MSG_ON_LOAD_CHILDREN: + { + Bundle options = data.getBundle(DATA_OPTIONS); + MediaSessionCompat.ensureClassLoader(options); + + Bundle notifyChildrenChangedOptions = + data.getBundle(DATA_NOTIFY_CHILDREN_CHANGED_OPTIONS); + MediaSessionCompat.ensureClassLoader(notifyChildrenChangedOptions); + + serviceCallback.onLoadChildren( + callbacksMessenger, + data.getString(DATA_MEDIA_ITEM_ID), + LegacyParcelableUtil.convertList( + data.getParcelableArrayList(DATA_MEDIA_ITEM_LIST), MediaItem.CREATOR), + options, + notifyChildrenChangedOptions); + break; + } + default: + Log.w( + TAG, + "Unhandled message: " + + msg + + "\n Client version: " + + CLIENT_VERSION_CURRENT + + "\n Service version: " + + msg.arg1); + } + } catch (BadParcelableException e) { + // Do not print the exception here, since it is already done by the Parcel class. + Log.e(TAG, "Could not unparcel the data."); + // If an error happened while connecting, disconnect from the service. + if (msg.what == SERVICE_MSG_ON_CONNECT) { + serviceCallback.onConnectionFailed(callbacksMessenger); + } + } + } + + void setCallbacksMessenger(@Nullable Messenger callbacksMessenger) { + mCallbacksMessengerRef = new WeakReference<>(callbacksMessenger); + } + } + + private static class ServiceBinderWrapper { + private Messenger mMessenger; + @Nullable private Bundle mRootHints; + + public ServiceBinderWrapper(IBinder target, @Nullable Bundle rootHints) { + mMessenger = new Messenger(target); + mRootHints = rootHints; + } + + void connect(Context context, Messenger callbacksMessenger) throws RemoteException { + Bundle data = new Bundle(); + data.putString(DATA_PACKAGE_NAME, context.getPackageName()); + data.putInt(DATA_CALLING_PID, Process.myPid()); + data.putBundle(DATA_ROOT_HINTS, mRootHints); + sendRequest(CLIENT_MSG_CONNECT, data, callbacksMessenger); + } + + void disconnect(Messenger callbacksMessenger) throws RemoteException { + sendRequest(CLIENT_MSG_DISCONNECT, null, callbacksMessenger); + } + + void addSubscription( + String parentId, + IBinder callbackToken, + @Nullable Bundle options, + Messenger callbacksMessenger) + throws RemoteException { + Bundle data = new Bundle(); + data.putString(DATA_MEDIA_ITEM_ID, parentId); + data.putBinder(DATA_CALLBACK_TOKEN, callbackToken); + data.putBundle(DATA_OPTIONS, options); + sendRequest(CLIENT_MSG_ADD_SUBSCRIPTION, data, callbacksMessenger); + } + + void removeSubscription( + String parentId, @Nullable IBinder callbackToken, Messenger callbacksMessenger) + throws RemoteException { + Bundle data = new Bundle(); + data.putString(DATA_MEDIA_ITEM_ID, parentId); + data.putBinder(DATA_CALLBACK_TOKEN, callbackToken); + sendRequest(CLIENT_MSG_REMOVE_SUBSCRIPTION, data, callbacksMessenger); + } + + void getMediaItem(String mediaId, ResultReceiver receiver, Messenger callbacksMessenger) + throws RemoteException { + Bundle data = new Bundle(); + data.putString(DATA_MEDIA_ITEM_ID, mediaId); + data.putParcelable(DATA_RESULT_RECEIVER, receiver); + sendRequest(CLIENT_MSG_GET_MEDIA_ITEM, data, callbacksMessenger); + } + + void registerCallbackMessenger(Context context, Messenger callbackMessenger) + throws RemoteException { + Bundle data = new Bundle(); + data.putString(DATA_PACKAGE_NAME, context.getPackageName()); + data.putInt(DATA_CALLING_PID, Process.myPid()); + data.putBundle(DATA_ROOT_HINTS, mRootHints); + sendRequest(CLIENT_MSG_REGISTER_CALLBACK_MESSENGER, data, callbackMessenger); + } + + void unregisterCallbackMessenger(Messenger callbackMessenger) throws RemoteException { + sendRequest(CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER, null, callbackMessenger); + } + + void search( + String query, + @Nullable Bundle extras, + ResultReceiver receiver, + Messenger callbacksMessenger) + throws RemoteException { + Bundle data = new Bundle(); + data.putString(DATA_SEARCH_QUERY, query); + data.putBundle(DATA_SEARCH_EXTRAS, extras); + data.putParcelable(DATA_RESULT_RECEIVER, receiver); + sendRequest(CLIENT_MSG_SEARCH, data, callbacksMessenger); + } + + void sendCustomAction( + String action, + @Nullable Bundle extras, + ResultReceiver receiver, + Messenger callbacksMessenger) + throws RemoteException { + Bundle data = new Bundle(); + data.putString(DATA_CUSTOM_ACTION, action); + data.putBundle(DATA_CUSTOM_ACTION_EXTRAS, extras); + data.putParcelable(DATA_RESULT_RECEIVER, receiver); + sendRequest(CLIENT_MSG_SEND_CUSTOM_ACTION, data, callbacksMessenger); + } + + private void sendRequest(int what, @Nullable Bundle data, Messenger cbMessenger) + throws RemoteException { + Message msg = Message.obtain(); + msg.what = what; + msg.arg1 = CLIENT_VERSION_CURRENT; + if (data != null) { + msg.setData(data); + } + msg.replyTo = cbMessenger; + mMessenger.send(msg); + } + } + + @SuppressLint("RestrictedApi") + private static class ItemReceiver extends ResultReceiver { + private final String mMediaId; + private final ItemCallback mCallback; + + ItemReceiver(String mediaId, ItemCallback callback, Handler handler) { + super(handler); + mMediaId = mediaId; + mCallback = callback; + } + + @Override + @SuppressWarnings("deprecation") + protected void onReceiveResult(int resultCode, @Nullable Bundle resultData) { + if (resultData != null) { + resultData = MediaSessionCompat.unparcelWithClassLoader(resultData); + } + if (resultCode != MediaBrowserServiceCompat.RESULT_OK + || resultData == null + || !resultData.containsKey(MediaBrowserServiceCompat.KEY_MEDIA_ITEM)) { + mCallback.onError(mMediaId); + return; + } + MediaItem item = + LegacyParcelableUtil.convert( + resultData.getParcelable(MediaBrowserServiceCompat.KEY_MEDIA_ITEM), + MediaItem.CREATOR); + mCallback.onItemLoaded(item); + } + } + + @SuppressLint("RestrictedApi") + private static class SearchResultReceiver extends ResultReceiver { + private final String mQuery; + @Nullable private final Bundle mExtras; + private final SearchCallback mCallback; + + SearchResultReceiver( + String query, @Nullable Bundle extras, SearchCallback callback, Handler handler) { + super(handler); + mQuery = query; + mExtras = extras; + mCallback = callback; + } + + @Override + @SuppressWarnings("deprecation") + protected void onReceiveResult(int resultCode, @Nullable Bundle resultData) { + if (resultData != null) { + resultData = MediaSessionCompat.unparcelWithClassLoader(resultData); + } + if (resultCode != MediaBrowserServiceCompat.RESULT_OK + || resultData == null + || !resultData.containsKey(MediaBrowserServiceCompat.KEY_SEARCH_RESULTS)) { + mCallback.onError(mQuery, mExtras); + return; + } + Parcelable[] items = + resultData.getParcelableArray(MediaBrowserServiceCompat.KEY_SEARCH_RESULTS); + if (items != null) { + List results = new ArrayList<>(items.length); + for (Parcelable item : items) { + results.add(LegacyParcelableUtil.convert(item, MediaItem.CREATOR)); + } + mCallback.onSearchResult(mQuery, mExtras, results); + } else { + mCallback.onError(mQuery, mExtras); + } + } + } + + @SuppressLint("RestrictedApi") + private static class CustomActionResultReceiver extends ResultReceiver { + private final String mAction; + @Nullable private final Bundle mExtras; + @Nullable private final CustomActionCallback mCallback; + + CustomActionResultReceiver( + String action, + @Nullable Bundle extras, + @Nullable CustomActionCallback callback, + Handler handler) { + super(handler); + mAction = action; + mExtras = extras; + mCallback = callback; + } + + @Override + protected void onReceiveResult(int resultCode, @Nullable Bundle resultData) { + if (mCallback == null) { + return; + } + MediaSessionCompat.ensureClassLoader(resultData); + switch (resultCode) { + case MediaBrowserServiceCompat.RESULT_PROGRESS_UPDATE: + mCallback.onProgressUpdate(mAction, mExtras, resultData); + break; + case MediaBrowserServiceCompat.RESULT_OK: + mCallback.onResult(mAction, mExtras, resultData); + break; + case MediaBrowserServiceCompat.RESULT_ERROR: + mCallback.onError(mAction, mExtras, resultData); + break; + default: + Log.w( + TAG, + "Unknown result code: " + + resultCode + + " (extras=" + + mExtras + + ", resultData=" + + resultData + + ")"); + break; + } + } + } + + @RequiresApi(21) + private static class Api21Impl { + private Api21Impl() {} + + @DoNotInline + static MediaDescription getDescription(MediaBrowser.MediaItem item) { + return item.getDescription(); + } + + @DoNotInline + static int getFlags(MediaBrowser.MediaItem item) { + return item.getFlags(); + } + } +} diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserCompatUtils.java b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserCompatUtils.java new file mode 100644 index 0000000000..8021fc2243 --- /dev/null +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserCompatUtils.java @@ -0,0 +1,79 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.session.legacy; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY; +import static androidx.media3.common.util.Assertions.checkStateNotNull; + +import android.os.Bundle; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.media3.common.util.UnstableApi; + +/** */ +@UnstableApi +@RestrictTo(LIBRARY) +public class MediaBrowserCompatUtils { + public static boolean areSameOptions(@Nullable Bundle options1, @Nullable Bundle options2) { + if (options1 == options2) { + return true; + } else if (options1 == null) { + checkStateNotNull(options2); + return options2.getInt(MediaBrowserCompat.EXTRA_PAGE, -1) == -1 + && options2.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, -1) == -1; + } else if (options2 == null && options1 != null) { + checkStateNotNull(options1); + return options1.getInt(MediaBrowserCompat.EXTRA_PAGE, -1) == -1 + && options1.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, -1) == -1; + } else { + checkStateNotNull(options1); + checkStateNotNull(options2); + return options1.getInt(MediaBrowserCompat.EXTRA_PAGE, -1) + == options2.getInt(MediaBrowserCompat.EXTRA_PAGE, -1) + && options1.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, -1) + == options2.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, -1); + } + } + + public static boolean hasDuplicatedItems(@Nullable Bundle options1, @Nullable Bundle options2) { + int page1 = options1 == null ? -1 : options1.getInt(MediaBrowserCompat.EXTRA_PAGE, -1); + int page2 = options2 == null ? -1 : options2.getInt(MediaBrowserCompat.EXTRA_PAGE, -1); + int pageSize1 = options1 == null ? -1 : options1.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, -1); + int pageSize2 = options2 == null ? -1 : options2.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, -1); + + int startIndex1, startIndex2, endIndex1, endIndex2; + if (page1 == -1 || pageSize1 == -1) { + startIndex1 = 0; + endIndex1 = Integer.MAX_VALUE; + } else { + startIndex1 = pageSize1 * page1; + endIndex1 = startIndex1 + pageSize1 - 1; + } + + if (page2 == -1 || pageSize2 == -1) { + startIndex2 = 0; + endIndex2 = Integer.MAX_VALUE; + } else { + startIndex2 = pageSize2 * page2; + endIndex2 = startIndex2 + pageSize2 - 1; + } + + // For better readability, leaving the exclamation mark here. + return !(endIndex1 < startIndex2 || endIndex2 < startIndex1); + } + + private MediaBrowserCompatUtils() {} +} diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserProtocol.java b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserProtocol.java new file mode 100644 index 0000000000..cb32e85fa4 --- /dev/null +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserProtocol.java @@ -0,0 +1,181 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.session.legacy; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY; + +import android.os.Bundle; +import androidx.annotation.RestrictTo; +import androidx.media3.common.util.UnstableApi; + +/** Defines the communication protocol for media browsers and media browser services. */ +@UnstableApi +@RestrictTo(LIBRARY) +public class MediaBrowserProtocol { + + public static final String DATA_CALLBACK_TOKEN = "data_callback_token"; + public static final String DATA_CALLING_UID = "data_calling_uid"; + public static final String DATA_CALLING_PID = "data_calling_pid"; + public static final String DATA_MEDIA_ITEM_ID = "data_media_item_id"; + public static final String DATA_MEDIA_ITEM_LIST = "data_media_item_list"; + public static final String DATA_MEDIA_SESSION_TOKEN = "data_media_session_token"; + public static final String DATA_OPTIONS = "data_options"; + public static final String DATA_NOTIFY_CHILDREN_CHANGED_OPTIONS = + "data_notify_children_changed_options"; + public static final String DATA_PACKAGE_NAME = "data_package_name"; + public static final String DATA_RESULT_RECEIVER = "data_result_receiver"; + public static final String DATA_ROOT_HINTS = "data_root_hints"; + public static final String DATA_SEARCH_EXTRAS = "data_search_extras"; + public static final String DATA_SEARCH_QUERY = "data_search_query"; + public static final String DATA_CUSTOM_ACTION = "data_custom_action"; + public static final String DATA_CUSTOM_ACTION_EXTRAS = "data_custom_action_extras"; + + public static final String EXTRA_CLIENT_VERSION = "extra_client_version"; + public static final String EXTRA_CALLING_PID = "extra_calling_pid"; + public static final String EXTRA_SERVICE_VERSION = "extra_service_version"; + public static final String EXTRA_MESSENGER_BINDER = "extra_messenger"; + public static final String EXTRA_SESSION_BINDER = "extra_session_binder"; + + /** + * MediaBrowserCompat will check the version of the connected MediaBrowserServiceCompat, and it + * will not send messages if they are introduced in the higher version of the + * MediaBrowserServiceCompat. + */ + public static final int SERVICE_VERSION_1 = 1; + + /** + * To prevent ClassNotFoundException from Parcelables, MediaBrowser(Service)Compat tries to avoid + * using framework code as much as possible (b/62648808). For backward compatibility, service v2 + * is introduced so that the browser can distinguish whether the service supports subscribing + * through compat code. + */ + public static final int SERVICE_VERSION_2 = 2; + + public static final int SERVICE_VERSION_CURRENT = SERVICE_VERSION_2; + + /* + * Messages sent from the media browser service compat to the media browser compat. + * (Compat implementation for IMediaBrowserServiceCallbacks) + * DO NOT RENUMBER THESE! + */ + + /** + * (service v1) Sent after {@link MediaBrowserCompat#connect()} when the request has successfully + * completed. - arg1 : The service version - data DATA_MEDIA_ITEM_ID : A string for the root media + * item id DATA_MEDIA_SESSION_TOKEN : Media session token DATA_ROOT_HINTS : An optional root hints + * bundle of service-specific arguments + */ + public static final int SERVICE_MSG_ON_CONNECT = 1; + + /** + * (service v1) Sent after {@link MediaBrowserCompat#connect()} when the connection to the media + * browser failed. - arg1 : service version + */ + public static final int SERVICE_MSG_ON_CONNECT_FAILED = 2; + + /** + * (service v1) Sent when the list of children is loaded or updated. - arg1 : The service version + * - data DATA_MEDIA_ITEM_ID : A string for the parent media item id DATA_MEDIA_ITEM_LIST : An + * array list for the media item children DATA_OPTIONS : A bundle of service-specific arguments + * sent from the media browse to the media browser service DATA_NOTIFY_CHILDREN_CHANGED_OPTIONS : + * A bundle of service-specific arguments sent from the media browser service to the media browser + * by calling {@link MediaBrowserServiceCompat#notifyChildrenChanged(String, Bundle)} + */ + public static final int SERVICE_MSG_ON_LOAD_CHILDREN = 3; + + /** + * MediaBrowserServiceCompat will check the version of the MediaBrowserCompat, and it will not + * send messages if they are introduced in the higher version of the MediaBrowserCompat. + */ + public static final int CLIENT_VERSION_1 = 1; + + public static final int CLIENT_VERSION_CURRENT = CLIENT_VERSION_1; + + /* + * Messages sent from the media browser compat to the media browser service compat. + * (Compat implementation for IMediaBrowserService) + * DO NOT RENUMBER THESE! + */ + + /** + * (client v1) Sent to connect to the media browse service compat. - arg1 : The client version - + * data DATA_PACKAGE_NAME : A string for the package name of MediaBrowserCompat DATA_ROOT_HINTS : + * An optional root hints bundle of service-specific arguments - replyTo : Callback messenger + */ + public static final int CLIENT_MSG_CONNECT = 1; + + /** + * (client v1) Sent to disconnect from the media browse service compat. - arg1 : The client + * version - replyTo : Callback messenger + */ + public static final int CLIENT_MSG_DISCONNECT = 2; + + /** + * (client v1) Sent to subscribe for changes to the children of the specified media id. - arg1 : + * The client version - data DATA_MEDIA_ITEM_ID : A string for a media item id DATA_OPTIONS : A + * bundle of service-specific arguments sent from the media browser to the media browser service + * DATA_CALLBACK_TOKEN : An IBinder of service-specific arguments sent from the media browser to + * the media browser service - replyTo : Callback messenger + */ + public static final int CLIENT_MSG_ADD_SUBSCRIPTION = 3; + + /** + * (client v1) Sent to unsubscribe for changes to the children of the specified media id. - arg1 : + * The client version - data DATA_MEDIA_ITEM_ID : A string for a media item id DATA_CALLBACK_TOKEN + * : An IBinder of service-specific arguments sent from the media browser to the media browser + * service - replyTo : Callback messenger + */ + public static final int CLIENT_MSG_REMOVE_SUBSCRIPTION = 4; + + /** + * (client v1) Sent to retrieve a specific media item from the connected service. - arg1 : The + * client version - data DATA_MEDIA_ITEM_ID : A string for a media item id DATA_RESULT_RECEIVER : + * Result receiver to get the result - replyTo : Callback messenger + */ + public static final int CLIENT_MSG_GET_MEDIA_ITEM = 5; + + /** + * (client v1) Sent to register the client messenger - arg1 : The client version - data + * DATA_ROOT_HINTS : An optional root hints bundle of service-specific arguments - replyTo : + * Callback messenger + */ + public static final int CLIENT_MSG_REGISTER_CALLBACK_MESSENGER = 6; + + /** + * (client v1) Sent to unregister the client messenger - arg1 : The client version - replyTo : + * Callback messenger + */ + public static final int CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER = 7; + + /** + * (client v1) Sent to retrieve a specific media item from the connected service. - arg1 : The + * client version - data DATA_SEARCH_QUERY : A string for search query that contains keywords + * separated by space. DATA_SEARCH_EXTRAS : A bundle of service-specific arguments to send to the + * media browser service. DATA_RESULT_RECEIVER : Result receiver to get the result. - replyTo : + * Callback messenger + */ + public static final int CLIENT_MSG_SEARCH = 8; + + /** + * (client v1) Sent to request a custom action from the media browser. - arg1 : The client version + * - data DATA_CUSTOM_ACTION : A string for the custom action. DATA_CUSTOM_ACTION_EXTRAS : A + * bundle of service-specific arguments to send to the media browser service. DATA_RESULT_RECEIVER + * : Result receiver to get the result. - replyTo : Callback messenger + */ + public static final int CLIENT_MSG_SEND_CUSTOM_ACTION = 9; + + private MediaBrowserProtocol() {} +} diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserServiceCompat.java b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserServiceCompat.java new file mode 100644 index 0000000000..7f670a52d7 --- /dev/null +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaBrowserServiceCompat.java @@ -0,0 +1,2098 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.session.legacy; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY; +import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.session.legacy.MediaBrowserProtocol.CLIENT_MSG_ADD_SUBSCRIPTION; +import static androidx.media3.session.legacy.MediaBrowserProtocol.CLIENT_MSG_CONNECT; +import static androidx.media3.session.legacy.MediaBrowserProtocol.CLIENT_MSG_DISCONNECT; +import static androidx.media3.session.legacy.MediaBrowserProtocol.CLIENT_MSG_GET_MEDIA_ITEM; +import static androidx.media3.session.legacy.MediaBrowserProtocol.CLIENT_MSG_REGISTER_CALLBACK_MESSENGER; +import static androidx.media3.session.legacy.MediaBrowserProtocol.CLIENT_MSG_REMOVE_SUBSCRIPTION; +import static androidx.media3.session.legacy.MediaBrowserProtocol.CLIENT_MSG_SEARCH; +import static androidx.media3.session.legacy.MediaBrowserProtocol.CLIENT_MSG_SEND_CUSTOM_ACTION; +import static androidx.media3.session.legacy.MediaBrowserProtocol.CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER; +import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_CALLBACK_TOKEN; +import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_CALLING_PID; +import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_CALLING_UID; +import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_CUSTOM_ACTION; +import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_CUSTOM_ACTION_EXTRAS; +import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_MEDIA_ITEM_ID; +import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_MEDIA_ITEM_LIST; +import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_MEDIA_SESSION_TOKEN; +import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_NOTIFY_CHILDREN_CHANGED_OPTIONS; +import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_OPTIONS; +import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_PACKAGE_NAME; +import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_RESULT_RECEIVER; +import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_ROOT_HINTS; +import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_SEARCH_EXTRAS; +import static androidx.media3.session.legacy.MediaBrowserProtocol.DATA_SEARCH_QUERY; +import static androidx.media3.session.legacy.MediaBrowserProtocol.EXTRA_CALLING_PID; +import static androidx.media3.session.legacy.MediaBrowserProtocol.EXTRA_CLIENT_VERSION; +import static androidx.media3.session.legacy.MediaBrowserProtocol.EXTRA_MESSENGER_BINDER; +import static androidx.media3.session.legacy.MediaBrowserProtocol.EXTRA_SERVICE_VERSION; +import static androidx.media3.session.legacy.MediaBrowserProtocol.EXTRA_SESSION_BINDER; +import static androidx.media3.session.legacy.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT; +import static androidx.media3.session.legacy.MediaBrowserProtocol.SERVICE_MSG_ON_CONNECT_FAILED; +import static androidx.media3.session.legacy.MediaBrowserProtocol.SERVICE_MSG_ON_LOAD_CHILDREN; +import static androidx.media3.session.legacy.MediaBrowserProtocol.SERVICE_VERSION_CURRENT; +import static androidx.media3.session.legacy.MediaSessionManager.RemoteUserInfo.LEGACY_CONTROLLER; +import static androidx.media3.session.legacy.MediaSessionManager.RemoteUserInfo.UNKNOWN_PID; +import static androidx.media3.session.legacy.MediaSessionManager.RemoteUserInfo.UNKNOWN_UID; + +import android.annotation.SuppressLint; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.media.browse.MediaBrowser; +import android.media.session.MediaSession; +import android.os.Binder; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Messenger; +import android.os.Parcel; +import android.os.RemoteException; +import android.service.media.MediaBrowserService; +import android.support.v4.os.ResultReceiver; +import android.text.TextUtils; +import android.util.Log; +import androidx.annotation.CallSuper; +import androidx.annotation.IntDef; +import androidx.annotation.MainThread; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.RestrictTo; +import androidx.collection.ArrayMap; +import androidx.core.util.Pair; +import androidx.media3.common.util.NullableType; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.legacy.MediaSessionManager.RemoteUserInfo; +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * Base class for media browse services. + * + *

Media browse services enable applications to browse media content provided by an application + * and ask the application to start playing it. They may also be used to control content that is + * already playing by way of a {@link MediaSessionCompat}. To extend this class, you must declare + * the service in your manifest file with an intent filter with the {@link #SERVICE_INTERFACE} + * action. + * + *

For example: + * + *

+ * <service android:name=".MyMediaBrowserServiceCompat"
+ *          android:label="@string/service_name" >
+ *     <intent-filter>
+ *         <action android:name="android.media.browse.MediaBrowserService" />
+ *     </intent-filter>
+ * </service>
+ * 
+ * + *
+ * + *

Developer Guides

+ * + *

For information about building your media application, read the Media Apps developer guide.

+ */ +@UnstableApi +@RestrictTo(LIBRARY) +public abstract class MediaBrowserServiceCompat extends Service { + static final String TAG = "MBServiceCompat"; + static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private static final float EPSILON = 0.00001f; + + private @MonotonicNonNull MediaBrowserServiceImpl mImpl; + + /** The {@link Intent} that must be declared as handled by the service. */ + public static final String SERVICE_INTERFACE = "android.media.browse.MediaBrowserService"; + + /** A key for passing the MediaItem to the ResultReceiver in getItem. */ + public static final String KEY_MEDIA_ITEM = "media_item"; + + /** A key for passing the list of MediaItems to the ResultReceiver in search. */ + public static final String KEY_SEARCH_RESULTS = "search_results"; + + static final int RESULT_FLAG_OPTION_NOT_HANDLED = 1 << 0; + static final int RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED = 1 << 1; + static final int RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED = 1 << 2; + + /** */ + public static final int RESULT_ERROR = -1; + + /** */ + public static final int RESULT_OK = 0; + + /** */ + public static final int RESULT_PROGRESS_UPDATE = 1; + + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = { + RESULT_FLAG_OPTION_NOT_HANDLED, + RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED, + RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED + }) + private @interface ResultFlags {} + + private final ServiceBinderImpl mServiceBinderImpl = new ServiceBinderImpl(); + final ConnectionRecord mConnectionFromFwk = + new ConnectionRecord(LEGACY_CONTROLLER, UNKNOWN_PID, UNKNOWN_UID, null, null); + final ArrayList mPendingConnections = new ArrayList<>(); + final ArrayMap mConnections = new ArrayMap<>(); + @Nullable ConnectionRecord mCurConnection; + + @SuppressWarnings({ + "argument.type.incompatible", + "assignment.type.incompatible" + }) // Using this before construtor completes + final ServiceHandler mHandler = new ServiceHandler(/* service= */ this); + + @Nullable MediaSessionCompat.Token mSession; + + interface MediaBrowserServiceImpl { + void onCreate(); + + @Nullable + IBinder onBind(Intent intent); + + void setSessionToken(MediaSessionCompat.Token token); + + void notifyChildrenChanged(String parentId, @Nullable Bundle options); + + void notifyChildrenChanged(RemoteUserInfo remoteUserInfo, String parentId, Bundle options); + + @Nullable + Bundle getBrowserRootHints(); + + RemoteUserInfo getCurrentBrowserInfo(); + } + + class MediaBrowserServiceImplBase implements MediaBrowserServiceImpl { + private @MonotonicNonNull Messenger mMessenger; + + @Override + public void onCreate() { + mMessenger = new Messenger(mHandler); + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + if (SERVICE_INTERFACE.equals(intent.getAction())) { + return checkNotNull(mMessenger).getBinder(); + } + return null; + } + + @Override + public void setSessionToken(final MediaSessionCompat.Token token) { + mHandler.post( + new Runnable() { + @Override + public void run() { + Iterator iter = mConnections.values().iterator(); + while (iter.hasNext()) { + ConnectionRecord connection = iter.next(); + try { + BrowserRoot root = checkNotNull(connection.root); + checkNotNull(connection.callbacks) + .onConnect(root.getRootId(), token, root.getExtras()); + } catch (RemoteException e) { + Log.w(TAG, "Connection for " + connection.pkg + " is no longer valid."); + iter.remove(); + } + } + } + }); + } + + @Override + public void notifyChildrenChanged(String parentId, @Nullable Bundle options) { + mHandler.post( + new Runnable() { + @Override + public void run() { + for (IBinder binder : mConnections.keySet()) { + ConnectionRecord connection = checkNotNull(mConnections.get(binder)); + notifyChildrenChangedOnHandler(connection, parentId, options); + } + } + }); + } + + @Override + public void notifyChildrenChanged( + final RemoteUserInfo remoteUserInfo, final String parentId, final Bundle options) { + mHandler.post( + new Runnable() { + @Override + public void run() { + for (int i = 0; i < mConnections.size(); i++) { + ConnectionRecord connection = mConnections.valueAt(i); + if (connection.browserInfo.equals(remoteUserInfo)) { + notifyChildrenChangedOnHandler(connection, parentId, options); + break; + } + } + } + }); + } + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + void notifyChildrenChangedOnHandler( + ConnectionRecord connection, String parentId, @Nullable Bundle options) { + List> callbackList = + connection.subscriptions.get(parentId); + if (callbackList != null) { + for (Pair<@NullableType IBinder, @NullableType Bundle> callback : callbackList) { + if (MediaBrowserCompatUtils.hasDuplicatedItems(options, callback.second)) { + performLoadChildren(parentId, connection, callback.second, options); + } + } + } + // Don't break, because multiple remoteUserInfo may match. + } + + @Nullable + @Override + public Bundle getBrowserRootHints() { + if (mCurConnection == null) { + throw new IllegalStateException( + "This should be called inside of onLoadChildren," + + " onLoadItem, onSearch, or onCustomAction methods"); + } + return mCurConnection.rootHints == null ? null : new Bundle(mCurConnection.rootHints); + } + + @Override + public RemoteUserInfo getCurrentBrowserInfo() { + if (mCurConnection == null) { + throw new IllegalStateException( + "This should be called inside of onLoadChildren," + + " onLoadItem, onSearch, or onCustomAction methods"); + } + return mCurConnection.browserInfo; + } + } + + @RequiresApi(21) + class MediaBrowserServiceImplApi21 implements MediaBrowserServiceImpl { + final List mRootExtrasList = new ArrayList<>(); + @MonotonicNonNull MediaBrowserService mServiceFwk; + @MonotonicNonNull Messenger mMessenger; + + @Override + public void onCreate() { + mServiceFwk = new MediaBrowserServiceApi21(MediaBrowserServiceCompat.this); + mServiceFwk.onCreate(); + } + + @Override + public IBinder onBind(Intent intent) { + return checkNotNull(mServiceFwk).onBind(intent); + } + + @Override + public void setSessionToken(final MediaSessionCompat.Token token) { + mHandler.postOrRun( + new Runnable() { + @Override + public void run() { + setSessionTokenOnHandler(token); + } + }); + } + + void setSessionTokenOnHandler(MediaSessionCompat.Token token) { + if (!mRootExtrasList.isEmpty()) { + IMediaSession extraBinder = token.getExtraBinder(); + if (extraBinder != null) { + for (Bundle rootExtras : mRootExtrasList) { + rootExtras.putBinder(EXTRA_SESSION_BINDER, extraBinder.asBinder()); + } + } + mRootExtrasList.clear(); + } + checkNotNull(mServiceFwk) + .setSessionToken(checkNotNull((MediaSession.Token) token.getToken())); + } + + @Override + public void notifyChildrenChanged(String parentId, @Nullable Bundle options) { + notifyChildrenChangedForFramework(parentId, options); + notifyChildrenChangedForCompat(parentId, options); + } + + @Override + public void notifyChildrenChanged( + final RemoteUserInfo remoteUserInfo, final String parentId, final Bundle options) { + // TODO(Post-P): Need a way to notify to a specific browser in framework. + notifyChildrenChangedForCompat(remoteUserInfo, parentId, options); + } + + @Nullable + public BrowserRoot onGetRoot( + String clientPackageName, int clientUid, @Nullable Bundle rootHints) { + Bundle rootExtras = null; + int clientPid = UNKNOWN_PID; + if (rootHints != null && rootHints.getInt(EXTRA_CLIENT_VERSION, 0) != 0) { + rootHints.remove(EXTRA_CLIENT_VERSION); + mMessenger = new Messenger(mHandler); + rootExtras = new Bundle(); + rootExtras.putInt(EXTRA_SERVICE_VERSION, SERVICE_VERSION_CURRENT); + rootExtras.putBinder(EXTRA_MESSENGER_BINDER, mMessenger.getBinder()); + if (mSession != null) { + IMediaSession extraBinder = mSession.getExtraBinder(); + rootExtras.putBinder( + EXTRA_SESSION_BINDER, extraBinder == null ? null : extraBinder.asBinder()); + } else { + mRootExtrasList.add(rootExtras); + } + clientPid = rootHints.getInt(EXTRA_CALLING_PID, UNKNOWN_PID); + rootHints.remove(EXTRA_CALLING_PID); + } + ConnectionRecord connection = + new ConnectionRecord(clientPackageName, clientPid, clientUid, rootHints, null); + // We aren't sure whether this connection request would be accepted. + // Temporarily set mCurConnection just to make getCurrentBrowserInfo() working. + mCurConnection = connection; + BrowserRoot root = + MediaBrowserServiceCompat.this.onGetRoot(clientPackageName, clientUid, rootHints); + mCurConnection = null; + if (root == null) { + return null; + } + if (mMessenger != null) { + // Keeps the connection request from the MediaBrowserCompat to reuse the package + // name here. + // Note: Connection will be completed after it gets extra binder call with + // CLIENT_MSG_REGISTER_CALLBACK_MESSENGER. + mPendingConnections.add(connection); + } + @Nullable Bundle existingRootExtras = root.getExtras(); + if (rootExtras == null) { + rootExtras = existingRootExtras; + } else if (existingRootExtras != null) { + rootExtras.putAll(existingRootExtras); + } + return new BrowserRoot(root.getRootId(), rootExtras); + } + + public void onLoadChildren(String parentId, final ResultWrapper> resultWrapper) { + final Result> result = + new Result>(parentId) { + @Override + void onResultSent(@Nullable List list) { + List parcelList; + if (list == null) { + // A null children list here indicates that the requested parentId + // is invalid. Unfortunately before API 24 the platform + // MediaBrowserService's implementation of Result inside + // performLoadChildren (invoked below) throws an exception if + // given a null list (b/19127753). This means there's no clear + // way to communicate an invalid parentId, so in order to avoid + // an exception below API 24 we transform null to an empty list + // here (meaning it looks like parentId is valid but has no + // children). + parcelList = Build.VERSION.SDK_INT >= 24 ? null : Collections.emptyList(); + } else { + parcelList = new ArrayList<>(list.size()); + for (MediaBrowserCompat.MediaItem item : list) { + Parcel parcel = Parcel.obtain(); + item.writeToParcel(parcel, 0); + parcelList.add(parcel); + } + } + resultWrapper.sendResult(parcelList); + } + + @Override + public void detach() { + resultWrapper.detach(); + } + }; + mCurConnection = mConnectionFromFwk; + MediaBrowserServiceCompat.this.onLoadChildren(parentId, result); + mCurConnection = null; + } + + void notifyChildrenChangedForFramework(String parentId, @Nullable Bundle options) { + checkNotNull(mServiceFwk).notifyChildrenChanged(parentId); + } + + void notifyChildrenChangedForCompat(String parentId, @Nullable Bundle options) { + mHandler.post( + new Runnable() { + @Override + public void run() { + for (IBinder binder : mConnections.keySet()) { + ConnectionRecord connection = checkNotNull(mConnections.get(binder)); + notifyChildrenChangedForCompatOnHandler(connection, parentId, options); + } + } + }); + } + + void notifyChildrenChangedForCompat( + final RemoteUserInfo remoteUserInfo, final String parentId, final Bundle options) { + mHandler.post( + new Runnable() { + @Override + public void run() { + for (int i = 0; i < mConnections.size(); i++) { + ConnectionRecord connection = mConnections.valueAt(i); + if (connection.browserInfo.equals(remoteUserInfo)) { + notifyChildrenChangedForCompatOnHandler(connection, parentId, options); + } + } + } + }); + } + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + void notifyChildrenChangedForCompatOnHandler( + ConnectionRecord connection, String parentId, @Nullable Bundle options) { + List> callbackList = + connection.subscriptions.get(parentId); + if (callbackList != null) { + for (Pair<@NullableType IBinder, @NullableType Bundle> callback : callbackList) { + if (MediaBrowserCompatUtils.hasDuplicatedItems(options, callback.second)) { + performLoadChildren(parentId, connection, callback.second, options); + } + } + } + } + + @Nullable + @Override + public Bundle getBrowserRootHints() { + if (mMessenger == null) { + // TODO: Handle getBrowserRootHints when connected with framework MediaBrowser. + return null; + } + if (mCurConnection == null) { + throw new IllegalStateException( + "This should be called inside of onGetRoot," + + " onLoadChildren, onLoadItem, onSearch, or onCustomAction methods"); + } + return mCurConnection.rootHints == null ? null : new Bundle(mCurConnection.rootHints); + } + + @Override + public RemoteUserInfo getCurrentBrowserInfo() { + if (mCurConnection == null) { + throw new IllegalStateException( + "This should be called inside of onGetRoot," + + " onLoadChildren, onLoadItem, onSearch, or onCustomAction methods"); + } + return mCurConnection.browserInfo; + } + + @RequiresApi(21) + class MediaBrowserServiceApi21 extends MediaBrowserService { + @SuppressWarnings("method.invocation.invalid") // Calling base method from constructor + MediaBrowserServiceApi21(Context context) { + attachBaseContext(context); + } + + @Nullable + @Override + public MediaBrowserService.BrowserRoot onGetRoot( + String clientPackageName, int clientUid, @Nullable Bundle rootHints) { + MediaSessionCompat.ensureClassLoader(rootHints); + MediaBrowserServiceCompat.BrowserRoot browserRootCompat = + MediaBrowserServiceImplApi21.this.onGetRoot( + clientPackageName, clientUid, rootHints == null ? null : new Bundle(rootHints)); + return browserRootCompat == null + ? null + : new MediaBrowserService.BrowserRoot( + browserRootCompat.mRootId, browserRootCompat.mExtras); + } + + @Override + public void onLoadChildren(String parentId, Result> result) { + MediaBrowserServiceImplApi21.this.onLoadChildren( + parentId, new ResultWrapper>(result)); + } + } + } + + @RequiresApi(23) + class MediaBrowserServiceImplApi23 extends MediaBrowserServiceImplApi21 { + @Override + public void onCreate() { + mServiceFwk = new MediaBrowserServiceApi23(MediaBrowserServiceCompat.this); + mServiceFwk.onCreate(); + } + + public void onLoadItem(String itemId, final ResultWrapper resultWrapper) { + final Result result = + new Result(itemId) { + @Override + void onResultSent(@Nullable MediaBrowserCompat.MediaItem item) { + if (item == null) { + resultWrapper.sendResult(null); + } else { + Parcel parcelItem = Parcel.obtain(); + item.writeToParcel(parcelItem, 0); + resultWrapper.sendResult(parcelItem); + } + } + + @Override + public void detach() { + resultWrapper.detach(); + } + }; + mCurConnection = mConnectionFromFwk; + MediaBrowserServiceCompat.this.onLoadItem(itemId, result); + mCurConnection = null; + } + + class MediaBrowserServiceApi23 extends MediaBrowserServiceApi21 { + MediaBrowserServiceApi23(Context context) { + super(context); + } + + @Override + public void onLoadItem(String itemId, Result result) { + MediaBrowserServiceImplApi23.this.onLoadItem(itemId, new ResultWrapper(result)); + } + } + } + + @RequiresApi(26) + class MediaBrowserServiceImplApi26 extends MediaBrowserServiceImplApi23 { + @Override + public void onCreate() { + mServiceFwk = new MediaBrowserServiceApi26(MediaBrowserServiceCompat.this); + mServiceFwk.onCreate(); + } + + public void onLoadChildren( + String parentId, final ResultWrapper> resultWrapper, final Bundle options) { + final Result> result = + new Result>(parentId) { + @Override + void onResultSent(@Nullable List list) { + if (list == null) { + resultWrapper.sendResult(null); + return; + } + if ((getFlags() & RESULT_FLAG_OPTION_NOT_HANDLED) != 0) { + // If onLoadChildren(options) is not overridden, the list we get + // here is not paginated. Therefore, we need to manually cut + // the list. In other words, we need to apply options here. + list = applyOptions(list, options); + } + List parcelList = new ArrayList<>(list == null ? 0 : list.size()); + if (list != null) { + for (MediaBrowserCompat.MediaItem item : list) { + Parcel parcel = Parcel.obtain(); + item.writeToParcel(parcel, 0); + parcelList.add(parcel); + } + } + resultWrapper.sendResult(parcelList); + } + + @Override + public void detach() { + resultWrapper.detach(); + } + }; + mCurConnection = mConnectionFromFwk; + MediaBrowserServiceCompat.this.onLoadChildren(parentId, result, options); + mCurConnection = null; + } + + @Nullable + @Override + public Bundle getBrowserRootHints() { + if (mCurConnection == null) { + throw new IllegalStateException( + "This should be called inside of onGetRoot," + + " onLoadChildren, onLoadItem, onSearch, or onCustomAction methods"); + } + if (mCurConnection == mConnectionFromFwk) { + return checkNotNull(mServiceFwk).getBrowserRootHints(); + } + return mCurConnection.rootHints == null ? null : new Bundle(mCurConnection.rootHints); + } + + @Override + void notifyChildrenChangedForFramework(final String parentId, @Nullable Bundle options) { + if (options != null) { + checkNotNull(mServiceFwk).notifyChildrenChanged(parentId, options); + } else { + super.notifyChildrenChangedForFramework(parentId, options); + } + } + + class MediaBrowserServiceApi26 extends MediaBrowserServiceApi23 { + MediaBrowserServiceApi26(Context context) { + super(context); + } + + @Override + public void onLoadChildren( + String parentId, Result> result, Bundle options) { + MediaSessionCompat.ensureClassLoader(options); + mCurConnection = mConnectionFromFwk; + MediaBrowserServiceImplApi26.this.onLoadChildren( + parentId, new ResultWrapper>(result), options); + mCurConnection = null; + } + } + } + + @RequiresApi(28) + class MediaBrowserServiceImplApi28 extends MediaBrowserServiceImplApi26 { + @Override + public RemoteUserInfo getCurrentBrowserInfo() { + if (mCurConnection == null) { + throw new IllegalStateException( + "This should be called inside of onGetRoot," + + " onLoadChildren, onLoadItem, onSearch, or onCustomAction methods"); + } + if (mCurConnection == mConnectionFromFwk) { + return new RemoteUserInfo(checkNotNull(mServiceFwk).getCurrentBrowserInfo()); + } + return mCurConnection.browserInfo; + } + } + + private static final class ServiceHandler extends Handler { + + // Must only be accessed on the main thread. + @Nullable private MediaBrowserServiceCompat mService; + + @MainThread + ServiceHandler(MediaBrowserServiceCompat service) { + mService = service; + } + + @Override + @MainThread + public void handleMessage(Message msg) { + if (mService != null) { + mService.handleMessageInternal(msg); + } else { + removeCallbacksAndMessages(/* token= */ null); + } + } + + @MainThread + public void release() { + mService = null; + } + + @Override + public boolean sendMessageAtTime(Message msg, long uptimeMillis) { + // Binder.getCallingUid() in handleMessage will return the uid of this process. + // In order to get the right calling uid, Binder.getCallingUid() should be called here. + Bundle data = msg.getData(); + data.setClassLoader(checkNotNull(MediaBrowserCompat.class.getClassLoader())); + data.putInt(DATA_CALLING_UID, Binder.getCallingUid()); + int pid = Binder.getCallingPid(); + if (pid > 0) { + data.putInt(DATA_CALLING_PID, pid); + } else if (!data.containsKey(DATA_CALLING_PID)) { + // If the MediaBrowserCompat didn't send its PID, then put UNKNOWN_PID. + data.putInt(DATA_CALLING_PID, UNKNOWN_PID); + } + return super.sendMessageAtTime(msg, uptimeMillis); + } + + public void postOrRun(Runnable r) { + if (Thread.currentThread() == getLooper().getThread()) { + r.run(); + } else { + post(r); + } + } + } + + /** All the info about a connection. */ + private class ConnectionRecord implements IBinder.DeathRecipient { + @Nullable public final String pkg; + public final int pid; + public final int uid; + public final RemoteUserInfo browserInfo; + @Nullable public final Bundle rootHints; + @Nullable public final ServiceCallbacks callbacks; + public final HashMap< + @NullableType String, List>> + subscriptions = new HashMap<>(); + @Nullable public BrowserRoot root; + + ConnectionRecord( + @Nullable String pkg, + int pid, + int uid, + @Nullable Bundle rootHints, + @Nullable ServiceCallbacks callback) { + this.pkg = pkg; + this.pid = pid; + this.uid = uid; + this.browserInfo = new RemoteUserInfo(pkg, pid, uid); + this.rootHints = rootHints; + this.callbacks = callback; + } + + @Override + public void binderDied() { + mHandler.post( + new Runnable() { + @Override + public void run() { + mConnections.remove(checkNotNull(callbacks).asBinder()); + } + }); + } + } + + /** + * Completion handler for asynchronous callback methods in {@link MediaBrowserServiceCompat}. + * + *

Each of the methods that takes one of these to send the result must call either {@link + * #sendResult} or {@link #sendError} to respond to the caller with the given results or errors. + * If those functions return without calling {@link #sendResult} or {@link #sendError}, they must + * instead call {@link #detach} before returning, and then may call {@link #sendResult} or {@link + * #sendError} when they are done. If {@link #sendResult}, {@link #sendError}, or {@link #detach} + * is called twice, an exception will be thrown. + * + *

Those functions might also want to call {@link #sendProgressUpdate} to send interim updates + * to the caller. If it is called after calling {@link #sendResult} or {@link #sendError}, an + * exception will be thrown. + * + * @see MediaBrowserServiceCompat#onLoadChildren + * @see MediaBrowserServiceCompat#onLoadItem + * @see MediaBrowserServiceCompat#onSearch + * @see MediaBrowserServiceCompat#onCustomAction + */ + public static class Result { + @Nullable private final Object mDebug; + private boolean mDetachCalled; + private boolean mSendResultCalled; + private boolean mSendErrorCalled; + private int mFlags; + + Result(@Nullable Object debug) { + mDebug = debug; + } + + /** Send the result back to the caller. */ + public void sendResult(@Nullable T result) { + if (mSendResultCalled || mSendErrorCalled) { + throw new IllegalStateException( + "sendResult() called when either sendResult() or " + + "sendError() had already been called for: " + + mDebug); + } + mSendResultCalled = true; + onResultSent(result); + } + + /** + * Send an interim update to the caller. This method is supported only when it is used in {@link + * #onCustomAction}. + * + * @param extras A bundle that contains extra data. + */ + public void sendProgressUpdate(@Nullable Bundle extras) { + if (mSendResultCalled || mSendErrorCalled) { + throw new IllegalStateException( + "sendProgressUpdate() called when either " + + "sendResult() or sendError() had already been called for: " + + mDebug); + } + checkExtraFields(extras); + onProgressUpdateSent(extras); + } + + /** + * Notify the caller of a failure. This is supported only when it is used in {@link + * #onCustomAction}. + * + * @param extras A bundle that contains extra data. + */ + public void sendError(@Nullable Bundle extras) { + if (mSendResultCalled || mSendErrorCalled) { + throw new IllegalStateException( + "sendError() called when either sendResult() or " + + "sendError() had already been called for: " + + mDebug); + } + mSendErrorCalled = true; + onErrorSent(extras); + } + + /** + * Detach this message from the current thread and allow the {@link #sendResult} call to happen + * later. + */ + public void detach() { + if (mDetachCalled) { + throw new IllegalStateException( + "detach() called when detach() had already" + " been called for: " + mDebug); + } + if (mSendResultCalled) { + throw new IllegalStateException( + "detach() called when sendResult() had already" + " been called for: " + mDebug); + } + if (mSendErrorCalled) { + throw new IllegalStateException( + "detach() called when sendError() had already" + " been called for: " + mDebug); + } + mDetachCalled = true; + } + + boolean isDone() { + return mDetachCalled || mSendResultCalled || mSendErrorCalled; + } + + void setFlags(@ResultFlags int flags) { + mFlags = flags; + } + + int getFlags() { + return mFlags; + } + + /** + * Called when the result is sent, after assertions about not being called twice have happened. + */ + void onResultSent(@Nullable T result) {} + + /** Called when an interim update is sent. */ + void onProgressUpdateSent(@Nullable Bundle extras) { + throw new UnsupportedOperationException( + "It is not supported to send an interim update " + "for " + mDebug); + } + + /** + * Called when an error is sent, after assertions about not being called twice have happened. + */ + void onErrorSent(@Nullable Bundle extras) { + throw new UnsupportedOperationException("It is not supported to send an error for " + mDebug); + } + + private void checkExtraFields(@Nullable Bundle extras) { + if (extras == null) { + return; + } + if (extras.containsKey(MediaBrowserCompat.EXTRA_DOWNLOAD_PROGRESS)) { + float value = extras.getFloat(MediaBrowserCompat.EXTRA_DOWNLOAD_PROGRESS); + if (value < -EPSILON || value > 1.0f + EPSILON) { + throw new IllegalArgumentException( + "The value of the EXTRA_DOWNLOAD_PROGRESS " + + "field must be a float number within [0.0, 1.0]"); + } + } + } + } + + private class ServiceBinderImpl { + ServiceBinderImpl() {} + + public void connect( + @Nullable String pkg, + int pid, + int uid, + @Nullable Bundle rootHints, + ServiceCallbacks callbacks) { + + if (!isValidPackage(pkg, uid)) { + throw new IllegalArgumentException("Package/uid mismatch: uid=" + uid + " package=" + pkg); + } + + mHandler.postOrRun( + new Runnable() { + @Override + public void run() { + final IBinder b = callbacks.asBinder(); + + // Clear out the old subscriptions. We are getting new ones. + mConnections.remove(b); + + ConnectionRecord connection = + new ConnectionRecord(pkg, pid, uid, rootHints, callbacks); + mCurConnection = connection; + BrowserRoot root = MediaBrowserServiceCompat.this.onGetRoot(pkg, uid, rootHints); + connection.root = root; + mCurConnection = null; + + // If they didn't return something, don't allow this client. + if (root == null) { + Log.i(TAG, "No root for client " + pkg + " from service " + getClass().getName()); + try { + callbacks.onConnectFailed(); + } catch (RemoteException ex) { + Log.w(TAG, "Calling onConnectFailed() failed. Ignoring. " + "pkg=" + pkg); + } + } else { + try { + mConnections.put(b, connection); + b.linkToDeath(connection, 0); + if (mSession != null) { + callbacks.onConnect(root.getRootId(), mSession, root.getExtras()); + } + } catch (RemoteException ex) { + Log.w(TAG, "Calling onConnect() failed. Dropping client. " + "pkg=" + pkg); + mConnections.remove(b); + } + } + } + }); + } + + public void disconnect(ServiceCallbacks callbacks) { + mHandler.postOrRun( + new Runnable() { + @Override + public void run() { + final IBinder b = callbacks.asBinder(); + + // Clear out the old subscriptions. We are getting new ones. + final ConnectionRecord old = mConnections.remove(b); + if (old != null) { + // TODO + checkNotNull(old.callbacks).asBinder().unlinkToDeath(old, 0); + } + } + }); + } + + public void addSubscription( + @Nullable String id, + @Nullable IBinder token, + @Nullable Bundle options, + ServiceCallbacks callbacks) { + mHandler.postOrRun( + new Runnable() { + @Override + public void run() { + IBinder b = callbacks.asBinder(); + + // Get the record for the connection + ConnectionRecord connection = mConnections.get(b); + if (connection == null) { + Log.w(TAG, "addSubscription for callback that isn't registered id=" + id); + return; + } + + MediaBrowserServiceCompat.this.addSubscription(id, connection, token, options); + } + }); + } + + public void removeSubscription( + @Nullable String id, @Nullable IBinder token, ServiceCallbacks callbacks) { + mHandler.postOrRun( + new Runnable() { + @Override + public void run() { + final IBinder b = callbacks.asBinder(); + + ConnectionRecord connection = mConnections.get(b); + if (connection == null) { + Log.w(TAG, "removeSubscription for callback that isn't registered id=" + id); + return; + } + if (!MediaBrowserServiceCompat.this.removeSubscription(id, connection, token)) { + Log.w(TAG, "removeSubscription called for " + id + " which is not subscribed"); + } + } + }); + } + + public void getMediaItem( + @Nullable String mediaId, + @Nullable ResultReceiver receiver, + final ServiceCallbacks callbacks) { + if (TextUtils.isEmpty(mediaId) || receiver == null) { + return; + } + + mHandler.postOrRun( + new Runnable() { + @Override + public void run() { + final IBinder b = callbacks.asBinder(); + + ConnectionRecord connection = mConnections.get(b); + if (connection == null) { + Log.w(TAG, "getMediaItem for callback that isn't registered id=" + mediaId); + return; + } + performLoadItem(mediaId, connection, receiver); + } + }); + } + + // Used when {@link MediaBrowserProtocol#EXTRA_MESSENGER_BINDER} is used. + public void registerCallbacks( + ServiceCallbacks callbacks, + @Nullable String pkg, + int pid, + int uid, + @Nullable Bundle rootHints) { + mHandler.postOrRun( + new Runnable() { + @Override + public void run() { + final IBinder b = callbacks.asBinder(); + // Clear out the old subscriptions. We are getting new ones. + mConnections.remove(b); + + ConnectionRecord connection = null; + Iterator iter = mPendingConnections.iterator(); + while (iter.hasNext()) { + ConnectionRecord pendingConnection = iter.next(); + // Note: We cannot use Map/Set for mPendingConnections but List because + // multiple MediaBrowserCompats with the same UID can request connect. + if (pendingConnection.uid == uid) { + // If caller hasn't set pkg and pid, do the best effort to get it. + if (TextUtils.isEmpty(pkg) || pid <= 0) { + // Note: Do not assign pendingConnection directly because it doesn't + // have callback information. + connection = + new ConnectionRecord( + pendingConnection.pkg, + pendingConnection.pid, + pendingConnection.uid, + rootHints, + callbacks); + } + iter.remove(); + break; + } + } + if (connection == null) { + connection = new ConnectionRecord(pkg, pid, uid, rootHints, callbacks); + } + mConnections.put(b, connection); + try { + b.linkToDeath(connection, 0); + } catch (RemoteException e) { + Log.w(TAG, "IBinder is already dead."); + } + } + }); + } + + // Used when {@link MediaBrowserProtocol#EXTRA_MESSENGER_BINDER} is used. + public void unregisterCallbacks(final ServiceCallbacks callbacks) { + mHandler.postOrRun( + new Runnable() { + @Override + public void run() { + final IBinder b = callbacks.asBinder(); + ConnectionRecord old = mConnections.remove(b); + if (old != null) { + b.unlinkToDeath(old, 0); + } + } + }); + } + + public void search( + @Nullable String query, + @Nullable Bundle extras, + @Nullable ResultReceiver receiver, + final ServiceCallbacks callbacks) { + if (TextUtils.isEmpty(query) || receiver == null) { + return; + } + + mHandler.postOrRun( + new Runnable() { + @Override + public void run() { + final IBinder b = callbacks.asBinder(); + + ConnectionRecord connection = mConnections.get(b); + if (connection == null) { + Log.w(TAG, "search for callback that isn't registered query=" + query); + return; + } + performSearch(query, extras, connection, receiver); + } + }); + } + + public void sendCustomAction( + @Nullable String action, + @Nullable Bundle extras, + @Nullable ResultReceiver receiver, + ServiceCallbacks callbacks) { + if (TextUtils.isEmpty(action) || receiver == null) { + return; + } + + mHandler.postOrRun( + new Runnable() { + @Override + public void run() { + final IBinder b = callbacks.asBinder(); + + ConnectionRecord connection = mConnections.get(b); + if (connection == null) { + Log.w( + TAG, + "sendCustomAction for callback that isn't registered action=" + + action + + ", extras=" + + extras); + return; + } + performCustomAction(action, extras, connection, receiver); + } + }); + } + } + + private interface ServiceCallbacks { + IBinder asBinder(); + + void onConnect(String root, @Nullable MediaSessionCompat.Token session, @Nullable Bundle extras) + throws RemoteException; + + void onConnectFailed() throws RemoteException; + + void onLoadChildren( + @Nullable String mediaId, + @Nullable List list, + @Nullable Bundle options, + @Nullable Bundle notifyChildrenChangedOptions) + throws RemoteException; + } + + private static class ServiceCallbacksCompat implements ServiceCallbacks { + final Messenger mCallbacks; + + ServiceCallbacksCompat(Messenger callbacks) { + mCallbacks = callbacks; + } + + @Override + public IBinder asBinder() { + return mCallbacks.getBinder(); + } + + @Override + public void onConnect( + String root, @Nullable MediaSessionCompat.Token session, @Nullable Bundle extras) + throws RemoteException { + if (extras == null) { + extras = new Bundle(); + } + extras.putInt(EXTRA_SERVICE_VERSION, SERVICE_VERSION_CURRENT); + Bundle data = new Bundle(); + data.putString(DATA_MEDIA_ITEM_ID, root); + data.putParcelable( + DATA_MEDIA_SESSION_TOKEN, + LegacyParcelableUtil.convert( + session, android.support.v4.media.session.MediaSessionCompat.Token.CREATOR)); + data.putBundle(DATA_ROOT_HINTS, extras); + sendRequest(SERVICE_MSG_ON_CONNECT, data); + } + + @Override + public void onConnectFailed() throws RemoteException { + sendRequest(SERVICE_MSG_ON_CONNECT_FAILED, null); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public void onLoadChildren( + @Nullable String mediaId, + @Nullable List list, + @Nullable Bundle options, + @Nullable Bundle notifyChildrenChangedOptions) + throws RemoteException { + Bundle data = new Bundle(); + data.putString(DATA_MEDIA_ITEM_ID, mediaId); + data.putBundle(DATA_OPTIONS, options); + data.putBundle(DATA_NOTIFY_CHILDREN_CHANGED_OPTIONS, notifyChildrenChangedOptions); + if (list != null) { + data.putParcelableArrayList( + DATA_MEDIA_ITEM_LIST, + LegacyParcelableUtil.convertList( + list, android.support.v4.media.MediaBrowserCompat.MediaItem.CREATOR)); + } + sendRequest(SERVICE_MSG_ON_LOAD_CHILDREN, data); + } + + private void sendRequest(int what, @Nullable Bundle data) throws RemoteException { + Message msg = Message.obtain(); + msg.what = what; + msg.arg1 = SERVICE_VERSION_CURRENT; + if (data != null) { + msg.setData(data); + } + mCallbacks.send(msg); + } + } + + @RequiresApi(21) + @SuppressWarnings({"rawtypes", "unchecked"}) + static class ResultWrapper { + MediaBrowserService.Result mResultFwk; + + ResultWrapper(MediaBrowserService.Result result) { + mResultFwk = result; + } + + public void sendResult(@Nullable T result) { + if (result instanceof List) { + mResultFwk.sendResult(parcelListToItemList((List) result)); + } else if (result instanceof Parcel) { + Parcel parcel = (Parcel) result; + parcel.setDataPosition(0); + mResultFwk.sendResult(MediaBrowser.MediaItem.CREATOR.createFromParcel(parcel)); + parcel.recycle(); + } else { + // The result is null or an invalid instance. + mResultFwk.sendResult(null); + } + } + + public void detach() { + mResultFwk.detach(); + } + + @Nullable + List parcelListToItemList(List parcelList) { + if (parcelList == null) { + return null; + } + List items = new ArrayList<>(parcelList.size()); + for (Parcel parcel : parcelList) { + parcel.setDataPosition(0); + items.add(MediaBrowser.MediaItem.CREATOR.createFromParcel(parcel)); + parcel.recycle(); + } + return items; + } + } + + /** + * Attaches to the base context. This method is added to change the visibility of {@link + * Service#attachBaseContext(Context)}. + * + *

Note that we cannot simply override {@link Service#attachBaseContext(Context)} and hide it + * because lint checks considers the overridden method as the new public API that needs update of + * current.txt. + */ + public void attachToBaseContext(Context base) { + attachBaseContext(base); + } + + @Override + public void onCreate() { + super.onCreate(); + if (Build.VERSION.SDK_INT >= 28) { + mImpl = new MediaBrowserServiceImplApi28(); + } else if (Build.VERSION.SDK_INT >= 26) { + mImpl = new MediaBrowserServiceImplApi26(); + } else if (Build.VERSION.SDK_INT >= 23) { + mImpl = new MediaBrowserServiceImplApi23(); + } else if (Build.VERSION.SDK_INT >= 21) { + mImpl = new MediaBrowserServiceImplApi21(); + } else { + mImpl = new MediaBrowserServiceImplBase(); + } + mImpl.onCreate(); + } + + @CallSuper + @MainThread + @Override + public void onDestroy() { + mHandler.release(); + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return checkNotNull(mImpl).onBind(intent); + } + + @Override + public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {} + + /** + * Called to get the root information for browsing by a particular client. + * + *

The implementation should verify that the client package has permission to access browse + * media information before returning the root id; it should return null if the client is not + * allowed to access this information. + * + * @param clientPackageName The package name of the application which is requesting access to + * browse media. + * @param clientUid The uid of the application which is requesting access to browse media. + * @param rootHints An optional bundle of service-specific arguments to send to the media browse + * service when connecting and retrieving the root id for browsing, or null if none. The + * contents of this bundle may affect the information returned when browsing. + * @return The {@link BrowserRoot} for accessing this app's content or null. + * @see BrowserRoot#EXTRA_RECENT + * @see BrowserRoot#EXTRA_OFFLINE + * @see BrowserRoot#EXTRA_SUGGESTED + */ + public abstract @Nullable BrowserRoot onGetRoot( + @Nullable String clientPackageName, int clientUid, @Nullable Bundle rootHints); + + /** + * Called to get information about the children of a media item. + * + *

Implementations must call {@link Result#sendResult result.sendResult} with the list of + * children. If loading the children will be an expensive operation that should be performed on + * another thread, {@link Result#detach result.detach} may be called before returning from this + * function, and then {@link Result#sendResult result.sendResult} called when the loading is + * complete. + * + *

In case the media item does not have any children, call {@link Result#sendResult} with an + * empty list. When the given {@code parentId} is invalid, implementations must call {@link + * Result#sendResult result.sendResult} with {@code null}, which will invoke {@link + * MediaBrowserCompat.SubscriptionCallback#onError}. + * + * @param parentId The id of the parent media item whose children are to be queried. + * @param result The Result to send the list of children to. + */ + public abstract void onLoadChildren( + @Nullable String parentId, Result> result); + + /** + * Called to get information about the children of a media item. + * + *

Implementations must call {@link Result#sendResult result.sendResult} with the list of + * children. If loading the children will be an expensive operation that should be performed on + * another thread, {@link Result#detach result.detach} may be called before returning from this + * function, and then {@link Result#sendResult result.sendResult} called when the loading is + * complete. + * + *

In case the media item does not have any children, call {@link Result#sendResult} with an + * empty list. When the given {@code parentId} is invalid, implementations must call {@link + * Result#sendResult result.sendResult} with {@code null}, which will invoke {@link + * MediaBrowserCompat.SubscriptionCallback#onError}. + * + * @param parentId The id of the parent media item whose children are to be queried. + * @param result The Result to send the list of children to. + * @param options A bundle of service-specific arguments sent from the media browse. The + * information returned through the result should be affected by the contents of this bundle. + */ + public void onLoadChildren( + @Nullable String parentId, + Result> result, + Bundle options) { + // To support backward compatibility, when the implementation of MediaBrowserService doesn't + // override onLoadChildren() with options, onLoadChildren() without options will be used + // instead, and the options will be applied in the implementation of result.onResultSent(). + result.setFlags(RESULT_FLAG_OPTION_NOT_HANDLED); + onLoadChildren(parentId, result); + } + + /** + * Called when a {@link MediaBrowserCompat#subscribe} is called. + * + * @param id id + * @param option option + */ + public void onSubscribe(@Nullable String id, @Nullable Bundle option) {} + + /** + * Called when a {@link MediaBrowserCompat#unsubscribe} is called. + * + * @param id + */ + public void onUnsubscribe(@Nullable String id) {} + + /** + * Called to get information about a specific media item. + * + *

Implementations must call {@link Result#sendResult result.sendResult}. If loading the item + * will be an expensive operation {@link Result#detach result.detach} may be called before + * returning from this function, and then {@link Result#sendResult result.sendResult} called when + * the item has been loaded. + * + *

When the given {@code itemId} is invalid, implementations must call {@link Result#sendResult + * result.sendResult} with {@code null}. + * + *

The default implementation will invoke {@link MediaBrowserCompat.ItemCallback#onError}. + * + * @param itemId The id for the specific {@link MediaBrowserCompat.MediaItem}. + * @param result The Result to send the item to, or null if the id is invalid. + */ + public void onLoadItem(String itemId, Result result) { + result.setFlags(RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED); + result.sendResult(null); + } + + /** + * Called to get the search result. + * + *

Implementations must call {@link Result#sendResult result.sendResult}. If the search will be + * an expensive operation {@link Result#detach result.detach} may be called before returning from + * this function, and then {@link Result#sendResult result.sendResult} called when the search has + * been completed. + * + *

In case there are no search results, call {@link Result#sendResult result.sendResult} with + * an empty list. In case there are some errors happened, call {@link Result#sendResult + * result.sendResult} with {@code null}, which will invoke {@link + * MediaBrowserCompat.SearchCallback#onError}. + * + *

The default implementation will invoke {@link MediaBrowserCompat.SearchCallback#onError}. + * + * @param query The search query sent from the media browser. It contains keywords separated by + * space. + * @param extras The bundle of service-specific arguments sent from the media browser. + * @param result The {@link Result} to send the search result. + */ + public void onSearch( + String query, @Nullable Bundle extras, Result> result) { + result.setFlags(RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED); + result.sendResult(null); + } + + /** + * Called to request a custom action to this service. + * + *

Implementations must call either {@link Result#sendResult} or {@link Result#sendError}. If + * the requested custom action will be an expensive operation {@link Result#detach} may be called + * before returning from this function, and then the service can send the result later when the + * custom action is completed. Implementation can also call {@link Result#sendProgressUpdate} to + * send an interim update to the requester. + * + *

If the requested custom action is not supported by this service, call {@link + * Result#sendError}. The default implementation will invoke {@link Result#sendError}. + * + * @param action The custom action sent from the media browser. + * @param extras The bundle of service-specific arguments sent from the media browser. + * @param result The {@link Result} to send the result of the requested custom action. + * @see MediaBrowserCompat#CUSTOM_ACTION_DOWNLOAD + * @see MediaBrowserCompat#CUSTOM_ACTION_REMOVE_DOWNLOADED_FILE + */ + public void onCustomAction(String action, Bundle extras, Result result) { + result.sendError(null); + } + + /** + * Call to set the media session. + * + *

This should be called as soon as possible during the service's startup. It may only be + * called once. + * + * @param token The token for the service's {@link MediaSessionCompat}. + */ + public void setSessionToken(MediaSessionCompat.Token token) { + if (token == null) { + throw new IllegalArgumentException("Session token may not be null"); + } + if (mSession != null) { + throw new IllegalStateException("The session token has already been set"); + } + mSession = token; + checkNotNull(mImpl).setSessionToken(token); + } + + /** Gets the session token, or null if it has not yet been created or if it has been destroyed. */ + @Nullable + public MediaSessionCompat.Token getSessionToken() { + return mSession; + } + + /** + * Gets the root hints sent from the currently connected {@link MediaBrowserCompat}. The root + * hints are service-specific arguments included in an optional bundle sent to the media browser + * service when connecting and retrieving the root id for browsing, or null if none. The contents + * of this bundle may affect the information returned when browsing. + * + *

Note that this will return null when connected to {@link android.media.browse.MediaBrowser} + * and running on API 23 or lower. + * + * @throws IllegalStateException If this method is called outside of {@link #onLoadChildren}, + * {@link #onLoadItem} or {@link #onSearch}. + * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_RECENT + * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_OFFLINE + * @see MediaBrowserServiceCompat.BrowserRoot#EXTRA_SUGGESTED + */ + @Nullable + public final Bundle getBrowserRootHints() { + return checkNotNull(mImpl).getBrowserRootHints(); + } + + /** + * Gets the browser information who sent the current request. + * + * @throws IllegalStateException If this method is called outside of {@link #onGetRoot} or {@link + * #onLoadChildren} or {@link #onLoadItem}. + * @see MediaSessionManager#isTrustedForMediaControl(RemoteUserInfo) + */ + public final RemoteUserInfo getCurrentBrowserInfo() { + return checkNotNull(mImpl).getCurrentBrowserInfo(); + } + + /** + * Notifies all connected media browsers that the children of the specified parent id have changed + * in some way. This will cause browsers to fetch subscribed content again. + * + * @param parentId The id of the parent media item whose children changed. + */ + public void notifyChildrenChanged(String parentId) { + if (parentId == null) { + throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged"); + } + checkNotNull(mImpl).notifyChildrenChanged(parentId, null); + } + + /** + * Notifies all connected media browsers that the children of the specified parent id have changed + * in some way. This will cause browsers to fetch subscribed content again. + * + * @param parentId The id of the parent media item whose children changed. + * @param options A bundle of service-specific arguments to send to the media browse. The contents + * of this bundle may contain the information about the change. + */ + public void notifyChildrenChanged(String parentId, Bundle options) { + if (parentId == null) { + throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged"); + } + if (options == null) { + throw new IllegalArgumentException("options cannot be null in notifyChildrenChanged"); + } + checkNotNull(mImpl).notifyChildrenChanged(parentId, options); + } + + /** + * Notifies a connected media browsers that the children of the specified parent id have changed + * in some way. This will cause browsers to fetch subscribed content again. + * + * @param remoteUserInfo to receive this event. + * @param parentId The id of the parent media item whose children changed. + * @param options A bundle of service-specific arguments to send to the media browse. The contents + * of this bundle may contain the information about the change. + */ + public void notifyChildrenChanged( + RemoteUserInfo remoteUserInfo, String parentId, Bundle options) { + if (remoteUserInfo == null) { + throw new IllegalArgumentException( + "remoteUserInfo cannot be null in" + " notifyChildrenChanged"); + } + if (parentId == null) { + throw new IllegalArgumentException("parentId cannot be null in notifyChildrenChanged"); + } + if (options == null) { + throw new IllegalArgumentException("options cannot be null in notifyChildrenChanged"); + } + checkNotNull(mImpl).notifyChildrenChanged(remoteUserInfo, parentId, options); + } + + // Package visibility to avoid synthetic accessor. + /* package */ + @SuppressLint("RestrictedApi") + void handleMessageInternal(Message msg) { + Bundle data = msg.getData(); + switch (msg.what) { + case CLIENT_MSG_CONNECT: + { + Bundle rootHints = data.getBundle(DATA_ROOT_HINTS); + MediaSessionCompat.ensureClassLoader(rootHints); + + mServiceBinderImpl.connect( + data.getString(DATA_PACKAGE_NAME), + data.getInt(DATA_CALLING_PID), + data.getInt(DATA_CALLING_UID), + rootHints, + new ServiceCallbacksCompat(msg.replyTo)); + break; + } + case CLIENT_MSG_DISCONNECT: + mServiceBinderImpl.disconnect(new ServiceCallbacksCompat(msg.replyTo)); + break; + case CLIENT_MSG_ADD_SUBSCRIPTION: + { + Bundle options = data.getBundle(DATA_OPTIONS); + MediaSessionCompat.ensureClassLoader(options); + + mServiceBinderImpl.addSubscription( + data.getString(DATA_MEDIA_ITEM_ID), + data.getBinder(DATA_CALLBACK_TOKEN), + options, + new ServiceCallbacksCompat(msg.replyTo)); + break; + } + case CLIENT_MSG_REMOVE_SUBSCRIPTION: + mServiceBinderImpl.removeSubscription( + data.getString(DATA_MEDIA_ITEM_ID), + data.getBinder(DATA_CALLBACK_TOKEN), + new ServiceCallbacksCompat(msg.replyTo)); + break; + case CLIENT_MSG_GET_MEDIA_ITEM: + mServiceBinderImpl.getMediaItem( + data.getString(DATA_MEDIA_ITEM_ID), + data.getParcelable(DATA_RESULT_RECEIVER), + new ServiceCallbacksCompat(msg.replyTo)); + break; + case CLIENT_MSG_REGISTER_CALLBACK_MESSENGER: + { + @Nullable Bundle rootHints = data.getBundle(DATA_ROOT_HINTS); + MediaSessionCompat.ensureClassLoader(rootHints); + + mServiceBinderImpl.registerCallbacks( + new ServiceCallbacksCompat(msg.replyTo), + data.getString(DATA_PACKAGE_NAME), + data.getInt(DATA_CALLING_PID), + data.getInt(DATA_CALLING_UID), + rootHints); + break; + } + case CLIENT_MSG_UNREGISTER_CALLBACK_MESSENGER: + mServiceBinderImpl.unregisterCallbacks(new ServiceCallbacksCompat(msg.replyTo)); + break; + case CLIENT_MSG_SEARCH: + { + @Nullable Bundle searchExtras = data.getBundle(DATA_SEARCH_EXTRAS); + MediaSessionCompat.ensureClassLoader(searchExtras); + + mServiceBinderImpl.search( + data.getString(DATA_SEARCH_QUERY), + searchExtras, + data.getParcelable(DATA_RESULT_RECEIVER), + new ServiceCallbacksCompat(msg.replyTo)); + break; + } + case CLIENT_MSG_SEND_CUSTOM_ACTION: + { + @Nullable Bundle customActionExtras = data.getBundle(DATA_CUSTOM_ACTION_EXTRAS); + MediaSessionCompat.ensureClassLoader(customActionExtras); + + mServiceBinderImpl.sendCustomAction( + data.getString(DATA_CUSTOM_ACTION), + customActionExtras, + data.getParcelable(DATA_RESULT_RECEIVER), + new ServiceCallbacksCompat(msg.replyTo)); + break; + } + default: + Log.w( + TAG, + "Unhandled message: " + + msg + + "\n Service version: " + + SERVICE_VERSION_CURRENT + + "\n Client version: " + + msg.arg1); + } + } + + /** Return whether the given package is one of the ones that is owned by the uid. */ + @EnsuresNonNullIf(result = true, expression = "#1") + boolean isValidPackage(@Nullable String pkg, int uid) { + if (pkg == null) { + return false; + } + final PackageManager pm = getPackageManager(); + final String[] packages = pm.getPackagesForUid(uid); + if (packages == null) { + return false; + } + final int N = packages.length; + for (int i = 0; i < N; i++) { + if (packages[i].equals(pkg)) { + return true; + } + } + return false; + } + + /** Save the subscription and if it is a new subscription send the results. */ + void addSubscription( + @Nullable String id, + ConnectionRecord connection, + @Nullable IBinder token, + @Nullable Bundle options) { + // Save the subscription + List> callbackList = + connection.subscriptions.get(id); + if (callbackList == null) { + callbackList = new ArrayList<>(); + } + for (Pair<@NullableType IBinder, @NullableType Bundle> callback : callbackList) { + if (token == callback.first + && MediaBrowserCompatUtils.areSameOptions(options, callback.second)) { + return; + } + } + callbackList.add(new Pair<>(token, options)); + connection.subscriptions.put(id, callbackList); + // send the results + performLoadChildren(id, connection, options, null); + + mCurConnection = connection; + onSubscribe(id, options); + mCurConnection = null; + } + + /** Remove the subscription. */ + boolean removeSubscription( + @Nullable String id, ConnectionRecord connection, @Nullable IBinder token) { + try { + if (token == null) { + return connection.subscriptions.remove(id) != null; + } + boolean removed = false; + List> callbackList = + connection.subscriptions.get(id); + if (callbackList != null) { + Iterator> iter = callbackList.iterator(); + while (iter.hasNext()) { + if (token == iter.next().first) { + removed = true; + iter.remove(); + } + } + if (callbackList.size() == 0) { + connection.subscriptions.remove(id); + } + } + return removed; + } finally { + mCurConnection = connection; + onUnsubscribe(id); + mCurConnection = null; + } + } + + /** + * Call onLoadChildren and then send the results back to the connection. + * + *

Callers must make sure that this connection is still connected. + */ + void performLoadChildren( + @Nullable String parentId, + final ConnectionRecord connection, + @Nullable Bundle subscribeOptions, + @Nullable Bundle notifyChildrenChangedOptions) { + final Result> result = + new Result>(parentId) { + @Override + void onResultSent(@Nullable List list) { + if (mConnections.get(checkNotNull(connection.callbacks).asBinder()) != connection) { + if (DEBUG) { + Log.d( + TAG, + "Not sending onLoadChildren result for connection that has" + + " been disconnected. pkg=" + + connection.pkg + + " id=" + + parentId); + } + return; + } + + List filteredList = + (getFlags() & RESULT_FLAG_OPTION_NOT_HANDLED) != 0 + ? applyOptions(list, subscribeOptions) + : list; + try { + connection.callbacks.onLoadChildren( + parentId, filteredList, subscribeOptions, notifyChildrenChangedOptions); + } catch (RemoteException ex) { + // The other side is in the process of crashing. + Log.w( + TAG, + "Calling onLoadChildren() failed for id=" + + parentId + + " package=" + + connection.pkg); + } + } + }; + + mCurConnection = connection; + if (subscribeOptions == null) { + onLoadChildren(parentId, result); + } else { + onLoadChildren(parentId, result, subscribeOptions); + } + mCurConnection = null; + + if (!result.isDone()) { + throw new IllegalStateException( + "onLoadChildren must call detach() or sendResult()" + + " before returning for package=" + + connection.pkg + + " id=" + + parentId); + } + } + + @Nullable + List applyOptions( + @Nullable List list, @Nullable Bundle options) { + if (list == null) { + return null; + } + if (options == null) { + return list; + } + int page = options.getInt(MediaBrowserCompat.EXTRA_PAGE, -1); + int pageSize = options.getInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, -1); + if (page == -1 && pageSize == -1) { + return list; + } + int fromIndex = pageSize * page; + int toIndex = fromIndex + pageSize; + if (page < 0 || pageSize < 1 || fromIndex >= list.size()) { + return Collections.emptyList(); + } + if (toIndex > list.size()) { + toIndex = list.size(); + } + return list.subList(fromIndex, toIndex); + } + + void performLoadItem(String itemId, ConnectionRecord connection, final ResultReceiver receiver) { + final Result result = + new Result(itemId) { + @SuppressLint("RestrictedApi") + @Override + void onResultSent(@Nullable MediaBrowserCompat.MediaItem item) { + if ((getFlags() & RESULT_FLAG_ON_LOAD_ITEM_NOT_IMPLEMENTED) != 0) { + receiver.send(RESULT_ERROR, null); + return; + } + Bundle bundle = new Bundle(); + bundle.putParcelable( + KEY_MEDIA_ITEM, + LegacyParcelableUtil.convert( + item, android.support.v4.media.MediaBrowserCompat.MediaItem.CREATOR)); + receiver.send(RESULT_OK, bundle); + } + }; + + mCurConnection = connection; + onLoadItem(itemId, result); + mCurConnection = null; + + if (!result.isDone()) { + throw new IllegalStateException( + "onLoadItem must call detach() or sendResult()" + " before returning for id=" + itemId); + } + } + + void performSearch( + String query, @Nullable Bundle extras, ConnectionRecord connection, ResultReceiver receiver) { + final Result> result = + new Result>(query) { + @SuppressLint("RestrictedApi") + @Override + void onResultSent(@Nullable List items) { + if ((getFlags() & RESULT_FLAG_ON_SEARCH_NOT_IMPLEMENTED) != 0 || items == null) { + receiver.send(RESULT_ERROR, null); + return; + } + Bundle bundle = new Bundle(); + List legacyPackageList = + LegacyParcelableUtil.convertList( + items, android.support.v4.media.MediaBrowserCompat.MediaItem.CREATOR); + bundle.putParcelableArray( + KEY_SEARCH_RESULTS, + legacyPackageList.toArray( + new android.support.v4.media.MediaBrowserCompat.MediaItem[0])); + receiver.send(RESULT_OK, bundle); + } + }; + + mCurConnection = connection; + onSearch(query, extras, result); + mCurConnection = null; + + if (!result.isDone()) { + throw new IllegalStateException( + "onSearch must call detach() or sendResult()" + " before returning for query=" + query); + } + } + + void performCustomAction( + String action, + @Nullable Bundle extras, + ConnectionRecord connection, + ResultReceiver receiver) { + @SuppressLint("RestrictedApi") + final Result result = + new Result(action) { + @Override + void onResultSent(@Nullable Bundle result) { + receiver.send(RESULT_OK, result); + } + + @Override + void onProgressUpdateSent(@Nullable Bundle data) { + receiver.send(RESULT_PROGRESS_UPDATE, data); + } + + @Override + void onErrorSent(@Nullable Bundle data) { + receiver.send(RESULT_ERROR, data); + } + }; + + mCurConnection = connection; + onCustomAction(action, extras == null ? Bundle.EMPTY : extras, result); + mCurConnection = null; + + if (!result.isDone()) { + throw new IllegalStateException( + "onCustomAction must call detach() or sendResult() or " + + "sendError() before returning for action=" + + action + + " extras=" + + extras); + } + } + + /** + * Contains information that the browser service needs to send to the client when first connected. + */ + public static final class BrowserRoot { + /** + * The lookup key for a boolean that indicates whether the browser service should return a + * browser root for recently played media items. + * + *

When creating a media browser for a given media browser service, this key can be supplied + * as a root hint for retrieving media items that are recently played. If the media browser + * service can provide such media items, the implementation must return the key in the root hint + * when {@link #onGetRoot(String, int, Bundle)} is called back. + * + *

The root hint may contain multiple keys. + * + * @see #EXTRA_OFFLINE + * @see #EXTRA_SUGGESTED + */ + public static final String EXTRA_RECENT = "android.service.media.extra.RECENT"; + + /** + * The lookup key for a boolean that indicates whether the browser service should return a + * browser root for offline media items. + * + *

When creating a media browser for a given media browser service, this key can be supplied + * as a root hint for retrieving media items that are can be played without an internet + * connection. If the media browser service can provide such media items, the implementation + * must return the key in the root hint when {@link #onGetRoot(String, int, Bundle)} is called + * back. + * + *

The root hint may contain multiple keys. + * + * @see #EXTRA_RECENT + * @see #EXTRA_SUGGESTED + */ + public static final String EXTRA_OFFLINE = "android.service.media.extra.OFFLINE"; + + /** + * The lookup key for a boolean that indicates whether the browser service should return a + * browser root for suggested media items. + * + *

When creating a media browser for a given media browser service, this key can be supplied + * as a root hint for retrieving the media items suggested by the media browser service. The + * list of media items passed in {@link + * MediaBrowserCompat.SubscriptionCallback#onChildrenLoaded(String, List)} is considered ordered + * by relevance, first being the top suggestion. If the media browser service can provide such + * media items, the implementation must return the key in the root hint when {@link + * #onGetRoot(String, int, Bundle)} is called back. + * + *

The root hint may contain multiple keys. + * + * @see #EXTRA_RECENT + * @see #EXTRA_OFFLINE + */ + public static final String EXTRA_SUGGESTED = "android.service.media.extra.SUGGESTED"; + + /** + * The lookup key for a string that indicates specific keywords which will be considered when + * the browser service suggests media items. + * + *

When creating a media browser for a given media browser service, this key can be supplied + * as a root hint together with {@link #EXTRA_SUGGESTED} for retrieving suggested media items + * related with the keywords. The list of media items passed in {@link + * android.media.browse.MediaBrowser.SubscriptionCallback#onChildrenLoaded(String, List)} is + * considered ordered by relevance, first being the top suggestion. If the media browser service + * can provide such media items, the implementation must return the key in the root hint when + * {@link #onGetRoot(String, int, Bundle)} is called back. + * + *

The root hint may contain multiple keys. + * + * @see #EXTRA_RECENT + * @see #EXTRA_OFFLINE + * @see #EXTRA_SUGGESTED + * @deprecated The search functionality is now supported by the methods {@link + * MediaBrowserCompat#search} and {@link #onSearch}. Use those methods instead. + */ + @Deprecated + public static final String EXTRA_SUGGESTION_KEYWORDS = + "android.service.media.extra.SUGGESTION_KEYWORDS"; + + private final String mRootId; + @Nullable private final Bundle mExtras; + + /** + * Constructs a browser root. + * + * @param rootId The root id for browsing. + * @param extras Any extras about the browser service. + */ + public BrowserRoot(String rootId, @Nullable Bundle extras) { + if (rootId == null) { + throw new IllegalArgumentException( + "The root id in BrowserRoot cannot be null. " + "Use null for BrowserRoot instead"); + } + mRootId = rootId; + mExtras = extras; + } + + /** Gets the root id for browsing. */ + public String getRootId() { + return mRootId; + } + + /** Gets any extras about the browser service. */ + @Nullable + public Bundle getExtras() { + return mExtras; + } + } +} diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaButtonReceiver.java b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaButtonReceiver.java new file mode 100644 index 0000000000..b1cb477c18 --- /dev/null +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaButtonReceiver.java @@ -0,0 +1,397 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.session.legacy; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY; +import static androidx.media3.common.util.Assertions.checkNotNull; + +import android.app.ForegroundServiceStartNotAllowedException; +import android.app.PendingIntent; +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Build; +import android.util.Log; +import android.view.KeyEvent; +import androidx.annotation.DoNotInline; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.RestrictTo; +import androidx.core.content.ContextCompat; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.legacy.PlaybackStateCompat.MediaKeyAction; +import java.util.List; + +/** + * A media button receiver receives and helps translate hardware media playback buttons, such as + * those found on wired and wireless headsets, into the appropriate callbacks in your app. + * + *

You can add this MediaButtonReceiver to your app by adding it directly to your + * AndroidManifest.xml: + * + *

+ * <receiver android:name="androidx.media.session.MediaButtonReceiver" >
+ *   <intent-filter>
+ *     <action android:name="android.intent.action.MEDIA_BUTTON" />
+ *   </intent-filter>
+ * </receiver>
+ * 
+ * + * This class assumes you have a {@link Service} in your app that controls media playback via a + * {@link MediaSessionCompat}. Once a key event is received by MediaButtonReceiver, this class tries + * to find a {@link Service} that can handle {@link Intent#ACTION_MEDIA_BUTTON}, and a {@link + * MediaBrowserServiceCompat} in turn. If an appropriate service is found, this class forwards the + * key event to the service. If neither is available or more than one valid service/media browser + * service is found, an {@link IllegalStateException} will be thrown. Thus, your app should have one + * of the following services to get a key event properly. + * + *

Service Handling ACTION_MEDIA_BUTTON

+ * + * A service can receive a key event by including an intent filter that handles {@link + * Intent#ACTION_MEDIA_BUTTON}: + * + *
+ * <service android:name="com.example.android.MediaPlaybackService" >
+ *   <intent-filter>
+ *     <action android:name="android.intent.action.MEDIA_BUTTON" />
+ *   </intent-filter>
+ * </service>
+ * 
+ * + * Events can then be handled in {@link Service#onStartCommand(Intent, int, int)} by calling {@link + * MediaButtonReceiver#handleIntent(MediaSessionCompat, Intent)}, passing in your current {@link + * MediaSessionCompat}: + * + *
+ * private MediaSessionCompat mMediaSessionCompat = ...;
+ *
+ * public int onStartCommand(Intent intent, int flags, int startId) {
+ *   MediaButtonReceiver.handleIntent(mMediaSessionCompat, intent);
+ *   return super.onStartCommand(intent, flags, startId);
+ * }
+ * 
+ * + * This ensures that the correct callbacks to {@link MediaSessionCompat.Callback} will be triggered + * based on the incoming {@link KeyEvent}. + * + *

Note: Once the service is started, it must start to run in the + * foreground. + * + *

MediaBrowserService

+ * + * If you already have a {@link MediaBrowserServiceCompat} in your app, MediaButtonReceiver will + * deliver the received key events to the {@link MediaBrowserServiceCompat} by default. You can + * handle them in your {@link MediaSessionCompat.Callback}. + */ +@UnstableApi +@RestrictTo(LIBRARY) +public class MediaButtonReceiver extends BroadcastReceiver { + private static final String TAG = "MediaButtonReceiver"; + + @Override + public void onReceive(Context context, Intent intent) { + if (intent == null + || !Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction()) + || !intent.hasExtra(Intent.EXTRA_KEY_EVENT)) { + Log.d(TAG, "Ignore unsupported intent: " + intent); + return; + } + ComponentName mediaButtonServiceComponentName = + getServiceComponentByAction(context, Intent.ACTION_MEDIA_BUTTON); + if (mediaButtonServiceComponentName != null) { + intent.setComponent(mediaButtonServiceComponentName); + try { + ContextCompat.startForegroundService(context, intent); + } catch (/* ForegroundServiceStartNotAllowedException */ IllegalStateException e) { + if (Build.VERSION.SDK_INT >= 31 + && Api31.instanceOfForegroundServiceStartNotAllowedException(e)) { + onForegroundServiceStartNotAllowedException( + intent, Api31.castToForegroundServiceStartNotAllowedException(e)); + } else { + throw e; + } + } + return; + } + ComponentName mediaBrowserServiceComponentName = + getServiceComponentByAction(context, MediaBrowserServiceCompat.SERVICE_INTERFACE); + if (mediaBrowserServiceComponentName != null) { + PendingResult pendingResult = goAsync(); + Context applicationContext = context.getApplicationContext(); + MediaButtonConnectionCallback connectionCallback = + new MediaButtonConnectionCallback(applicationContext, intent, pendingResult); + MediaBrowserCompat mediaBrowser = + new MediaBrowserCompat( + applicationContext, mediaBrowserServiceComponentName, connectionCallback, null); + connectionCallback.setMediaBrowser(mediaBrowser); + mediaBrowser.connect(); + return; + } + throw new IllegalStateException( + "Could not find any Service that handles " + + Intent.ACTION_MEDIA_BUTTON + + " or implements a media browser service."); + } + + /** + * This method is called when an exception is thrown when calling {@link + * Context#startForegroundService(Intent)} as a result of receiving a media button event. + * + *

By default, this method only logs the exception and it can be safely overridden. Apps that + * find that such a media button event has been legitimately sent, may choose to override this + * method and take the opportunity to post a notification from where the user journey can + * continue. + * + *

This exception can be thrown if a broadcast media button event is received and a media + * service is found in the manifest that is registered to handle {@link + * Intent#ACTION_MEDIA_BUTTON}. If this happens on API 31+ and the app is in the background then + * an exception is thrown. + * + *

Normally, a media button intent should only be required to be sent by the system in case of + * a Bluetooth media button event that wants to restart the app. However, in such a case the app + * gets an exemption and is allowed to start the foreground service. In this case this method will + * never be called. + * + *

In all other cases, apps should use a {@linkplain MediaBrowserCompat media browser} to bind + * to and start the service instead of broadcasting an intent. + * + * @param intent The intent that was used {@linkplain Context#startForegroundService(Intent) for + * starting the foreground service}. + * @param e The exception thrown by the system and caught by this broadcast receiver. + */ + @RequiresApi(31) + protected void onForegroundServiceStartNotAllowedException( + Intent intent, ForegroundServiceStartNotAllowedException e) { + Log.e( + TAG, + "caught exception when trying to start a foreground service from the " + + "background: " + + e.getMessage()); + } + + private static class MediaButtonConnectionCallback extends MediaBrowserCompat.ConnectionCallback { + private final Context mContext; + private final Intent mIntent; + private final PendingResult mPendingResult; + + @Nullable private MediaBrowserCompat mMediaBrowser; + + MediaButtonConnectionCallback(Context context, Intent intent, PendingResult pendingResult) { + mContext = context; + mIntent = intent; + mPendingResult = pendingResult; + } + + void setMediaBrowser(MediaBrowserCompat mediaBrowser) { + mMediaBrowser = mediaBrowser; + } + + @SuppressWarnings("deprecation") + @Override + public void onConnected() { + MediaControllerCompat mediaController = + new MediaControllerCompat(mContext, checkNotNull(mMediaBrowser).getSessionToken()); + KeyEvent ke = mIntent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); + mediaController.dispatchMediaButtonEvent(ke); + finish(); + } + + @Override + public void onConnectionSuspended() { + finish(); + } + + @Override + public void onConnectionFailed() { + finish(); + } + + private void finish() { + checkNotNull(mMediaBrowser).disconnect(); + mPendingResult.finish(); + } + } + ; + + /** + * Extracts any available {@link KeyEvent} from an {@link Intent#ACTION_MEDIA_BUTTON} intent, + * passing it onto the {@link MediaSessionCompat} using {@link + * MediaControllerCompat#dispatchMediaButtonEvent(KeyEvent)}, which in turn will trigger callbacks + * to the {@link MediaSessionCompat.Callback} registered via {@link + * MediaSessionCompat#setCallback(MediaSessionCompat.Callback)}. + * + * @param mediaSessionCompat A {@link MediaSessionCompat} that has a {@link + * MediaSessionCompat.Callback} set. + * @param intent The intent to parse. + * @return The extracted {@link KeyEvent} if found, or null. + */ + @Nullable + @SuppressWarnings("deprecation") + public static KeyEvent handleIntent(MediaSessionCompat mediaSessionCompat, Intent intent) { + if (mediaSessionCompat == null + || intent == null + || !Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction()) + || !intent.hasExtra(Intent.EXTRA_KEY_EVENT)) { + return null; + } + KeyEvent ke = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); + MediaControllerCompat mediaController = mediaSessionCompat.getController(); + mediaController.dispatchMediaButtonEvent(ke); + return ke; + } + + /** + * Creates a broadcast pending intent that will send a media button event. The {@code action} will + * be translated to the appropriate {@link KeyEvent}, and it will be sent to the registered media + * button receiver in the given context. The {@code action} should be one of the following: + * + *

    + *
  • {@link PlaybackStateCompat#ACTION_PLAY} + *
  • {@link PlaybackStateCompat#ACTION_PAUSE} + *
  • {@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT} + *
  • {@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS} + *
  • {@link PlaybackStateCompat#ACTION_STOP} + *
  • {@link PlaybackStateCompat#ACTION_FAST_FORWARD} + *
  • {@link PlaybackStateCompat#ACTION_REWIND} + *
  • {@link PlaybackStateCompat#ACTION_PLAY_PAUSE} + *
+ * + * @param context The context of the application. + * @param action The action to be sent via the pending intent. + * @return Created pending intent, or null if cannot find a unique registered media button + * receiver or if the {@code action} is unsupported/invalid. + */ + @Nullable + public static PendingIntent buildMediaButtonPendingIntent( + Context context, @MediaKeyAction long action) { + ComponentName mbrComponent = getMediaButtonReceiverComponent(context); + if (mbrComponent == null) { + Log.w( + TAG, + "A unique media button receiver could not be found in the given context, so " + + "couldn't build a pending intent."); + return null; + } + return buildMediaButtonPendingIntent(context, mbrComponent, action); + } + + /** + * Creates a broadcast pending intent that will send a media button event. The {@code action} will + * be translated to the appropriate {@link KeyEvent}, and sent to the provided media button + * receiver via the pending intent. The {@code action} should be one of the following: + * + *
    + *
  • {@link PlaybackStateCompat#ACTION_PLAY} + *
  • {@link PlaybackStateCompat#ACTION_PAUSE} + *
  • {@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT} + *
  • {@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS} + *
  • {@link PlaybackStateCompat#ACTION_STOP} + *
  • {@link PlaybackStateCompat#ACTION_FAST_FORWARD} + *
  • {@link PlaybackStateCompat#ACTION_REWIND} + *
  • {@link PlaybackStateCompat#ACTION_PLAY_PAUSE} + *
+ * + * @param context The context of the application. + * @param mbrComponent The full component name of a media button receiver where you want to send + * this intent. + * @param action The action to be sent via the pending intent. + * @return Created pending intent, or null if the given component name is null or the {@code + * action} is unsupported/invalid. + */ + @Nullable + public static PendingIntent buildMediaButtonPendingIntent( + Context context, ComponentName mbrComponent, @MediaKeyAction long action) { + if (mbrComponent == null) { + Log.w(TAG, "The component name of media button receiver should be provided."); + return null; + } + int keyCode = PlaybackStateCompat.toKeyCode(action); + if (keyCode == KeyEvent.KEYCODE_UNKNOWN) { + Log.w(TAG, "Cannot build a media button pending intent with the given action: " + action); + return null; + } + Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); + intent.setComponent(mbrComponent); + intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); + intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); + return PendingIntent.getBroadcast( + context, keyCode, intent, Build.VERSION.SDK_INT >= 31 ? PendingIntent.FLAG_MUTABLE : 0); + } + + /** */ + @Nullable + public static ComponentName getMediaButtonReceiverComponent(Context context) { + Intent queryIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); + queryIntent.setPackage(context.getPackageName()); + PackageManager pm = context.getPackageManager(); + List resolveInfos = pm.queryBroadcastReceivers(queryIntent, 0); + if (resolveInfos.size() == 1) { + ResolveInfo resolveInfo = resolveInfos.get(0); + return new ComponentName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name); + } else if (resolveInfos.size() > 1) { + Log.w( + TAG, + "More than one BroadcastReceiver that handles " + + Intent.ACTION_MEDIA_BUTTON + + " was found, returning null."); + } + return null; + } + + @Nullable + @SuppressWarnings("deprecation") + private static ComponentName getServiceComponentByAction(Context context, String action) { + PackageManager pm = context.getPackageManager(); + Intent queryIntent = new Intent(action); + queryIntent.setPackage(context.getPackageName()); + List resolveInfos = pm.queryIntentServices(queryIntent, 0 /* flags */); + if (resolveInfos.size() == 1) { + ResolveInfo resolveInfo = resolveInfos.get(0); + return new ComponentName(resolveInfo.serviceInfo.packageName, resolveInfo.serviceInfo.name); + } else if (resolveInfos.isEmpty()) { + return null; + } else { + throw new IllegalStateException( + "Expected 1 service that handles " + action + ", found " + resolveInfos.size()); + } + } + + @RequiresApi(31) + private static final class Api31 { + /** + * Returns true if the passed exception is a {@link ForegroundServiceStartNotAllowedException}. + */ + @DoNotInline + public static boolean instanceOfForegroundServiceStartNotAllowedException( + IllegalStateException e) { + return e instanceof ForegroundServiceStartNotAllowedException; + } + + /** + * Casts the {@link IllegalStateException} to a {@link + * ForegroundServiceStartNotAllowedException} and throws an exception if the cast fails. + */ + @DoNotInline + public static ForegroundServiceStartNotAllowedException + castToForegroundServiceStartNotAllowedException(IllegalStateException e) { + return (ForegroundServiceStartNotAllowedException) e; + } + } +} diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaConstants.java b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaConstants.java new file mode 100644 index 0000000000..6926d47049 --- /dev/null +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaConstants.java @@ -0,0 +1,926 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.session.legacy; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY; + +import android.annotation.SuppressLint; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Bundle; +import android.os.Parcelable; +import android.service.media.MediaBrowserService; +import androidx.annotation.RestrictTo; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.legacy.MediaBrowserCompat.ConnectionCallback; +import java.util.ArrayList; + +/** Media constants for sharing constants between media provider and consumer apps */ +@UnstableApi +@RestrictTo(LIBRARY) +public final class MediaConstants { + /** + * Bundle key used for the account name in {@link MediaSessionCompat session} extras. + * + *

TYPE: String + * + * @see MediaControllerCompat#getExtras + * @see MediaSessionCompat#setExtras + */ + @SuppressLint("IntentName") + public static final String SESSION_EXTRAS_KEY_ACCOUNT_NAME = + "androidx.media.MediaSessionCompat.Extras.KEY_ACCOUNT_NAME"; + + /** + * Bundle key used for the account type in {@link MediaSessionCompat session} extras. The value + * would vary across media applications. + * + *

TYPE: String + * + * @see MediaControllerCompat#getExtras + * @see MediaSessionCompat#setExtras + */ + @SuppressLint("IntentName") + public static final String SESSION_EXTRAS_KEY_ACCOUNT_TYPE = + "androidx.media.MediaSessionCompat.Extras.KEY_ACCOUNT_TYPE"; + + /** + * Bundle key used for the account auth token value in {@link MediaSessionCompat session} extras. + * The value would vary across media applications. + * + *

TYPE: byte[] + * + * @see MediaControllerCompat#getExtras + * @see MediaSessionCompat#setExtras + */ + @SuppressLint("IntentName") + public static final String SESSION_EXTRAS_KEY_AUTHTOKEN = + "androidx.media.MediaSessionCompat.Extras.KEY_AUTHTOKEN"; + + /** + * Bundle key passed from {@link MediaSessionCompat} to the hosting {@link MediaControllerCompat} + * to indicate a preference that a region of space for the skip to next control should always be + * blocked out in the UI, even when the {@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT skip to + * next standard action} is not supported. This may be used when the session temporarily hides + * skip to next by design. + * + *

TYPE: boolean + * + * @see MediaControllerCompat#getExtras() + * @see MediaSessionCompat#setExtras(Bundle) + */ + @SuppressLint("IntentName") + public static final String SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT = + "android.media.playback.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT"; + + /** + * Bundle key passed from {@link MediaSessionCompat} to the hosting {@link MediaControllerCompat} + * to indicate a preference that a region of space for the skip to previous control should always + * be blocked out in the UI, even when the {@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS skip + * to previous standard action} is not supported. This may be used when the session temporarily + * hides skip to previous by design. + * + *

TYPE: boolean + * + * @see MediaControllerCompat#getExtras() + * @see MediaSessionCompat#setExtras(Bundle) + */ + @SuppressLint("IntentName") + public static final String SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV = + "android.media.playback.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS"; + + /** + * Bundle key used for media content id in {@link MediaMetadataCompat metadata}, should contain + * the same ID provided to Media Actions + * Catalog in reference to this title (e.g., episode, movie). This key can contain the content + * ID of the currently playing episode or movie and can be used to help users continue watching + * after this session is paused or stopped. + * + *

TYPE: String + * + * @see MediaMetadataCompat + */ + @SuppressLint("IntentName") + public static final String METADATA_KEY_CONTENT_ID = + "androidx.media.MediaMetadatCompat.METADATA_KEY_CONTENT_ID"; + + /** + * Bundle key used for next episode's media content ID in {@link MediaMetadataCompat metadata}, + * following the same ID and format provided to Media Actions Catalog in reference to + * the next episode of the current title episode. This key can contain the content ID of the + * episode immediately following the currently playing episode and can be used to help users + * continue watching after this episode is over. This value is only valid for TV Episode content + * type and should be left blank for other content. + * + *

TYPE: String + * + * @see MediaMetadataCompat + */ + @SuppressLint("IntentName") + public static final String METADATA_KEY_NEXT_EPISODE_CONTENT_ID = + "androidx.media.MediaMetadatCompat.METADATA_KEY_NEXT_EPISODE_CONTENT_ID"; + + /** + * Bundle key used for the TV series's media content ID in {@link MediaMetadataCompat metadata}, + * following the same ID and format provided to Media Actions Catalog in reference to + * the TV series of the current title episode. This value is only valid for TV Episode content + * type and should be left blank for other content. + * + *

TYPE: String + * + * @see MediaMetadataCompat + */ + @SuppressLint("IntentName") + public static final String METADATA_KEY_SERIES_CONTENT_ID = + "androidx.media.MediaMetadatCompat.METADATA_KEY_SERIES_CONTENT_ID"; + + /** + * Key sent through a key-value mapping in {@link MediaMetadataCompat#getLong(String)} or in the + * {@link MediaDescriptionCompat#getExtras()} bundle to the hosting {@link MediaBrowserCompat} to + * indicate that the corresponding {@link MediaMetadataCompat} or {@link + * MediaBrowserCompat.MediaItem} has explicit content (i.e. user discretion is advised when + * viewing or listening to this content). + * + *

TYPE: long (to enable, use value {@link #METADATA_VALUE_ATTRIBUTE_PRESENT}) + * + * @see MediaMetadataCompat#getLong(String) + * @see MediaMetadataCompat.Builder#putLong(String, long) + * @see MediaDescriptionCompat#getExtras() + * @see MediaDescriptionCompat.Builder#setExtras(Bundle) + */ + @SuppressLint("IntentName") + public static final String METADATA_KEY_IS_EXPLICIT = "android.media.IS_EXPLICIT"; + + /** + * Key sent through a key-value mapping in {@link MediaMetadataCompat#getLong(String)} or in the + * {@link MediaDescriptionCompat#getExtras()} bundle to the hosting {@link MediaBrowserCompat} to + * indicate that the corresponding {@link MediaMetadataCompat} or {@link + * MediaBrowserCompat.MediaItem} is an advertisement. + * + *

TYPE: long (to enable, use value {@link #METADATA_VALUE_ATTRIBUTE_PRESENT}) + * + * @see MediaMetadataCompat#getLong(String) + * @see MediaMetadataCompat.Builder#putLong(String, long) + * @see MediaDescriptionCompat#getExtras() + * @see MediaDescriptionCompat.Builder#setExtras(Bundle) + */ + @SuppressLint("IntentName") + public static final String METADATA_KEY_IS_ADVERTISEMENT = "android.media.metadata.ADVERTISEMENT"; + + /** + * Value sent through a key-value mapping of {@link MediaMetadataCompat}, or through {@link + * Bundle} extras on a different data type, to indicate the presence of an attribute described by + * its corresponding key. + * + * @see MediaMetadataCompat#getLong(String) + * @see MediaMetadataCompat.Builder#putLong(String, long) + */ + public static final long METADATA_VALUE_ATTRIBUTE_PRESENT = 1L; + + /** + * Bundle key passed through root hints to the {@link MediaBrowserServiceCompat} to indicate the + * maximum number of children of the root node that can be supported by the hosting {@link + * MediaBrowserCompat}. Excess root children may be omitted or made less discoverable by the host. + * + *

TYPE: int + * + * @see MediaBrowserServiceCompat#onGetRoot(String, int, Bundle) + * @see MediaBrowserServiceCompat#getBrowserRootHints() + * @see MediaBrowserCompat#MediaBrowserCompat(Context,ComponentName,ConnectionCallback,Bundle) + */ + @SuppressLint("IntentName") + public static final String BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_LIMIT = + "androidx.media.MediaBrowserCompat.Extras.KEY_ROOT_CHILDREN_LIMIT"; + + /** + * Bundle key passed through root hints to the {@link MediaBrowserServiceCompat} to indicate which + * flags exposed by {@link MediaBrowserCompat.MediaItem#getFlags()} from children of the root node + * are supported by the hosting {@link MediaBrowserCompat}. Root children with unsupported flags + * may be omitted or made less discoverable by the host. + * + *

TYPE: int, a bit field which can be used as a mask. For example, if the value masked (using + * bitwise AND) with {@link MediaBrowserCompat.MediaItem#FLAG_BROWSABLE} is nonzero, then the host + * supports browsable root children. Conversely, if the masked result is zero, then the host does + * not support them. + * + * @see MediaBrowserServiceCompat#onGetRoot(String, int, Bundle) + * @see MediaBrowserServiceCompat#getBrowserRootHints() + * @see MediaBrowserCompat#MediaBrowserCompat(Context,ComponentName,ConnectionCallback,Bundle) + */ + @SuppressLint("IntentName") + public static final String BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS = + "androidx.media.MediaBrowserCompat.Extras.KEY_ROOT_CHILDREN_SUPPORTED_FLAGS"; + + /** + * Bundle key passed through root hints to the {@link MediaBrowserServiceCompat} to indicate the + * recommended size, in pixels, for media art bitmaps. Much smaller images may not render well, + * and much larger images may cause inefficient resource consumption. + * + * @see MediaBrowserServiceCompat#onGetRoot(String, int, Bundle) + * @see MediaBrowserServiceCompat#getBrowserRootHints() + * @see MediaBrowserCompat#MediaBrowserCompat(Context,ComponentName,ConnectionCallback,Bundle) + * @see MediaDescriptionCompat#getIconUri() + * @see MediaDescriptionCompat.Builder#setIconUri(Uri) + * @see MediaDescriptionCompat#getIconBitmap() + * @see MediaDescriptionCompat.Builder#setIconBitmap(Bitmap) + */ + @SuppressLint("IntentName") + public static final String BROWSER_ROOT_HINTS_KEY_MEDIA_ART_SIZE_PIXELS = + "android.media.extras.MEDIA_ART_SIZE_HINT_PIXELS"; + + /** + * Bundle key used to indicate that the {@link MediaBrowserServiceCompat} supports showing a + * settings page. + * + *

Use this key to populate the {@link Bundle} that you pass to the constructor of the {@link + * MediaBrowserServiceCompat.BrowserRoot} returned by {@link MediaBrowserServiceCompat#onGetRoot}. + * Use {@link Bundle#putParcelable(String, Parcelable)} to set a {@link PendingIntent} for this + * key. The {@link PendingIntent} is created using the {@code CarPendingIntent#getCarApp()} + * method. + * + *

The {@link Intent} carried by the pending intent needs to have the component name set to a + * Car App Library + * service that needs to exist in the same application package as the media browser service. + * + *

TYPE: {@link PendingIntent}. + * + * @see MediaBrowserCompat#getExtras() + * @see MediaBrowserServiceCompat#onGetRoot(String, int, Bundle) + * @see MediaBrowserServiceCompat.BrowserRoot#BrowserRoot(String, Bundle) + */ + @SuppressLint("IntentName") + public static final String + BROWSER_SERVICE_EXTRAS_KEY_APPLICATION_PREFERENCES_USING_CAR_APP_LIBRARY_INTENT = + "androidx.media.BrowserRoot.Extras" + + ".APPLICATION_PREFERENCES_USING_CAR_APP_LIBRARY_INTENT"; + + /** + * Bundle key sent through {@link MediaBrowserCompat#getExtras()} to the hosting {@link + * MediaBrowserCompat} to indicate that the {@link MediaBrowserServiceCompat} supports the method + * {@link MediaBrowserServiceCompat#onSearch(String, Bundle, MediaBrowserServiceCompat.Result)}. + * If sent as {@code true}, the host may expose affordances which call the search method. + * + *

TYPE: boolean + * + * @see MediaBrowserCompat#getExtras() + * @see MediaBrowserServiceCompat.BrowserRoot#BrowserRoot(String, Bundle) + */ + @SuppressLint("IntentName") + public static final String BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED = + "android.media.browse.SEARCH_SUPPORTED"; + + /** + * Bundle key used to pass a browseable {@link android.media.browse.MediaBrowser.MediaItem} that + * represents 'Favorite' content or some other notion of preset/pinned content. + * + *

Use this key to indicate to consumers (e.g. Auto and Automotive) that they can display + * and/or subscribe to this item. + * + *

When this item is subscribed to, it is expected that the {@link MediaBrowserService} or + * {@link MediaBrowserServiceCompat} loads content that the user has marked for easy or quick + * access - e.g. favorite radio stations, pinned playlists, etc. + * + *

TYPE: MediaBrowser.MediaItem - note this should not be a {@link + * MediaBrowserCompat.MediaItem} + * + * @see MediaBrowserCompat#getExtras() + * @see MediaBrowserServiceCompat#onGetRoot(String, int, Bundle) + * @see MediaBrowserServiceCompat.BrowserRoot#BrowserRoot(String, Bundle) + */ + @SuppressLint("IntentName") + public static final String BROWSER_SERVICE_EXTRAS_KEY_FAVORITES_MEDIA_ITEM = + "androidx.media.BrowserRoot.Extras.FAVORITES_MEDIA_ITEM"; + + /** + * Bundle key passed from the {@link MediaBrowserServiceCompat} to the hosting {@link + * MediaBrowserCompat} to indicate a preference about how playable instances of {@link + * MediaBrowserCompat.MediaItem} are presented. + * + *

If exposed through {@link MediaBrowserCompat#getExtras()}, the preference applies to all + * playable items within the browse tree. + * + *

If exposed through {@link MediaDescriptionCompat#getExtras()}, the preference applies to + * only the immediate playable children of the corresponding browsable item. It takes precedence + * over preferences sent through {@link MediaBrowserCompat#getExtras()}. + * + *

TYPE: int. Possible values are separate constants. + * + * @see MediaBrowserCompat#getExtras() + * @see MediaBrowserServiceCompat.BrowserRoot#BrowserRoot(String, Bundle) + * @see MediaDescriptionCompat#getExtras() + * @see MediaDescriptionCompat.Builder#setExtras(Bundle) + * @see #DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM + * @see #DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM + */ + @SuppressLint("IntentName") + public static final String DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE = + "android.media.browse.CONTENT_STYLE_PLAYABLE_HINT"; + + /** + * Bundle key passed from the {@link MediaBrowserServiceCompat} to the hosting {@link + * MediaBrowserCompat} to indicate a preference about how browsable instances of {@link + * MediaBrowserCompat.MediaItem} are presented. + * + *

If exposed through {@link MediaBrowserCompat#getExtras()}, the preference applies to all + * browsable items within the browse tree. + * + *

If exposed through {@link MediaDescriptionCompat#getExtras()}, the preference applies to + * only the immediate browsable children of the corresponding browsable item. It takes precedence + * over preferences sent through {@link MediaBrowserCompat#getExtras()}. + * + *

TYPE: int. Possible values are separate constants. + * + * @see MediaBrowserCompat#getExtras() + * @see MediaBrowserServiceCompat.BrowserRoot#BrowserRoot(String, Bundle) + * @see MediaDescriptionCompat#getExtras() + * @see MediaDescriptionCompat.Builder#setExtras(Bundle) + * @see #DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM + * @see #DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM + * @see #DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_LIST_ITEM + * @see #DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_GRID_ITEM + */ + @SuppressLint("IntentName") + public static final String DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE = + "android.media.browse.CONTENT_STYLE_BROWSABLE_HINT"; + + /** + * Bundle key sent through {@link MediaDescriptionCompat#getExtras()} to the hosting {@link + * MediaBrowserCompat} to indicate a preference about how the corresponding {@link + * MediaBrowserCompat.MediaItem} is presented. + * + *

This preference takes precedence over those expressed by {@link + * #DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE} and {@link + * #DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE}. + * + *

TYPE: int. Possible values are separate constants. + * + * @see MediaDescriptionCompat#getExtras() + * @see MediaDescriptionCompat.Builder#setExtras(Bundle) + * @see #DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM + * @see #DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM + * @see #DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_LIST_ITEM + * @see #DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_GRID_ITEM + */ + @SuppressLint("IntentName") + public static final String DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_SINGLE_ITEM = + "android.media.browse.CONTENT_STYLE_SINGLE_ITEM_HINT"; + + /** + * Bundle value passed from the {@link MediaBrowserServiceCompat} to the hosting {@link + * MediaBrowserCompat} to indicate a preference that certain instances of {@link + * MediaBrowserCompat.MediaItem} should be presented as list items. + * + * @see #DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE + * @see #DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE + */ + public static final int DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM = 1; + + /** + * Bundle value passed from the {@link MediaBrowserServiceCompat} to the hosting {@link + * MediaBrowserCompat} to indicate a preference that certain instances of {@link + * MediaBrowserCompat.MediaItem} should be presented as grid items. + * + * @see #DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE + * @see #DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE + */ + public static final int DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM = 2; + + /** + * Bundle value passed from the {@link MediaBrowserServiceCompat} to the hosting {@link + * MediaBrowserCompat} to indicate a preference that browsable instances of {@link + * MediaBrowserCompat.MediaItem} should be presented as "category" list items. This means the + * items must provide tintable vector drawable icons that render well when they: + * + *

    + *
  • do not fill all of the available area + *
  • are tinted by the system to provide sufficient contrast against the background + *
+ * + * @see #DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE + */ + public static final int DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_LIST_ITEM = 3; + + /** + * Bundle value passed from the {@link MediaBrowserServiceCompat} to the hosting {@link + * MediaBrowserCompat} to indicate a preference that browsable instances of {@link + * MediaBrowserCompat.MediaItem} should be presented as "category" grid items. This means the + * items must provide tintable vector drawable icons that render well when they: + * + *
    + *
  • do not fill all of the available area + *
  • are tinted by the system to provide sufficient contrast against the background + *
+ * + * @see #DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE + */ + public static final int DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_GRID_ITEM = 4; + + /** + * Bundle key sent through {@link MediaDescriptionCompat#getExtras()} to the hosting {@link + * MediaBrowserCompat} to indicate that certain instances of {@link MediaBrowserCompat.MediaItem} + * are related as a group, with a title that is specified through the bundle value. Items that are + * children of the same browsable node and have the same title are members of the same group. The + * host may present a group's items as a contiguous block and display the title alongside the + * group. + * + *

TYPE: String. Should be human readable and localized. + * + * @see MediaDescriptionCompat#getExtras() + * @see MediaDescriptionCompat.Builder#setExtras(Bundle) + */ + @SuppressLint("IntentName") + public static final String DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE = + "android.media.browse.CONTENT_STYLE_GROUP_TITLE_HINT"; + + /** + * Bundle key sent through {@link MediaDescriptionCompat#getExtras()} to the hosting {@link + * MediaBrowserCompat} to indicate the playback completion status of the corresponding {@link + * MediaBrowserCompat.MediaItem}. + * + *

TYPE: int. Possible values are separate constants. + * + * @see MediaDescriptionCompat#getExtras() + * @see MediaDescriptionCompat.Builder#setExtras(Bundle) + * @see #DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED + * @see #DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED + * @see #DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED + */ + @SuppressLint("IntentName") + public static final String DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS = + "android.media.extra.PLAYBACK_STATUS"; + + /** + * Bundle value sent through {@link MediaDescriptionCompat#getExtras()} to the hosting {@link + * MediaBrowserCompat} to indicate that the corresponding {@link MediaBrowserCompat.MediaItem} has + * not been played by the user. + * + * @see MediaDescriptionCompat#getExtras() + * @see MediaDescriptionCompat.Builder#setExtras(Bundle) + * @see #DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS + */ + public static final int DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED = 0; + + /** + * Bundle value sent through {@link MediaDescriptionCompat#getExtras()} to the hosting {@link + * MediaBrowserCompat} to indicate that the corresponding {@link MediaBrowserCompat.MediaItem} has + * been partially played by the user. + * + * @see MediaDescriptionCompat#getExtras() + * @see MediaDescriptionCompat.Builder#setExtras(Bundle) + * @see #DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS + */ + public static final int DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED = 1; + + /** + * Bundle value sent through {@link MediaDescriptionCompat#getExtras()} to the hosting {@link + * MediaBrowserCompat} to indicate that the corresponding {@link MediaBrowserCompat.MediaItem} has + * been fully played by the user. + * + * @see MediaDescriptionCompat#getExtras() + * @see MediaDescriptionCompat.Builder#setExtras(Bundle) + * @see #DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS + */ + public static final int DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED = 2; + + /** + * Bundle key sent through {@link MediaDescriptionCompat#getExtras()} to the hosting {@link + * MediaBrowserCompat} to indicate an amount of completion progress for the corresponding {@link + * MediaBrowserCompat.MediaItem}. This extra augments {@link + * #DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED the partially played status} by + * indicating how much has been played by the user. + * + *

TYPE: double, a value between 0.0 and 1.0, inclusive. 0.0 indicates no completion progress + * (item is not started) and 1.0 indicates full completion progress (item is fully played). Values + * in between indicate partial progress (for example, 0.75 indicates the item is 75% complete). + * + * @see MediaDescriptionCompat#getExtras() + * @see MediaDescriptionCompat.Builder#setExtras(Bundle) + */ + @SuppressLint("IntentName") + public static final String DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE = + "androidx.media.MediaItem.Extras.COMPLETION_PERCENTAGE"; + + /** + * {@link Bundle} key used to store supported custom browser actions for {@link MediaBrowserCompat + * media browsers} that support custom browser actions. + * + *

The browser indicates support for custom browser actions by including the key {@link + * MediaConstants#BROWSER_ROOT_HINTS_KEY_CUSTOM_BROWSER_ACTION_LIMIT} with a non-zero value in the + * root hints bundle passed to {@link MediaBrowserServiceCompat#onGetRoot}. + * + *

Use this key to add an {@link ArrayList} to the {@link Bundle} passed in {@link + * MediaBrowserServiceCompat.BrowserRoot}. {@link MediaBrowserServiceCompat} should add this + * bundle to the {@link MediaBrowserServiceCompat.BrowserRoot} when {@link + * MediaBrowserServiceCompat#onGetRoot(String, int, Bundle)} is called. Use {@link + * Bundle#putParcelableArrayList(String, ArrayList)} with a list of bundles, each defining a + * custom browser action, to set supported custom browser actions. + * + *

TYPE: arraylist, an ArrayList of {@link Bundle}s, with each bundle defining a browse custom + * action. + * + *

A custom browser action is defined by an {@linkplain + * MediaConstants#EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID action ID}, an {@linkplain + * MediaConstants#EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL action label}, an {@linkplain + * MediaConstants#EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI action icon URI}, and optionally an + * {@linkplain MediaConstants#EXTRAS_KEY_CUSTOM_BROWSER_ACTION_EXTRAS action extras bundle}. + * + *

Custom browser action example: + * + *

    + *
  • Action ID: "com.example.audioapp.download" + *
      + *
    • Key: {@link MediaConstants#EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID} + *
    + *
  • Action label: "Download Song" + *
      + *
    • Key: {@link MediaConstants#EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL} + *
    • Localized String label for action + *
    + *
  • Action Icon URI: "content://com.example.public/download" + *
      + *
    • Key: {@link MediaConstants#EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI} + *
    • Tintable vector drawable + *
    + *
  • Action extras: {bundle} + *
      + *
    • Key: {@link MediaConstants#EXTRAS_KEY_CUSTOM_BROWSER_ACTION_EXTRAS} + *
    • Bundle extras + *
    + *
+ */ + public static final String BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST = + "androidx.media.utils.extras.CUSTOM_BROWSER_ACTION_ROOT_LIST"; + + /** + * {@link Bundle} key used to define a string list of custom browser actions for a {@link + * MediaBrowserCompat.MediaItem}. e.g. "download","favorite","add to queue" + * + *

Supported {@link MediaBrowserCompat media browsers} use this {@link Bundle} key to build a + * list of custom browser actions for each {@link MediaBrowserCompat.MediaItem}. + * + *

This key is sent through {@link MediaDescriptionCompat#getExtras()} to the {@link + * MediaBrowserCompat} to indicate supported custom browser actions for the corresponding {@link + * MediaBrowserCompat.MediaItem}. + * + *

Use {@linkplain Bundle#putStringArrayList(String, ArrayList) a string array list} with a + * list of custom browser action IDs. Set this bundle in the {@link MediaBrowserCompat.MediaItem} + * using {@link MediaDescriptionCompat.Builder#setExtras(Bundle)} to set the supported browse + * custom actions for the {@link MediaBrowserCompat.MediaItem}. + * + *

Each value action in this list must be an action ID defined in {@linkplain + * MediaBrowserServiceCompat.BrowserRoot browser root} with {@link Bundle} key {@link + * MediaConstants#BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST}. + * + *

TYPE: ArrayList<String>, list of String custom browser action IDs. + * + * @see MediaDescriptionCompat#getExtras() + * @see MediaDescriptionCompat.Builder#setExtras(Bundle) + * @see MediaConstants#BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST + */ + public static final String DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST = + "androidx.media.utils.extras.CUSTOM_BROWSER_ACTION_ID_LIST"; + + /** + * {@link Bundle} key used to define the ID for a custom browser action. + * + *

TYPE: String, String ID for a custom browser action. + * + * @see MediaConstants#BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST + * @see MediaConstants#DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST + */ + public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID = + "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_ID"; + + /** + * {@link Bundle} key used to define the label for a custom browser action. Label is a localized + * string that labels the action for the user. + * + *

TYPE: String, String label for a custom browser action. This must be localized. + * + * @see MediaConstants#BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST + * @see MediaConstants#DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST + */ + public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_LABEL = + "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_LABEL"; + + /** + * {@link Bundle} key used to define the icon URI for a custom browser action. + * + *

TYPE: String, String content provider URI for a tintable vector drawable icon. + * + * @see MediaConstants#BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST + * @see MediaConstants#DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST + */ + public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ICON_URI = + "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_ICON_URI"; + + /** + * {@link Bundle} key used to define an extras bundle for a custom browser action. + * + *

Use {@link Bundle#putBundle(String, Bundle)} on the custom browser action bundle to add this + * extras bundle to the custom browser action. + * + *

TYPE: Bundle. + * + * @see MediaConstants#BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST + * @see MediaConstants#DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST + */ + public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_EXTRAS = + "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_EXTRAS"; + + /** + * {@link Bundle} key used to define the total number of actions allowed per item. Passed to + * {@link MediaBrowserServiceCompat} using {@link MediaBrowserServiceCompat#onGetRoot(String, int, + * Bundle)} in root hints bundle. + * + *

Presence of this key and positive value in the root hints indicates that custom browse + * actions feature is supported. Actions beyond this limit will be truncated. + * + *

TYPE: int, number of actions each item is limited to. + * + * @see MediaBrowserServiceCompat#onGetRoot(String, int, Bundle) + * @see MediaConstants#BROWSER_SERVICE_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ROOT_LIST + * @see MediaConstants#DESCRIPTION_EXTRAS_KEY_CUSTOM_BROWSER_ACTION_ID_LIST + */ + public static final String BROWSER_ROOT_HINTS_KEY_CUSTOM_BROWSER_ACTION_LIMIT = + "androidx.media.utils.MediaBrowserCompat.extras.CUSTOM_BROWSER_ACTION_LIMIT"; + + /** + * {@link Bundle} key used to define the ID of the {@link MediaBrowserCompat.MediaItem} associated + * with the invoked action. + * + *

A {@link MediaBrowserCompat} that supports custom browser actions can set this key in the + * parameter extra bundle when using {@link MediaBrowserCompat#sendCustomAction(String, Bundle, + * MediaBrowserCompat.CustomActionCallback)}. + * + *

A {@link MediaBrowserServiceCompat} that supports custom browser actions should override + * {@link MediaBrowserServiceCompat#onCustomAction( String, Bundle, + * MediaBrowserServiceCompat.Result)} to receive extras bundle set by {@link MediaBrowserCompat + * media browsers}. + * + *

TYPE: string, string ID of the {@link MediaBrowserCompat.MediaItem} on which the custom + * action was invoked. + * + * @see MediaBrowserServiceCompat#onCustomAction(String, Bundle, MediaBrowserServiceCompat.Result) + * @see MediaBrowserCompat#sendCustomAction(String, Bundle, + * MediaBrowserCompat.CustomActionCallback) + */ + public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID = + "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_MEDIA_ITEM_ID"; + + /** + * {@link Bundle} key set in {@link MediaBrowserServiceCompat.Result} to indicate which browse + * node should be displayed next. + * + *

A {@link MediaBrowserServiceCompat} that supports custom browser actions can set this key in + * the {@link MediaBrowserServiceCompat.Result} passed in {@link + * MediaBrowserServiceCompat#onCustomAction(String, Bundle, MediaBrowserServiceCompat.Result)}. + * + *

If this key is present in a {@link MediaBrowserCompat.CustomActionCallback} data {@link + * Bundle} the {@link MediaBrowserCompat} will update the current browse node when {@link + * MediaBrowserCompat.CustomActionCallback#onResult(String, Bundle, Bundle)} is called by the + * {@link MediaBrowserServiceCompat}. The new browse node will be fetched by {@link + * MediaBrowserCompat#getItem(String, MediaBrowserCompat.ItemCallback)}. + * + *

A {@link MediaBrowserServiceCompat} that supports custom browser actions must implement + * {@link MediaBrowserServiceCompat#onLoadItem(String, MediaBrowserServiceCompat.Result)} to use + * this feature. + * + *

TYPE: string, string {@link MediaBrowserCompat.MediaItem} ID to set as new browse node. + * + * @see MediaBrowserCompat#sendCustomAction(String, Bundle, + * MediaBrowserCompat.CustomActionCallback) + * @see MediaBrowserCompat.CustomActionCallback + * @see MediaBrowserServiceCompat#onCustomAction(String, Bundle, MediaBrowserServiceCompat.Result) + */ + public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_BROWSE_NODE = + "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_RESULT_BROWSE_NODE"; + + /** + * {@link Bundle} key set in {@link MediaBrowserServiceCompat.Result} to show the currently + * playing item. + * + *

A {@link MediaBrowserServiceCompat} that supports custom browser actions can set this key in + * the {@link MediaBrowserServiceCompat.Result} passed in {@link + * MediaBrowserServiceCompat#onCustomAction(String, Bundle, MediaBrowserServiceCompat.Result)}. + * + *

If this key is present and the value is true in {@link + * MediaBrowserCompat.CustomActionCallback} {@link MediaBrowserServiceCompat.Result}, the + * currently playing item will be shown when {@link + * MediaBrowserCompat.CustomActionCallback#onResult(String, Bundle, Bundle)} is called by the + * {@link MediaBrowserServiceCompat}. + * + *

TYPE: boolean, boolean value of true will show currently playing item. + * + * @see MediaBrowserCompat#sendCustomAction(String, Bundle, + * MediaBrowserCompat.CustomActionCallback) + * @see MediaBrowserCompat.CustomActionCallback + * @see MediaBrowserServiceCompat#onCustomAction(String, Bundle, MediaBrowserServiceCompat.Result) + */ + public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_SHOW_PLAYING_ITEM = + "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_RESULT_SHOW_PLAYING_ITEM"; + + /** + * {@link Bundle} key set in {@link MediaBrowserServiceCompat.Result} to refresh a {@link + * MediaBrowserCompat.MediaItem} in the browse tree. + * + *

A {@link MediaBrowserServiceCompat} that supports custom browser actions can set this key in + * the {@link MediaBrowserServiceCompat.Result} passed in {@link + * MediaBrowserServiceCompat#onCustomAction(String, Bundle, MediaBrowserServiceCompat.Result)}. + * + *

If this key is present in {@link MediaBrowserCompat.CustomActionCallback} {@link + * MediaBrowserServiceCompat.Result}, the item will be refreshed with {@link + * MediaBrowserCompat#getItem(String, MediaBrowserCompat.ItemCallback)} when {@link + * MediaBrowserCompat.CustomActionCallback#onProgressUpdate(String, Bundle, Bundle)} or {@link + * MediaBrowserCompat.CustomActionCallback#onResult(String, Bundle, Bundle)} is called by the + * {@link MediaBrowserServiceCompat}. + * + *

A {@link MediaBrowserServiceCompat} that supports custom browser actions must implement + * {@link MediaBrowserServiceCompat#onLoadItem(String, MediaBrowserServiceCompat.Result)} in order + * to update the state of the item. + * + *

TYPE: string, string {@link MediaBrowserCompat.MediaItem} ID to refresh. + * + * @see MediaBrowserCompat#sendCustomAction(String, Bundle, + * MediaBrowserCompat.CustomActionCallback) + * @see MediaBrowserCompat.CustomActionCallback + * @see MediaBrowserServiceCompat#onCustomAction(String, Bundle, MediaBrowserServiceCompat.Result) + */ + public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM = + "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_RESULT_REFRESH_ITEM"; + + /** + * {@link Bundle} key set in {@link MediaBrowserServiceCompat.Result} to set a message for the + * user. + * + *

A {@link MediaBrowserServiceCompat} that supports custom browser actions can set this key in + * the {@link MediaBrowserServiceCompat.Result} passed in {@link + * MediaBrowserServiceCompat#onCustomAction(String, Bundle, MediaBrowserServiceCompat.Result)}. + * + *

If this key is present in {@link MediaBrowserCompat.CustomActionCallback} {@link + * MediaBrowserServiceCompat.Result}, the message will be shown to the user when {@link + * MediaBrowserCompat.CustomActionCallback#onProgressUpdate(String, Bundle, Bundle)} or {@link + * MediaBrowserCompat.CustomActionCallback#onResult(String, Bundle, Bundle)} is called by the + * {@link MediaBrowserServiceCompat}. + * + *

TYPE: string, localized message string to show the user. + * + * @see MediaBrowserCompat#sendCustomAction(String, Bundle, + * MediaBrowserCompat.CustomActionCallback) + * @see MediaBrowserCompat.CustomActionCallback + * @see MediaBrowserServiceCompat#onCustomAction(String, Bundle, MediaBrowserServiceCompat.Result) + */ + public static final String EXTRAS_KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE = + "androidx.media.utils.extras.KEY_CUSTOM_BROWSER_ACTION_RESULT_MESSAGE"; + + /** + * Bundle key used for the media ID in {@link PlaybackStateCompat playback state} extras. It's for + * associating the playback state with the media being played so the value is expected to be same + * with {@link MediaMetadataCompat#METADATA_KEY_MEDIA_ID media id} of the current metadata. + * + *

TYPE: String + * + * @see PlaybackStateCompat#getExtras + * @see PlaybackStateCompat.Builder#setExtras + */ + @SuppressLint("IntentName") + public static final String PLAYBACK_STATE_EXTRAS_KEY_MEDIA_ID = + "androidx.media.PlaybackStateCompat.Extras.KEY_MEDIA_ID"; + + /** + * Bundle key passed through {@link PlaybackStateCompat#getExtras()} to the hosting {@link + * MediaControllerCompat} which maps to a label. The label is associated with {@link + * #PLAYBACK_STATE_EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT the action} that allow users to + * resolve the current playback state error. + * + *

The label should be short; a more detailed explanation can be provided to the user via + * {@link PlaybackStateCompat#getErrorMessage()}. + * + *

TYPE: String. Should be human readable and localized. + * + * @see PlaybackStateCompat#getExtras() + * @see PlaybackStateCompat.Builder#setExtras(Bundle) + * @see #PLAYBACK_STATE_EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT + */ + @SuppressLint("IntentName") + public static final String PLAYBACK_STATE_EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL = + "android.media.extras.ERROR_RESOLUTION_ACTION_LABEL"; + + /** + * Bundle key passed through {@link PlaybackStateCompat#getExtras()} to the hosting {@link + * MediaControllerCompat} which maps to a pending intent. When launched, the intent should allow + * users to resolve the current playback state error. {@link + * #PLAYBACK_STATE_EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL A label} should be included in the + * same Bundle. The key {@link + * #BROWSER_SERVICE_EXTRAS_KEY_APPLICATION_PREFERENCES_USING_CAR_APP_LIBRARY_INTENT} should be + * used instead if the intent points to a Car App Library service. + * + *

The intent is NOT auto launched and the user first sees an actionable button with label set + * to {@link #PLAYBACK_STATE_EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL}. Clicking that button + * launches the intent. + * + *

TYPE: PendingIntent. Should be inserted into the Bundle {@link Bundle#putParcelable(String, + * Parcelable) as a Parcelable}. + * + * @see PlaybackStateCompat#getExtras() + * @see PlaybackStateCompat.Builder#setExtras(Bundle) + * @see #PLAYBACK_STATE_EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL + * @see #BROWSER_SERVICE_EXTRAS_KEY_APPLICATION_PREFERENCES_USING_CAR_APP_LIBRARY_INTENT + */ + @SuppressLint("IntentName") + public static final String PLAYBACK_STATE_EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT = + "android.media.extras.ERROR_RESOLUTION_ACTION_INTENT"; + + /** + * Bundle key passed through {@link PlaybackStateCompat#getExtras()} to the {@link + * MediaControllerCompat} which maps to a {@link PendingIntent}. When launched, the {@link + * PendingIntent} should allow users to resolve the current playback state error. The intent + * should have the component name set to a Car App Library service which exists in the same + * application package as the media browser service. The intent may be launched directly unlike + * the behavior when using {@link #PLAYBACK_STATE_EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT}. + * + *

Applications must also set the error message and {@link + * #PLAYBACK_STATE_EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL} for cases in which the intent cannot + * be auto launched. + * + *

TYPE: {@link PendingIntent}. Should be inserted into the Bundle {@link + * Bundle#putParcelable(String, Parcelable) as a Parcelable}. + * + * @see PlaybackStateCompat#getExtras() + * @see PlaybackStateCompat.Builder#setExtras(Bundle) + */ + @SuppressLint("IntentName") + public static final String + PLAYBACK_STATE_EXTRAS_KEY_ERROR_RESOLUTION_USING_CAR_APP_LIBRARY_INTENT = + "androidx.media.PlaybackStateCompat.Extras" + + ".ERROR_RESOLUTION_USING_CAR_APP_LIBRARY_INTENT"; + + /** + * Bundle key passed through the {@code extras} of {@link + * MediaControllerCompat.TransportControls#prepareFromMediaId(String, Bundle)}, {@link + * MediaControllerCompat.TransportControls#prepareFromSearch(String, Bundle)}, {@link + * MediaControllerCompat.TransportControls#prepareFromUri(Uri, Bundle)}, {@link + * MediaControllerCompat.TransportControls#playFromMediaId(String, Bundle)}, {@link + * MediaControllerCompat.TransportControls#playFromSearch(String, Bundle)}, or {@link + * MediaControllerCompat.TransportControls#playFromUri(Uri, Bundle)} to indicate the stream type + * to be used by the session when playing or preparing the media. + * + *

TYPE: int + * + * @see MediaControllerCompat.TransportControls#prepareFromMediaId(String, Bundle) + * @see MediaControllerCompat.TransportControls#prepareFromSearch(String, Bundle) + * @see MediaControllerCompat.TransportControls#prepareFromUri(Uri, Bundle) + * @see MediaControllerCompat.TransportControls#playFromMediaId(String, Bundle) + * @see MediaControllerCompat.TransportControls#playFromSearch(String, Bundle) + * @see MediaControllerCompat.TransportControls#playFromUri(Uri, Bundle) + */ + @SuppressLint("IntentName") + public static final String TRANSPORT_CONTROLS_EXTRAS_KEY_LEGACY_STREAM_TYPE = + "android.media.session.extra.LEGACY_STREAM_TYPE"; + + /** + * Bundle key passed through the {@code extras} of {@link + * MediaControllerCompat.TransportControls#prepareFromMediaId(String, Bundle)}, {@link + * MediaControllerCompat.TransportControls#prepareFromSearch(String, Bundle)}, {@link + * MediaControllerCompat.TransportControls#prepareFromUri(Uri, Bundle)}, {@link + * MediaControllerCompat.TransportControls#playFromMediaId(String, Bundle)}, {@link + * MediaControllerCompat.TransportControls#playFromSearch(String, Bundle)}, or {@link + * MediaControllerCompat.TransportControls#playFromUri(Uri, Bundle)} to indicate whether the + * session should shuffle the media to be played or not. The extra parameter is limited to the + * current request and doesn't affect the {@link MediaSessionCompat#setShuffleMode(int) shuffle + * mode}. + * + *

TYPE: boolean + * + * @see MediaControllerCompat.TransportControls#prepareFromMediaId(String, Bundle) + * @see MediaControllerCompat.TransportControls#prepareFromSearch(String, Bundle) + * @see MediaControllerCompat.TransportControls#prepareFromUri(Uri, Bundle) + * @see MediaControllerCompat.TransportControls#playFromMediaId(String, Bundle) + * @see MediaControllerCompat.TransportControls#playFromSearch(String, Bundle) + * @see MediaControllerCompat.TransportControls#playFromUri(Uri, Bundle) + */ + @SuppressLint("IntentName") + public static final String TRANSPORT_CONTROLS_EXTRAS_KEY_SHUFFLE = + "androidx.media.MediaControllerCompat.TransportControls.extras.KEY_SHUFFLE"; + + private MediaConstants() {} +} diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaControllerCompat.java b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaControllerCompat.java new file mode 100644 index 0000000000..790d58eb69 --- /dev/null +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaControllerCompat.java @@ -0,0 +1,2636 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.session.legacy; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY; +import static androidx.media3.common.util.Assertions.checkNotNull; + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.Context; +import android.media.AudioManager; +import android.media.MediaMetadata; +import android.media.Rating; +import android.media.session.MediaController; +import android.media.session.MediaSession; +import android.media.session.PlaybackState; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.text.TextUtils; +import android.util.Log; +import android.view.KeyEvent; +import androidx.annotation.GuardedBy; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.RestrictTo; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.R; +import androidx.media3.session.legacy.MediaSessionCompat.QueueItem; +import androidx.media3.session.legacy.PlaybackStateCompat.CustomAction; +import androidx.versionedparcelable.ParcelUtils; +import androidx.versionedparcelable.VersionedParcelable; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Allows an app to interact with an ongoing media session. Media buttons and other commands can be + * sent to the session. A callback may be registered to receive updates from the session, such as + * metadata and play state changes. + * + *

A MediaController can be created if you have a {@link MediaSessionCompat.Token} from the + * session owner. + * + *

MediaController objects are thread-safe. + * + *

This is a helper for accessing features in {@link android.media.session.MediaSession} + * introduced after API level 4 in a backwards compatible fashion. + * + *

If MediaControllerCompat is created with a {@link MediaSessionCompat.Token + * session token} from another process, following methods will not work directly after the creation + * if the {@link MediaSessionCompat.Token session token} is not passed through a {@link + * MediaBrowserCompat}: + * + *

    + *
  • {@link #getPlaybackState()}.{@link PlaybackStateCompat#getExtras() getExtras()} + *
  • {@link #getRatingType()} + *
  • {@link #getRepeatMode()} + *
  • {@link #getSessionInfo()} + *
  • {@link #getShuffleMode()} + *
  • {@link #isCaptioningEnabled()} + *
+ * + *
+ * + *

Developer Guides

+ * + *

For information about building your media application, read the Media Apps developer guide.

+ */ +@UnstableApi +@RestrictTo(LIBRARY) +public final class MediaControllerCompat { + static final String TAG = "MediaControllerCompat"; + + /** */ + public static final String COMMAND_GET_EXTRA_BINDER = + "android.support.v4.media.session.command.GET_EXTRA_BINDER"; + + /** */ + public static final String COMMAND_ADD_QUEUE_ITEM = + "android.support.v4.media.session.command.ADD_QUEUE_ITEM"; + + /** */ + public static final String COMMAND_ADD_QUEUE_ITEM_AT = + "android.support.v4.media.session.command.ADD_QUEUE_ITEM_AT"; + + /** */ + public static final String COMMAND_REMOVE_QUEUE_ITEM = + "android.support.v4.media.session.command.REMOVE_QUEUE_ITEM"; + + /** */ + public static final String COMMAND_REMOVE_QUEUE_ITEM_AT = + "android.support.v4.media.session.command.REMOVE_QUEUE_ITEM_AT"; + + /** */ + public static final String COMMAND_ARGUMENT_MEDIA_DESCRIPTION = + "android.support.v4.media.session.command.ARGUMENT_MEDIA_DESCRIPTION"; + + /** */ + public static final String COMMAND_ARGUMENT_INDEX = + "android.support.v4.media.session.command.ARGUMENT_INDEX"; + + /** + * Sets a {@link MediaControllerCompat} in the {@code activity} for later retrieval via {@link + * #getMediaController(Activity)}. + * + *

On API 21 and later, {@link Activity#setMediaController(MediaController)} will also be + * called. + * + * @param activity The activity to set the {@code mediaController} in, must not be null. + * @param mediaController The controller for the session which should receive media keys and + * volume changes on API 21 and later. + * @see #getMediaController(Activity) + * @see Activity#setMediaController(android.media.session.MediaController) + */ + public static void setMediaController(Activity activity, MediaControllerCompat mediaController) { + activity + .getWindow() + .getDecorView() + .setTag(R.id.media_controller_compat_view_tag, mediaController); + if (android.os.Build.VERSION.SDK_INT >= 21) { + MediaControllerImplApi21.setMediaController(activity, mediaController); + } + } + + /** + * Retrieves the {@link MediaControllerCompat} set in the activity by {@link + * #setMediaController(Activity, MediaControllerCompat)} for sending media key and volume events. + * + *

This is compatible with {@link Activity#getMediaController()}. + * + * @param activity The activity to get the media controller from, must not be null. + * @return The controller which should receive events. + * @see #setMediaController(Activity, MediaControllerCompat) + */ + @Nullable + public static MediaControllerCompat getMediaController(Activity activity) { + Object tag = activity.getWindow().getDecorView().getTag(R.id.media_controller_compat_view_tag); + if (tag instanceof MediaControllerCompat) { + return (MediaControllerCompat) tag; + } else if (android.os.Build.VERSION.SDK_INT >= 21) { + return MediaControllerImplApi21.getMediaController(activity); + } + return null; + } + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + static void validateCustomAction(@Nullable String action, @Nullable Bundle args) { + if (action == null) { + return; + } + switch (action) { + case MediaSessionCompat.ACTION_FOLLOW: + case MediaSessionCompat.ACTION_UNFOLLOW: + if (args == null || !args.containsKey(MediaSessionCompat.ARGUMENT_MEDIA_ATTRIBUTE)) { + throw new IllegalArgumentException( + "An extra field " + + MediaSessionCompat.ARGUMENT_MEDIA_ATTRIBUTE + + " is required " + + "for this action " + + action + + "."); + } + break; + } + } + + private final MediaControllerImpl mImpl; + private final MediaSessionCompat.Token mToken; + // This set is used to keep references to registered callbacks to prevent them being GCed, + // since we only keep weak references for callbacks in this class and its inner classes. + private final Set mRegisteredCallbacks; + + /** + * Creates a media controller from a session. + * + * @param context A context. + * @param session The session to be controlled. + */ + public MediaControllerCompat(Context context, MediaSessionCompat session) { + this(context, session.getSessionToken()); + } + + /** + * Creates a media controller from a session token which may have been obtained from another + * process. + * + * @param context A context. + * @param sessionToken The token of the session to be controlled. + */ + public MediaControllerCompat(Context context, MediaSessionCompat.Token sessionToken) { + if (sessionToken == null) { + throw new IllegalArgumentException("sessionToken must not be null"); + } + mRegisteredCallbacks = Collections.synchronizedSet(new HashSet<>()); + mToken = sessionToken; + + if (Build.VERSION.SDK_INT >= 29) { + mImpl = new MediaControllerImplApi29(context, sessionToken); + } else if (Build.VERSION.SDK_INT >= 21) { + mImpl = new MediaControllerImplApi21(context, sessionToken); + } else { + mImpl = new MediaControllerImplBase(sessionToken); + } + } + + /** + * Gets a {@link TransportControls} instance for this session. + * + * @return A controls instance + */ + public TransportControls getTransportControls() { + return mImpl.getTransportControls(); + } + + /** + * Sends the specified media button event to the session. Only media keys can be sent by this + * method, other keys will be ignored. + * + * @param keyEvent The media button event to dispatch. + * @return true if the event was sent to the session, false otherwise. + */ + public boolean dispatchMediaButtonEvent(@Nullable KeyEvent keyEvent) { + if (keyEvent == null) { + throw new IllegalArgumentException("KeyEvent may not be null"); + } + return mImpl.dispatchMediaButtonEvent(keyEvent); + } + + /** + * Gets the current playback state for this session. + * + *

If the session is not ready, {@link PlaybackStateCompat#getExtras()} on the result of this + * method may return null. + * + * @return The current PlaybackState or null + * @see #isSessionReady + * @see Callback#onSessionReady + */ + @Nullable + public PlaybackStateCompat getPlaybackState() { + return mImpl.getPlaybackState(); + } + + /** + * Gets the current metadata for this session. + * + * @return The current MediaMetadata or null. + */ + @Nullable + public MediaMetadataCompat getMetadata() { + return mImpl.getMetadata(); + } + + /** + * Gets the current play queue for this session if one is set. If you only care about the current + * item {@link #getMetadata()} should be used. + * + * @return The current play queue or null. + */ + @Nullable + public List getQueue() { + return mImpl.getQueue(); + } + + /** + * Adds a queue item from the given {@code description} at the end of the play queue of this + * session. Not all sessions may support this. To know whether the session supports this, get the + * session's flags with {@link #getFlags()} and check that the flag {@link + * MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS} is set. + * + * @param description The {@link MediaDescriptionCompat} for creating the {@link + * MediaSessionCompat.QueueItem} to be inserted. + * @throws UnsupportedOperationException If this session doesn't support this. + * @see #getFlags() + * @see MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS + */ + public void addQueueItem(MediaDescriptionCompat description) { + mImpl.addQueueItem(description); + } + + /** + * Adds a queue item from the given {@code description} at the specified position in the play + * queue of this session. Shifts the queue item currently at that position (if any) and any + * subsequent queue items to the right (adds one to their indices). Not all sessions may support + * this. To know whether the session supports this, get the session's flags with {@link + * #getFlags()} and check that the flag {@link MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS} is + * set. + * + * @param description The {@link MediaDescriptionCompat} for creating the {@link + * MediaSessionCompat.QueueItem} to be inserted. + * @param index The index at which the created {@link MediaSessionCompat.QueueItem} is to be + * inserted. + * @throws UnsupportedOperationException If this session doesn't support this. + * @see #getFlags() + * @see MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS + */ + public void addQueueItem(MediaDescriptionCompat description, int index) { + mImpl.addQueueItem(description, index); + } + + /** + * Removes the first occurrence of the specified {@link MediaSessionCompat.QueueItem} with the + * given {@link MediaDescriptionCompat description} in the play queue of the associated session. + * Not all sessions may support this. To know whether the session supports this, get the session's + * flags with {@link #getFlags()} and check that the flag {@link + * MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS} is set. + * + * @param description The {@link MediaDescriptionCompat} for denoting the {@link + * MediaSessionCompat.QueueItem} to be removed. + * @throws UnsupportedOperationException If this session doesn't support this. + * @see #getFlags() + * @see MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS + */ + public void removeQueueItem(MediaDescriptionCompat description) { + mImpl.removeQueueItem(description); + } + + /** + * Removes a queue item at the specified position in the play queue of this session. Not all + * sessions may support this. To know whether the session supports this, get the session's flags + * with {@link #getFlags()} and check that the flag {@link + * MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS} is set. + * + * @param index The index of the element to be removed. + * @throws UnsupportedOperationException If this session doesn't support this. + * @see #getFlags() + * @see MediaSessionCompat#FLAG_HANDLES_QUEUE_COMMANDS + * @deprecated Use {@link #removeQueueItem(MediaDescriptionCompat)} instead. + */ + @Deprecated + public void removeQueueItemAt(int index) { + List queue = getQueue(); + if (queue != null && index >= 0 && index < queue.size()) { + QueueItem item = queue.get(index); + if (item != null) { + removeQueueItem(item.getDescription()); + } + } + } + + /** Gets the queue title for this session. */ + @Nullable + public CharSequence getQueueTitle() { + return mImpl.getQueueTitle(); + } + + /** Gets the extras for this session. */ + @Nullable + public Bundle getExtras() { + return mImpl.getExtras(); + } + + /** + * Gets the rating type supported by the session. One of: + * + *

    + *
  • {@link RatingCompat#RATING_NONE} + *
  • {@link RatingCompat#RATING_HEART} + *
  • {@link RatingCompat#RATING_THUMB_UP_DOWN} + *
  • {@link RatingCompat#RATING_3_STARS} + *
  • {@link RatingCompat#RATING_4_STARS} + *
  • {@link RatingCompat#RATING_5_STARS} + *
  • {@link RatingCompat#RATING_PERCENTAGE} + *
+ * + *

If the session is not ready, it will return {@link RatingCompat#RATING_NONE}. + * + * @return The supported rating type, or {@link RatingCompat#RATING_NONE} if the value is not set + * or the session is not ready. + * @see #isSessionReady + * @see Callback#onSessionReady + */ + public int getRatingType() { + return mImpl.getRatingType(); + } + + /** + * Returns whether captioning is enabled for this session. + * + *

If the session is not ready, it will return a {@code false}. + * + * @return {@code true} if captioning is enabled, {@code false} if disabled or not set. + * @see #isSessionReady + * @see Callback#onSessionReady + */ + public boolean isCaptioningEnabled() { + return mImpl.isCaptioningEnabled(); + } + + /** + * Gets the repeat mode for this session. + * + * @return The latest repeat mode set to the session, {@link PlaybackStateCompat#REPEAT_MODE_NONE} + * if not set, or {@link PlaybackStateCompat#REPEAT_MODE_INVALID} if the session is not ready + * yet. + * @see #isSessionReady + * @see Callback#onSessionReady + */ + public int getRepeatMode() { + return mImpl.getRepeatMode(); + } + + /** + * Gets the shuffle mode for this session. + * + * @return The latest shuffle mode set to the session, or {@link + * PlaybackStateCompat#SHUFFLE_MODE_NONE} if disabled or not set, or {@link + * PlaybackStateCompat#SHUFFLE_MODE_INVALID} if the session is not ready yet. + * @see #isSessionReady + * @see Callback#onSessionReady + */ + public int getShuffleMode() { + return mImpl.getShuffleMode(); + } + + /** + * Gets the flags for this session. Flags are defined in {@link MediaSessionCompat}. + * + * @return The current set of flags for the session. + */ + public long getFlags() { + return mImpl.getFlags(); + } + + /** + * Gets the current playback info for this session. + * + * @return The current playback info or null. + */ + @Nullable + public PlaybackInfo getPlaybackInfo() { + return mImpl.getPlaybackInfo(); + } + + /** + * Gets an intent for launching UI associated with this session if one exists. + * + * @return A {@link PendingIntent} to launch UI or null. + */ + @Nullable + public PendingIntent getSessionActivity() { + return mImpl.getSessionActivity(); + } + + /** + * Gets the token for the session that this controller is connected to. + * + * @return The session's token. + */ + public MediaSessionCompat.Token getSessionToken() { + return mToken; + } + + /** + * Gets the SessionToken in media2 as VersionedParcelable for the session that this controller is + * connected to. + * + * @return The session's token as VersionedParcelable. + */ + @Nullable + public VersionedParcelable getSession2Token() { + return mToken.getSession2Token(); + } + + /** + * Sets the volume of the output this session is playing on. The command will be ignored if it + * does not support {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}. The flags in {@link + * AudioManager} may be used to affect the handling. + * + * @see #getPlaybackInfo() + * @param value The value to set it to, between 0 and the reported max. + * @param flags Flags from {@link AudioManager} to include with the volume request. + */ + public void setVolumeTo(int value, int flags) { + mImpl.setVolumeTo(value, flags); + } + + /** + * Adjusts the volume of the output this session is playing on. The direction must be one of + * {@link AudioManager#ADJUST_LOWER}, {@link AudioManager#ADJUST_RAISE}, or {@link + * AudioManager#ADJUST_SAME}. The command will be ignored if the session does not support {@link + * VolumeProviderCompat#VOLUME_CONTROL_RELATIVE} or {@link + * VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}. The flags in {@link AudioManager} may be used to + * affect the handling. + * + * @see #getPlaybackInfo() + * @param direction The direction to adjust the volume in. + * @param flags Any flags to pass with the command. + */ + public void adjustVolume(int direction, int flags) { + mImpl.adjustVolume(direction, flags); + } + + /** + * Adds a callback to receive updates from the Session. Updates will be posted on the caller's + * thread. + * + * @param callback The callback object, must not be null. + */ + public void registerCallback(Callback callback) { + registerCallback(callback, null); + } + + /** + * Adds a callback to receive updates from the session. Updates will be posted on the specified + * handler's thread. + * + * @param callback The callback object, must not be null. + * @param handler The handler to post updates on. If null the callers thread will be used. + */ + @SuppressWarnings("deprecation") + public void registerCallback(Callback callback, @Nullable Handler handler) { + if (callback == null) { + throw new IllegalArgumentException("callback must not be null"); + } + if (!mRegisteredCallbacks.add(callback)) { + Log.w(TAG, "the callback has already been registered"); + return; + } + if (handler == null) { + handler = new Handler(); + } + callback.setHandler(handler); + mImpl.registerCallback(callback, handler); + } + + /** + * Stops receiving updates on the specified callback. If an update has already been posted you may + * still receive it after calling this method. + * + * @param callback The callback to remove + */ + public void unregisterCallback(Callback callback) { + if (callback == null) { + throw new IllegalArgumentException("callback must not be null"); + } + if (!mRegisteredCallbacks.remove(callback)) { + Log.w(TAG, "the callback has never been registered"); + return; + } + try { + mImpl.unregisterCallback(callback); + } finally { + callback.setHandler(null); + } + } + + /** + * Sends a generic command to the session. It is up to the session creator to decide what commands + * and parameters they will support. As such, commands should only be sent to sessions that the + * controller owns. + * + * @param command The command to send + * @param params Any parameters to include with the command. Can be {@code null}. + * @param cb The callback to receive the result on. Can be {@code null}. + */ + public void sendCommand(String command, @Nullable Bundle params, @Nullable ResultReceiver cb) { + if (TextUtils.isEmpty(command)) { + throw new IllegalArgumentException("command must neither be null nor empty"); + } + mImpl.sendCommand(command, params, cb); + } + + /** + * Returns whether the session is ready or not. + * + *

If the session is not ready, following methods can work incorrectly. + * + *

    + *
  • {@link #getPlaybackState()} + *
  • {@link #getRatingType()} + *
  • {@link #getRepeatMode()} + *
  • {@link #getSessionInfo()}} + *
  • {@link #getShuffleMode()} + *
  • {@link #isCaptioningEnabled()} + *
+ * + * @return true if the session is ready, false otherwise. + * @see Callback#onSessionReady() + */ + public boolean isSessionReady() { + return mImpl.isSessionReady(); + } + + /** + * Gets the session owner's package name. + * + * @return The package name of the session owner. + */ + @Nullable + public String getPackageName() { + return mImpl.getPackageName(); + } + + /** + * Gets the additional session information which was set when the session was created. The + * returned {@link Bundle} can include additional unchanging information about the session. For + * example, it can include the version of the session application, or other app-specific + * unchanging information. + * + * @return The additional session information, or {@link Bundle#EMPTY} if the session didn't set + * the information or if the session is not ready. + * @see #isSessionReady + * @see Callback#onSessionReady + */ + public Bundle getSessionInfo() { + return mImpl.getSessionInfo(); + } + + /** + * Gets the underlying framework {@link android.media.session.MediaController} object. + * + *

This method is only supported on API 21+. + * + * @return The underlying {@link android.media.session.MediaController} object, or null if none. + */ + @Nullable + public Object getMediaController() { + return mImpl.getMediaController(); + } + + /** + * Callback for receiving updates on from the session. A Callback can be registered using {@link + * #registerCallback} + */ + public abstract static class Callback implements IBinder.DeathRecipient { + @Nullable final MediaController.Callback mCallbackFwk; + @Nullable MessageHandler mHandler; + @Nullable IMediaControllerCallback mIControllerCallback; + + // Sharing this in constructor + @SuppressWarnings({"assignment.type.incompatible", "argument.type.incompatible"}) + public Callback() { + if (android.os.Build.VERSION.SDK_INT >= 21) { + mCallbackFwk = new MediaControllerCallbackApi21(this); + } else { + mCallbackFwk = null; + mIControllerCallback = new StubCompat(this); + } + } + + /** + * Override to handle the session being ready. + * + * @see MediaControllerCompat#isSessionReady + */ + public void onSessionReady() {} + + /** + * Override to handle the session being destroyed. The session is no longer valid after this + * call and calls to it will be ignored. + */ + public void onSessionDestroyed() {} + + /** + * Override to handle custom events sent by the session owner without a specified interface. + * Controllers should only handle these for sessions they own. + * + * @param event The event from the session. + * @param extras Optional parameters for the event. + */ + public void onSessionEvent(@Nullable String event, @Nullable Bundle extras) {} + + /** + * Override to handle changes in playback state. + * + * @param state The new playback state of the session + */ + public void onPlaybackStateChanged(@Nullable PlaybackStateCompat state) {} + + /** + * Override to handle changes to the current metadata. + * + * @param metadata The current metadata for the session or null if none. + * @see MediaMetadataCompat + */ + public void onMetadataChanged(@Nullable MediaMetadataCompat metadata) {} + + /** + * Override to handle changes to items in the queue. + * + * @see MediaSessionCompat.QueueItem + * @param queue A list of items in the current play queue. It should include the currently + * playing item as well as previous and upcoming items if applicable. + */ + public void onQueueChanged(@Nullable List queue) {} + + /** + * Override to handle changes to the queue title. + * + * @param title The title that should be displayed along with the play queue such as "Now + * Playing". May be null if there is no such title. + */ + public void onQueueTitleChanged(@Nullable CharSequence title) {} + + /** + * Override to handle changes to the {@link MediaSessionCompat} extras. + * + * @param extras The extras that can include other information associated with the {@link + * MediaSessionCompat}. + */ + public void onExtrasChanged(@Nullable Bundle extras) {} + + /** + * Override to handle changes to the audio info. + * + * @param info The current audio info for this session. + */ + public void onAudioInfoChanged(@Nullable PlaybackInfo info) {} + + /** + * Override to handle changes to the captioning enabled status. + * + * @param enabled {@code true} if captioning is enabled, {@code false} otherwise. + */ + public void onCaptioningEnabledChanged(boolean enabled) {} + + /** + * Override to handle changes to the repeat mode. + * + * @param repeatMode The repeat mode. It should be one of followings: {@link + * PlaybackStateCompat#REPEAT_MODE_NONE}, {@link PlaybackStateCompat#REPEAT_MODE_ONE}, + * {@link PlaybackStateCompat#REPEAT_MODE_ALL}, {@link + * PlaybackStateCompat#REPEAT_MODE_GROUP} + */ + public void onRepeatModeChanged(@PlaybackStateCompat.RepeatMode int repeatMode) {} + + /** + * Override to handle changes to the shuffle mode. + * + * @param shuffleMode The shuffle mode. Must be one of the following: {@link + * PlaybackStateCompat#SHUFFLE_MODE_NONE}, {@link PlaybackStateCompat#SHUFFLE_MODE_ALL}, + * {@link PlaybackStateCompat#SHUFFLE_MODE_GROUP} + */ + public void onShuffleModeChanged(@PlaybackStateCompat.ShuffleMode int shuffleMode) {} + + @Override + public void binderDied() { + postToHandler(MessageHandler.MSG_DESTROYED, null, null); + } + + /** Set the handler to use for callbacks. */ + void setHandler(@Nullable Handler handler) { + if (handler == null) { + if (mHandler != null) { + mHandler.mRegistered = false; + mHandler.removeCallbacksAndMessages(null); + mHandler = null; + } + } else { + mHandler = new MessageHandler(handler.getLooper()); + mHandler.mRegistered = true; + } + } + + void postToHandler(int what, @Nullable Object obj, @Nullable Bundle data) { + if (mHandler != null) { + Message msg = mHandler.obtainMessage(what, obj); + if (data != null) { + msg.setData(data); + } + msg.sendToTarget(); + } + } + + // Callback methods in this class are run on handler which was given to registerCallback(). + @RequiresApi(21) + private static class MediaControllerCallbackApi21 extends MediaController.Callback { + private final WeakReference mCallback; + + MediaControllerCallbackApi21(MediaControllerCompat.Callback callback) { + mCallback = new WeakReference<>(callback); + } + + @Override + public void onSessionDestroyed() { + MediaControllerCompat.Callback callback = mCallback.get(); + if (callback != null) { + callback.onSessionDestroyed(); + } + } + + @Override + public void onSessionEvent(String event, @Nullable Bundle extras) { + MediaSessionCompat.ensureClassLoader(extras); + MediaControllerCompat.Callback callback = mCallback.get(); + if (callback != null) { + if (callback.mIControllerCallback != null && android.os.Build.VERSION.SDK_INT < 23) { + // Ignore. ExtraCallback will handle this. + } else { + callback.onSessionEvent(event, extras); + } + } + } + + @Override + public void onPlaybackStateChanged(@Nullable PlaybackState stateObj) { + MediaControllerCompat.Callback callback = mCallback.get(); + if (callback != null) { + if (callback.mIControllerCallback != null) { + // Ignore. ExtraCallback will handle this. + } else { + callback.onPlaybackStateChanged(PlaybackStateCompat.fromPlaybackState(stateObj)); + } + } + } + + @Override + public void onMetadataChanged(@Nullable MediaMetadata metadataObj) { + MediaControllerCompat.Callback callback = mCallback.get(); + if (callback != null) { + callback.onMetadataChanged(MediaMetadataCompat.fromMediaMetadata(metadataObj)); + } + } + + @Override + public void onQueueChanged(@Nullable List queue) { + MediaControllerCompat.Callback callback = mCallback.get(); + if (callback != null) { + callback.onQueueChanged(QueueItem.fromQueueItemList(queue)); + } + } + + @Override + public void onQueueTitleChanged(@Nullable CharSequence title) { + MediaControllerCompat.Callback callback = mCallback.get(); + if (callback != null) { + callback.onQueueTitleChanged(title); + } + } + + @Override + public void onExtrasChanged(@Nullable Bundle extras) { + MediaSessionCompat.ensureClassLoader(extras); + MediaControllerCompat.Callback callback = mCallback.get(); + if (callback != null) { + callback.onExtrasChanged(extras); + } + } + + @Override + public void onAudioInfoChanged(@Nullable MediaController.PlaybackInfo info) { + MediaControllerCompat.Callback callback = mCallback.get(); + if (callback != null && info != null) { + callback.onAudioInfoChanged( + new PlaybackInfo( + info.getPlaybackType(), + checkNotNull(AudioAttributesCompat.wrap(info.getAudioAttributes())), + info.getVolumeControl(), + info.getMaxVolume(), + info.getCurrentVolume())); + } + } + } + + private static class StubCompat extends IMediaControllerCallback.Stub { + private final WeakReference mCallback; + + StubCompat(MediaControllerCompat.Callback callback) { + mCallback = new WeakReference<>(callback); + } + + @Override + public void onEvent(@Nullable String event, @Nullable Bundle extras) throws RemoteException { + MediaControllerCompat.Callback callback = mCallback.get(); + if (callback != null) { + callback.postToHandler(MessageHandler.MSG_EVENT, event, extras); + } + } + + @Override + public void onSessionDestroyed() throws RemoteException { + MediaControllerCompat.Callback callback = mCallback.get(); + if (callback != null) { + callback.postToHandler(MessageHandler.MSG_DESTROYED, null, null); + } + } + + @Override + public void onPlaybackStateChanged(@Nullable PlaybackStateCompat state) + throws RemoteException { + MediaControllerCompat.Callback callback = mCallback.get(); + if (callback != null) { + callback.postToHandler(MessageHandler.MSG_UPDATE_PLAYBACK_STATE, state, null); + } + } + + @Override + public void onMetadataChanged(@Nullable MediaMetadataCompat metadata) throws RemoteException { + MediaControllerCompat.Callback callback = mCallback.get(); + if (callback != null) { + callback.postToHandler(MessageHandler.MSG_UPDATE_METADATA, metadata, null); + } + } + + @Override + public void onQueueChanged(@Nullable List queue) throws RemoteException { + MediaControllerCompat.Callback callback = mCallback.get(); + if (callback != null) { + callback.postToHandler(MessageHandler.MSG_UPDATE_QUEUE, queue, null); + } + } + + @Override + public void onQueueTitleChanged(@Nullable CharSequence title) throws RemoteException { + MediaControllerCompat.Callback callback = mCallback.get(); + if (callback != null) { + callback.postToHandler(MessageHandler.MSG_UPDATE_QUEUE_TITLE, title, null); + } + } + + @Override + public void onCaptioningEnabledChanged(boolean enabled) throws RemoteException { + MediaControllerCompat.Callback callback = mCallback.get(); + if (callback != null) { + callback.postToHandler(MessageHandler.MSG_UPDATE_CAPTIONING_ENABLED, enabled, null); + } + } + + @Override + public void onRepeatModeChanged(int repeatMode) throws RemoteException { + MediaControllerCompat.Callback callback = mCallback.get(); + if (callback != null) { + callback.postToHandler(MessageHandler.MSG_UPDATE_REPEAT_MODE, repeatMode, null); + } + } + + @Override + public void onShuffleModeChangedRemoved(boolean enabled) throws RemoteException { + // Do nothing. + } + + @Override + public void onShuffleModeChanged(int shuffleMode) throws RemoteException { + MediaControllerCompat.Callback callback = mCallback.get(); + if (callback != null) { + callback.postToHandler(MessageHandler.MSG_UPDATE_SHUFFLE_MODE, shuffleMode, null); + } + } + + @Override + public void onExtrasChanged(@Nullable Bundle extras) throws RemoteException { + MediaControllerCompat.Callback callback = mCallback.get(); + if (callback != null) { + callback.postToHandler(MessageHandler.MSG_UPDATE_EXTRAS, extras, null); + } + } + + @Override + public void onVolumeInfoChanged(@Nullable ParcelableVolumeInfo info) throws RemoteException { + MediaControllerCompat.Callback callback = mCallback.get(); + if (callback != null) { + PlaybackInfo pi = null; + if (info != null) { + pi = + new PlaybackInfo( + info.volumeType, + info.audioStream, + info.controlType, + info.maxVolume, + info.currentVolume); + } + callback.postToHandler(MessageHandler.MSG_UPDATE_VOLUME, pi, null); + } + } + + @Override + public void onSessionReady() throws RemoteException { + MediaControllerCompat.Callback callback = mCallback.get(); + if (callback != null) { + callback.postToHandler(MessageHandler.MSG_SESSION_READY, null, null); + } + } + } + + private class MessageHandler extends Handler { + private static final int MSG_EVENT = 1; + private static final int MSG_UPDATE_PLAYBACK_STATE = 2; + private static final int MSG_UPDATE_METADATA = 3; + private static final int MSG_UPDATE_VOLUME = 4; + private static final int MSG_UPDATE_QUEUE = 5; + private static final int MSG_UPDATE_QUEUE_TITLE = 6; + private static final int MSG_UPDATE_EXTRAS = 7; + private static final int MSG_DESTROYED = 8; + private static final int MSG_UPDATE_REPEAT_MODE = 9; + private static final int MSG_UPDATE_CAPTIONING_ENABLED = 11; + private static final int MSG_UPDATE_SHUFFLE_MODE = 12; + private static final int MSG_SESSION_READY = 13; + + boolean mRegistered = false; + + MessageHandler(Looper looper) { + super(looper); + } + + @Override + @SuppressWarnings("unchecked") + public void handleMessage(Message msg) { + if (!mRegistered) { + return; + } + switch (msg.what) { + case MSG_EVENT: + { + Bundle extras = msg.getData(); + MediaSessionCompat.ensureClassLoader(extras); + onSessionEvent((String) msg.obj, extras); + break; + } + case MSG_UPDATE_PLAYBACK_STATE: + onPlaybackStateChanged((PlaybackStateCompat) msg.obj); + break; + case MSG_UPDATE_METADATA: + onMetadataChanged((MediaMetadataCompat) msg.obj); + break; + case MSG_UPDATE_QUEUE: + onQueueChanged((List) msg.obj); + break; + case MSG_UPDATE_QUEUE_TITLE: + onQueueTitleChanged((CharSequence) msg.obj); + break; + case MSG_UPDATE_CAPTIONING_ENABLED: + onCaptioningEnabledChanged((boolean) msg.obj); + break; + case MSG_UPDATE_REPEAT_MODE: + onRepeatModeChanged((int) msg.obj); + break; + case MSG_UPDATE_SHUFFLE_MODE: + onShuffleModeChanged((int) msg.obj); + break; + case MSG_UPDATE_EXTRAS: + { + Bundle extras = (Bundle) msg.obj; + MediaSessionCompat.ensureClassLoader(extras); + onExtrasChanged(extras); + break; + } + case MSG_UPDATE_VOLUME: + onAudioInfoChanged((PlaybackInfo) msg.obj); + break; + case MSG_DESTROYED: + onSessionDestroyed(); + break; + case MSG_SESSION_READY: + onSessionReady(); + break; + } + } + } + } + + /** + * Interface for controlling media playback on a session. This allows an app to send media + * transport commands to the session. + */ + public abstract static class TransportControls { + /** + * Used as an integer extra field in {@link #playFromMediaId(String, Bundle)} or {@link + * #prepareFromMediaId(String, Bundle)} to indicate the stream type to be used by the media + * player when playing or preparing the specified media id. See {@link AudioManager} for a list + * of stream types. + * + * @deprecated Use {@link MediaConstants#TRANSPORT_CONTROLS_EXTRAS_KEY_LEGACY_STREAM_TYPE} + * instead. + */ + @Deprecated + public static final String EXTRA_LEGACY_STREAM_TYPE = + MediaConstants.TRANSPORT_CONTROLS_EXTRAS_KEY_LEGACY_STREAM_TYPE; + + TransportControls() {} + + /** + * Request that the player prepare for playback. This can decrease the time it takes to start + * playback when a play command is received. Preparation is not required. You can call {@link + * #play} without calling this method beforehand. + */ + public abstract void prepare(); + + /** + * Request that the player prepare playback for a specific media id. This can decrease the time + * it takes to start playback when a play command is received. Preparation is not required. You + * can call {@link #playFromMediaId} without calling this method beforehand. + * + * @param mediaId The id of the requested media. + * @param extras Optional extras that can include extra information about the media item to be + * prepared. + */ + public abstract void prepareFromMediaId(String mediaId, @Nullable Bundle extras); + + /** + * Request that the player prepare playback for a specific search query. This can decrease the + * time it takes to start playback when a play command is received. An empty or null query + * should be treated as a request to prepare any music. Preparation is not required. You can + * call {@link #playFromSearch} without calling this method beforehand. + * + * @param query The search query. + * @param extras Optional extras that can include extra information about the query. + */ + public abstract void prepareFromSearch(String query, @Nullable Bundle extras); + + /** + * Request that the player prepare playback for a specific {@link Uri}. This can decrease the + * time it takes to start playback when a play command is received. Preparation is not required. + * You can call {@link #playFromUri} without calling this method beforehand. + * + * @param uri The URI of the requested media. + * @param extras Optional extras that can include extra information about the media item to be + * prepared. + */ + public abstract void prepareFromUri(Uri uri, @Nullable Bundle extras); + + /** Request that the player start its playback at its current position. */ + public abstract void play(); + + /** + * Request that the player start playback for a specific media id. + * + * @param mediaId The id of the requested media. + * @param extras Optional extras that can include extra information about the media item to be + * played. + */ + public abstract void playFromMediaId(String mediaId, @Nullable Bundle extras); + + /** + * Request that the player start playback for a specific search query. An empty or null query + * should be treated as a request to play any music. + * + * @param query The search query. + * @param extras Optional extras that can include extra information about the query. + */ + public abstract void playFromSearch(String query, @Nullable Bundle extras); + + /** + * Request that the player start playback for a specific {@link Uri}. + * + * @param uri The URI of the requested media. + * @param extras Optional extras that can include extra information about the media item to be + * played. + */ + public abstract void playFromUri(Uri uri, @Nullable Bundle extras); + + /** + * Plays an item with a specific id in the play queue. If you specify an id that is not in the + * play queue, the behavior is undefined. + */ + public abstract void skipToQueueItem(long id); + + /** Request that the player pause its playback and stay at its current position. */ + public abstract void pause(); + + /** + * Request that the player stop its playback; it may clear its state in whatever way is + * appropriate. + */ + public abstract void stop(); + + /** + * Moves to a new location in the media stream. + * + * @param pos Position to move to, in milliseconds. + */ + public abstract void seekTo(long pos); + + /** + * Starts fast forwarding. If playback is already fast forwarding this may increase the rate. + */ + public abstract void fastForward(); + + /** Skips to the next item. */ + public abstract void skipToNext(); + + /** Starts rewinding. If playback is already rewinding this may increase the rate. */ + public abstract void rewind(); + + /** Skips to the previous item. */ + public abstract void skipToPrevious(); + + /** + * Rates the current content. This will cause the rating to be set for the current user. The + * rating type of the given {@link RatingCompat} must match the type returned by {@link + * #getRatingType()}. + * + * @param rating The rating to set for the current content + */ + public abstract void setRating(RatingCompat rating); + + /** + * Rates a media item. This will cause the rating to be set for the specific media item. The + * rating type of the given {@link RatingCompat} must match the type returned by {@link + * #getRatingType()}. + * + * @param rating The rating to set for the media item. + * @param extras Optional arguments that can include information about the media item to be + * rated. + * @see MediaSessionCompat#ARGUMENT_MEDIA_ATTRIBUTE + * @see MediaSessionCompat#ARGUMENT_MEDIA_ATTRIBUTE_VALUE + */ + public abstract void setRating(RatingCompat rating, @Nullable Bundle extras); + + /** + * Sets the playback speed. A value of {@code 1.0f} is the default playback value, and a + * negative value indicates reverse playback. {@code 0.0f} is not allowed. + * + * @param speed The playback speed + * @throws IllegalArgumentException if the {@code speed} is equal to zero. + */ + public void setPlaybackSpeed(float speed) {} + + /** + * Enables/disables captioning for this session. + * + * @param enabled {@code true} to enable captioning, {@code false} to disable. + */ + public abstract void setCaptioningEnabled(boolean enabled); + + /** + * Sets the repeat mode for this session. + * + * @param repeatMode The repeat mode. Must be one of the following: {@link + * PlaybackStateCompat#REPEAT_MODE_NONE}, {@link PlaybackStateCompat#REPEAT_MODE_ONE}, + * {@link PlaybackStateCompat#REPEAT_MODE_ALL}, {@link + * PlaybackStateCompat#REPEAT_MODE_GROUP} + */ + public abstract void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode); + + /** + * Sets the shuffle mode for this session. + * + * @param shuffleMode The shuffle mode. Must be one of the following: {@link + * PlaybackStateCompat#SHUFFLE_MODE_NONE}, {@link PlaybackStateCompat#SHUFFLE_MODE_ALL}, + * {@link PlaybackStateCompat#SHUFFLE_MODE_GROUP} + */ + public abstract void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode); + + /** + * Sends a custom action for the {@link MediaSessionCompat} to perform. + * + * @param customAction The action to perform. + * @param args Optional arguments to supply to the {@link MediaSessionCompat} for this custom + * action. + */ + public abstract void sendCustomAction( + PlaybackStateCompat.CustomAction customAction, @Nullable Bundle args); + + /** + * Sends the id and args from a custom action for the {@link MediaSessionCompat} to perform. + * + * @see #sendCustomAction(PlaybackStateCompat.CustomAction action, Bundle args) + * @see MediaSessionCompat#ACTION_FLAG_AS_INAPPROPRIATE + * @see MediaSessionCompat#ACTION_SKIP_AD + * @see MediaSessionCompat#ACTION_FOLLOW + * @see MediaSessionCompat#ACTION_UNFOLLOW + * @param action The action identifier of the {@link PlaybackStateCompat.CustomAction} as + * specified by the {@link MediaSessionCompat}. + * @param args Optional arguments to supply to the {@link MediaSessionCompat} for this custom + * action. + */ + public abstract void sendCustomAction(String action, @Nullable Bundle args); + } + + /** Holds information about the way volume is handled for this session. */ + public static final class PlaybackInfo { + /** The session uses local playback. */ + public static final int PLAYBACK_TYPE_LOCAL = 1; + + /** The session uses remote playback. */ + public static final int PLAYBACK_TYPE_REMOTE = 2; + + private final int mPlaybackType; + private final AudioAttributesCompat mAudioAttrsCompat; + private final int mVolumeControl; + private final int mMaxVolume; + private final int mCurrentVolume; + + PlaybackInfo(int type, int stream, int control, int max, int current) { + this( + type, + new AudioAttributesCompat.Builder().setLegacyStreamType(stream).build(), + control, + max, + current); + } + + PlaybackInfo(int type, AudioAttributesCompat attrsCompat, int control, int max, int current) { + mPlaybackType = type; + mAudioAttrsCompat = attrsCompat; + mVolumeControl = control; + mMaxVolume = max; + mCurrentVolume = current; + } + + /** + * Gets the type of volume handling, either local or remote. One of: + * + *

    + *
  • {@link PlaybackInfo#PLAYBACK_TYPE_LOCAL} + *
  • {@link PlaybackInfo#PLAYBACK_TYPE_REMOTE} + *
+ * + * @return The type of volume handling this session is using. + */ + public int getPlaybackType() { + return mPlaybackType; + } + + /** + * Gets the stream this is currently controlling volume on. When the volume type is {@link + * PlaybackInfo#PLAYBACK_TYPE_REMOTE} this value does not have meaning and should be ignored. + * + * @deprecated Use {@link PlaybackInfo#getAudioAttributes()} instead. + * @return The stream this session is playing on. + */ + @Deprecated + public int getAudioStream() { + return mAudioAttrsCompat.getLegacyStreamType(); + } + + /** + * Get the audio attributes for this session. The attributes will affect volume handling for the + * session. When the volume type is {@link PlaybackInfo#PLAYBACK_TYPE_REMOTE} these may be + * ignored by the remote volume handler. + * + * @return The attributes for this session. + */ + public AudioAttributesCompat getAudioAttributes() { + return mAudioAttrsCompat; + } + + /** + * Gets the type of volume control that can be used. One of: + * + *
    + *
  • {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE} + *
  • {@link VolumeProviderCompat#VOLUME_CONTROL_RELATIVE} + *
  • {@link VolumeProviderCompat#VOLUME_CONTROL_FIXED} + *
+ * + * @return The type of volume control that may be used with this session. + */ + public int getVolumeControl() { + return mVolumeControl; + } + + /** + * Gets the maximum volume that may be set for this session. + * + * @return The maximum allowed volume where this session is playing. + */ + public int getMaxVolume() { + return mMaxVolume; + } + + /** + * Gets the current volume for this session. + * + * @return The current volume where this session is playing. + */ + public int getCurrentVolume() { + return mCurrentVolume; + } + } + + interface MediaControllerImpl { + void registerCallback(Callback callback, Handler handler); + + void unregisterCallback(Callback callback); + + boolean dispatchMediaButtonEvent(KeyEvent keyEvent); + + TransportControls getTransportControls(); + + @Nullable + PlaybackStateCompat getPlaybackState(); + + @Nullable + MediaMetadataCompat getMetadata(); + + @Nullable + List getQueue(); + + void addQueueItem(MediaDescriptionCompat description); + + void addQueueItem(MediaDescriptionCompat description, int index); + + void removeQueueItem(MediaDescriptionCompat description); + + @Nullable + CharSequence getQueueTitle(); + + @Nullable + Bundle getExtras(); + + int getRatingType(); + + boolean isCaptioningEnabled(); + + int getRepeatMode(); + + int getShuffleMode(); + + long getFlags(); + + @Nullable + PlaybackInfo getPlaybackInfo(); + + @Nullable + PendingIntent getSessionActivity(); + + void setVolumeTo(int value, int flags); + + void adjustVolume(int direction, int flags); + + void sendCommand(String command, @Nullable Bundle params, @Nullable ResultReceiver cb); + + boolean isSessionReady(); + + @Nullable + String getPackageName(); + + Bundle getSessionInfo(); + + @Nullable + Object getMediaController(); + } + + static class MediaControllerImplBase implements MediaControllerImpl { + private IMediaSession mBinder; + @Nullable private TransportControls mTransportControls; + @Nullable private Bundle mSessionInfo; + + MediaControllerImplBase(MediaSessionCompat.Token token) { + mBinder = IMediaSession.Stub.asInterface((IBinder) token.getToken()); + } + + @Override + public void registerCallback(Callback callback, Handler handler) { + if (callback == null) { + throw new IllegalArgumentException("callback may not be null."); + } + try { + mBinder.asBinder().linkToDeath(callback, 0); + mBinder.registerCallbackListener(checkNotNull(callback.mIControllerCallback)); + callback.postToHandler(Callback.MessageHandler.MSG_SESSION_READY, null, null); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in registerCallback.", e); + callback.postToHandler(Callback.MessageHandler.MSG_DESTROYED, null, null); + } + } + + @Override + public void unregisterCallback(Callback callback) { + if (callback == null) { + throw new IllegalArgumentException("callback may not be null."); + } + try { + mBinder.unregisterCallbackListener(checkNotNull(callback.mIControllerCallback)); + mBinder.asBinder().unlinkToDeath(callback, 0); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in unregisterCallback.", e); + } + } + + @Override + public boolean dispatchMediaButtonEvent(KeyEvent event) { + if (event == null) { + throw new IllegalArgumentException("event may not be null."); + } + try { + mBinder.sendMediaButton(event); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in dispatchMediaButtonEvent.", e); + } + return false; + } + + @Override + public TransportControls getTransportControls() { + if (mTransportControls == null) { + mTransportControls = new TransportControlsBase(mBinder); + } + + return mTransportControls; + } + + @Nullable + @Override + public PlaybackStateCompat getPlaybackState() { + try { + return mBinder.getPlaybackState(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getPlaybackState.", e); + } + return null; + } + + @Nullable + @Override + public MediaMetadataCompat getMetadata() { + try { + return mBinder.getMetadata(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getMetadata.", e); + } + return null; + } + + @Nullable + @Override + public List getQueue() { + try { + return mBinder.getQueue(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getQueue.", e); + } + return null; + } + + @Override + public void addQueueItem(MediaDescriptionCompat description) { + try { + long flags = mBinder.getFlags(); + if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) { + throw new UnsupportedOperationException( + "This session doesn't support queue management operations"); + } + mBinder.addQueueItem(description); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in addQueueItem.", e); + } + } + + @Override + public void addQueueItem(MediaDescriptionCompat description, int index) { + try { + long flags = mBinder.getFlags(); + if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) { + throw new UnsupportedOperationException( + "This session doesn't support queue management operations"); + } + mBinder.addQueueItemAt(description, index); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in addQueueItemAt.", e); + } + } + + @Override + public void removeQueueItem(MediaDescriptionCompat description) { + try { + long flags = mBinder.getFlags(); + if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) { + throw new UnsupportedOperationException( + "This session doesn't support queue management operations"); + } + mBinder.removeQueueItem(description); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in removeQueueItem.", e); + } + } + + @Nullable + @Override + public CharSequence getQueueTitle() { + try { + return mBinder.getQueueTitle(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getQueueTitle.", e); + } + return null; + } + + @Nullable + @Override + public Bundle getExtras() { + try { + return mBinder.getExtras(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getExtras.", e); + } + return null; + } + + @Override + public int getRatingType() { + try { + return mBinder.getRatingType(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getRatingType.", e); + } + return 0; + } + + @Override + public boolean isCaptioningEnabled() { + try { + return mBinder.isCaptioningEnabled(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in isCaptioningEnabled.", e); + } + return false; + } + + @Override + public int getRepeatMode() { + try { + return mBinder.getRepeatMode(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getRepeatMode.", e); + } + return PlaybackStateCompat.REPEAT_MODE_INVALID; + } + + @Override + public int getShuffleMode() { + try { + return mBinder.getShuffleMode(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getShuffleMode.", e); + } + return PlaybackStateCompat.SHUFFLE_MODE_INVALID; + } + + @Override + public long getFlags() { + try { + return mBinder.getFlags(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getFlags.", e); + } + return 0; + } + + @Nullable + @Override + public PlaybackInfo getPlaybackInfo() { + try { + ParcelableVolumeInfo info = mBinder.getVolumeAttributes(); + if (info == null) { + return null; + } + PlaybackInfo pi = + new PlaybackInfo( + info.volumeType, + info.audioStream, + info.controlType, + info.maxVolume, + info.currentVolume); + return pi; + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getPlaybackInfo.", e); + } + return null; + } + + @Nullable + @Override + public PendingIntent getSessionActivity() { + try { + return mBinder.getLaunchPendingIntent(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getSessionActivity.", e); + } + return null; + } + + @Override + public void setVolumeTo(int value, int flags) { + try { + mBinder.setVolumeTo(value, flags, null); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in setVolumeTo.", e); + } + } + + @Override + public void adjustVolume(int direction, int flags) { + try { + mBinder.adjustVolume(direction, flags, null); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in adjustVolume.", e); + } + } + + @Override + public void sendCommand(String command, @Nullable Bundle params, @Nullable ResultReceiver cb) { + try { + mBinder.sendCommand( + command, params, cb == null ? null : new MediaSessionCompat.ResultReceiverWrapper(cb)); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in sendCommand.", e); + } + } + + @Override + public boolean isSessionReady() { + return true; + } + + @Nullable + @Override + public String getPackageName() { + try { + return mBinder.getPackageName(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getPackageName.", e); + } + return null; + } + + @Override + public Bundle getSessionInfo() { + try { + mSessionInfo = mBinder.getSessionInfo(); + } catch (RemoteException e) { + Log.d(TAG, "Dead object in getSessionInfo.", e); + } + + mSessionInfo = MediaSessionCompat.unparcelWithClassLoader(mSessionInfo); + return mSessionInfo == null ? Bundle.EMPTY : new Bundle(mSessionInfo); + } + + @Nullable + @Override + public Object getMediaController() { + return null; + } + } + + static class TransportControlsBase extends TransportControls { + private IMediaSession mBinder; + + public TransportControlsBase(IMediaSession binder) { + mBinder = binder; + } + + @Override + public void prepare() { + try { + mBinder.prepare(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in prepare.", e); + } + } + + @Override + public void prepareFromMediaId(String mediaId, @Nullable Bundle extras) { + try { + mBinder.prepareFromMediaId(mediaId, extras); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in prepareFromMediaId.", e); + } + } + + @Override + public void prepareFromSearch(String query, @Nullable Bundle extras) { + try { + mBinder.prepareFromSearch(query, extras); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in prepareFromSearch.", e); + } + } + + @Override + public void prepareFromUri(Uri uri, @Nullable Bundle extras) { + try { + mBinder.prepareFromUri(uri, extras); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in prepareFromUri.", e); + } + } + + @Override + public void play() { + try { + mBinder.play(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in play.", e); + } + } + + @Override + public void playFromMediaId(String mediaId, @Nullable Bundle extras) { + try { + mBinder.playFromMediaId(mediaId, extras); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in playFromMediaId.", e); + } + } + + @Override + public void playFromSearch(String query, @Nullable Bundle extras) { + try { + mBinder.playFromSearch(query, extras); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in playFromSearch.", e); + } + } + + @Override + public void playFromUri(Uri uri, @Nullable Bundle extras) { + try { + mBinder.playFromUri(uri, extras); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in playFromUri.", e); + } + } + + @Override + public void skipToQueueItem(long id) { + try { + mBinder.skipToQueueItem(id); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in skipToQueueItem.", e); + } + } + + @Override + public void pause() { + try { + mBinder.pause(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in pause.", e); + } + } + + @Override + public void stop() { + try { + mBinder.stop(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in stop.", e); + } + } + + @Override + public void seekTo(long pos) { + try { + mBinder.seekTo(pos); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in seekTo.", e); + } + } + + @Override + public void fastForward() { + try { + mBinder.fastForward(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in fastForward.", e); + } + } + + @Override + public void skipToNext() { + try { + mBinder.next(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in skipToNext.", e); + } + } + + @Override + public void rewind() { + try { + mBinder.rewind(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in rewind.", e); + } + } + + @Override + public void skipToPrevious() { + try { + mBinder.previous(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in skipToPrevious.", e); + } + } + + @Override + public void setRating(RatingCompat rating) { + try { + mBinder.rate(rating); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in setRating.", e); + } + } + + @Override + public void setRating(RatingCompat rating, @Nullable Bundle extras) { + try { + mBinder.rateWithExtras(rating, extras); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in setRating.", e); + } + } + + @Override + public void setPlaybackSpeed(float speed) { + if (speed == 0.0f) { + throw new IllegalArgumentException("speed must not be zero"); + } + try { + mBinder.setPlaybackSpeed(speed); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in setPlaybackSpeed.", e); + } + } + + @Override + public void setCaptioningEnabled(boolean enabled) { + try { + mBinder.setCaptioningEnabled(enabled); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in setCaptioningEnabled.", e); + } + } + + @Override + public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) { + try { + mBinder.setRepeatMode(repeatMode); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in setRepeatMode.", e); + } + } + + @Override + public void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode) { + try { + mBinder.setShuffleMode(shuffleMode); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in setShuffleMode.", e); + } + } + + @Override + public void sendCustomAction(CustomAction customAction, @Nullable Bundle args) { + sendCustomAction(customAction.getAction(), args); + } + + @Override + public void sendCustomAction(String action, @Nullable Bundle args) { + validateCustomAction(action, args); + try { + mBinder.sendCustomAction(action, args); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in sendCustomAction.", e); + } + } + } + + @RequiresApi(21) + static class MediaControllerImplApi21 implements MediaControllerImpl { + protected final MediaController mControllerFwk; + + final Object mLock = new Object(); + + @GuardedBy("mLock") + private final List mPendingCallbacks = new ArrayList<>(); + + private HashMap mCallbackMap = new HashMap<>(); + + @Nullable protected Bundle mSessionInfo; + + final MediaSessionCompat.Token mSessionToken; + + // Calling method from constructor + @SuppressWarnings({"assignment.type.incompatible", "method.invocation.invalid"}) + MediaControllerImplApi21(Context context, MediaSessionCompat.Token sessionToken) { + mSessionToken = sessionToken; + mControllerFwk = + new MediaController(context, (MediaSession.Token) checkNotNull(mSessionToken.getToken())); + if (mSessionToken.getExtraBinder() == null) { + requestExtraBinder(); + } + } + + @Override + public final void registerCallback(Callback callback, Handler handler) { + mControllerFwk.registerCallback(checkNotNull(callback.mCallbackFwk), handler); + synchronized (mLock) { + IMediaSession extraBinder = mSessionToken.getExtraBinder(); + if (extraBinder != null) { + ExtraCallback extraCallback = new ExtraCallback(callback); + mCallbackMap.put(callback, extraCallback); + callback.mIControllerCallback = extraCallback; + try { + extraBinder.registerCallbackListener(extraCallback); + callback.postToHandler(Callback.MessageHandler.MSG_SESSION_READY, null, null); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in registerCallback.", e); + } + } else { + callback.mIControllerCallback = null; + mPendingCallbacks.add(callback); + } + } + } + + @Override + public final void unregisterCallback(Callback callback) { + mControllerFwk.unregisterCallback(checkNotNull(callback.mCallbackFwk)); + synchronized (mLock) { + IMediaSession extraBinder = mSessionToken.getExtraBinder(); + if (extraBinder != null) { + try { + ExtraCallback extraCallback = mCallbackMap.remove(callback); + if (extraCallback != null) { + callback.mIControllerCallback = null; + extraBinder.unregisterCallbackListener(extraCallback); + } + } catch (RemoteException e) { + Log.e(TAG, "Dead object in unregisterCallback.", e); + } + } else { + mPendingCallbacks.remove(callback); + } + } + } + + @Override + public boolean dispatchMediaButtonEvent(KeyEvent event) { + return mControllerFwk.dispatchMediaButtonEvent(event); + } + + @Override + public TransportControls getTransportControls() { + MediaController.TransportControls controlsFwk = mControllerFwk.getTransportControls(); + if (Build.VERSION.SDK_INT >= 29) { + return new TransportControlsApi29(controlsFwk); + } else if (Build.VERSION.SDK_INT >= 24) { + return new TransportControlsApi24(controlsFwk); + } else if (Build.VERSION.SDK_INT >= 23) { + return new TransportControlsApi23(controlsFwk); + } else { + return new TransportControlsApi21(controlsFwk); + } + } + + @Nullable + @Override + public PlaybackStateCompat getPlaybackState() { + IMediaSession extraBinder = mSessionToken.getExtraBinder(); + if (extraBinder != null) { + try { + return extraBinder.getPlaybackState(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getPlaybackState.", e); + } + } + PlaybackState stateFwk = mControllerFwk.getPlaybackState(); + return stateFwk != null ? PlaybackStateCompat.fromPlaybackState(stateFwk) : null; + } + + @Nullable + @Override + public MediaMetadataCompat getMetadata() { + MediaMetadata metadataFwk = mControllerFwk.getMetadata(); + return metadataFwk != null ? MediaMetadataCompat.fromMediaMetadata(metadataFwk) : null; + } + + @Nullable + @Override + public List getQueue() { + List queueFwks = mControllerFwk.getQueue(); + return queueFwks != null ? QueueItem.fromQueueItemList(queueFwks) : null; + } + + @Override + public void addQueueItem(MediaDescriptionCompat description) { + long flags = getFlags(); + if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) { + throw new UnsupportedOperationException( + "This session doesn't support queue management operations"); + } + Bundle params = new Bundle(); + params.putParcelable( + COMMAND_ARGUMENT_MEDIA_DESCRIPTION, + LegacyParcelableUtil.convert( + description, android.support.v4.media.MediaDescriptionCompat.CREATOR)); + sendCommand(COMMAND_ADD_QUEUE_ITEM, params, null); + } + + @Override + public void addQueueItem(MediaDescriptionCompat description, int index) { + long flags = getFlags(); + if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) { + throw new UnsupportedOperationException( + "This session doesn't support queue management operations"); + } + Bundle params = new Bundle(); + params.putParcelable( + COMMAND_ARGUMENT_MEDIA_DESCRIPTION, + LegacyParcelableUtil.convert( + description, android.support.v4.media.MediaDescriptionCompat.CREATOR)); + params.putInt(COMMAND_ARGUMENT_INDEX, index); + sendCommand(COMMAND_ADD_QUEUE_ITEM_AT, params, null); + } + + @Override + public void removeQueueItem(MediaDescriptionCompat description) { + long flags = getFlags(); + if ((flags & MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS) == 0) { + throw new UnsupportedOperationException( + "This session doesn't support queue management operations"); + } + Bundle params = new Bundle(); + params.putParcelable( + COMMAND_ARGUMENT_MEDIA_DESCRIPTION, + LegacyParcelableUtil.convert( + description, android.support.v4.media.MediaDescriptionCompat.CREATOR)); + sendCommand(COMMAND_REMOVE_QUEUE_ITEM, params, null); + } + + @Nullable + @Override + public CharSequence getQueueTitle() { + return mControllerFwk.getQueueTitle(); + } + + @Nullable + @Override + public Bundle getExtras() { + return mControllerFwk.getExtras(); + } + + @Override + public int getRatingType() { + if (android.os.Build.VERSION.SDK_INT < 22) { + try { + IMediaSession extraBinder = mSessionToken.getExtraBinder(); + if (extraBinder != null) { + return extraBinder.getRatingType(); + } + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getRatingType.", e); + } + } + return mControllerFwk.getRatingType(); + } + + @Override + public boolean isCaptioningEnabled() { + IMediaSession extraBinder = mSessionToken.getExtraBinder(); + if (extraBinder != null) { + try { + return extraBinder.isCaptioningEnabled(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in isCaptioningEnabled.", e); + } + } + return false; + } + + @Override + public int getRepeatMode() { + IMediaSession extraBinder = mSessionToken.getExtraBinder(); + if (extraBinder != null) { + try { + return extraBinder.getRepeatMode(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getRepeatMode.", e); + } + } + return PlaybackStateCompat.REPEAT_MODE_INVALID; + } + + @Override + public int getShuffleMode() { + IMediaSession extraBinder = mSessionToken.getExtraBinder(); + if (extraBinder != null) { + try { + return extraBinder.getShuffleMode(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getShuffleMode.", e); + } + } + return PlaybackStateCompat.SHUFFLE_MODE_INVALID; + } + + @Override + public long getFlags() { + return mControllerFwk.getFlags(); + } + + @Nullable + @Override + public PlaybackInfo getPlaybackInfo() { + MediaController.PlaybackInfo volumeInfoFwk = mControllerFwk.getPlaybackInfo(); + return volumeInfoFwk != null + ? new PlaybackInfo( + volumeInfoFwk.getPlaybackType(), + checkNotNull(AudioAttributesCompat.wrap(volumeInfoFwk.getAudioAttributes())), + volumeInfoFwk.getVolumeControl(), + volumeInfoFwk.getMaxVolume(), + volumeInfoFwk.getCurrentVolume()) + : null; + } + + @Nullable + @Override + public PendingIntent getSessionActivity() { + return mControllerFwk.getSessionActivity(); + } + + @Override + public void setVolumeTo(int value, int flags) { + mControllerFwk.setVolumeTo(value, flags); + } + + @Override + public void adjustVolume(int direction, int flags) { + mControllerFwk.adjustVolume(direction, flags); + } + + @Override + public void sendCommand(String command, @Nullable Bundle params, @Nullable ResultReceiver cb) { + mControllerFwk.sendCommand(command, params, cb); + } + + @Override + public boolean isSessionReady() { + return mSessionToken.getExtraBinder() != null; + } + + @Override + public String getPackageName() { + return mControllerFwk.getPackageName(); + } + + @Override + public Bundle getSessionInfo() { + if (mSessionInfo != null) { + return new Bundle(mSessionInfo); + } + + IMediaSession extraBinder = mSessionToken.getExtraBinder(); + if (extraBinder != null) { + try { + mSessionInfo = extraBinder.getSessionInfo(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getSessionInfo.", e); + mSessionInfo = Bundle.EMPTY; + } + } + + mSessionInfo = MediaSessionCompat.unparcelWithClassLoader(mSessionInfo); + return mSessionInfo == null ? Bundle.EMPTY : new Bundle(mSessionInfo); + } + + @Nullable + @Override + public Object getMediaController() { + return mControllerFwk; + } + + private void requestExtraBinder() { + sendCommand(COMMAND_GET_EXTRA_BINDER, null, new ExtraBinderRequestResultReceiver(this)); + } + + @GuardedBy("mLock") + void processPendingCallbacksLocked() { + IMediaSession extraBinder = mSessionToken.getExtraBinder(); + if (extraBinder == null) { + return; + } + for (Callback callback : mPendingCallbacks) { + ExtraCallback extraCallback = new ExtraCallback(callback); + mCallbackMap.put(callback, extraCallback); + callback.mIControllerCallback = extraCallback; + try { + extraBinder.registerCallbackListener(extraCallback); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in registerCallback.", e); + break; + } + callback.postToHandler(Callback.MessageHandler.MSG_SESSION_READY, null, null); + } + mPendingCallbacks.clear(); + } + + @SuppressWarnings("argument.type.incompatible") // Activity.setMediaController is not annotated + static void setMediaController(Activity activity, MediaControllerCompat mediaControllerCompat) { + MediaController controllerFwk = null; + if (mediaControllerCompat != null) { + Object sessionTokenObj = mediaControllerCompat.getSessionToken().getToken(); + controllerFwk = new MediaController(activity, (MediaSession.Token) sessionTokenObj); + } + activity.setMediaController(controllerFwk); + } + + @Nullable + static MediaControllerCompat getMediaController(Activity activity) { + MediaController controllerFwk = activity.getMediaController(); + if (controllerFwk == null) { + return null; + } + MediaSession.Token sessionTokenFwk = controllerFwk.getSessionToken(); + return new MediaControllerCompat( + activity, MediaSessionCompat.Token.fromToken(sessionTokenFwk)); + } + + private static class ExtraBinderRequestResultReceiver extends ResultReceiver { + private WeakReference mMediaControllerImpl; + + ExtraBinderRequestResultReceiver(MediaControllerImplApi21 mediaControllerImpl) { + super(null /* handler */); + mMediaControllerImpl = new WeakReference<>(mediaControllerImpl); + } + + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + MediaControllerImplApi21 mediaControllerImpl = mMediaControllerImpl.get(); + if (mediaControllerImpl == null || resultData == null) { + return; + } + synchronized (mediaControllerImpl.mLock) { + mediaControllerImpl.mSessionToken.setExtraBinder( + IMediaSession.Stub.asInterface( + resultData.getBinder(MediaSessionCompat.KEY_EXTRA_BINDER))); + mediaControllerImpl.mSessionToken.setSession2Token( + ParcelUtils.getVersionedParcelable( + resultData, MediaSessionCompat.KEY_SESSION2_TOKEN)); + mediaControllerImpl.processPendingCallbacksLocked(); + } + } + } + + private static class ExtraCallback extends Callback.StubCompat { + ExtraCallback(Callback callback) { + super(callback); + } + + @Override + public void onSessionDestroyed() throws RemoteException { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void onMetadataChanged(@Nullable MediaMetadataCompat metadata) throws RemoteException { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void onQueueChanged(@Nullable List queue) throws RemoteException { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void onQueueTitleChanged(@Nullable CharSequence title) throws RemoteException { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void onExtrasChanged(@Nullable Bundle extras) throws RemoteException { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void onVolumeInfoChanged(@Nullable ParcelableVolumeInfo info) throws RemoteException { + // Will not be called. + throw new AssertionError(); + } + } + } + + @RequiresApi(29) + static class MediaControllerImplApi29 extends MediaControllerImplApi21 { + MediaControllerImplApi29(Context context, MediaSessionCompat.Token sessionToken) { + super(context, sessionToken); + } + + @Override + public Bundle getSessionInfo() { + if (mSessionInfo != null) { + return new Bundle(mSessionInfo); + } + mSessionInfo = mControllerFwk.getSessionInfo(); + mSessionInfo = MediaSessionCompat.unparcelWithClassLoader(mSessionInfo); + return mSessionInfo == null ? Bundle.EMPTY : new Bundle(mSessionInfo); + } + } + + @RequiresApi(21) + static class TransportControlsApi21 extends TransportControls { + protected final MediaController.TransportControls mControlsFwk; + + TransportControlsApi21(MediaController.TransportControls controlsFwk) { + mControlsFwk = controlsFwk; + } + + @Override + public void prepare() { + sendCustomAction(MediaSessionCompat.ACTION_PREPARE, null); + } + + @Override + public void prepareFromMediaId(String mediaId, @Nullable Bundle extras) { + Bundle bundle = new Bundle(); + bundle.putString(MediaSessionCompat.ACTION_ARGUMENT_MEDIA_ID, mediaId); + bundle.putBundle(MediaSessionCompat.ACTION_ARGUMENT_EXTRAS, extras); + sendCustomAction(MediaSessionCompat.ACTION_PREPARE_FROM_MEDIA_ID, bundle); + } + + @Override + public void prepareFromSearch(String query, @Nullable Bundle extras) { + Bundle bundle = new Bundle(); + bundle.putString(MediaSessionCompat.ACTION_ARGUMENT_QUERY, query); + bundle.putBundle(MediaSessionCompat.ACTION_ARGUMENT_EXTRAS, extras); + sendCustomAction(MediaSessionCompat.ACTION_PREPARE_FROM_SEARCH, bundle); + } + + @Override + public void prepareFromUri(Uri uri, @Nullable Bundle extras) { + Bundle bundle = new Bundle(); + bundle.putParcelable(MediaSessionCompat.ACTION_ARGUMENT_URI, uri); + bundle.putBundle(MediaSessionCompat.ACTION_ARGUMENT_EXTRAS, extras); + sendCustomAction(MediaSessionCompat.ACTION_PREPARE_FROM_URI, bundle); + } + + @Override + public void play() { + mControlsFwk.play(); + } + + @Override + public void pause() { + mControlsFwk.pause(); + } + + @Override + public void stop() { + mControlsFwk.stop(); + } + + @Override + public void seekTo(long pos) { + mControlsFwk.seekTo(pos); + } + + @Override + public void fastForward() { + mControlsFwk.fastForward(); + } + + @Override + public void rewind() { + mControlsFwk.rewind(); + } + + @Override + public void skipToNext() { + mControlsFwk.skipToNext(); + } + + @Override + public void skipToPrevious() { + mControlsFwk.skipToPrevious(); + } + + @SuppressWarnings("argument.type.incompatible") // Platform controller accepts null rating + @Override + public void setRating(RatingCompat rating) { + mControlsFwk.setRating(rating != null ? (Rating) rating.getRating() : null); + } + + @Override + public void setRating(RatingCompat rating, @Nullable Bundle extras) { + Bundle bundle = new Bundle(); + bundle.putParcelable( + MediaSessionCompat.ACTION_ARGUMENT_RATING, + LegacyParcelableUtil.convert(rating, android.support.v4.media.RatingCompat.CREATOR)); + bundle.putBundle(MediaSessionCompat.ACTION_ARGUMENT_EXTRAS, extras); + sendCustomAction(MediaSessionCompat.ACTION_SET_RATING, bundle); + } + + @Override + public void setPlaybackSpeed(float speed) { + if (speed == 0.0f) { + throw new IllegalArgumentException("speed must not be zero"); + } + Bundle bundle = new Bundle(); + bundle.putFloat(MediaSessionCompat.ACTION_ARGUMENT_PLAYBACK_SPEED, speed); + sendCustomAction(MediaSessionCompat.ACTION_SET_PLAYBACK_SPEED, bundle); + } + + @Override + public void setCaptioningEnabled(boolean enabled) { + Bundle bundle = new Bundle(); + bundle.putBoolean(MediaSessionCompat.ACTION_ARGUMENT_CAPTIONING_ENABLED, enabled); + sendCustomAction(MediaSessionCompat.ACTION_SET_CAPTIONING_ENABLED, bundle); + } + + @Override + public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) { + Bundle bundle = new Bundle(); + bundle.putInt(MediaSessionCompat.ACTION_ARGUMENT_REPEAT_MODE, repeatMode); + sendCustomAction(MediaSessionCompat.ACTION_SET_REPEAT_MODE, bundle); + } + + @Override + public void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode) { + Bundle bundle = new Bundle(); + bundle.putInt(MediaSessionCompat.ACTION_ARGUMENT_SHUFFLE_MODE, shuffleMode); + sendCustomAction(MediaSessionCompat.ACTION_SET_SHUFFLE_MODE, bundle); + } + + @SuppressWarnings("argument.type.incompatible") // Platform controller accepts null extras + @Override + public void playFromMediaId(String mediaId, @Nullable Bundle extras) { + mControlsFwk.playFromMediaId(mediaId, extras); + } + + @SuppressWarnings("argument.type.incompatible") // Platform controller accepts null extras + @Override + public void playFromSearch(String query, @Nullable Bundle extras) { + mControlsFwk.playFromSearch(query, extras); + } + + @Override + public void playFromUri(Uri uri, @Nullable Bundle extras) { + if (uri == null || Uri.EMPTY.equals(uri)) { + throw new IllegalArgumentException("You must specify a non-empty Uri for playFromUri."); + } + Bundle bundle = new Bundle(); + bundle.putParcelable(MediaSessionCompat.ACTION_ARGUMENT_URI, uri); + bundle.putBundle(MediaSessionCompat.ACTION_ARGUMENT_EXTRAS, extras); + sendCustomAction(MediaSessionCompat.ACTION_PLAY_FROM_URI, bundle); + } + + @Override + public void skipToQueueItem(long id) { + mControlsFwk.skipToQueueItem(id); + } + + @Override + public void sendCustomAction(CustomAction customAction, @Nullable Bundle args) { + validateCustomAction(customAction.getAction(), args); + mControlsFwk.sendCustomAction(customAction.getAction(), args); + } + + @Override + public void sendCustomAction(String action, @Nullable Bundle args) { + validateCustomAction(action, args); + mControlsFwk.sendCustomAction(action, args); + } + } + + @RequiresApi(23) + static class TransportControlsApi23 extends TransportControlsApi21 { + TransportControlsApi23(MediaController.TransportControls controlsFwk) { + super(controlsFwk); + } + + @SuppressWarnings("argument.type.incompatible") // Framework controller is missing annotation + @Override + public void playFromUri(Uri uri, @Nullable Bundle extras) { + mControlsFwk.playFromUri(uri, extras); + } + } + + @RequiresApi(24) + static class TransportControlsApi24 extends TransportControlsApi23 { + TransportControlsApi24(MediaController.TransportControls controlsFwk) { + super(controlsFwk); + } + + @Override + public void prepare() { + mControlsFwk.prepare(); + } + + @SuppressWarnings("argument.type.incompatible") // Framework controller is missing annotation + @Override + public void prepareFromMediaId(String mediaId, @Nullable Bundle extras) { + mControlsFwk.prepareFromMediaId(mediaId, extras); + } + + @SuppressWarnings("argument.type.incompatible") // Platform controller accepts null extra + @Override + public void prepareFromSearch(String query, @Nullable Bundle extras) { + mControlsFwk.prepareFromSearch(query, extras); + } + + @SuppressWarnings("argument.type.incompatible") // Platform controller accepts null extra + @Override + public void prepareFromUri(Uri uri, @Nullable Bundle extras) { + mControlsFwk.prepareFromUri(uri, extras); + } + } + + @RequiresApi(29) + static class TransportControlsApi29 extends TransportControlsApi24 { + TransportControlsApi29(MediaController.TransportControls controlsFwk) { + super(controlsFwk); + } + + @Override + public void setPlaybackSpeed(float speed) { + if (speed == 0.0f) { + throw new IllegalArgumentException("speed must not be zero"); + } + mControlsFwk.setPlaybackSpeed(speed); + } + } +} diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaDescriptionCompat.java b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaDescriptionCompat.java new file mode 100644 index 0000000000..7019bd0ca2 --- /dev/null +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaDescriptionCompat.java @@ -0,0 +1,657 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.session.legacy; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY; +import static androidx.media3.common.util.Assertions.checkNotNull; + +import android.annotation.SuppressLint; +import android.graphics.Bitmap; +import android.media.MediaDescription; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import androidx.annotation.DoNotInline; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.RestrictTo; +import androidx.media3.common.util.UnstableApi; + +/** + * A simple set of metadata for a media item suitable for display. This can be created using the + * Builder or retrieved from existing metadata using {@link MediaMetadataCompat#getDescription()}. + */ +@UnstableApi +@RestrictTo(LIBRARY) +@SuppressLint("BanParcelableUsage") +public final class MediaDescriptionCompat implements Parcelable { + private static final String TAG = "MediaDescriptionCompat"; + + /** + * Used as a long extra field to indicate the bluetooth folder type of the media item as specified + * in the section 6.10.2.2 of the Bluetooth AVRCP 1.5. This is valid only for {@link + * MediaBrowserCompat.MediaItem} with {@link MediaBrowserCompat.MediaItem#FLAG_BROWSABLE}. The + * value should be one of the following: + * + *
    + *
  • {@link #BT_FOLDER_TYPE_MIXED} + *
  • {@link #BT_FOLDER_TYPE_TITLES} + *
  • {@link #BT_FOLDER_TYPE_ALBUMS} + *
  • {@link #BT_FOLDER_TYPE_ARTISTS} + *
  • {@link #BT_FOLDER_TYPE_GENRES} + *
  • {@link #BT_FOLDER_TYPE_PLAYLISTS} + *
  • {@link #BT_FOLDER_TYPE_YEARS} + *
+ * + * @see #getExtras() + */ + public static final String EXTRA_BT_FOLDER_TYPE = "android.media.extra.BT_FOLDER_TYPE"; + + /** + * The type of folder that is unknown or contains media elements of mixed types as specified in + * the section 6.10.2.2 of the Bluetooth AVRCP 1.5. + */ + public static final long BT_FOLDER_TYPE_MIXED = 0; + + /** + * The type of folder that contains media elements only as specified in the section 6.10.2.2 of + * the Bluetooth AVRCP 1.5. + */ + public static final long BT_FOLDER_TYPE_TITLES = 1; + + /** + * The type of folder that contains folders categorized by album as specified in the section + * 6.10.2.2 of the Bluetooth AVRCP 1.5. + */ + public static final long BT_FOLDER_TYPE_ALBUMS = 2; + + /** + * The type of folder that contains folders categorized by artist as specified in the section + * 6.10.2.2 of the Bluetooth AVRCP 1.5. + */ + public static final long BT_FOLDER_TYPE_ARTISTS = 3; + + /** + * The type of folder that contains folders categorized by genre as specified in the section + * 6.10.2.2 of the Bluetooth AVRCP 1.5. + */ + public static final long BT_FOLDER_TYPE_GENRES = 4; + + /** + * The type of folder that contains folders categorized by playlist as specified in the section + * 6.10.2.2 of the Bluetooth AVRCP 1.5. + */ + public static final long BT_FOLDER_TYPE_PLAYLISTS = 5; + + /** + * The type of folder that contains folders categorized by year as specified in the section + * 6.10.2.2 of the Bluetooth AVRCP 1.5. + */ + public static final long BT_FOLDER_TYPE_YEARS = 6; + + /** + * Used as a long extra field to indicate the download status of the media item. The value should + * be one of the following: + * + *
    + *
  • {@link #STATUS_NOT_DOWNLOADED} + *
  • {@link #STATUS_DOWNLOADING} + *
  • {@link #STATUS_DOWNLOADED} + *
+ * + * @see #getExtras() + */ + public static final String EXTRA_DOWNLOAD_STATUS = "android.media.extra.DOWNLOAD_STATUS"; + + /** + * The status value to indicate the media item is not downloaded. + * + * @see #EXTRA_DOWNLOAD_STATUS + */ + public static final long STATUS_NOT_DOWNLOADED = 0; + + /** + * The status value to indicate the media item is being downloaded. + * + * @see #EXTRA_DOWNLOAD_STATUS + */ + public static final long STATUS_DOWNLOADING = 1; + + /** + * The status value to indicate the media item is downloaded for later offline playback. + * + * @see #EXTRA_DOWNLOAD_STATUS + */ + public static final long STATUS_DOWNLOADED = 2; + + /** + * Custom key to store a media URI on API 21-22 devices (before it became part of the framework + * class) when parceling/converting to and from framework objects. + */ + public static final String DESCRIPTION_KEY_MEDIA_URI = + "android.support.v4.media.description.MEDIA_URI"; + + /** Custom key to store whether the original Bundle provided by the developer was null */ + public static final String DESCRIPTION_KEY_NULL_BUNDLE_FLAG = + "android.support.v4.media.description.NULL_BUNDLE_FLAG"; + + /** A unique persistent id for the content or null. */ + @Nullable private final String mMediaId; + + /** A primary title suitable for display or null. */ + @Nullable private final CharSequence mTitle; + + /** A subtitle suitable for display or null. */ + @Nullable private final CharSequence mSubtitle; + + /** A description suitable for display or null. */ + @Nullable private final CharSequence mDescription; + + /** A bitmap icon suitable for display or null. */ + @Nullable private final Bitmap mIcon; + + /** A Uri for an icon suitable for display or null. */ + @Nullable private final Uri mIconUri; + + /** Extras for opaque use by apps/system. */ + @Nullable private final Bundle mExtras; + + /** A Uri to identify this content. */ + @Nullable private final Uri mMediaUri; + + /** A cached copy of the equivalent framework object. */ + @Nullable private MediaDescription mDescriptionFwk; + + MediaDescriptionCompat( + @Nullable String mediaId, + @Nullable CharSequence title, + @Nullable CharSequence subtitle, + @Nullable CharSequence description, + @Nullable Bitmap icon, + @Nullable Uri iconUri, + @Nullable Bundle extras, + @Nullable Uri mediaUri) { + mMediaId = mediaId; + mTitle = title; + mSubtitle = subtitle; + mDescription = description; + mIcon = icon; + mIconUri = iconUri; + mExtras = extras; + mMediaUri = mediaUri; + } + + @SuppressWarnings("deprecation") + MediaDescriptionCompat(Parcel in) { + mMediaId = in.readString(); + mTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + mSubtitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + mDescription = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + + ClassLoader loader = getClass().getClassLoader(); + mIcon = in.readParcelable(loader); + mIconUri = in.readParcelable(loader); + mExtras = in.readBundle(loader); + mMediaUri = in.readParcelable(loader); + } + + /** Returns the media id or null. See {@link MediaMetadataCompat#METADATA_KEY_MEDIA_ID}. */ + @Nullable + public String getMediaId() { + return mMediaId; + } + + /** + * Returns a title suitable for display or null. + * + * @return A title or null. + */ + @Nullable + public CharSequence getTitle() { + return mTitle; + } + + /** + * Returns a subtitle suitable for display or null. + * + * @return A subtitle or null. + */ + @Nullable + public CharSequence getSubtitle() { + return mSubtitle; + } + + /** + * Returns a description suitable for display or null. + * + * @return A description or null. + */ + @Nullable + public CharSequence getDescription() { + return mDescription; + } + + /** + * Returns a bitmap icon suitable for display or null. + * + * @return An icon or null. + */ + @Nullable + public Bitmap getIconBitmap() { + return mIcon; + } + + /** + * Returns a Uri for an icon suitable for display or null. + * + * @return An icon uri or null. + */ + @Nullable + public Uri getIconUri() { + return mIconUri; + } + + /** + * Returns any extras that were added to the description. + * + * @return A bundle of extras or null. + */ + @Nullable + public Bundle getExtras() { + return mExtras; + } + + /** + * Returns a Uri representing this content or null. + * + * @return A media Uri or null. + */ + @Nullable + public Uri getMediaUri() { + return mMediaUri; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + if (Build.VERSION.SDK_INT < 21) { + dest.writeString(mMediaId); + TextUtils.writeToParcel(mTitle, dest, flags); + TextUtils.writeToParcel(mSubtitle, dest, flags); + TextUtils.writeToParcel(mDescription, dest, flags); + dest.writeParcelable(mIcon, flags); + dest.writeParcelable(mIconUri, flags); + dest.writeBundle(mExtras); + dest.writeParcelable(mMediaUri, flags); + } else { + ((MediaDescription) getMediaDescription()).writeToParcel(dest, flags); + } + } + + @Override + public String toString() { + return mTitle + ", " + mSubtitle + ", " + mDescription; + } + + /** + * Gets the underlying framework {@link android.media.MediaDescription} object. + * + *

This method is only supported on {@link android.os.Build.VERSION_CODES#LOLLIPOP} and later. + * + * @return An equivalent {@link android.media.MediaDescription} object, or null if none. + */ + @RequiresApi(21) + public Object getMediaDescription() { + if (mDescriptionFwk != null) { + return mDescriptionFwk; + } + MediaDescription.Builder bob = Api21Impl.createBuilder(); + Api21Impl.setMediaId(bob, mMediaId); + Api21Impl.setTitle(bob, mTitle); + Api21Impl.setSubtitle(bob, mSubtitle); + Api21Impl.setDescription(bob, mDescription); + Api21Impl.setIconBitmap(bob, mIcon); + Api21Impl.setIconUri(bob, mIconUri); + // Media URI was not added until API 23, so add it to the Bundle of extras to + // ensure the data is not lost - this ensures that + // fromMediaDescription(getMediaDescription(mediaDescriptionCompat)) returns + // an equivalent MediaDescriptionCompat on all API levels + if (Build.VERSION.SDK_INT < 23 && mMediaUri != null) { + Bundle extras; + if (mExtras == null) { + extras = new Bundle(); + extras.putBoolean(DESCRIPTION_KEY_NULL_BUNDLE_FLAG, true); + } else { + extras = new Bundle(mExtras); + } + extras.putParcelable(DESCRIPTION_KEY_MEDIA_URI, mMediaUri); + Api21Impl.setExtras(bob, extras); + } else { + Api21Impl.setExtras(bob, mExtras); + } + if (Build.VERSION.SDK_INT >= 23) { + Api23Impl.setMediaUri(bob, mMediaUri); + } + mDescriptionFwk = Api21Impl.build(bob); + + return mDescriptionFwk; + } + + /** + * Creates an instance from a framework {@link android.media.MediaDescription} object. + * + *

This method is only supported on API 21+. + * + * @param descriptionObj A {@link android.media.MediaDescription} object, or null if none. + * @return An equivalent {@link MediaMetadataCompat} object, or null if none. + */ + @Nullable + @SuppressWarnings("deprecation") + public static MediaDescriptionCompat fromMediaDescription(Object descriptionObj) { + if (descriptionObj != null && Build.VERSION.SDK_INT >= 21) { + Builder bob = new Builder(); + MediaDescription description = (MediaDescription) descriptionObj; + bob.setMediaId(Api21Impl.getMediaId(description)); + bob.setTitle(Api21Impl.getTitle(description)); + bob.setSubtitle(Api21Impl.getSubtitle(description)); + bob.setDescription(Api21Impl.getDescription(description)); + bob.setIconBitmap(Api21Impl.getIconBitmap(description)); + bob.setIconUri(Api21Impl.getIconUri(description)); + Bundle extras = Api21Impl.getExtras(description); + extras = MediaSessionCompat.unparcelWithClassLoader(extras); + if (extras != null) { + extras = new Bundle(extras); + } + Uri mediaUri = null; + if (extras != null) { + mediaUri = extras.getParcelable(DESCRIPTION_KEY_MEDIA_URI); + if (mediaUri != null) { + if (extras.containsKey(DESCRIPTION_KEY_NULL_BUNDLE_FLAG) && extras.size() == 2) { + // The extras were only created for the media URI, so we set it back to null to + // ensure mediaDescriptionCompat.getExtras() equals + // fromMediaDescription(getMediaDescription(mediaDescriptionCompat)).getExtras() + extras = null; + } else { + // Remove media URI keys to ensure mediaDescriptionCompat.getExtras().keySet() + // equals fromMediaDescription(getMediaDescription(mediaDescriptionCompat)) + // .getExtras().keySet() + extras.remove(DESCRIPTION_KEY_MEDIA_URI); + extras.remove(DESCRIPTION_KEY_NULL_BUNDLE_FLAG); + } + } + } + bob.setExtras(extras); + if (mediaUri != null) { + bob.setMediaUri(mediaUri); + } else if (Build.VERSION.SDK_INT >= 23) { + bob.setMediaUri(Api23Impl.getMediaUri(description)); + } + MediaDescriptionCompat descriptionCompat = bob.build(); + descriptionCompat.mDescriptionFwk = description; + + return descriptionCompat; + } else { + return null; + } + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public MediaDescriptionCompat createFromParcel(Parcel in) { + if (Build.VERSION.SDK_INT < 21) { + return new MediaDescriptionCompat(in); + } else { + return checkNotNull( + fromMediaDescription(MediaDescription.CREATOR.createFromParcel(in))); + } + } + + @Override + public MediaDescriptionCompat[] newArray(int size) { + return new MediaDescriptionCompat[size]; + } + }; + + /** Builder for {@link MediaDescriptionCompat} objects. */ + public static final class Builder { + @Nullable private String mMediaId; + @Nullable private CharSequence mTitle; + @Nullable private CharSequence mSubtitle; + @Nullable private CharSequence mDescription; + @Nullable private Bitmap mIcon; + @Nullable private Uri mIconUri; + @Nullable private Bundle mExtras; + @Nullable private Uri mMediaUri; + + /** Creates an initially empty builder. */ + public Builder() {} + + /** + * Sets the media id. + * + * @param mediaId The unique id for the item or null. + * @return this + */ + public Builder setMediaId(@Nullable String mediaId) { + mMediaId = mediaId; + return this; + } + + /** + * Sets the title. + * + * @param title A title suitable for display to the user or null. + * @return this + */ + public Builder setTitle(@Nullable CharSequence title) { + mTitle = title; + return this; + } + + /** + * Sets the subtitle. + * + * @param subtitle A subtitle suitable for display to the user or null. + * @return this + */ + public Builder setSubtitle(@Nullable CharSequence subtitle) { + mSubtitle = subtitle; + return this; + } + + /** + * Sets the description. + * + * @param description A description suitable for display to the user or null. + * @return this + */ + public Builder setDescription(@Nullable CharSequence description) { + mDescription = description; + return this; + } + + /** + * Sets the icon. + * + * @param icon A {@link Bitmap} icon suitable for display to the user or null. + * @return this + */ + public Builder setIconBitmap(@Nullable Bitmap icon) { + mIcon = icon; + return this; + } + + /** + * Sets the icon uri. + * + * @param iconUri A {@link Uri} for an icon suitable for display to the user or null. + * @return this + */ + public Builder setIconUri(@Nullable Uri iconUri) { + mIconUri = iconUri; + return this; + } + + /** + * Sets a bundle of extras. + * + * @param extras The extras to include with this description or null. + * @return this + */ + public Builder setExtras(@Nullable Bundle extras) { + mExtras = extras; + return this; + } + + /** + * Sets the media uri. + * + * @param mediaUri The content's {@link Uri} for the item or null. + * @return this + */ + public Builder setMediaUri(@Nullable Uri mediaUri) { + mMediaUri = mediaUri; + return this; + } + + /** + * Creates a {@link MediaDescriptionCompat} instance with the specified fields. + * + * @return A MediaDescriptionCompat instance. + */ + public MediaDescriptionCompat build() { + return new MediaDescriptionCompat( + mMediaId, mTitle, mSubtitle, mDescription, mIcon, mIconUri, mExtras, mMediaUri); + } + } + + @RequiresApi(21) + private static class Api21Impl { + private Api21Impl() {} + + @DoNotInline + static MediaDescription.Builder createBuilder() { + return new MediaDescription.Builder(); + } + + @DoNotInline + static void setMediaId(MediaDescription.Builder builder, @Nullable String mediaId) { + builder.setMediaId(mediaId); + } + + @DoNotInline + static void setTitle(MediaDescription.Builder builder, @Nullable CharSequence title) { + builder.setTitle(title); + } + + @DoNotInline + static void setSubtitle(MediaDescription.Builder builder, @Nullable CharSequence subtitle) { + builder.setSubtitle(subtitle); + } + + @DoNotInline + static void setDescription( + MediaDescription.Builder builder, @Nullable CharSequence description) { + builder.setDescription(description); + } + + @DoNotInline + static void setIconBitmap(MediaDescription.Builder builder, @Nullable Bitmap icon) { + builder.setIconBitmap(icon); + } + + @DoNotInline + static void setIconUri(MediaDescription.Builder builder, @Nullable Uri iconUri) { + builder.setIconUri(iconUri); + } + + @DoNotInline + static void setExtras(MediaDescription.Builder builder, @Nullable Bundle extras) { + builder.setExtras(extras); + } + + @DoNotInline + static MediaDescription build(MediaDescription.Builder builder) { + return builder.build(); + } + + @DoNotInline + @Nullable + static String getMediaId(MediaDescription description) { + return description.getMediaId(); + } + + @DoNotInline + @Nullable + static CharSequence getTitle(MediaDescription description) { + return description.getTitle(); + } + + @DoNotInline + @Nullable + static CharSequence getSubtitle(MediaDescription description) { + return description.getSubtitle(); + } + + @DoNotInline + @Nullable + static CharSequence getDescription(MediaDescription description) { + return description.getDescription(); + } + + @DoNotInline + @Nullable + static Bitmap getIconBitmap(MediaDescription description) { + return description.getIconBitmap(); + } + + @DoNotInline + @Nullable + static Uri getIconUri(MediaDescription description) { + return description.getIconUri(); + } + + @DoNotInline + @Nullable + static Bundle getExtras(MediaDescription description) { + return description.getExtras(); + } + } + + @RequiresApi(23) + private static class Api23Impl { + private Api23Impl() {} + + @DoNotInline + static void setMediaUri(MediaDescription.Builder builder, @Nullable Uri mediaUri) { + builder.setMediaUri(mediaUri); + } + + @DoNotInline + @Nullable + static Uri getMediaUri(MediaDescription description) { + return description.getMediaUri(); + } + } +} diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaMetadataCompat.java b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaMetadataCompat.java new file mode 100644 index 0000000000..2ec44adde0 --- /dev/null +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaMetadataCompat.java @@ -0,0 +1,833 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.session.legacy; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY; +import static androidx.media3.common.util.Assertions.checkNotNull; + +import android.annotation.SuppressLint; +import android.graphics.Bitmap; +import android.media.MediaMetadata; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.Log; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.RestrictTo; +import androidx.annotation.StringDef; +import androidx.collection.ArrayMap; +import androidx.media3.common.util.NullableType; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.legacy.MediaControllerCompat.TransportControls; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Set; + +/** Contains metadata about an item, such as the title, artist, etc. */ +@UnstableApi +@RestrictTo(LIBRARY) +@SuppressLint("BanParcelableUsage") +public final class MediaMetadataCompat implements Parcelable { + private static final String TAG = "MediaMetadata"; + + /** The title of the media. */ + public static final String METADATA_KEY_TITLE = "android.media.metadata.TITLE"; + + /** The artist of the media. */ + public static final String METADATA_KEY_ARTIST = "android.media.metadata.ARTIST"; + + /** + * The duration of the media in ms. A negative duration indicates that the duration is unknown (or + * infinite). + */ + public static final String METADATA_KEY_DURATION = "android.media.metadata.DURATION"; + + /** The album title for the media. */ + public static final String METADATA_KEY_ALBUM = "android.media.metadata.ALBUM"; + + /** The author of the media. */ + public static final String METADATA_KEY_AUTHOR = "android.media.metadata.AUTHOR"; + + /** The writer of the media. */ + public static final String METADATA_KEY_WRITER = "android.media.metadata.WRITER"; + + /** The composer of the media. */ + public static final String METADATA_KEY_COMPOSER = "android.media.metadata.COMPOSER"; + + /** The compilation status of the media. */ + public static final String METADATA_KEY_COMPILATION = "android.media.metadata.COMPILATION"; + + /** + * The date the media was created or published. The format is unspecified but RFC 3339 is + * recommended. + */ + public static final String METADATA_KEY_DATE = "android.media.metadata.DATE"; + + /** The year the media was created or published as a long. */ + public static final String METADATA_KEY_YEAR = "android.media.metadata.YEAR"; + + /** The genre of the media. */ + public static final String METADATA_KEY_GENRE = "android.media.metadata.GENRE"; + + /** The track number for the media. */ + public static final String METADATA_KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER"; + + /** The number of tracks in the media's original source. */ + public static final String METADATA_KEY_NUM_TRACKS = "android.media.metadata.NUM_TRACKS"; + + /** The disc number for the media's original source. */ + public static final String METADATA_KEY_DISC_NUMBER = "android.media.metadata.DISC_NUMBER"; + + /** The artist for the album of the media's original source. */ + public static final String METADATA_KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST"; + + /** + * The artwork for the media as a {@link Bitmap}. + * + *

The artwork should be relatively small and may be scaled down if it is too large. For higher + * resolution artwork {@link #METADATA_KEY_ART_URI} should be used instead. + */ + public static final String METADATA_KEY_ART = "android.media.metadata.ART"; + + /** The artwork for the media as a Uri style String. */ + public static final String METADATA_KEY_ART_URI = "android.media.metadata.ART_URI"; + + /** + * The artwork for the album of the media's original source as a {@link Bitmap}. The artwork + * should be relatively small and may be scaled down if it is too large. For higher resolution + * artwork {@link #METADATA_KEY_ALBUM_ART_URI} should be used instead. + */ + public static final String METADATA_KEY_ALBUM_ART = "android.media.metadata.ALBUM_ART"; + + /** The artwork for the album of the media's original source as a Uri style String. */ + public static final String METADATA_KEY_ALBUM_ART_URI = "android.media.metadata.ALBUM_ART_URI"; + + /** + * The user's rating for the media. + * + * @see RatingCompat + */ + public static final String METADATA_KEY_USER_RATING = "android.media.metadata.USER_RATING"; + + /** + * The overall rating for the media. + * + * @see RatingCompat + */ + public static final String METADATA_KEY_RATING = "android.media.metadata.RATING"; + + /** + * A title that is suitable for display to the user. This will generally be the same as {@link + * #METADATA_KEY_TITLE} but may differ for some formats. When displaying media described by this + * metadata this should be preferred if present. + */ + public static final String METADATA_KEY_DISPLAY_TITLE = "android.media.metadata.DISPLAY_TITLE"; + + /** + * A subtitle that is suitable for display to the user. When displaying a second line for media + * described by this metadata this should be preferred to other fields if present. + */ + public static final String METADATA_KEY_DISPLAY_SUBTITLE = + "android.media.metadata.DISPLAY_SUBTITLE"; + + /** + * A description that is suitable for display to the user. When displaying more information for + * media described by this metadata this should be preferred to other fields if present. + */ + public static final String METADATA_KEY_DISPLAY_DESCRIPTION = + "android.media.metadata.DISPLAY_DESCRIPTION"; + + /** + * An icon or thumbnail that is suitable for display to the user. When displaying an icon for + * media described by this metadata this should be preferred to other fields if present. This must + * be a {@link Bitmap}. + * + *

The icon should be relatively small and may be scaled down if it is too large. For higher + * resolution artwork {@link #METADATA_KEY_DISPLAY_ICON_URI} should be used instead. + */ + public static final String METADATA_KEY_DISPLAY_ICON = "android.media.metadata.DISPLAY_ICON"; + + /** + * An icon or thumbnail that is suitable for display to the user. When displaying more information + * for media described by this metadata the display description should be preferred to other + * fields when present. This must be a Uri style String. + */ + public static final String METADATA_KEY_DISPLAY_ICON_URI = + "android.media.metadata.DISPLAY_ICON_URI"; + + /** + * A String key for identifying the content. This value is specific to the service providing the + * content. If used, this should be a persistent unique key for the underlying content. + */ + public static final String METADATA_KEY_MEDIA_ID = "android.media.metadata.MEDIA_ID"; + + /** + * A Uri formatted String representing the content. This value is specific to the service + * providing the content. It may be used with {@link TransportControls#playFromUri(Uri, Bundle)} + * to initiate playback when provided by a {@link MediaBrowserCompat} connected to the same app. + */ + public static final String METADATA_KEY_MEDIA_URI = "android.media.metadata.MEDIA_URI"; + + /** + * The bluetooth folder type of the media specified in the section 6.10.2.2 of the Bluetooth AVRCP + * 1.5. It should be one of the following: + * + *

    + *
  • {@link MediaDescriptionCompat#BT_FOLDER_TYPE_MIXED} + *
  • {@link MediaDescriptionCompat#BT_FOLDER_TYPE_TITLES} + *
  • {@link MediaDescriptionCompat#BT_FOLDER_TYPE_ALBUMS} + *
  • {@link MediaDescriptionCompat#BT_FOLDER_TYPE_ARTISTS} + *
  • {@link MediaDescriptionCompat#BT_FOLDER_TYPE_GENRES} + *
  • {@link MediaDescriptionCompat#BT_FOLDER_TYPE_PLAYLISTS} + *
  • {@link MediaDescriptionCompat#BT_FOLDER_TYPE_YEARS} + *
+ */ + public static final String METADATA_KEY_BT_FOLDER_TYPE = "android.media.metadata.BT_FOLDER_TYPE"; + + /** + * Whether the media is an advertisement. A value of 0 indicates it is not an advertisement. A + * value of 1 or non-zero indicates it is an advertisement. If not specified, this value is set to + * 0 by default. + */ + public static final String METADATA_KEY_ADVERTISEMENT = "android.media.metadata.ADVERTISEMENT"; + + /** + * The download status of the media which will be used for later offline playback. It should be + * one of the following: + * + *
    + *
  • {@link MediaDescriptionCompat#STATUS_NOT_DOWNLOADED} + *
  • {@link MediaDescriptionCompat#STATUS_DOWNLOADING} + *
  • {@link MediaDescriptionCompat#STATUS_DOWNLOADED} + *
+ */ + public static final String METADATA_KEY_DOWNLOAD_STATUS = + "android.media.metadata.DOWNLOAD_STATUS"; + + @StringDef( + value = { + METADATA_KEY_TITLE, + METADATA_KEY_ARTIST, + METADATA_KEY_ALBUM, + METADATA_KEY_AUTHOR, + METADATA_KEY_WRITER, + METADATA_KEY_COMPOSER, + METADATA_KEY_COMPILATION, + METADATA_KEY_DATE, + METADATA_KEY_GENRE, + METADATA_KEY_ALBUM_ARTIST, + METADATA_KEY_ART_URI, + METADATA_KEY_ALBUM_ART_URI, + METADATA_KEY_DISPLAY_TITLE, + METADATA_KEY_DISPLAY_SUBTITLE, + METADATA_KEY_DISPLAY_DESCRIPTION, + METADATA_KEY_DISPLAY_ICON_URI, + METADATA_KEY_MEDIA_ID, + METADATA_KEY_MEDIA_URI + }, + open = true) + @Retention(RetentionPolicy.SOURCE) + private @interface TextKey {} + + @StringDef( + value = { + METADATA_KEY_DURATION, + METADATA_KEY_YEAR, + METADATA_KEY_TRACK_NUMBER, + METADATA_KEY_NUM_TRACKS, + METADATA_KEY_DISC_NUMBER, + METADATA_KEY_BT_FOLDER_TYPE, + METADATA_KEY_ADVERTISEMENT, + METADATA_KEY_DOWNLOAD_STATUS + }, + open = true) + @Retention(RetentionPolicy.SOURCE) + private @interface LongKey {} + + @StringDef({METADATA_KEY_ART, METADATA_KEY_ALBUM_ART, METADATA_KEY_DISPLAY_ICON}) + @Retention(RetentionPolicy.SOURCE) + private @interface BitmapKey {} + + @StringDef({METADATA_KEY_USER_RATING, METADATA_KEY_RATING}) + @Retention(RetentionPolicy.SOURCE) + private @interface RatingKey {} + + static final int METADATA_TYPE_LONG = 0; + static final int METADATA_TYPE_TEXT = 1; + static final int METADATA_TYPE_BITMAP = 2; + static final int METADATA_TYPE_RATING = 3; + static final ArrayMap METADATA_KEYS_TYPE; + + static { + METADATA_KEYS_TYPE = new ArrayMap(); + METADATA_KEYS_TYPE.put(METADATA_KEY_TITLE, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_ARTIST, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_DURATION, METADATA_TYPE_LONG); + METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_AUTHOR, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_WRITER, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_COMPOSER, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_COMPILATION, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_DATE, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_YEAR, METADATA_TYPE_LONG); + METADATA_KEYS_TYPE.put(METADATA_KEY_GENRE, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_TRACK_NUMBER, METADATA_TYPE_LONG); + METADATA_KEYS_TYPE.put(METADATA_KEY_NUM_TRACKS, METADATA_TYPE_LONG); + METADATA_KEYS_TYPE.put(METADATA_KEY_DISC_NUMBER, METADATA_TYPE_LONG); + METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ARTIST, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_ART, METADATA_TYPE_BITMAP); + METADATA_KEYS_TYPE.put(METADATA_KEY_ART_URI, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ART, METADATA_TYPE_BITMAP); + METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ART_URI, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_USER_RATING, METADATA_TYPE_RATING); + METADATA_KEYS_TYPE.put(METADATA_KEY_RATING, METADATA_TYPE_RATING); + METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_TITLE, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_SUBTITLE, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_DESCRIPTION, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_ICON, METADATA_TYPE_BITMAP); + METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_ICON_URI, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_MEDIA_ID, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_BT_FOLDER_TYPE, METADATA_TYPE_LONG); + METADATA_KEYS_TYPE.put(METADATA_KEY_MEDIA_URI, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_ADVERTISEMENT, METADATA_TYPE_LONG); + METADATA_KEYS_TYPE.put(METADATA_KEY_DOWNLOAD_STATUS, METADATA_TYPE_LONG); + } + + private static final @TextKey String[] PREFERRED_DESCRIPTION_ORDER = { + METADATA_KEY_TITLE, + METADATA_KEY_ARTIST, + METADATA_KEY_ALBUM, + METADATA_KEY_ALBUM_ARTIST, + METADATA_KEY_WRITER, + METADATA_KEY_AUTHOR, + METADATA_KEY_COMPOSER + }; + + private static final @BitmapKey String[] PREFERRED_BITMAP_ORDER = { + METADATA_KEY_DISPLAY_ICON, METADATA_KEY_ART, METADATA_KEY_ALBUM_ART + }; + + private static final @TextKey String[] PREFERRED_URI_ORDER = { + METADATA_KEY_DISPLAY_ICON_URI, METADATA_KEY_ART_URI, METADATA_KEY_ALBUM_ART_URI + }; + + final Bundle mBundle; + @Nullable private MediaMetadata mMetadataFwk; + @Nullable private MediaDescriptionCompat mDescription; + + MediaMetadataCompat(Bundle bundle) { + mBundle = new Bundle(bundle); + MediaSessionCompat.ensureClassLoader(mBundle); + } + + MediaMetadataCompat(Parcel in) { + mBundle = checkNotNull(in.readBundle(MediaSessionCompat.class.getClassLoader())); + } + + /** + * Returns true if the given key is contained in the metadata + * + * @param key a String key + * @return true if the key exists in this metadata, false otherwise + */ + public boolean containsKey(String key) { + return mBundle.containsKey(key); + } + + /** + * Returns the value associated with the given key, or null if no mapping of the desired type + * exists for the given key or a null value is explicitly associated with the key. + * + * @param key The key the value is stored under + * @return a CharSequence value, or null + */ + @Nullable + public CharSequence getText(@TextKey String key) { + return mBundle.getCharSequence(key); + } + + /** + * Returns the value associated with the given key, or null if no mapping of the desired type + * exists for the given key or a null value is explicitly associated with the key. + * + * @param key The key the value is stored under + * @return a String value, or null + */ + @Nullable + public String getString(@TextKey String key) { + CharSequence text = mBundle.getCharSequence(key); + if (text != null) { + return text.toString(); + } + return null; + } + + /** + * Returns the value associated with the given key, or 0L if no long exists for the given key. + * + * @param key The key the value is stored under + * @return a long value + */ + public long getLong(@LongKey String key) { + return mBundle.getLong(key, 0); + } + + /** + * Return a {@link RatingCompat} for the given key or null if no rating exists for the given key. + * + * @param key The key the value is stored under + * @return A {@link RatingCompat} or null + */ + @Nullable + @SuppressWarnings("deprecation") + public RatingCompat getRating(@RatingKey String key) { + RatingCompat rating = null; + try { + // On platform version 19 or higher, mBundle stores a Rating object. Convert it to + // RatingCompat. + rating = RatingCompat.fromRating(mBundle.getParcelable(key)); + } catch (Exception e) { + // ignore, value was not a bitmap + Log.w(TAG, "Failed to retrieve a key as Rating.", e); + } + return rating; + } + + /** + * Return a {@link Bitmap} for the given key or null if no bitmap exists for the given key. + * + * @param key The key the value is stored under + * @return A {@link Bitmap} or null + */ + @Nullable + @SuppressWarnings("deprecation") + public Bitmap getBitmap(@BitmapKey String key) { + Bitmap bmp = null; + try { + bmp = mBundle.getParcelable(key); + } catch (Exception e) { + // ignore, value was not a bitmap + Log.w(TAG, "Failed to retrieve a key as Bitmap.", e); + } + return bmp; + } + + /** + * Returns a simple description of this metadata for display purposes. + * + * @return A simple description of this metadata. + */ + public MediaDescriptionCompat getDescription() { + if (mDescription != null) { + return mDescription; + } + + String mediaId = getString(METADATA_KEY_MEDIA_ID); + + @NullableType CharSequence[] text = new CharSequence[3]; + Bitmap icon = null; + Uri iconUri = null; + + // First handle the case where display data is set already + CharSequence displayText = getText(METADATA_KEY_DISPLAY_TITLE); + if (!TextUtils.isEmpty(displayText)) { + // If they have a display title use only display data, otherwise use + // our best bets + text[0] = displayText; + text[1] = getText(METADATA_KEY_DISPLAY_SUBTITLE); + text[2] = getText(METADATA_KEY_DISPLAY_DESCRIPTION); + } else { + // Use whatever fields we can + int textIndex = 0; + int keyIndex = 0; + while (textIndex < text.length && keyIndex < PREFERRED_DESCRIPTION_ORDER.length) { + CharSequence next = getText(PREFERRED_DESCRIPTION_ORDER[keyIndex++]); + if (!TextUtils.isEmpty(next)) { + // Fill in the next empty bit of text + text[textIndex++] = next; + } + } + } + + // Get the best art bitmap we can find + for (int i = 0; i < PREFERRED_BITMAP_ORDER.length; i++) { + Bitmap next = getBitmap(PREFERRED_BITMAP_ORDER[i]); + if (next != null) { + icon = next; + break; + } + } + + // Get the best Uri we can find + for (int i = 0; i < PREFERRED_URI_ORDER.length; i++) { + String next = getString(PREFERRED_URI_ORDER[i]); + if (!TextUtils.isEmpty(next)) { + iconUri = Uri.parse(next); + break; + } + } + + Uri mediaUri = null; + String mediaUriStr = getString(METADATA_KEY_MEDIA_URI); + if (!TextUtils.isEmpty(mediaUriStr)) { + mediaUri = Uri.parse(mediaUriStr); + } + + MediaDescriptionCompat.Builder bob = new MediaDescriptionCompat.Builder(); + bob.setMediaId(mediaId); + bob.setTitle(text[0]); + bob.setSubtitle(text[1]); + bob.setDescription(text[2]); + bob.setIconBitmap(icon); + bob.setIconUri(iconUri); + bob.setMediaUri(mediaUri); + + Bundle bundle = new Bundle(); + if (mBundle.containsKey(METADATA_KEY_BT_FOLDER_TYPE)) { + bundle.putLong( + MediaDescriptionCompat.EXTRA_BT_FOLDER_TYPE, getLong(METADATA_KEY_BT_FOLDER_TYPE)); + } + if (mBundle.containsKey(METADATA_KEY_DOWNLOAD_STATUS)) { + bundle.putLong( + MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS, getLong(METADATA_KEY_DOWNLOAD_STATUS)); + } + if (!bundle.isEmpty()) { + bob.setExtras(bundle); + } + mDescription = bob.build(); + + return mDescription; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeBundle(mBundle); + } + + /** + * Get the number of fields in this metadata. + * + * @return The number of fields in the metadata. + */ + public int size() { + return mBundle.size(); + } + + /** + * Returns a Set containing the Strings used as keys in this metadata. + * + * @return a Set of String keys + */ + public Set keySet() { + return mBundle.keySet(); + } + + /** + * Gets a copy of the bundle for this metadata object. This is available to support backwards + * compatibility. + * + * @return A copy of the bundle for this metadata object. + */ + public Bundle getBundle() { + return new Bundle(mBundle); + } + + /** + * Creates an instance from a framework {@link android.media.MediaMetadata} object. + * + *

This method is only supported on {@link android.os.Build.VERSION_CODES#LOLLIPOP} and later. + * + * @param metadataObj A {@link android.media.MediaMetadata} object, or null if none. + * @return An equivalent {@link MediaMetadataCompat} object, or null if none. + */ + @Nullable + public static MediaMetadataCompat fromMediaMetadata(@Nullable Object metadataObj) { + if (metadataObj != null && Build.VERSION.SDK_INT >= 21) { + Parcel p = Parcel.obtain(); + ((MediaMetadata) metadataObj).writeToParcel(p, 0); + p.setDataPosition(0); + MediaMetadataCompat metadata = MediaMetadataCompat.CREATOR.createFromParcel(p); + p.recycle(); + metadata.mMetadataFwk = (MediaMetadata) metadataObj; + return metadata; + } else { + return null; + } + } + + /** + * Gets the underlying framework {@link android.media.MediaMetadata} object. + * + *

This method is only supported on {@link android.os.Build.VERSION_CODES#LOLLIPOP} and later. + * + * @return An equivalent {@link android.media.MediaMetadata} object, or null if none. + */ + @RequiresApi(21) + public Object getMediaMetadata() { + if (mMetadataFwk == null) { + Parcel p = Parcel.obtain(); + try { + writeToParcel(p, 0); + p.setDataPosition(0); + mMetadataFwk = MediaMetadata.CREATOR.createFromParcel(p); + return mMetadataFwk; + } finally { + p.recycle(); + } + } + return mMetadataFwk; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public MediaMetadataCompat createFromParcel(Parcel in) { + return new MediaMetadataCompat(in); + } + + @Override + public MediaMetadataCompat[] newArray(int size) { + return new MediaMetadataCompat[size]; + } + }; + + /** + * Use to build MediaMetadata objects. The system defined metadata keys must use the appropriate + * data type. + */ + public static final class Builder { + private final Bundle mBundle; + + /** + * Create an empty Builder. Any field that should be included in the {@link MediaMetadataCompat} + * must be added. + */ + public Builder() { + mBundle = new Bundle(); + } + + /** + * Create a Builder using a {@link MediaMetadataCompat} instance to set the initial values. All + * fields in the source metadata will be included in the new metadata. Fields can be overwritten + * by adding the same key. + * + * @param source + */ + public Builder(MediaMetadataCompat source) { + mBundle = new Bundle(source.mBundle); + MediaSessionCompat.ensureClassLoader(mBundle); + } + + /** + * Create a Builder using a {@link MediaMetadataCompat} instance to set initial values, but + * replace bitmaps with a scaled down copy if they are larger than maxBitmapSize. + * + * @param source The original metadata to copy. + * @param maxBitmapSize The maximum height/width for bitmaps contained in the metadata. + */ + @SuppressWarnings("deprecation") + public Builder(MediaMetadataCompat source, int maxBitmapSize) { + this(source); + for (String key : mBundle.keySet()) { + Object value = mBundle.get(key); + if (value instanceof Bitmap) { + Bitmap bmp = (Bitmap) value; + if (bmp.getHeight() > maxBitmapSize || bmp.getWidth() > maxBitmapSize) { + putBitmap(key, scaleBitmap(bmp, maxBitmapSize)); + } + } + } + } + + /** + * Put a CharSequence value into the metadata. Custom keys may be used, but if the METADATA_KEYs + * defined in this class are used they may only be one of the following: + * + *

    + *
  • {@link #METADATA_KEY_TITLE} + *
  • {@link #METADATA_KEY_ARTIST} + *
  • {@link #METADATA_KEY_ALBUM} + *
  • {@link #METADATA_KEY_AUTHOR} + *
  • {@link #METADATA_KEY_WRITER} + *
  • {@link #METADATA_KEY_COMPOSER} + *
  • {@link #METADATA_KEY_DATE} + *
  • {@link #METADATA_KEY_GENRE} + *
  • {@link #METADATA_KEY_ALBUM_ARTIST} + *
  • {@link #METADATA_KEY_ART_URI} + *
  • {@link #METADATA_KEY_ALBUM_ART_URI} + *
  • {@link #METADATA_KEY_DISPLAY_TITLE} + *
  • {@link #METADATA_KEY_DISPLAY_SUBTITLE} + *
  • {@link #METADATA_KEY_DISPLAY_DESCRIPTION} + *
  • {@link #METADATA_KEY_DISPLAY_ICON_URI} + *
+ * + * @param key The key for referencing this value + * @param value The CharSequence value to store + * @return The Builder to allow chaining + */ + public Builder putText(@TextKey String key, CharSequence value) { + Integer type = METADATA_KEYS_TYPE.get(key); + if (type != null && type != METADATA_TYPE_TEXT) { + throw new IllegalArgumentException( + "The " + key + " key cannot be used to put a CharSequence"); + } + mBundle.putCharSequence(key, value); + return this; + } + + /** + * Put a String value into the metadata. Custom keys may be used, but if the METADATA_KEYs + * defined in this class are used they may only be one of the following: + * + *
    + *
  • {@link #METADATA_KEY_TITLE} + *
  • {@link #METADATA_KEY_ARTIST} + *
  • {@link #METADATA_KEY_ALBUM} + *
  • {@link #METADATA_KEY_AUTHOR} + *
  • {@link #METADATA_KEY_WRITER} + *
  • {@link #METADATA_KEY_COMPOSER} + *
  • {@link #METADATA_KEY_DATE} + *
  • {@link #METADATA_KEY_GENRE} + *
  • {@link #METADATA_KEY_ALBUM_ARTIST} + *
  • {@link #METADATA_KEY_ART_URI} + *
  • {@link #METADATA_KEY_ALBUM_ART_URI} + *
  • {@link #METADATA_KEY_DISPLAY_TITLE} + *
  • {@link #METADATA_KEY_DISPLAY_SUBTITLE} + *
  • {@link #METADATA_KEY_DISPLAY_DESCRIPTION} + *
  • {@link #METADATA_KEY_DISPLAY_ICON_URI} + *
+ * + * @param key The key for referencing this value + * @param value The String value to store + * @return The Builder to allow chaining + */ + public Builder putString(@TextKey String key, String value) { + Integer type = METADATA_KEYS_TYPE.get(key); + if (type != null && type != METADATA_TYPE_TEXT) { + throw new IllegalArgumentException("The " + key + " key cannot be used to put a String"); + } + mBundle.putCharSequence(key, value); + return this; + } + + /** + * Put a long value into the metadata. Custom keys may be used, but if the METADATA_KEYs defined + * in this class are used they may only be one of the following: + * + *
    + *
  • {@link #METADATA_KEY_DURATION} + *
  • {@link #METADATA_KEY_TRACK_NUMBER} + *
  • {@link #METADATA_KEY_NUM_TRACKS} + *
  • {@link #METADATA_KEY_DISC_NUMBER} + *
  • {@link #METADATA_KEY_YEAR} + *
  • {@link #METADATA_KEY_BT_FOLDER_TYPE} + *
  • {@link #METADATA_KEY_ADVERTISEMENT} + *
  • {@link #METADATA_KEY_DOWNLOAD_STATUS} + *
+ * + * @param key The key for referencing this value + * @param value The String value to store + * @return The Builder to allow chaining + */ + public Builder putLong(@LongKey String key, long value) { + Integer type = METADATA_KEYS_TYPE.get(key); + if (type != null && type != METADATA_TYPE_LONG) { + throw new IllegalArgumentException("The " + key + " key cannot be used to put a long"); + } + mBundle.putLong(key, value); + return this; + } + + /** + * Put a {@link RatingCompat} into the metadata. Custom keys may be used, but if the + * METADATA_KEYs defined in this class are used they may only be one of the following: + * + *
    + *
  • {@link #METADATA_KEY_RATING} + *
  • {@link #METADATA_KEY_USER_RATING} + *
+ * + * @param key The key for referencing this value + * @param value The String value to store + * @return The Builder to allow chaining + */ + public Builder putRating(@RatingKey String key, RatingCompat value) { + Integer type = METADATA_KEYS_TYPE.get(key); + if (type != null && type != METADATA_TYPE_RATING) { + throw new IllegalArgumentException("The " + key + " key cannot be used to put a Rating"); + } + // On platform version 19 or higher, use Rating instead of RatingCompat so mBundle + // can be unmarshalled. + mBundle.putParcelable(key, (Parcelable) value.getRating()); + return this; + } + + /** + * Put a {@link Bitmap} into the metadata. Custom keys may be used, but if the METADATA_KEYs + * defined in this class are used they may only be one of the following: + * + *
    + *
  • {@link #METADATA_KEY_ART} + *
  • {@link #METADATA_KEY_ALBUM_ART} + *
  • {@link #METADATA_KEY_DISPLAY_ICON} + *
+ * + * Large bitmaps may be scaled down when {@link + * androidx.media3.session.legacy.MediaSessionCompat#setMetadata} is called. To pass full + * resolution images {@link Uri Uris} should be used with {@link #putString}. + * + * @param key The key for referencing this value + * @param value The Bitmap to store + * @return The Builder to allow chaining + */ + public Builder putBitmap(@BitmapKey String key, Bitmap value) { + Integer type = METADATA_KEYS_TYPE.get(key); + if (type != null && type != METADATA_TYPE_BITMAP) { + throw new IllegalArgumentException("The " + key + " key cannot be used to put a Bitmap"); + } + mBundle.putParcelable(key, value); + return this; + } + + /** + * Creates a {@link MediaMetadataCompat} instance with the specified fields. + * + * @return The new MediaMetadata instance + */ + public MediaMetadataCompat build() { + return new MediaMetadataCompat(mBundle); + } + + private Bitmap scaleBitmap(Bitmap bmp, int maxSize) { + float maxSizeF = maxSize; + float widthScale = maxSizeF / bmp.getWidth(); + float heightScale = maxSizeF / bmp.getHeight(); + float scale = Math.min(widthScale, heightScale); + int height = (int) (bmp.getHeight() * scale); + int width = (int) (bmp.getWidth() * scale); + return Bitmap.createScaledBitmap(bmp, width, height, true); + } + } +} diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaSessionCompat.java b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaSessionCompat.java new file mode 100644 index 0000000000..11d65531b5 --- /dev/null +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaSessionCompat.java @@ -0,0 +1,4673 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.session.legacy; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY; +import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.common.util.Assertions.checkState; +import static androidx.media3.session.legacy.MediaSessionManager.RemoteUserInfo.LEGACY_CONTROLLER; +import static androidx.media3.session.legacy.MediaSessionManager.RemoteUserInfo.UNKNOWN_PID; +import static androidx.media3.session.legacy.MediaSessionManager.RemoteUserInfo.UNKNOWN_UID; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.media.AudioAttributes; +import android.media.AudioManager; +import android.media.MediaDescription; +import android.media.MediaMetadata; +import android.media.MediaMetadataEditor; +import android.media.MediaMetadataRetriever; +import android.media.Rating; +import android.media.RemoteControlClient; +import android.media.VolumeProvider; +import android.media.session.MediaSession; +import android.media.session.PlaybackState; +import android.net.Uri; +import android.os.BadParcelableException; +import android.os.Binder; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.os.SystemClock; +import android.text.TextUtils; +import android.util.Log; +import android.util.TypedValue; +import android.view.KeyEvent; +import android.view.ViewConfiguration; +import androidx.annotation.DoNotInline; +import androidx.annotation.GuardedBy; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.RestrictTo; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.legacy.MediaSessionManager.RemoteUserInfo; +import androidx.versionedparcelable.ParcelUtils; +import androidx.versionedparcelable.VersionedParcelable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.ref.WeakReference; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * Allows interaction with media controllers, volume keys, media buttons, and transport controls. + * + *

A MediaSession should be created when an app wants to publish media playback information or + * handle media keys. In general an app only needs one session for all playback, though multiple + * sessions can be created to provide finer grain controls of media. + * + *

Once a session is created the owner of the session may pass its {@link #getSessionToken() + * session token} to other processes to allow them to create a {@link MediaControllerCompat} to + * interact with the session. + * + *

To receive commands, media keys, and other events a {@link Callback} must be set with {@link + * #setCallback(Callback)}. + * + *

When an app is finished performing playback it must call {@link #release()} to clean up the + * session and notify any controllers. + * + *

MediaSessionCompat objects are not thread safe and all calls should be made from the same + * thread. + * + *

This is a helper for accessing features in {@link android.media.session.MediaSession} + * introduced after API level 4 in a backwards compatible fashion. + * + *

Developer Guides

+ * + *

For information about building your media application, read the Media Apps developer guide. + */ +@UnstableApi +@RestrictTo(LIBRARY) +public class MediaSessionCompat { + static final String TAG = "MediaSessionCompat"; + + private final MediaSessionImpl mImpl; + private final MediaControllerCompat mController; + private final ArrayList mActiveListeners = new ArrayList<>(); + + @IntDef( + flag = true, + value = { + FLAG_HANDLES_MEDIA_BUTTONS, + FLAG_HANDLES_TRANSPORT_CONTROLS, + FLAG_HANDLES_QUEUE_COMMANDS + }) + @Retention(RetentionPolicy.SOURCE) + private @interface SessionFlags {} + + /** + * Sets this flag on the session to indicate that it can handle media button events. + * + * @deprecated This flag is no longer used. All media sessions are expected to handle media button + * events now. For backward compatibility, this flag will be always set. + */ + @Deprecated public static final int FLAG_HANDLES_MEDIA_BUTTONS = 1 << 0; + + /** + * Sets this flag on the session to indicate that it handles transport control commands through + * its {@link Callback}. + * + * @deprecated This flag is no longer used. All media sessions are expected to handle transport + * controls now. For backward compatibility, this flag will be always set. + */ + @Deprecated public static final int FLAG_HANDLES_TRANSPORT_CONTROLS = 1 << 1; + + /** + * Sets this flag on the session to indicate that it handles queue management commands through its + * {@link Callback}. + */ + public static final int FLAG_HANDLES_QUEUE_COMMANDS = 1 << 2; + + /** + * Predefined custom action to flag the media that is currently playing as inappropriate. + * + * @see Callback#onCustomAction + */ + public static final String ACTION_FLAG_AS_INAPPROPRIATE = + "android.support.v4.media.session.action.FLAG_AS_INAPPROPRIATE"; + + /** + * Predefined custom action to skip the advertisement that is currently playing. + * + * @see Callback#onCustomAction + */ + public static final String ACTION_SKIP_AD = "android.support.v4.media.session.action.SKIP_AD"; + + /** + * Predefined custom action to follow an artist, album, or playlist. The extra bundle must have + * {@link #ARGUMENT_MEDIA_ATTRIBUTE} to indicate the type of the follow action. The bundle can + * also have an optional string argument, {@link #ARGUMENT_MEDIA_ATTRIBUTE_VALUE}, to specify the + * target to follow (e.g., the name of the artist to follow). If this argument is omitted, the + * currently playing media will be the target of the action. Thus, the session must perform the + * follow action with the current metadata. If there's no specified attribute in the current + * metadata, the controller must not omit this argument. + * + * @see #ARGUMENT_MEDIA_ATTRIBUTE + * @see #ARGUMENT_MEDIA_ATTRIBUTE_VALUE + * @see Callback#onCustomAction + */ + public static final String ACTION_FOLLOW = "android.support.v4.media.session.action.FOLLOW"; + + /** + * Predefined custom action to unfollow an artist, album, or playlist. The extra bundle must have + * {@link #ARGUMENT_MEDIA_ATTRIBUTE} to indicate the type of the unfollow action. The bundle can + * also have an optional string argument, {@link #ARGUMENT_MEDIA_ATTRIBUTE_VALUE}, to specify the + * target to unfollow (e.g., the name of the artist to unfollow). If this argument is omitted, the + * currently playing media will be the target of the action. Thus, the session must perform the + * unfollow action with the current metadata. If there's no specified attribute in the current + * metadata, the controller must not omit this argument. + * + * @see #ARGUMENT_MEDIA_ATTRIBUTE + * @see #ARGUMENT_MEDIA_ATTRIBUTE_VALUE + * @see Callback#onCustomAction + */ + public static final String ACTION_UNFOLLOW = "android.support.v4.media.session.action.UNFOLLOW"; + + /** + * Argument to indicate the media attribute. It should be one of the following: + * + *

    + *
  • {@link #MEDIA_ATTRIBUTE_ARTIST} + *
  • {@link #MEDIA_ATTRIBUTE_PLAYLIST} + *
  • {@link #MEDIA_ATTRIBUTE_ALBUM} + *
+ */ + public static final String ARGUMENT_MEDIA_ATTRIBUTE = + "android.support.v4.media.session.ARGUMENT_MEDIA_ATTRIBUTE"; + + /** + * String argument to indicate the value of the media attribute (e.g., the name of the artist). + */ + public static final String ARGUMENT_MEDIA_ATTRIBUTE_VALUE = + "android.support.v4.media.session.ARGUMENT_MEDIA_ATTRIBUTE_VALUE"; + + /** + * The value of {@link #ARGUMENT_MEDIA_ATTRIBUTE} indicating the artist. + * + * @see #ARGUMENT_MEDIA_ATTRIBUTE + */ + public static final int MEDIA_ATTRIBUTE_ARTIST = 0; + + /** + * The value of {@link #ARGUMENT_MEDIA_ATTRIBUTE} indicating the album. + * + * @see #ARGUMENT_MEDIA_ATTRIBUTE + */ + public static final int MEDIA_ATTRIBUTE_ALBUM = 1; + + /** + * The value of {@link #ARGUMENT_MEDIA_ATTRIBUTE} indicating the playlist. + * + * @see #ARGUMENT_MEDIA_ATTRIBUTE + */ + public static final int MEDIA_ATTRIBUTE_PLAYLIST = 2; + + /** Custom action to invoke playFromUri() for the forward compatibility. */ + public static final String ACTION_PLAY_FROM_URI = + "android.support.v4.media.session.action.PLAY_FROM_URI"; + + /** Custom action to invoke prepare() for the forward compatibility. */ + public static final String ACTION_PREPARE = "android.support.v4.media.session.action.PREPARE"; + + /** Custom action to invoke prepareFromMediaId() for the forward compatibility. */ + public static final String ACTION_PREPARE_FROM_MEDIA_ID = + "android.support.v4.media.session.action.PREPARE_FROM_MEDIA_ID"; + + /** Custom action to invoke prepareFromSearch() for the forward compatibility. */ + public static final String ACTION_PREPARE_FROM_SEARCH = + "android.support.v4.media.session.action.PREPARE_FROM_SEARCH"; + + /** Custom action to invoke prepareFromUri() for the forward compatibility. */ + public static final String ACTION_PREPARE_FROM_URI = + "android.support.v4.media.session.action.PREPARE_FROM_URI"; + + /** Custom action to invoke setCaptioningEnabled() for the forward compatibility. */ + public static final String ACTION_SET_CAPTIONING_ENABLED = + "android.support.v4.media.session.action.SET_CAPTIONING_ENABLED"; + + /** Custom action to invoke setRepeatMode() for the forward compatibility. */ + public static final String ACTION_SET_REPEAT_MODE = + "android.support.v4.media.session.action.SET_REPEAT_MODE"; + + /** Custom action to invoke setShuffleMode() for the forward compatibility. */ + public static final String ACTION_SET_SHUFFLE_MODE = + "android.support.v4.media.session.action.SET_SHUFFLE_MODE"; + + /** Custom action to invoke setRating() with extra fields. */ + public static final String ACTION_SET_RATING = + "android.support.v4.media.session.action.SET_RATING"; + + /** Custom action to invoke setPlaybackSpeed() with extra fields. */ + public static final String ACTION_SET_PLAYBACK_SPEED = + "android.support.v4.media.session.action.SET_PLAYBACK_SPEED"; + + /** Argument for use with {@link #ACTION_PREPARE_FROM_MEDIA_ID} indicating media id to play. */ + public static final String ACTION_ARGUMENT_MEDIA_ID = + "android.support.v4.media.session.action.ARGUMENT_MEDIA_ID"; + + /** Argument for use with {@link #ACTION_PREPARE_FROM_SEARCH} indicating search query. */ + public static final String ACTION_ARGUMENT_QUERY = + "android.support.v4.media.session.action.ARGUMENT_QUERY"; + + /** + * Argument for use with {@link #ACTION_PREPARE_FROM_URI} and {@link #ACTION_PLAY_FROM_URI} + * indicating URI to play. + */ + public static final String ACTION_ARGUMENT_URI = + "android.support.v4.media.session.action.ARGUMENT_URI"; + + /** Argument for use with {@link #ACTION_SET_RATING} indicating the rate to be set. */ + public static final String ACTION_ARGUMENT_RATING = + "android.support.v4.media.session.action.ARGUMENT_RATING"; + + /** Argument for use with {@link #ACTION_SET_PLAYBACK_SPEED} indicating the speed to be set. */ + public static final String ACTION_ARGUMENT_PLAYBACK_SPEED = + "android.support.v4.media.session.action.ARGUMENT_PLAYBACK_SPEED"; + + /** Argument for use with various actions indicating extra bundle. */ + public static final String ACTION_ARGUMENT_EXTRAS = + "android.support.v4.media.session.action.ARGUMENT_EXTRAS"; + + /** + * Argument for use with {@link #ACTION_SET_CAPTIONING_ENABLED} indicating whether captioning is + * enabled. + */ + public static final String ACTION_ARGUMENT_CAPTIONING_ENABLED = + "android.support.v4.media.session.action.ARGUMENT_CAPTIONING_ENABLED"; + + /** Argument for use with {@link #ACTION_SET_REPEAT_MODE} indicating repeat mode. */ + public static final String ACTION_ARGUMENT_REPEAT_MODE = + "android.support.v4.media.session.action.ARGUMENT_REPEAT_MODE"; + + /** Argument for use with {@link #ACTION_SET_SHUFFLE_MODE} indicating shuffle mode. */ + public static final String ACTION_ARGUMENT_SHUFFLE_MODE = + "android.support.v4.media.session.action.ARGUMENT_SHUFFLE_MODE"; + + /** */ + public static final String KEY_TOKEN = "android.support.v4.media.session.TOKEN"; + + /** */ + public static final String KEY_EXTRA_BINDER = "android.support.v4.media.session.EXTRA_BINDER"; + + /** */ + public static final String KEY_SESSION2_TOKEN = "android.support.v4.media.session.SESSION_TOKEN2"; + + // Maximum size of the bitmap in dp. + private static final int MAX_BITMAP_SIZE_IN_DP = 320; + + private static final String DATA_CALLING_PACKAGE = "data_calling_pkg"; + private static final String DATA_CALLING_PID = "data_calling_pid"; + private static final String DATA_CALLING_UID = "data_calling_uid"; + private static final String DATA_EXTRAS = "data_extras"; + + // Maximum size of the bitmap in px. It shouldn't be changed. + static int sMaxBitmapSize; + + /** + * Creates a new session. You must call {@link #release()} when finished with the session. + * + *

The session will automatically be registered with the system but will not be published until + * {@link #setActive(boolean) setActive(true)} is called. + * + *

For API 20 or earlier, note that a media button receiver is required for handling {@link + * Intent#ACTION_MEDIA_BUTTON}. This constructor will attempt to find an appropriate {@link + * BroadcastReceiver} from your manifest. See {@link MediaButtonReceiver} for more details. + * + * @param context The context to use to create the session. + * @param tag A short name for debugging purposes. + */ + public MediaSessionCompat(Context context, String tag) { + this(context, tag, null, null); + } + + /** + * Creates a new session with a specified media button receiver (a component name and/or a pending + * intent). You must call {@link #release()} when finished with the session. + * + *

The session will automatically be registered with the system but will not be published until + * {@link #setActive(boolean) setActive(true)} is called. + * + *

For API 20 or earlier, note that a media button receiver is required for handling {@link + * Intent#ACTION_MEDIA_BUTTON}. This constructor will attempt to find an appropriate {@link + * BroadcastReceiver} from your manifest if it's not specified. See {@link MediaButtonReceiver} + * for more details. + * + * @param context The context to use to create the session. + * @param tag A short name for debugging purposes. + * @param mbrComponent The component name for your media button receiver. + * @param mbrIntent The PendingIntent for your receiver component that handles media button + * events. This is optional and will be used on between {@link + * android.os.Build.VERSION_CODES#JELLY_BEAN_MR2} and {@link + * android.os.Build.VERSION_CODES#KITKAT_WATCH} instead of the component name. + */ + public MediaSessionCompat( + Context context, + String tag, + @Nullable ComponentName mbrComponent, + @Nullable PendingIntent mbrIntent) { + this(context, tag, mbrComponent, mbrIntent, null); + } + + /** + * Creates a new session with a specified media button receiver (a component name and/or a pending + * intent). You must call {@link #release()} when finished with the session. + * + *

The session will automatically be registered with the system but will not be published until + * {@link #setActive(boolean) setActive(true)} is called. + * + *

For API 20 or earlier, note that a media button receiver is required for handling {@link + * Intent#ACTION_MEDIA_BUTTON}. This constructor will attempt to find an appropriate {@link + * BroadcastReceiver} from your manifest if it's not specified. See {@link MediaButtonReceiver} + * for more details. The {@code sessionInfo} can include additional unchanging information about + * this session. For example, it can include the version of the application, or other app-specific + * unchanging information. + * + * @param context The context to use to create the session. + * @param tag A short name for debugging purposes. + * @param mbrComponent The component name for your media button receiver. + * @param mbrIntent The PendingIntent for your receiver component that handles media button + * events. This is optional and will be used on between {@link + * android.os.Build.VERSION_CODES#JELLY_BEAN_MR2} and {@link + * android.os.Build.VERSION_CODES#KITKAT_WATCH} instead of the component name. + * @param sessionInfo A bundle for additional information about this session, or {@link + * Bundle#EMPTY} if none. Controllers can get this information by calling {@link + * MediaControllerCompat#getSessionInfo()}. An {@link IllegalArgumentException} will be thrown + * if this contains any non-framework Parcelable objects. + */ + public MediaSessionCompat( + Context context, + String tag, + @Nullable ComponentName mbrComponent, + @Nullable PendingIntent mbrIntent, + @Nullable Bundle sessionInfo) { + this(context, tag, mbrComponent, mbrIntent, sessionInfo, null /* session2Token */); + } + + /** */ + @SuppressWarnings({ + "method.invocation.invalid", + "argument.type.incompatible", + "assignment.type.incompatible" + }) // registering listener from constructor + public MediaSessionCompat( + Context context, + String tag, + @Nullable ComponentName mbrComponent, + @Nullable PendingIntent mbrIntent, + @Nullable Bundle sessionInfo, + @Nullable VersionedParcelable session2Token) { + if (context == null) { + throw new IllegalArgumentException("context must not be null"); + } + if (TextUtils.isEmpty(tag)) { + throw new IllegalArgumentException("tag must not be null or empty"); + } + + if (mbrComponent == null) { + mbrComponent = MediaButtonReceiver.getMediaButtonReceiverComponent(context); + if (mbrComponent == null) { + Log.w( + TAG, + "Couldn't find a unique registered media button receiver in the " + "given context."); + } + } + if (mbrComponent != null && mbrIntent == null) { + // construct a PendingIntent for the media button + Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); + // the associated intent will be handled by the component being registered + mediaButtonIntent.setComponent(mbrComponent); + mbrIntent = + PendingIntent.getBroadcast( + context, + 0 /* requestCode, ignored */, + mediaButtonIntent, + Build.VERSION.SDK_INT >= 31 ? PendingIntent.FLAG_MUTABLE : 0); + } + + if (android.os.Build.VERSION.SDK_INT >= 21) { + if (android.os.Build.VERSION.SDK_INT >= 29) { + mImpl = new MediaSessionImplApi29(context, tag, session2Token, sessionInfo); + } else if (android.os.Build.VERSION.SDK_INT >= 28) { + mImpl = new MediaSessionImplApi28(context, tag, session2Token, sessionInfo); + } else if (android.os.Build.VERSION.SDK_INT >= 22) { + mImpl = new MediaSessionImplApi22(context, tag, session2Token, sessionInfo); + } else { + mImpl = new MediaSessionImplApi21(context, tag, session2Token, sessionInfo); + } + // Set default callback to respond to controllers' extra binder requests. + Looper myLooper = Looper.myLooper(); + Handler handler = new Handler(myLooper != null ? myLooper : Looper.getMainLooper()); + setCallback(new Callback() {}, handler); + mImpl.setMediaButtonReceiver(mbrIntent); + } else { + mImpl = + new MediaSessionImplApi19( + context, tag, mbrComponent, mbrIntent, session2Token, sessionInfo); + } + mController = new MediaControllerCompat(context, this); + + if (sMaxBitmapSize == 0) { + sMaxBitmapSize = + (int) + (TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + MAX_BITMAP_SIZE_IN_DP, + context.getResources().getDisplayMetrics()) + + 0.5f); + } + } + + @SuppressWarnings({ + "method.invocation.invalid", + "argument.type.incompatible", + "assignment.type.incompatible" + }) // registering listener from constructor + private MediaSessionCompat(Context context, MediaSessionImpl impl) { + mImpl = impl; + mController = new MediaControllerCompat(context, this); + } + + /** + * Adds a callback to receive updates on for the MediaSession. This includes media button and + * volume events. The caller's thread will be used to post events. Set the callback to null to + * stop receiving events. + * + *

Don't reuse the callback among the sessions. Callbacks keep internal reference to the + * session when it's set, so it may misbehave. + * + * @param callback The callback object + */ + public void setCallback(Callback callback) { + setCallback(callback, null); + } + + /** + * Sets the callback to receive updates for the MediaSession. This includes media button and + * volume events. Set the callback to null to stop receiving events. + * + *

Don't reuse the callback among the sessions. Callbacks keep internal reference to the + * session when it's set, so it may misbehave. + * + * @param callback The callback to receive updates on. + * @param handler The handler that events should be posted on. + */ + @SuppressWarnings("deprecation") + public void setCallback(Callback callback, @Nullable Handler handler) { + if (callback == null) { + mImpl.setCallback(null, null); + } else { + mImpl.setCallback(callback, handler != null ? handler : new Handler()); + } + } + + /** + * Sets the {@link RegistrationCallback}. + * + * @param callback callback to listener callback registration. Can be null to stop. + * @param handler handler + */ + public void setRegistrationCallback(@Nullable RegistrationCallback callback, Handler handler) { + mImpl.setRegistrationCallback(callback, handler); + } + + /** + * Sets an intent for launching UI for this Session. This can be used as a quick link to an + * ongoing media screen. The intent should be for an activity that may be started using {@link + * Activity#startActivity(Intent)}. + * + * @param pi The intent to launch to show UI for this Session. + */ + public void setSessionActivity(PendingIntent pi) { + mImpl.setSessionActivity(pi); + } + + /** + * Sets a pending intent for your media button receiver to allow restarting playback after the + * session has been stopped. If your app is started in this way an {@link + * Intent#ACTION_MEDIA_BUTTON} intent will be sent via the pending intent. + * + *

This method will only work on {@link android.os.Build.VERSION_CODES#LOLLIPOP} and later. + * Earlier platform versions must include the media button receiver in the constructor. + * + * @param mbr The {@link PendingIntent} to send the media button event to. + */ + public void setMediaButtonReceiver(PendingIntent mbr) { + mImpl.setMediaButtonReceiver(mbr); + } + + /** + * Sets any flags for the session. + * + * @param flags The flags to set for this session. + */ + public void setFlags(@SessionFlags int flags) { + mImpl.setFlags(flags); + } + + /** + * Sets the stream this session is playing on. This will affect the system's volume handling for + * this session. If {@link #setPlaybackToRemote} was previously called it will stop receiving + * volume commands and the system will begin sending volume changes to the appropriate stream. + * + *

By default sessions are on {@link AudioManager#STREAM_MUSIC}. + * + * @param stream The {@link AudioManager} stream this session is playing on. + */ + public void setPlaybackToLocal(int stream) { + mImpl.setPlaybackToLocal(stream); + } + + /** + * Configures this session to use remote volume handling. This must be called to receive volume + * button events, otherwise the system will adjust the current stream volume for this session. If + * {@link #setPlaybackToLocal} was previously called that stream will stop receiving volume + * changes for this session. + * + *

On platforms earlier than {@link android.os.Build.VERSION_CODES#LOLLIPOP} this will only + * allow an app to handle volume commands sent directly to the session by a {@link + * MediaControllerCompat}. System routing of volume keys will not use the volume provider. + * + * @param volumeProvider The provider that will handle volume changes. May not be null. + */ + public void setPlaybackToRemote(VolumeProviderCompat volumeProvider) { + if (volumeProvider == null) { + throw new IllegalArgumentException("volumeProvider may not be null!"); + } + mImpl.setPlaybackToRemote(volumeProvider); + } + + /** + * Sets if this session is currently active and ready to receive commands. If set to false your + * session's controller may not be discoverable. You must set the session to active before it can + * start receiving media button events or transport commands. + * + *

On platforms earlier than {@link android.os.Build.VERSION_CODES#LOLLIPOP}, a media button + * event receiver should be set via the constructor to receive media button events. + * + * @param active Whether this session is active or not. + */ + public void setActive(boolean active) { + mImpl.setActive(active); + for (OnActiveChangeListener listener : mActiveListeners) { + listener.onActiveChanged(); + } + } + + /** + * Gets the current active state of this session. + * + * @return True if the session is active, false otherwise. + */ + public boolean isActive() { + return mImpl.isActive(); + } + + /** + * Sends a proprietary event to all MediaControllers listening to this Session. It's up to the + * Controller/Session owner to determine the meaning of any events. + * + * @param event The name of the event to send + * @param extras Any extras included with the event + */ + public void sendSessionEvent(String event, @Nullable Bundle extras) { + if (TextUtils.isEmpty(event)) { + throw new IllegalArgumentException("event cannot be null or empty"); + } + mImpl.sendSessionEvent(event, extras); + } + + /** + * This must be called when an app has finished performing playback. If playback is expected to + * start again shortly the session can be left open, but it must be released if your activity or + * service is being destroyed. + */ + public void release() { + mImpl.release(); + } + + /** + * Retrieves a token object that can be used by apps to create a {@link MediaControllerCompat} for + * interacting with this session. The owner of the session is responsible for deciding how to + * distribute these tokens. + * + *

On platform versions before {@link android.os.Build.VERSION_CODES#LOLLIPOP} this token may + * only be used within your app as there is no way to guarantee other apps are using the same + * version of the support library. + * + * @return A token that can be used to create a media controller for this session. + */ + public Token getSessionToken() { + return mImpl.getSessionToken(); + } + + /** + * Gets a controller for this session. This is a convenience method to avoid having to cache your + * own controller in process. + * + * @return A controller for this session. + */ + public MediaControllerCompat getController() { + return mController; + } + + /** + * Updates the current playback state. + * + * @param state The current state of playback + */ + public void setPlaybackState(PlaybackStateCompat state) { + mImpl.setPlaybackState(state); + } + + /** + * Updates the current metadata. New metadata can be created using {@link + * androidx.media3.session.legacy.MediaMetadataCompat.Builder}. This operation may take time + * proportional to the size of the bitmap to replace large bitmaps with a scaled down copy. + * + * @param metadata The new metadata + * @see androidx.media3.session.legacy.MediaMetadataCompat.Builder#putBitmap + */ + public void setMetadata(@Nullable MediaMetadataCompat metadata) { + mImpl.setMetadata(metadata); + } + + /** + * Updates the list of items in the play queue. It is an ordered list and should contain the + * current item, and previous or upcoming items if they exist. The id of each item should be + * unique within the play queue. Specify null if there is no current play queue. + * + *

The queue should be of reasonable size. If the play queue is unbounded within your app, it + * is better to send a reasonable amount in a sliding window instead. + * + * @param queue A list of items in the play queue. + */ + public void setQueue(@Nullable List queue) { + if (queue != null) { + Set set = new HashSet<>(); + for (QueueItem item : queue) { + if (item == null) { + throw new IllegalArgumentException("queue shouldn't have null items"); + } + if (set.contains(item.getQueueId())) { + Log.e( + TAG, + "Found duplicate queue id: " + item.getQueueId(), + new IllegalArgumentException("id of each queue item should be unique")); + } + set.add(item.getQueueId()); + } + } + mImpl.setQueue(queue); + } + + /** + * Sets the title of the play queue. The UI should display this title along with the play queue + * itself. e.g. "Play Queue", "Now Playing", or an album name. + * + * @param title The title of the play queue. + */ + public void setQueueTitle(CharSequence title) { + mImpl.setQueueTitle(title); + } + + /** + * Sets the style of rating used by this session. Apps trying to set the rating should use this + * style. Must be one of the following: + * + *

    + *
  • {@link RatingCompat#RATING_NONE} + *
  • {@link RatingCompat#RATING_3_STARS} + *
  • {@link RatingCompat#RATING_4_STARS} + *
  • {@link RatingCompat#RATING_5_STARS} + *
  • {@link RatingCompat#RATING_HEART} + *
  • {@link RatingCompat#RATING_PERCENTAGE} + *
  • {@link RatingCompat#RATING_THUMB_UP_DOWN} + *
+ */ + public void setRatingType(@RatingCompat.Style int type) { + mImpl.setRatingType(type); + } + + /** + * Enables/disables captioning for this session. + * + * @param enabled {@code true} to enable captioning, {@code false} to disable. + */ + public void setCaptioningEnabled(boolean enabled) { + mImpl.setCaptioningEnabled(enabled); + } + + /** + * Sets the repeat mode for this session. + * + *

Note that if this method is not called before, {@link MediaControllerCompat#getRepeatMode} + * will return {@link PlaybackStateCompat#REPEAT_MODE_NONE}. + * + * @param repeatMode The repeat mode. Must be one of the following: {@link + * PlaybackStateCompat#REPEAT_MODE_NONE}, {@link PlaybackStateCompat#REPEAT_MODE_ONE}, {@link + * PlaybackStateCompat#REPEAT_MODE_ALL}, {@link PlaybackStateCompat#REPEAT_MODE_GROUP} + */ + public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) { + mImpl.setRepeatMode(repeatMode); + } + + /** + * Sets the shuffle mode for this session. + * + *

Note that if this method is not called before, {@link MediaControllerCompat#getShuffleMode} + * will return {@link PlaybackStateCompat#SHUFFLE_MODE_NONE}. + * + * @param shuffleMode The shuffle mode. Must be one of the following: {@link + * PlaybackStateCompat#SHUFFLE_MODE_NONE}, {@link PlaybackStateCompat#SHUFFLE_MODE_ALL}, + * {@link PlaybackStateCompat#SHUFFLE_MODE_GROUP} + */ + public void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode) { + mImpl.setShuffleMode(shuffleMode); + } + + /** + * Sets some extras that can be associated with the {@link MediaSessionCompat}. No assumptions + * should be made as to how a {@link MediaControllerCompat} will handle these extras. Keys should + * be fully qualified (e.g. com.example.MY_EXTRA) to avoid conflicts. + * + * @param extras The extras associated with the session. + */ + public void setExtras(@Nullable Bundle extras) { + mImpl.setExtras(extras); + } + + /** + * Gets the underlying framework {@link android.media.session.MediaSession} object. + * + *

This method is only supported on API 21+. + * + * @return The underlying {@link android.media.session.MediaSession} object, or null if none. + */ + @Nullable + public Object getMediaSession() { + return mImpl.getMediaSession(); + } + + /** + * Gets the underlying framework {@link android.media.RemoteControlClient} object. + * + *

This method is only supported on APIs 14-20. On API 21+ {@link #getMediaSession()} should be + * used instead. + * + * @return The underlying {@link android.media.RemoteControlClient} object, or null if none. + */ + @Nullable + public Object getRemoteControlClient() { + return mImpl.getRemoteControlClient(); + } + + /** + * Gets the controller information who sent the current request. + * + *

Note: This is only valid while in a request callback, such as {@link Callback#onPlay}. + * + *

Note: From API 21 to 23, this method returns a fake {@link RemoteUserInfo} which has + * following values: + * + *

    + *
  • Package name is {@link MediaSessionManager.RemoteUserInfo#LEGACY_CONTROLLER}. + *
  • PID and UID will have negative values. + *
+ * + *

Note: From API 24 to 27, the {@link RemoteUserInfo} returned from this method will have + * negative uid and pid. Most of the cases it will have the correct package name, but sometimes it + * will fail to get the right one. + * + * @see MediaSessionManager.RemoteUserInfo#LEGACY_CONTROLLER + * @see MediaSessionManager#isTrustedForMediaControl(RemoteUserInfo) + */ + @Nullable + public final RemoteUserInfo getCurrentControllerInfo() { + return mImpl.getCurrentControllerInfo(); + } + + /** + * Returns the name of the package that sent the last media button, transport control, or command + * from controllers and the system. This is only valid while in a request callback, such as {@link + * Callback#onPlay}. This method is not available and returns null on pre-N devices. + */ + @Nullable + public String getCallingPackage() { + return mImpl.getCallingPackage(); + } + + /** + * Adds a listener to be notified when the active status of this session changes. This is + * primarily used by the support library and should not be needed by apps. + * + * @param listener The listener to add. + */ + public void addOnActiveChangeListener(OnActiveChangeListener listener) { + if (listener == null) { + throw new IllegalArgumentException("Listener may not be null"); + } + mActiveListeners.add(listener); + } + + /** + * Stops the listener from being notified when the active status of this session changes. + * + * @param listener The listener to remove. + */ + public void removeOnActiveChangeListener(OnActiveChangeListener listener) { + if (listener == null) { + throw new IllegalArgumentException("Listener may not be null"); + } + mActiveListeners.remove(listener); + } + + /** + * Creates an instance from a framework {@link android.media.session.MediaSession} object. + * + *

This method is only supported on API 21+. On API 20 and below, it returns null. + * + *

Note: A {@link MediaSessionCompat} object returned from this method may not provide the full + * functionality of {@link MediaSessionCompat} until setting a new {@link + * MediaSessionCompat.Callback}. To avoid this, when both a {@link MediaSessionCompat} and a + * framework {@link android.media.session.MediaSession} are needed, it is recommended to create a + * {@link MediaSessionCompat} first and get the framework session through {@link + * #getMediaSession()}. + * + * @param context The context to use to create the session. + * @param mediaSession A {@link android.media.session.MediaSession} object. + * @return An equivalent {@link MediaSessionCompat} object, or null if none. + */ + @Nullable + public static MediaSessionCompat fromMediaSession( + @Nullable Context context, @Nullable Object mediaSession) { + if (Build.VERSION.SDK_INT < 21 || context == null || mediaSession == null) { + return null; + } + MediaSessionImpl impl; + if (Build.VERSION.SDK_INT >= 29) { + impl = new MediaSessionImplApi29(mediaSession); + } else if (Build.VERSION.SDK_INT >= 28) { + impl = new MediaSessionImplApi28(mediaSession); + } else { + // API 21+ + impl = new MediaSessionImplApi21(mediaSession); + } + return new MediaSessionCompat(context, impl); + } + + /** A helper method for setting the application class loader to the given {@link Bundle}. */ + public static void ensureClassLoader(@Nullable Bundle bundle) { + if (bundle != null) { + bundle.setClassLoader(checkNotNull(MediaSessionCompat.class.getClassLoader())); + } + } + + /** + * Tries to unparcel the given {@link Bundle} with the application class loader and returns {@code + * null} if a {@link BadParcelableException} is thrown while unparcelling, otherwise the given + * bundle in which the application class loader is set. + */ + @Nullable + public static Bundle unparcelWithClassLoader(@Nullable Bundle bundle) { + if (bundle == null) { + return null; + } + ensureClassLoader(bundle); + try { + bundle.isEmpty(); // to call unparcel() + return bundle; + } catch (BadParcelableException e) { + // The exception details will be logged by Parcel class. + Log.e(TAG, "Could not unparcel the data."); + return null; + } + } + + @Nullable + @SuppressWarnings("WeakerAccess") /* synthetic access */ + static PlaybackStateCompat getStateWithUpdatedPosition( + @Nullable PlaybackStateCompat state, @Nullable MediaMetadataCompat metadata) { + if (state == null || state.getPosition() == PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN) { + return state; + } + + if (state.getState() == PlaybackStateCompat.STATE_PLAYING + || state.getState() == PlaybackStateCompat.STATE_FAST_FORWARDING + || state.getState() == PlaybackStateCompat.STATE_REWINDING) { + long updateTime = state.getLastPositionUpdateTime(); + if (updateTime > 0) { + long currentTime = SystemClock.elapsedRealtime(); + long position = + (long) (state.getPlaybackSpeed() * (currentTime - updateTime)) + state.getPosition(); + long duration = -1; + if (metadata != null && metadata.containsKey(MediaMetadataCompat.METADATA_KEY_DURATION)) { + duration = metadata.getLong(MediaMetadataCompat.METADATA_KEY_DURATION); + } + + if (duration >= 0 && position > duration) { + position = duration; + } else if (position < 0) { + position = 0; + } + return new PlaybackStateCompat.Builder(state) + .setState(state.getState(), position, state.getPlaybackSpeed(), currentTime) + .build(); + } + } + return state; + } + + /** + * Receives transport controls, media buttons, and commands from controllers and the system. The + * callback may be set using {@link #setCallback}. + * + *

Don't reuse the callback among the sessions. Callbacks keep internal reference to the + * session when it's set, so it may misbehave. + */ + public abstract static class Callback { + final Object mLock = new Object(); + @Nullable final MediaSession.Callback mCallbackFwk; + private boolean mMediaPlayPausePendingOnHandler; + + @GuardedBy("mLock") + WeakReference mSessionImpl; + + @Nullable + @GuardedBy("mLock") + @SuppressWarnings("WeakerAccess") /* synthetic access */ + CallbackHandler mCallbackHandler; + + public Callback() { + if (android.os.Build.VERSION.SDK_INT >= 21) { + mCallbackFwk = new MediaSessionCallbackApi21(); + } else { + mCallbackFwk = null; + } + mSessionImpl = new WeakReference<>(null); + } + + void setSessionImpl(@Nullable MediaSessionImpl impl, @Nullable Handler handler) { + synchronized (mLock) { + mSessionImpl = new WeakReference(impl); + if (mCallbackHandler != null) { + mCallbackHandler.removeCallbacksAndMessages(null); + } + mCallbackHandler = + impl == null || handler == null ? null : new CallbackHandler(handler.getLooper()); + } + } + + /** + * Called when a controller has sent a custom command to this session. The owner of the session + * may handle custom commands but is not required to. + * + * @param command The command name. + * @param extras Optional parameters for the command, may be null. + * @param cb A result receiver to which a result may be sent by the command, may be null. + */ + public void onCommand(String command, @Nullable Bundle extras, @Nullable ResultReceiver cb) {} + + /** + * Override to handle media button events. + * + *

The double tap of {@link KeyEvent#KEYCODE_MEDIA_PLAY_PAUSE} or {@link + * KeyEvent#KEYCODE_HEADSETHOOK} will call the {@link #onSkipToNext} by default. If the current + * SDK level is 27 or higher, the default double tap handling is done by framework so this + * method would do nothing for it. + * + * @param mediaButtonEvent The media button event intent. + * @return True if the event was handled, false otherwise. + */ + @SuppressWarnings("deprecation") + public boolean onMediaButtonEvent(Intent mediaButtonEvent) { + if (android.os.Build.VERSION.SDK_INT >= 27) { + // Double tap of play/pause as skipping to next is already handled by framework, + // so we don't need to repeat again here. + // Note: Double tap would be handled twice for OC-DR1 whose SDK version 26 and + // framework handles the double tap. + return false; + } + MediaSessionImpl impl; + Handler callbackHandler; + synchronized (mLock) { + impl = mSessionImpl.get(); + callbackHandler = mCallbackHandler; + } + if (impl == null || callbackHandler == null) { + return false; + } + KeyEvent keyEvent = mediaButtonEvent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); + if (keyEvent == null || keyEvent.getAction() != KeyEvent.ACTION_DOWN) { + return false; + } + RemoteUserInfo remoteUserInfo = impl.getCurrentControllerInfo(); + int keyCode = keyEvent.getKeyCode(); + switch (keyCode) { + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + case KeyEvent.KEYCODE_HEADSETHOOK: + if (keyEvent.getRepeatCount() == 0) { + if (mMediaPlayPausePendingOnHandler) { + callbackHandler.removeMessages( + CallbackHandler.MSG_MEDIA_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT); + mMediaPlayPausePendingOnHandler = false; + PlaybackStateCompat state = impl.getPlaybackState(); + long validActions = state == null ? 0 : state.getActions(); + // Consider double tap as the next. + if ((validActions & PlaybackStateCompat.ACTION_SKIP_TO_NEXT) != 0) { + onSkipToNext(); + } + } else { + mMediaPlayPausePendingOnHandler = true; + callbackHandler.sendMessageDelayed( + callbackHandler.obtainMessage( + CallbackHandler.MSG_MEDIA_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT, remoteUserInfo), + ViewConfiguration.getDoubleTapTimeout()); + } + } else { + // Consider long-press as a single tap. + handleMediaPlayPauseIfPendingOnHandler(impl, callbackHandler); + } + return true; + default: + // If another key is pressed within double tap timeout, consider the pending + // pending play/pause as a single tap to handle media keys in order. + handleMediaPlayPauseIfPendingOnHandler(impl, callbackHandler); + break; + } + return false; + } + + void handleMediaPlayPauseIfPendingOnHandler(MediaSessionImpl impl, Handler callbackHandler) { + if (!mMediaPlayPausePendingOnHandler) { + return; + } + mMediaPlayPausePendingOnHandler = false; + callbackHandler.removeMessages(CallbackHandler.MSG_MEDIA_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT); + PlaybackStateCompat state = impl.getPlaybackState(); + long validActions = state == null ? 0 : state.getActions(); + boolean isPlaying = state != null && state.getState() == PlaybackStateCompat.STATE_PLAYING; + boolean canPlay = + (validActions & (PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PLAY)) + != 0; + boolean canPause = + (validActions + & (PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_PAUSE)) + != 0; + if (isPlaying && canPause) { + onPause(); + } else if (!isPlaying && canPlay) { + onPlay(); + } + } + + /** + * Override to handle requests to prepare playback. Override {@link #onPlay} to handle requests + * for starting playback. + */ + public void onPrepare() {} + + /** + * Override to handle requests to prepare for playing a specific mediaId that was provided by + * your app. Override {@link #onPlayFromMediaId} to handle requests for starting playback. + */ + public void onPrepareFromMediaId(@Nullable String mediaId, @Nullable Bundle extras) {} + + /** + * Override to handle requests to prepare playback from a search query. An empty query indicates + * that the app may prepare any music. The implementation should attempt to make a smart choice + * about what to play. Override {@link #onPlayFromSearch} to handle requests for starting + * playback. + */ + public void onPrepareFromSearch(@Nullable String query, @Nullable Bundle extras) {} + + /** + * Override to handle requests to prepare a specific media item represented by a URI. Override + * {@link #onPlayFromUri} to handle requests for starting playback. + */ + public void onPrepareFromUri(@Nullable Uri uri, @Nullable Bundle extras) {} + + /** Override to handle requests to begin playback. */ + public void onPlay() {} + + /** Override to handle requests to play a specific mediaId that was provided by your app. */ + public void onPlayFromMediaId(@Nullable String mediaId, @Nullable Bundle extras) {} + + /** + * Override to handle requests to begin playback from a search query. An empty query indicates + * that the app may play any music. The implementation should attempt to make a smart choice + * about what to play. + */ + public void onPlayFromSearch(@Nullable String query, @Nullable Bundle extras) {} + + /** Override to handle requests to play a specific media item represented by a URI. */ + public void onPlayFromUri(@Nullable Uri uri, @Nullable Bundle extras) {} + + /** Override to handle requests to play an item with a given id from the play queue. */ + public void onSkipToQueueItem(long id) {} + + /** Override to handle requests to pause playback. */ + public void onPause() {} + + /** Override to handle requests to skip to the next media item. */ + public void onSkipToNext() {} + + /** Override to handle requests to skip to the previous media item. */ + public void onSkipToPrevious() {} + + /** Override to handle requests to fast forward. */ + public void onFastForward() {} + + /** Override to handle requests to rewind. */ + public void onRewind() {} + + /** Override to handle requests to stop playback. */ + public void onStop() {} + + /** + * Override to handle requests to seek to a specific position in ms. + * + * @param pos New position to move to, in milliseconds. + */ + public void onSeekTo(long pos) {} + + /** + * Override to handle the item being rated. + * + * @param rating The rating being set. + */ + public void onSetRating(@Nullable RatingCompat rating) {} + + /** + * Override to handle the item being rated. + * + * @param rating The rating being set. + * @param extras The extras can include information about the media item being rated. + */ + public void onSetRating(@Nullable RatingCompat rating, @Nullable Bundle extras) {} + + /** + * Override to handle the playback speed change. To update the new playback speed, create a new + * {@link PlaybackStateCompat} by using {@link PlaybackStateCompat.Builder#setState(int, long, + * float)}, and set it with {@link #setPlaybackState(PlaybackStateCompat)}. + * + *

A value of {@code 1.0f} is the default playback value, and a negative value indicates + * reverse playback. The {@code speed} will not be equal to zero. + * + * @param speed the playback speed + * @see #setPlaybackState(PlaybackStateCompat) + * @see PlaybackStateCompat.Builder#setState(int, long, float) + */ + public void onSetPlaybackSpeed(float speed) {} + + /** + * Override to handle requests to enable/disable captioning. + * + * @param enabled {@code true} to enable captioning, {@code false} to disable. + */ + public void onSetCaptioningEnabled(boolean enabled) {} + + /** + * Override to handle the setting of the repeat mode. + * + *

You should call {@link #setRepeatMode} before end of this method in order to notify the + * change to the {@link MediaControllerCompat}, or {@link MediaControllerCompat#getRepeatMode} + * could return an invalid value. + * + * @param repeatMode The repeat mode which is one of followings: {@link + * PlaybackStateCompat#REPEAT_MODE_NONE}, {@link PlaybackStateCompat#REPEAT_MODE_ONE}, + * {@link PlaybackStateCompat#REPEAT_MODE_ALL}, {@link + * PlaybackStateCompat#REPEAT_MODE_GROUP} + */ + public void onSetRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) {} + + /** + * Override to handle the setting of the shuffle mode. + * + *

You should call {@link #setShuffleMode} before the end of this method in order to notify + * the change to the {@link MediaControllerCompat}, or {@link + * MediaControllerCompat#getShuffleMode} could return an invalid value. + * + * @param shuffleMode The shuffle mode which is one of followings: {@link + * PlaybackStateCompat#SHUFFLE_MODE_NONE}, {@link PlaybackStateCompat#SHUFFLE_MODE_ALL}, + * {@link PlaybackStateCompat#SHUFFLE_MODE_GROUP} + */ + public void onSetShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode) {} + + /** + * Called when a {@link MediaControllerCompat} wants a {@link PlaybackStateCompat.CustomAction} + * to be performed. + * + * @param action The action that was originally sent in the {@link + * PlaybackStateCompat.CustomAction}. + * @param extras Optional extras specified by the {@link MediaControllerCompat}. + * @see #ACTION_FLAG_AS_INAPPROPRIATE + * @see #ACTION_SKIP_AD + * @see #ACTION_FOLLOW + * @see #ACTION_UNFOLLOW + */ + public void onCustomAction(String action, @Nullable Bundle extras) {} + + /** + * Called when a {@link MediaControllerCompat} wants to add a {@link QueueItem} with the given + * {@link MediaDescriptionCompat description} at the end of the play queue. + * + * @param description The {@link MediaDescriptionCompat} for creating the {@link QueueItem} to + * be inserted. + */ + public void onAddQueueItem(@Nullable MediaDescriptionCompat description) {} + + /** + * Called when a {@link MediaControllerCompat} wants to add a {@link QueueItem} with the given + * {@link MediaDescriptionCompat description} at the specified position in the play queue. + * + * @param description The {@link MediaDescriptionCompat} for creating the {@link QueueItem} to + * be inserted. + * @param index The index at which the created {@link QueueItem} is to be inserted. + */ + public void onAddQueueItem(@Nullable MediaDescriptionCompat description, int index) {} + + /** + * Called when a {@link MediaControllerCompat} wants to remove the first occurrence of the + * specified {@link QueueItem} with the given {@link MediaDescriptionCompat description} in the + * play queue. + * + * @param description The {@link MediaDescriptionCompat} for denoting the {@link QueueItem} to + * be removed. + */ + public void onRemoveQueueItem(@Nullable MediaDescriptionCompat description) {} + + /** + * Called when a {@link MediaControllerCompat} wants to remove a {@link QueueItem} at the + * specified position in the play queue. + * + * @param index The index of the element to be removed. + * @deprecated {@link #onRemoveQueueItem} will be called instead. + */ + @Deprecated + public void onRemoveQueueItemAt(int index) {} + + private class CallbackHandler extends Handler { + private static final int MSG_MEDIA_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT = 1; + + CallbackHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + if (msg.what == MSG_MEDIA_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT) { + // Here we manually set the caller info, since this is not directly called from + // the session callback. This is triggered by timeout. + MediaSessionImpl impl; + Handler callbackHandler; + synchronized (mLock) { + impl = mSessionImpl.get(); + callbackHandler = mCallbackHandler; + } + if (impl == null + || MediaSessionCompat.Callback.this != impl.getCallback() + || callbackHandler == null) { + return; + } + RemoteUserInfo info = (RemoteUserInfo) msg.obj; + impl.setCurrentControllerInfo(info); + handleMediaPlayPauseIfPendingOnHandler(impl, callbackHandler); + impl.setCurrentControllerInfo(null); + } + } + } + + @RequiresApi(21) + private class MediaSessionCallbackApi21 extends MediaSession.Callback { + MediaSessionCallbackApi21() {} + + @Override + @SuppressWarnings("deprecation") + public void onCommand(String command, @Nullable Bundle extras, @Nullable ResultReceiver cb) { + MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); + if (sessionImpl == null) { + return; + } + ensureClassLoader(extras); + setCurrentControllerInfo(sessionImpl); + try { + if (command.equals(MediaControllerCompat.COMMAND_GET_EXTRA_BINDER)) { + if (cb != null) { + Bundle result = new Bundle(); + Token token = sessionImpl.getSessionToken(); + IMediaSession extraBinder = token.getExtraBinder(); + result.putBinder( + KEY_EXTRA_BINDER, extraBinder == null ? null : extraBinder.asBinder()); + ParcelUtils.putVersionedParcelable( + result, KEY_SESSION2_TOKEN, token.getSession2Token()); + cb.send(0, result); + } + } else if (command.equals(MediaControllerCompat.COMMAND_ADD_QUEUE_ITEM)) { + if (extras != null) { + Callback.this.onAddQueueItem( + LegacyParcelableUtil.convert( + extras.getParcelable( + MediaControllerCompat.COMMAND_ARGUMENT_MEDIA_DESCRIPTION), + MediaDescriptionCompat.CREATOR)); + } + } else if (command.equals(MediaControllerCompat.COMMAND_ADD_QUEUE_ITEM_AT)) { + if (extras != null) { + Callback.this.onAddQueueItem( + LegacyParcelableUtil.convert( + extras.getParcelable( + MediaControllerCompat.COMMAND_ARGUMENT_MEDIA_DESCRIPTION), + MediaDescriptionCompat.CREATOR), + extras.getInt(MediaControllerCompat.COMMAND_ARGUMENT_INDEX)); + } + } else if (command.equals(MediaControllerCompat.COMMAND_REMOVE_QUEUE_ITEM)) { + if (extras != null) { + Callback.this.onRemoveQueueItem( + LegacyParcelableUtil.convert( + extras.getParcelable( + MediaControllerCompat.COMMAND_ARGUMENT_MEDIA_DESCRIPTION), + MediaDescriptionCompat.CREATOR)); + } + } else if (command.equals(MediaControllerCompat.COMMAND_REMOVE_QUEUE_ITEM_AT)) { + List queue = sessionImpl.mQueue; + if (queue != null && extras != null) { + int index = extras.getInt(MediaControllerCompat.COMMAND_ARGUMENT_INDEX, -1); + QueueItem item = (index >= 0 && index < queue.size()) ? queue.get(index) : null; + if (item != null) { + Callback.this.onRemoveQueueItem(item.getDescription()); + } + } + } else { + Callback.this.onCommand(command, extras, cb); + } + } catch (BadParcelableException e) { + // Do not print the exception here, since it is already done by the Parcel + // class. + Log.e(TAG, "Could not unparcel the extra data."); + } + clearCurrentControllerInfo(sessionImpl); + } + + @Override + public boolean onMediaButtonEvent(Intent mediaButtonIntent) { + MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); + if (sessionImpl == null) { + return false; + } + setCurrentControllerInfo(sessionImpl); + boolean result = Callback.this.onMediaButtonEvent(mediaButtonIntent); + clearCurrentControllerInfo(sessionImpl); + return result || super.onMediaButtonEvent(mediaButtonIntent); + } + + @Override + public void onPlay() { + MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); + if (sessionImpl == null) { + return; + } + setCurrentControllerInfo(sessionImpl); + Callback.this.onPlay(); + clearCurrentControllerInfo(sessionImpl); + } + + @Override + public void onPlayFromMediaId(String mediaId, @Nullable Bundle extras) { + MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); + if (sessionImpl == null) { + return; + } + ensureClassLoader(extras); + setCurrentControllerInfo(sessionImpl); + Callback.this.onPlayFromMediaId(mediaId, extras); + clearCurrentControllerInfo(sessionImpl); + } + + @Override + public void onPlayFromSearch(String search, @Nullable Bundle extras) { + MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); + if (sessionImpl == null) { + return; + } + ensureClassLoader(extras); + setCurrentControllerInfo(sessionImpl); + Callback.this.onPlayFromSearch(search, extras); + clearCurrentControllerInfo(sessionImpl); + } + + @RequiresApi(23) + @Override + public void onPlayFromUri(Uri uri, @Nullable Bundle extras) { + MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); + if (sessionImpl == null) { + return; + } + ensureClassLoader(extras); + setCurrentControllerInfo(sessionImpl); + Callback.this.onPlayFromUri(uri, extras); + clearCurrentControllerInfo(sessionImpl); + } + + @Override + public void onSkipToQueueItem(long id) { + MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); + if (sessionImpl == null) { + return; + } + setCurrentControllerInfo(sessionImpl); + Callback.this.onSkipToQueueItem(id); + clearCurrentControllerInfo(sessionImpl); + } + + @Override + public void onPause() { + MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); + if (sessionImpl == null) { + return; + } + setCurrentControllerInfo(sessionImpl); + Callback.this.onPause(); + clearCurrentControllerInfo(sessionImpl); + } + + @Override + public void onSkipToNext() { + MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); + if (sessionImpl == null) { + return; + } + setCurrentControllerInfo(sessionImpl); + Callback.this.onSkipToNext(); + clearCurrentControllerInfo(sessionImpl); + } + + @Override + public void onSkipToPrevious() { + MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); + if (sessionImpl == null) { + return; + } + setCurrentControllerInfo(sessionImpl); + Callback.this.onSkipToPrevious(); + clearCurrentControllerInfo(sessionImpl); + } + + @Override + public void onFastForward() { + MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); + if (sessionImpl == null) { + return; + } + setCurrentControllerInfo(sessionImpl); + Callback.this.onFastForward(); + clearCurrentControllerInfo(sessionImpl); + } + + @Override + public void onRewind() { + MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); + if (sessionImpl == null) { + return; + } + setCurrentControllerInfo(sessionImpl); + Callback.this.onRewind(); + clearCurrentControllerInfo(sessionImpl); + } + + @Override + public void onStop() { + MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); + if (sessionImpl == null) { + return; + } + setCurrentControllerInfo(sessionImpl); + Callback.this.onStop(); + clearCurrentControllerInfo(sessionImpl); + } + + @Override + public void onSeekTo(long pos) { + MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); + if (sessionImpl == null) { + return; + } + setCurrentControllerInfo(sessionImpl); + Callback.this.onSeekTo(pos); + clearCurrentControllerInfo(sessionImpl); + } + + @Override + public void onSetRating(Rating ratingFwk) { + MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); + if (sessionImpl == null) { + return; + } + setCurrentControllerInfo(sessionImpl); + Callback.this.onSetRating(RatingCompat.fromRating(ratingFwk)); + clearCurrentControllerInfo(sessionImpl); + } + + @Override + @SuppressWarnings("deprecation") + public void onCustomAction(String action, @Nullable Bundle extras) { + MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); + if (sessionImpl == null) { + return; + } + ensureClassLoader(extras); + setCurrentControllerInfo(sessionImpl); + + try { + if (action.equals(ACTION_PLAY_FROM_URI)) { + if (extras != null) { + Uri uri = extras.getParcelable(ACTION_ARGUMENT_URI); + Bundle bundle = extras.getBundle(ACTION_ARGUMENT_EXTRAS); + ensureClassLoader(bundle); + Callback.this.onPlayFromUri(uri, bundle); + } + } else if (action.equals(ACTION_PREPARE)) { + Callback.this.onPrepare(); + } else if (action.equals(ACTION_PREPARE_FROM_MEDIA_ID)) { + if (extras != null) { + String mediaId = extras.getString(ACTION_ARGUMENT_MEDIA_ID); + Bundle bundle = extras.getBundle(ACTION_ARGUMENT_EXTRAS); + ensureClassLoader(bundle); + Callback.this.onPrepareFromMediaId(mediaId, bundle); + } + } else if (action.equals(ACTION_PREPARE_FROM_SEARCH)) { + if (extras != null) { + String query = extras.getString(ACTION_ARGUMENT_QUERY); + Bundle bundle = extras.getBundle(ACTION_ARGUMENT_EXTRAS); + ensureClassLoader(bundle); + Callback.this.onPrepareFromSearch(query, bundle); + } + } else if (action.equals(ACTION_PREPARE_FROM_URI)) { + if (extras != null) { + Uri uri = extras.getParcelable(ACTION_ARGUMENT_URI); + Bundle bundle = extras.getBundle(ACTION_ARGUMENT_EXTRAS); + ensureClassLoader(bundle); + Callback.this.onPrepareFromUri(uri, bundle); + } + } else if (action.equals(ACTION_SET_CAPTIONING_ENABLED)) { + if (extras != null) { + boolean enabled = extras.getBoolean(ACTION_ARGUMENT_CAPTIONING_ENABLED); + Callback.this.onSetCaptioningEnabled(enabled); + } + } else if (action.equals(ACTION_SET_REPEAT_MODE)) { + if (extras != null) { + int repeatMode = extras.getInt(ACTION_ARGUMENT_REPEAT_MODE); + Callback.this.onSetRepeatMode(repeatMode); + } + } else if (action.equals(ACTION_SET_SHUFFLE_MODE)) { + if (extras != null) { + int shuffleMode = extras.getInt(ACTION_ARGUMENT_SHUFFLE_MODE); + Callback.this.onSetShuffleMode(shuffleMode); + } + } else if (action.equals(ACTION_SET_RATING)) { + if (extras != null) { + RatingCompat rating = + LegacyParcelableUtil.convert( + extras.getParcelable(ACTION_ARGUMENT_RATING), RatingCompat.CREATOR); + Bundle bundle = extras.getBundle(ACTION_ARGUMENT_EXTRAS); + ensureClassLoader(bundle); + Callback.this.onSetRating(rating, bundle); + } + } else if (action.equals(ACTION_SET_PLAYBACK_SPEED)) { + if (extras != null) { + float speed = extras.getFloat(ACTION_ARGUMENT_PLAYBACK_SPEED, 1.0f); + Callback.this.onSetPlaybackSpeed(speed); + } + } else { + Callback.this.onCustomAction(action, extras); + } + } catch (BadParcelableException e) { + // The exception details will be logged by Parcel class. + Log.e(TAG, "Could not unparcel the data."); + } + clearCurrentControllerInfo(sessionImpl); + } + + @RequiresApi(24) + @Override + public void onPrepare() { + MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); + if (sessionImpl == null) { + return; + } + setCurrentControllerInfo(sessionImpl); + Callback.this.onPrepare(); + clearCurrentControllerInfo(sessionImpl); + } + + @RequiresApi(24) + @Override + public void onPrepareFromMediaId(@Nullable String mediaId, @Nullable Bundle extras) { + MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); + if (sessionImpl == null) { + return; + } + ensureClassLoader(extras); + setCurrentControllerInfo(sessionImpl); + Callback.this.onPrepareFromMediaId(mediaId, extras); + clearCurrentControllerInfo(sessionImpl); + } + + @RequiresApi(24) + @Override + public void onPrepareFromSearch(@Nullable String query, @Nullable Bundle extras) { + MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); + if (sessionImpl == null) { + return; + } + ensureClassLoader(extras); + setCurrentControllerInfo(sessionImpl); + Callback.this.onPrepareFromSearch(query, extras); + clearCurrentControllerInfo(sessionImpl); + } + + @RequiresApi(24) + @Override + public void onPrepareFromUri(@Nullable Uri uri, @Nullable Bundle extras) { + MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); + if (sessionImpl == null) { + return; + } + ensureClassLoader(extras); + setCurrentControllerInfo(sessionImpl); + Callback.this.onPrepareFromUri(uri, extras); + clearCurrentControllerInfo(sessionImpl); + } + + @RequiresApi(29) + @Override + public void onSetPlaybackSpeed(float speed) { + MediaSessionImplApi21 sessionImpl = getSessionImplIfCallbackIsSet(); + if (sessionImpl == null) { + return; + } + setCurrentControllerInfo(sessionImpl); + Callback.this.onSetPlaybackSpeed(speed); + clearCurrentControllerInfo(sessionImpl); + } + + private void setCurrentControllerInfo(MediaSessionImpl sessionImpl) { + if (Build.VERSION.SDK_INT >= 28) { + // From API 28, this method has no effect since + // MediaSessionImplApi28#getCurrentControllerInfo() returns controller info from + // framework. + return; + } + String packageName = sessionImpl.getCallingPackage(); + if (TextUtils.isEmpty(packageName)) { + packageName = LEGACY_CONTROLLER; + } + sessionImpl.setCurrentControllerInfo( + new RemoteUserInfo(packageName, UNKNOWN_PID, UNKNOWN_UID)); + } + + private void clearCurrentControllerInfo(MediaSessionImpl sessionImpl) { + sessionImpl.setCurrentControllerInfo(null); + } + + // Returns the MediaSessionImplApi21 if this callback is still set by the session. + // This prevent callback methods to be called after session is release() or + // callback is changed. + @Nullable + private MediaSessionImplApi21 getSessionImplIfCallbackIsSet() { + MediaSessionImplApi21 sessionImpl; + synchronized (mLock) { + sessionImpl = (MediaSessionImplApi21) mSessionImpl.get(); + } + return sessionImpl != null && MediaSessionCompat.Callback.this == sessionImpl.getCallback() + ? sessionImpl + : null; + } + } + } + + /** Callback to be called when a controller has registered or unregistered controller callback. */ + public interface RegistrationCallback { + /** + * Called when a {@link MediaControllerCompat} registered callback. + * + * @param callingPid PID from Binder#getCallingPid() + * @param callingUid UID from Binder#getCallingUid() + */ + void onCallbackRegistered(int callingPid, int callingUid); + + /** + * Called when a {@link MediaControllerCompat} unregistered callback. + * + * @param callingPid PID from Binder#getCallingPid() + * @param callingUid UID from Binder#getCallingUid() + */ + void onCallbackUnregistered(int callingPid, int callingUid); + } + + /** + * Represents an ongoing session. This may be passed to apps by the session owner to allow them to + * create a {@link MediaControllerCompat} to communicate with the session. + */ + @SuppressLint("BanParcelableUsage") + public static final class Token implements Parcelable { + private final Object mLock = new Object(); + private final Object mInner; + + @Nullable + @GuardedBy("mLock") + private IMediaSession mExtraBinder; + + @Nullable + @GuardedBy("mLock") + private VersionedParcelable mSession2Token; + + Token(Object inner) { + this(inner, null, null); + } + + Token(Object inner, @Nullable IMediaSession extraBinder) { + this(inner, extraBinder, null); + } + + Token( + Object inner, + @Nullable IMediaSession extraBinder, + @Nullable VersionedParcelable session2Token) { + mInner = inner; + mExtraBinder = extraBinder; + mSession2Token = session2Token; + } + + /** + * Creates a compat Token from a framework {@link android.media.session.MediaSession.Token} + * object. + * + *

This method is only supported on {@link android.os.Build.VERSION_CODES#LOLLIPOP} and + * later. + * + * @param token The framework token object. + * @return A compat Token for use with {@link MediaControllerCompat}. + */ + @RequiresApi(21) + public static Token fromToken(Object token) { + return fromToken(token, null); + } + + /** + * Creates a compat Token from a framework {@link android.media.session.MediaSession.Token} + * object, and the extra binder. + * + *

This method is only supported on {@link android.os.Build.VERSION_CODES#LOLLIPOP} and + * later. + * + * @param token The framework token object. + * @param extraBinder The extra binder. + * @return A compat Token for use with {@link MediaControllerCompat}. + */ + @RequiresApi(21) + /* package */ static Token fromToken(Object token, @Nullable IMediaSession extraBinder) { + checkState(token != null); + if (!(token instanceof MediaSession.Token)) { + throw new IllegalArgumentException("token is not a valid MediaSession.Token object"); + } + return new Token(token, extraBinder); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + if (android.os.Build.VERSION.SDK_INT >= 21) { + dest.writeParcelable((Parcelable) mInner, flags); + } else { + dest.writeStrongBinder((IBinder) mInner); + } + } + + @Override + public int hashCode() { + if (mInner == null) { + return 0; + } + return mInner.hashCode(); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Token)) { + return false; + } + + Token other = (Token) obj; + if (mInner == null) { + return other.mInner == null; + } + if (other.mInner == null) { + return false; + } + return mInner.equals(other.mInner); + } + + /** + * Gets the underlying framework {@link android.media.session.MediaSession.Token} object. + * + *

This method is only supported on API 21+. + * + * @return The underlying {@link android.media.session.MediaSession.Token} object, or null if + * none. + */ + public Object getToken() { + return mInner; + } + + @Nullable + /* package */ IMediaSession getExtraBinder() { + synchronized (mLock) { + return mExtraBinder; + } + } + + /* package */ void setExtraBinder(@Nullable IMediaSession extraBinder) { + synchronized (mLock) { + mExtraBinder = extraBinder; + } + } + + /** */ + @Nullable + public VersionedParcelable getSession2Token() { + synchronized (mLock) { + return mSession2Token; + } + } + + /** */ + public void setSession2Token(@Nullable VersionedParcelable session2Token) { + synchronized (mLock) { + mSession2Token = session2Token; + } + } + + /** */ + public Bundle toBundle() { + Bundle bundle = new Bundle(); + bundle.putParcelable( + KEY_TOKEN, + LegacyParcelableUtil.convert( + this, android.support.v4.media.session.MediaSessionCompat.Token.CREATOR)); + synchronized (mLock) { + if (mExtraBinder != null) { + bundle.putBinder(KEY_EXTRA_BINDER, mExtraBinder.asBinder()); + } + if (mSession2Token != null) { + ParcelUtils.putVersionedParcelable(bundle, KEY_SESSION2_TOKEN, mSession2Token); + } + } + return bundle; + } + + /** + * Creates a compat Token from a bundle object. + * + * @param tokenBundle + * @return A compat Token for use with {@link MediaControllerCompat}. + */ + @SuppressWarnings("deprecation") + @Nullable + public static Token fromBundle(@Nullable Bundle tokenBundle) { + if (tokenBundle == null) { + return null; + } + ensureClassLoader(tokenBundle); + IMediaSession extraSession = + IMediaSession.Stub.asInterface(tokenBundle.getBinder(KEY_EXTRA_BINDER)); + VersionedParcelable session2Token = + ParcelUtils.getVersionedParcelable(tokenBundle, KEY_SESSION2_TOKEN); + Token token = LegacyParcelableUtil.convert(tokenBundle.getParcelable(KEY_TOKEN), CREATOR); + return token == null ? null : new Token(token.mInner, extraSession, session2Token); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @SuppressWarnings("deprecation") + @Override + public Token createFromParcel(Parcel in) { + Object inner; + if (android.os.Build.VERSION.SDK_INT >= 21) { + inner = in.readParcelable(null); + } else { + inner = in.readStrongBinder(); + } + return new Token(checkNotNull(inner)); + } + + @Override + public Token[] newArray(int size) { + return new Token[size]; + } + }; + } + + /** + * A single item that is part of the play queue. It contains a description of the item and its id + * in the queue. + */ + @SuppressLint("BanParcelableUsage") + public static final class QueueItem implements Parcelable { + /** This id is reserved. No items can be explicitly assigned this id. */ + public static final int UNKNOWN_ID = -1; + + private final MediaDescriptionCompat mDescription; + private final long mId; + + @Nullable private MediaSession.QueueItem mItemFwk; + + /** + * Creates a new {@link MediaSessionCompat.QueueItem}. + * + * @param description The {@link MediaDescriptionCompat} for this item. + * @param id An identifier for this item. It must be unique within the play queue and cannot be + * {@link #UNKNOWN_ID}. + */ + public QueueItem(MediaDescriptionCompat description, long id) { + this(null, description, id); + } + + private QueueItem( + @Nullable MediaSession.QueueItem queueItem, + @Nullable MediaDescriptionCompat description, + long id) { + if (description == null) { + throw new IllegalArgumentException("Description cannot be null"); + } + if (id == UNKNOWN_ID) { + throw new IllegalArgumentException("Id cannot be QueueItem.UNKNOWN_ID"); + } + mDescription = description; + mId = id; + mItemFwk = queueItem; + } + + QueueItem(Parcel in) { + mDescription = MediaDescriptionCompat.CREATOR.createFromParcel(in); + mId = in.readLong(); + } + + /** Gets the description for this item. */ + public MediaDescriptionCompat getDescription() { + return mDescription; + } + + /** Gets the queue id for this item. */ + public long getQueueId() { + return mId; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + mDescription.writeToParcel(dest, flags); + dest.writeLong(mId); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * Gets the underlying {@link android.media.session.MediaSession.QueueItem}. + * + *

On builds before {@link android.os.Build.VERSION_CODES#LOLLIPOP} null is returned. + * + * @return The underlying {@link android.media.session.MediaSession.QueueItem} or null. + */ + @Nullable + public Object getQueueItem() { + if (mItemFwk != null || android.os.Build.VERSION.SDK_INT < 21) { + return mItemFwk; + } + mItemFwk = + Api21Impl.createQueueItem((MediaDescription) mDescription.getMediaDescription(), mId); + return mItemFwk; + } + + /** + * Creates an instance from a framework {@link android.media.session.MediaSession.QueueItem} + * object. + * + *

This method is only supported on API 21+. + * + * @param queueItem A {@link android.media.session.MediaSession.QueueItem} object. + * @return An equivalent {@link QueueItem} object. + */ + @RequiresApi(21) + public static QueueItem fromQueueItem(Object queueItem) { + MediaSession.QueueItem queueItemObj = (MediaSession.QueueItem) queueItem; + Object descriptionObj = Api21Impl.getDescription(queueItemObj); + MediaDescriptionCompat description = + MediaDescriptionCompat.fromMediaDescription(descriptionObj); + long id = Api21Impl.getQueueId(queueItemObj); + return new QueueItem(queueItemObj, description, id); + } + + /** + * Creates a list of {@link QueueItem} objects from a framework {@link + * android.media.session.MediaSession.QueueItem} object list. + * + *

This method is only supported on API 21+. On API 20 and below, it returns null. + * + * @param itemList A list of {@link android.media.session.MediaSession.QueueItem} objects. + * @return An equivalent list of {@link QueueItem} objects, or null if none. + */ + @Nullable + public static List fromQueueItemList( + @Nullable List itemList) { + if (itemList == null || Build.VERSION.SDK_INT < 21) { + return null; + } + List items = new ArrayList<>(itemList.size()); + for (Object itemObj : itemList) { + items.add(fromQueueItem(itemObj)); + } + return items; + } + + public static final Creator CREATOR = + new Creator() { + + @Override + public MediaSessionCompat.QueueItem createFromParcel(Parcel p) { + return new MediaSessionCompat.QueueItem(p); + } + + @Override + public MediaSessionCompat.QueueItem[] newArray(int size) { + return new MediaSessionCompat.QueueItem[size]; + } + }; + + @Override + public String toString() { + return "MediaSession.QueueItem {" + "Description=" + mDescription + ", Id=" + mId + " }"; + } + + @RequiresApi(21) + private static class Api21Impl { + private Api21Impl() {} + + @DoNotInline + static MediaSession.QueueItem createQueueItem(MediaDescription description, long id) { + return new MediaSession.QueueItem(description, id); + } + + @DoNotInline + static MediaDescription getDescription(MediaSession.QueueItem queueItem) { + return queueItem.getDescription(); + } + + @DoNotInline + static long getQueueId(MediaSession.QueueItem queueItem) { + return queueItem.getQueueId(); + } + } + } + + /** + * This is a wrapper for {@link ResultReceiver} for sending over aidl interfaces. The framework + * version was not exposed to aidls until {@link android.os.Build.VERSION_CODES#LOLLIPOP}. + */ + @SuppressLint("BanParcelableUsage") + /* package */ static final class ResultReceiverWrapper implements Parcelable { + ResultReceiver mResultReceiver; + + public ResultReceiverWrapper(ResultReceiver resultReceiver) { + mResultReceiver = resultReceiver; + } + + ResultReceiverWrapper(Parcel in) { + mResultReceiver = ResultReceiver.CREATOR.createFromParcel(in); + } + + public static final Creator CREATOR = + new Creator() { + @Override + public ResultReceiverWrapper createFromParcel(Parcel p) { + return new ResultReceiverWrapper(p); + } + + @Override + public ResultReceiverWrapper[] newArray(int size) { + return new ResultReceiverWrapper[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + mResultReceiver.writeToParcel(dest, flags); + } + } + + public interface OnActiveChangeListener { + void onActiveChanged(); + } + + interface MediaSessionImpl { + void setCallback(@Nullable Callback callback, @Nullable Handler handler); + + void setRegistrationCallback(@Nullable RegistrationCallback callback, Handler handler); + + void setFlags(@SessionFlags int flags); + + void setPlaybackToLocal(int stream); + + void setPlaybackToRemote(VolumeProviderCompat volumeProvider); + + void setActive(boolean active); + + boolean isActive(); + + void sendSessionEvent(String event, @Nullable Bundle extras); + + void release(); + + Token getSessionToken(); + + void setPlaybackState(PlaybackStateCompat state); + + @Nullable + PlaybackStateCompat getPlaybackState(); + + void setMetadata(@Nullable MediaMetadataCompat metadata); + + void setSessionActivity(PendingIntent pi); + + void setMediaButtonReceiver(@Nullable PendingIntent mbr); + + void setQueue(@Nullable List queue); + + void setQueueTitle(CharSequence title); + + void setRatingType(@RatingCompat.Style int type); + + void setCaptioningEnabled(boolean enabled); + + void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode); + + void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode); + + void setExtras(@Nullable Bundle extras); + + @Nullable + Object getMediaSession(); + + @Nullable + Object getRemoteControlClient(); + + @Nullable + String getCallingPackage(); + + @Nullable + RemoteUserInfo getCurrentControllerInfo(); + + void setCurrentControllerInfo(@Nullable RemoteUserInfo remoteUserInfo); + + @Nullable + Callback getCallback(); + } + + static class MediaSessionImplBase implements MediaSessionImpl { + /***** RemoteControlClient States, we only need none as the others were public *******/ + static final int RCC_PLAYSTATE_NONE = 0; + + private final Context mContext; + private final ComponentName mMediaButtonReceiverComponentName; + private final PendingIntent mMediaButtonReceiverIntent; + private final MediaSessionStub mStub; + private final Token mToken; + @Nullable final Bundle mSessionInfo; + final AudioManager mAudioManager; + final RemoteControlClient mRcc; + + final Object mLock = new Object(); + final RemoteCallbackList mControllerCallbacks = + new RemoteCallbackList<>(); + + @Nullable private MessageHandler mHandler; + boolean mDestroyed = false; + boolean mIsActive = false; + @Nullable volatile Callback mCallback; + @Nullable private RemoteUserInfo mRemoteUserInfo; + + @SuppressWarnings("WeakerAccess") /* synthetic access */ + @Nullable + RegistrationCallbackHandler mRegistrationCallbackHandler; + + // For backward compatibility, these flags are always set. + @SessionFlags int mFlags = FLAG_HANDLES_MEDIA_BUTTONS | FLAG_HANDLES_TRANSPORT_CONTROLS; + + @Nullable MediaMetadataCompat mMetadata; + @Nullable PlaybackStateCompat mState; + @Nullable PendingIntent mSessionActivity; + @Nullable List mQueue; + @Nullable CharSequence mQueueTitle; + @RatingCompat.Style int mRatingType; + boolean mCaptioningEnabled; + @PlaybackStateCompat.RepeatMode int mRepeatMode; + @PlaybackStateCompat.ShuffleMode int mShuffleMode; + @Nullable Bundle mExtras; + + int mVolumeType; + int mLocalStream; + @Nullable VolumeProviderCompat mVolumeProvider; + + private VolumeProviderCompat.Callback mVolumeCallback = + new VolumeProviderCompat.Callback() { + @SuppressWarnings("method.invocation.invalid") // referencing method from constructor + @Override + public void onVolumeChanged(VolumeProviderCompat volumeProvider) { + if (mVolumeProvider != volumeProvider) { + return; + } + ParcelableVolumeInfo info = + new ParcelableVolumeInfo( + mVolumeType, + mLocalStream, + volumeProvider.getVolumeControl(), + volumeProvider.getMaxVolume(), + volumeProvider.getCurrentVolume()); + sendVolumeInfoChanged(info); + } + }; + + @SuppressWarnings({ + "assignment.type.incompatible", + "argument.type.incompatible" + }) // Sharing this in constructor + public MediaSessionImplBase( + Context context, + String tag, + ComponentName mbrComponent, + @Nullable PendingIntent mbrIntent, + @Nullable VersionedParcelable session2Token, + @Nullable Bundle sessionInfo) { + if (mbrComponent == null) { + throw new IllegalArgumentException("MediaButtonReceiver component may not be null"); + } + mContext = context; + mSessionInfo = sessionInfo; + mAudioManager = (AudioManager) checkNotNull(context.getSystemService(Context.AUDIO_SERVICE)); + mMediaButtonReceiverComponentName = mbrComponent; + mMediaButtonReceiverIntent = mbrIntent; + mStub = new MediaSessionStub(/* mediaSessionImpl= */ this, context.getPackageName(), tag); + mToken = new Token(mStub, /* extraBinder= */ null, session2Token); + + mRatingType = RatingCompat.RATING_NONE; + mVolumeType = MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_LOCAL; + mLocalStream = AudioManager.STREAM_MUSIC; + mRcc = new RemoteControlClient(mbrIntent); + } + + @Override + public void setCallback(@Nullable Callback callback, @Nullable Handler handler) { + synchronized (mLock) { + if (mHandler != null) { + mHandler.removeCallbacksAndMessages(null); + } + mHandler = + callback == null || handler == null ? null : new MessageHandler(handler.getLooper()); + if (mCallback != callback && mCallback != null) { + mCallback.setSessionImpl(null, null); + } + mCallback = callback; + if (mCallback != null) { + mCallback.setSessionImpl(this, handler); + } + } + } + + @Override + public void setRegistrationCallback(@Nullable RegistrationCallback callback, Handler handler) { + synchronized (mLock) { + if (mRegistrationCallbackHandler != null) { + mRegistrationCallbackHandler.removeCallbacksAndMessages(null); + } + if (callback != null) { + mRegistrationCallbackHandler = + new RegistrationCallbackHandler(handler.getLooper(), callback); + } else { + mRegistrationCallbackHandler = null; + } + } + } + + void postToHandler( + int what, int arg1, int arg2, @Nullable Object obj, @Nullable Bundle extras) { + synchronized (mLock) { + if (mHandler != null) { + Message msg = mHandler.obtainMessage(what, arg1, arg2, obj); + Bundle data = new Bundle(); + + int uid = Binder.getCallingUid(); + data.putInt(DATA_CALLING_UID, uid); + // Note: Different apps can have same uid, but only when they are signed with + // the same private key. This means those apps are from the same developer. + // Session apps can allow/reject controller by reading one of their names. + data.putString(DATA_CALLING_PACKAGE, getPackageNameForUid(uid)); + int pid = Binder.getCallingPid(); + if (pid > 0) { + data.putInt(DATA_CALLING_PID, pid); + } else { + // This cannot be happen for now, but added for future changes. + data.putInt(DATA_CALLING_PID, UNKNOWN_PID); + } + if (extras != null) { + data.putBundle(DATA_EXTRAS, extras); + } + msg.setData(data); + msg.sendToTarget(); + } + } + } + + String getPackageNameForUid(int uid) { + String result = mContext.getPackageManager().getNameForUid(uid); + if (TextUtils.isEmpty(result)) { + result = LEGACY_CONTROLLER; + } + return result; + } + + @Override + public void setFlags(@SessionFlags int flags) { + synchronized (mLock) { + // For backward compatibility, these flags are always set. + mFlags = flags | FLAG_HANDLES_MEDIA_BUTTONS | FLAG_HANDLES_TRANSPORT_CONTROLS; + } + } + + @Override + public void setPlaybackToLocal(int stream) { + if (mVolumeProvider != null) { + mVolumeProvider.setCallback(null); + } + mLocalStream = stream; + mVolumeType = MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_LOCAL; + ParcelableVolumeInfo info = + new ParcelableVolumeInfo( + mVolumeType, + mLocalStream, + VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE, + mAudioManager.getStreamMaxVolume(mLocalStream), + mAudioManager.getStreamVolume(mLocalStream)); + sendVolumeInfoChanged(info); + } + + @Override + public void setPlaybackToRemote(VolumeProviderCompat volumeProvider) { + if (volumeProvider == null) { + throw new IllegalArgumentException("volumeProvider may not be null"); + } + if (mVolumeProvider != null) { + mVolumeProvider.setCallback(null); + } + mVolumeType = MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE; + mVolumeProvider = volumeProvider; + ParcelableVolumeInfo info = + new ParcelableVolumeInfo( + mVolumeType, + mLocalStream, + mVolumeProvider.getVolumeControl(), + mVolumeProvider.getMaxVolume(), + mVolumeProvider.getCurrentVolume()); + sendVolumeInfoChanged(info); + + volumeProvider.setCallback(mVolumeCallback); + } + + @Override + public void setActive(boolean active) { + if (active == mIsActive) { + return; + } + mIsActive = active; + updateMbrAndRcc(); + } + + @Override + public boolean isActive() { + return mIsActive; + } + + @Override + public void sendSessionEvent(String event, @Nullable Bundle extras) { + sendEvent(event, extras); + } + + @Override + public void release() { + mIsActive = false; + mDestroyed = true; + updateMbrAndRcc(); + sendSessionDestroyed(); + setCallback(null, null); + } + + @Override + public Token getSessionToken() { + return mToken; + } + + @Override + public void setPlaybackState(@Nullable PlaybackStateCompat state) { + synchronized (mLock) { + mState = state; + } + sendState(state); + if (!mIsActive) { + // Don't set the state until after the RCC is registered + return; + } + if (state == null) { + mRcc.setPlaybackState(0); + mRcc.setTransportControlFlags(0); + } else { + // Set state + setRccState(checkNotNull(state)); + + // Set transport control flags + mRcc.setTransportControlFlags(getRccTransportControlFlagsFromActions(state.getActions())); + } + } + + @Nullable + @Override + public PlaybackStateCompat getPlaybackState() { + synchronized (mLock) { + return mState; + } + } + + void setRccState(PlaybackStateCompat state) { + mRcc.setPlaybackState(getRccStateFromState(state.getState())); + } + + int getRccStateFromState(int state) { + switch (state) { + case PlaybackStateCompat.STATE_CONNECTING: + case PlaybackStateCompat.STATE_BUFFERING: + return RemoteControlClient.PLAYSTATE_BUFFERING; + case PlaybackStateCompat.STATE_ERROR: + return RemoteControlClient.PLAYSTATE_ERROR; + case PlaybackStateCompat.STATE_FAST_FORWARDING: + return RemoteControlClient.PLAYSTATE_FAST_FORWARDING; + case PlaybackStateCompat.STATE_NONE: + return RCC_PLAYSTATE_NONE; + case PlaybackStateCompat.STATE_PAUSED: + return RemoteControlClient.PLAYSTATE_PAUSED; + case PlaybackStateCompat.STATE_PLAYING: + return RemoteControlClient.PLAYSTATE_PLAYING; + case PlaybackStateCompat.STATE_REWINDING: + return RemoteControlClient.PLAYSTATE_REWINDING; + case PlaybackStateCompat.STATE_SKIPPING_TO_PREVIOUS: + return RemoteControlClient.PLAYSTATE_SKIPPING_BACKWARDS; + case PlaybackStateCompat.STATE_SKIPPING_TO_NEXT: + case PlaybackStateCompat.STATE_SKIPPING_TO_QUEUE_ITEM: + return RemoteControlClient.PLAYSTATE_SKIPPING_FORWARDS; + case PlaybackStateCompat.STATE_STOPPED: + return RemoteControlClient.PLAYSTATE_STOPPED; + default: + return -1; + } + } + + int getRccTransportControlFlagsFromActions(long actions) { + int transportControlFlags = 0; + if ((actions & PlaybackStateCompat.ACTION_STOP) != 0) { + transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_STOP; + } + if ((actions & PlaybackStateCompat.ACTION_PAUSE) != 0) { + transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_PAUSE; + } + if ((actions & PlaybackStateCompat.ACTION_PLAY) != 0) { + transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_PLAY; + } + if ((actions & PlaybackStateCompat.ACTION_REWIND) != 0) { + transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_REWIND; + } + if ((actions & PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS) != 0) { + transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS; + } + if ((actions & PlaybackStateCompat.ACTION_SKIP_TO_NEXT) != 0) { + transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_NEXT; + } + if ((actions & PlaybackStateCompat.ACTION_FAST_FORWARD) != 0) { + transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_FAST_FORWARD; + } + if ((actions & PlaybackStateCompat.ACTION_PLAY_PAUSE) != 0) { + transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE; + } + return transportControlFlags; + } + + @Override + public void setMetadata(@Nullable MediaMetadataCompat metadata) { + if (metadata != null) { + // Clones {@link MediaMetadataCompat} and scales down bitmaps if they are large. + metadata = new MediaMetadataCompat.Builder(metadata, sMaxBitmapSize).build(); + } + + synchronized (mLock) { + mMetadata = metadata; + } + sendMetadata(metadata); + if (!mIsActive) { + // Don't set metadata until after the rcc has been registered + return; + } + RemoteControlClient.MetadataEditor editor = + buildRccMetadata(metadata == null ? null : metadata.getBundle()); + editor.apply(); + } + + @SuppressWarnings({"deprecation", "argument.type.incompatible"}) + RemoteControlClient.MetadataEditor buildRccMetadata(@Nullable Bundle metadata) { + RemoteControlClient.MetadataEditor editor = mRcc.editMetadata(true); + if (metadata == null) { + return editor; + } + if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_ART)) { + Bitmap art = metadata.getParcelable(MediaMetadataCompat.METADATA_KEY_ART); + if (art != null) { + // Clone the bitmap to prevent it from being recycled by RCC. + art = art.copy(art.getConfig(), false); + } + editor.putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, art); + } else if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_ALBUM_ART)) { + // Fall back to album art if the track art wasn't available + Bitmap art = metadata.getParcelable(MediaMetadataCompat.METADATA_KEY_ALBUM_ART); + if (art != null) { + // Clone the bitmap to prevent it from being recycled by RCC. + art = art.copy(art.getConfig(), false); + } + editor.putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, art); + } + if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_ALBUM)) { + editor.putString( + MediaMetadataRetriever.METADATA_KEY_ALBUM, + metadata.getString(MediaMetadataCompat.METADATA_KEY_ALBUM)); + } + if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST)) { + editor.putString( + MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, + metadata.getString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST)); + } + if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_ARTIST)) { + editor.putString( + MediaMetadataRetriever.METADATA_KEY_ARTIST, + metadata.getString(MediaMetadataCompat.METADATA_KEY_ARTIST)); + } + if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_AUTHOR)) { + editor.putString( + MediaMetadataRetriever.METADATA_KEY_AUTHOR, + metadata.getString(MediaMetadataCompat.METADATA_KEY_AUTHOR)); + } + if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_COMPILATION)) { + editor.putString( + MediaMetadataRetriever.METADATA_KEY_COMPILATION, + metadata.getString(MediaMetadataCompat.METADATA_KEY_COMPILATION)); + } + if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_COMPOSER)) { + editor.putString( + MediaMetadataRetriever.METADATA_KEY_COMPOSER, + metadata.getString(MediaMetadataCompat.METADATA_KEY_COMPOSER)); + } + if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_DATE)) { + editor.putString( + MediaMetadataRetriever.METADATA_KEY_DATE, + metadata.getString(MediaMetadataCompat.METADATA_KEY_DATE)); + } + if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_DISC_NUMBER)) { + editor.putLong( + MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER, + metadata.getLong(MediaMetadataCompat.METADATA_KEY_DISC_NUMBER)); + } + if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_DURATION)) { + editor.putLong( + MediaMetadataRetriever.METADATA_KEY_DURATION, + metadata.getLong(MediaMetadataCompat.METADATA_KEY_DURATION)); + } + if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_GENRE)) { + editor.putString( + MediaMetadataRetriever.METADATA_KEY_GENRE, + metadata.getString(MediaMetadataCompat.METADATA_KEY_GENRE)); + } + if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_TITLE)) { + editor.putString( + MediaMetadataRetriever.METADATA_KEY_TITLE, + metadata.getString(MediaMetadataCompat.METADATA_KEY_TITLE)); + } + if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER)) { + editor.putLong( + MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER, + metadata.getLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER)); + } + if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_WRITER)) { + editor.putString( + MediaMetadataRetriever.METADATA_KEY_WRITER, + metadata.getString(MediaMetadataCompat.METADATA_KEY_WRITER)); + } + return editor; + } + + @Override + public void setSessionActivity(PendingIntent pi) { + synchronized (mLock) { + mSessionActivity = pi; + } + } + + @Override + public void setMediaButtonReceiver(@Nullable PendingIntent mbr) { + // Do nothing, changing this is not supported before API 21. + } + + @Override + public void setQueue(@Nullable List queue) { + mQueue = queue; + sendQueue(queue); + } + + @Override + public void setQueueTitle(CharSequence title) { + mQueueTitle = title; + sendQueueTitle(title); + } + + @Nullable + @Override + public Object getMediaSession() { + return null; + } + + @Nullable + @Override + public Object getRemoteControlClient() { + return null; + } + + @Nullable + @Override + public String getCallingPackage() { + return null; + } + + @Override + public void setRatingType(@RatingCompat.Style int type) { + mRatingType = type; + } + + @Override + public void setCaptioningEnabled(boolean enabled) { + if (mCaptioningEnabled != enabled) { + mCaptioningEnabled = enabled; + sendCaptioningEnabled(enabled); + } + } + + @Override + public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) { + if (mRepeatMode != repeatMode) { + mRepeatMode = repeatMode; + sendRepeatMode(repeatMode); + } + } + + @Override + public void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode) { + if (mShuffleMode != shuffleMode) { + mShuffleMode = shuffleMode; + sendShuffleMode(shuffleMode); + } + } + + @Override + public void setExtras(@Nullable Bundle extras) { + mExtras = extras; + sendExtras(extras); + } + + @Nullable + @Override + public RemoteUserInfo getCurrentControllerInfo() { + synchronized (mLock) { + return mRemoteUserInfo; + } + } + + @Override + public void setCurrentControllerInfo(@Nullable RemoteUserInfo remoteUserInfo) { + synchronized (mLock) { + mRemoteUserInfo = remoteUserInfo; + } + } + + @Nullable + @Override + public Callback getCallback() { + synchronized (mLock) { + return mCallback; + } + } + + // Registers/unregisters components as needed. + void updateMbrAndRcc() { + if (mIsActive) { + // When session becomes active, register MBR and RCC. + registerMediaButtonEventReceiver( + mMediaButtonReceiverIntent, mMediaButtonReceiverComponentName); + mAudioManager.registerRemoteControlClient(mRcc); + + setMetadata(mMetadata); + setPlaybackState(mState); + } else { + // When inactive remove any registered components. + unregisterMediaButtonEventReceiver( + mMediaButtonReceiverIntent, mMediaButtonReceiverComponentName); + // RCC keeps the state while the system resets its state internally when + // we register RCC. Reset the state so that the states in RCC and the system + // are in sync when we re-register the RCC. + mRcc.setPlaybackState(0); + mAudioManager.unregisterRemoteControlClient(mRcc); + } + } + + void registerMediaButtonEventReceiver(PendingIntent mbrIntent, ComponentName mbrComponent) { + mAudioManager.registerMediaButtonEventReceiver(mbrComponent); + } + + void unregisterMediaButtonEventReceiver(PendingIntent mbrIntent, ComponentName mbrComponent) { + mAudioManager.unregisterMediaButtonEventReceiver(mbrComponent); + } + + void adjustVolume(int direction, int flags) { + if (mVolumeType == MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE) { + if (mVolumeProvider != null) { + mVolumeProvider.onAdjustVolume(direction); + } + } else { + mAudioManager.adjustStreamVolume(mLocalStream, direction, flags); + } + } + + void setVolumeTo(int value, int flags) { + if (mVolumeType == MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE) { + if (mVolumeProvider != null) { + mVolumeProvider.onSetVolumeTo(value); + } + } else { + mAudioManager.setStreamVolume(mLocalStream, value, flags); + } + } + + void sendVolumeInfoChanged(ParcelableVolumeInfo info) { + synchronized (mLock) { + int size = mControllerCallbacks.beginBroadcast(); + for (int i = size - 1; i >= 0; i--) { + IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); + try { + cb.onVolumeInfoChanged(info); + } catch (RemoteException e) { + } + } + mControllerCallbacks.finishBroadcast(); + } + } + + private void sendSessionDestroyed() { + synchronized (mLock) { + int size = mControllerCallbacks.beginBroadcast(); + for (int i = size - 1; i >= 0; i--) { + IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); + try { + cb.onSessionDestroyed(); + } catch (RemoteException e) { + } + } + mControllerCallbacks.finishBroadcast(); + mControllerCallbacks.kill(); + } + } + + private void sendEvent(String event, @Nullable Bundle extras) { + synchronized (mLock) { + int size = mControllerCallbacks.beginBroadcast(); + for (int i = size - 1; i >= 0; i--) { + IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); + try { + cb.onEvent(event, extras); + } catch (RemoteException e) { + } + } + mControllerCallbacks.finishBroadcast(); + } + } + + private void sendState(@Nullable PlaybackStateCompat state) { + synchronized (mLock) { + int size = mControllerCallbacks.beginBroadcast(); + for (int i = size - 1; i >= 0; i--) { + IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); + try { + cb.onPlaybackStateChanged(state); + } catch (RemoteException e) { + } + } + mControllerCallbacks.finishBroadcast(); + } + } + + private void sendMetadata(@Nullable MediaMetadataCompat metadata) { + synchronized (mLock) { + int size = mControllerCallbacks.beginBroadcast(); + for (int i = size - 1; i >= 0; i--) { + IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); + try { + cb.onMetadataChanged(metadata); + } catch (RemoteException e) { + } + } + mControllerCallbacks.finishBroadcast(); + } + } + + private void sendQueue(@Nullable List queue) { + synchronized (mLock) { + int size = mControllerCallbacks.beginBroadcast(); + for (int i = size - 1; i >= 0; i--) { + IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); + try { + cb.onQueueChanged(queue); + } catch (RemoteException e) { + } + } + mControllerCallbacks.finishBroadcast(); + } + } + + private void sendQueueTitle(CharSequence queueTitle) { + synchronized (mLock) { + int size = mControllerCallbacks.beginBroadcast(); + for (int i = size - 1; i >= 0; i--) { + IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); + try { + cb.onQueueTitleChanged(queueTitle); + } catch (RemoteException e) { + } + } + mControllerCallbacks.finishBroadcast(); + } + } + + private void sendCaptioningEnabled(boolean enabled) { + synchronized (mLock) { + int size = mControllerCallbacks.beginBroadcast(); + for (int i = size - 1; i >= 0; i--) { + IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); + try { + cb.onCaptioningEnabledChanged(enabled); + } catch (RemoteException e) { + } + } + mControllerCallbacks.finishBroadcast(); + } + } + + private void sendRepeatMode(int repeatMode) { + synchronized (mLock) { + int size = mControllerCallbacks.beginBroadcast(); + for (int i = size - 1; i >= 0; i--) { + IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); + try { + cb.onRepeatModeChanged(repeatMode); + } catch (RemoteException e) { + } + } + mControllerCallbacks.finishBroadcast(); + } + } + + private void sendShuffleMode(int shuffleMode) { + synchronized (mLock) { + int size = mControllerCallbacks.beginBroadcast(); + for (int i = size - 1; i >= 0; i--) { + IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); + try { + cb.onShuffleModeChanged(shuffleMode); + } catch (RemoteException e) { + } + } + mControllerCallbacks.finishBroadcast(); + } + } + + private void sendExtras(@Nullable Bundle extras) { + synchronized (mLock) { + int size = mControllerCallbacks.beginBroadcast(); + for (int i = size - 1; i >= 0; i--) { + IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); + try { + cb.onExtrasChanged(extras); + } catch (RemoteException e) { + } + } + mControllerCallbacks.finishBroadcast(); + } + } + + static class MediaSessionStub extends IMediaSession.Stub { + + private final AtomicReference mMediaSessionImplRef; + private final String mPackageName; + private final String mTag; + + MediaSessionStub(MediaSessionImplBase mediaSessionImpl, String packageName, String tag) { + mMediaSessionImplRef = new AtomicReference<>(mediaSessionImpl); + mPackageName = packageName; + mTag = tag; + } + + @Override + public void sendCommand( + @Nullable String command, @Nullable Bundle args, @Nullable ResultReceiverWrapper cb) { + if (command == null) { + return; + } + postToHandler( + MessageHandler.MSG_COMMAND, + new Command(command, args, cb == null ? null : cb.mResultReceiver)); + } + + @Override + public boolean sendMediaButton(@Nullable KeyEvent mediaButton) { + postToHandler(MessageHandler.MSG_MEDIA_BUTTON, mediaButton); + return true; + } + + @Override + public void registerCallbackListener(@Nullable IMediaControllerCallback cb) { + if (cb == null) { + return; + } + // If this session is already destroyed tell the caller and + // don't add them. + MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); + if (mediaSessionImpl == null) { + try { + cb.onSessionDestroyed(); + } catch (Exception e) { + // ignored + } + return; + } + int callingPid = Binder.getCallingPid(); + int callingUid = Binder.getCallingUid(); + RemoteUserInfo info = + new RemoteUserInfo( + mediaSessionImpl.getPackageNameForUid(callingUid), callingPid, callingUid); + mediaSessionImpl.mControllerCallbacks.register(cb, info); + + synchronized (mediaSessionImpl.mLock) { + if (mediaSessionImpl.mRegistrationCallbackHandler != null) { + mediaSessionImpl.mRegistrationCallbackHandler.postCallbackRegistered( + callingPid, callingUid); + } + } + } + + @Override + public void unregisterCallbackListener(@Nullable IMediaControllerCallback cb) { + if (cb == null) { + return; + } + MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); + if (mediaSessionImpl == null) { + return; + } + mediaSessionImpl.mControllerCallbacks.unregister(cb); + + int callingPid = Binder.getCallingPid(); + int callingUid = Binder.getCallingUid(); + synchronized (mediaSessionImpl.mLock) { + if (mediaSessionImpl.mRegistrationCallbackHandler != null) { + mediaSessionImpl.mRegistrationCallbackHandler.postCallbackUnregistered( + callingPid, callingUid); + } + } + } + + @Override + public String getPackageName() { + return mPackageName; + } + + @Nullable + @Override + public Bundle getSessionInfo() { + MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); + // mSessionInfo is final so doesn't need synchronize block + return mediaSessionImpl != null && mediaSessionImpl.mSessionInfo != null + ? new Bundle(mediaSessionImpl.mSessionInfo) + : null; + } + + @Override + public String getTag() { + // mTag is final so doesn't need synchronize block + return mTag; + } + + @Nullable + @Override + public PendingIntent getLaunchPendingIntent() { + MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); + if (mediaSessionImpl == null) { + return null; + } + synchronized (mediaSessionImpl.mLock) { + return mediaSessionImpl.mSessionActivity; + } + } + + @Override + @SessionFlags + public long getFlags() { + MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); + if (mediaSessionImpl == null) { + return 0; + } + synchronized (mediaSessionImpl.mLock) { + return mediaSessionImpl.mFlags; + } + } + + @Nullable + @Override + public ParcelableVolumeInfo getVolumeAttributes() { + MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); + if (mediaSessionImpl == null) { + return null; + } + synchronized (mediaSessionImpl.mLock) { + int volumeType = mediaSessionImpl.mVolumeType; + int stream = mediaSessionImpl.mLocalStream; + VolumeProviderCompat vp = mediaSessionImpl.mVolumeProvider; + int controlType; + int max; + int current; + if (volumeType == MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE) { + checkNotNull(vp); + controlType = vp.getVolumeControl(); + max = vp.getMaxVolume(); + current = vp.getCurrentVolume(); + } else { + controlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE; + max = mediaSessionImpl.mAudioManager.getStreamMaxVolume(stream); + current = mediaSessionImpl.mAudioManager.getStreamVolume(stream); + } + return new ParcelableVolumeInfo(volumeType, stream, controlType, max, current); + } + } + + @Override + public void adjustVolume(int direction, int flags, @Nullable String packageName) { + MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); + if (mediaSessionImpl != null) { + mediaSessionImpl.adjustVolume(direction, flags); + } + } + + @Override + public void setVolumeTo(int value, int flags, @Nullable String packageName) { + MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); + if (mediaSessionImpl != null) { + mediaSessionImpl.setVolumeTo(value, flags); + } + } + + @Override + public void prepare() throws RemoteException { + postToHandler(MessageHandler.MSG_PREPARE); + } + + @Override + public void prepareFromMediaId(@Nullable String mediaId, @Nullable Bundle extras) { + postToHandler(MessageHandler.MSG_PREPARE_MEDIA_ID, mediaId, extras); + } + + @Override + public void prepareFromSearch(@Nullable String query, @Nullable Bundle extras) { + postToHandler(MessageHandler.MSG_PREPARE_SEARCH, query, extras); + } + + @Override + public void prepareFromUri(@Nullable Uri uri, @Nullable Bundle extras) { + postToHandler(MessageHandler.MSG_PREPARE_URI, uri, extras); + } + + @Override + public void play() throws RemoteException { + postToHandler(MessageHandler.MSG_PLAY); + } + + @Override + public void playFromMediaId(@Nullable String mediaId, @Nullable Bundle extras) { + postToHandler(MessageHandler.MSG_PLAY_MEDIA_ID, mediaId, extras); + } + + @Override + public void playFromSearch(@Nullable String query, @Nullable Bundle extras) { + postToHandler(MessageHandler.MSG_PLAY_SEARCH, query, extras); + } + + @Override + public void playFromUri(@Nullable Uri uri, @Nullable Bundle extras) { + postToHandler(MessageHandler.MSG_PLAY_URI, uri, extras); + } + + @Override + public void skipToQueueItem(long id) { + postToHandler(MessageHandler.MSG_SKIP_TO_ITEM, id); + } + + @Override + public void pause() { + postToHandler(MessageHandler.MSG_PAUSE); + } + + @Override + public void stop() { + postToHandler(MessageHandler.MSG_STOP); + } + + @Override + public void next() { + postToHandler(MessageHandler.MSG_NEXT); + } + + @Override + public void previous() { + postToHandler(MessageHandler.MSG_PREVIOUS); + } + + @Override + public void fastForward() { + postToHandler(MessageHandler.MSG_FAST_FORWARD); + } + + @Override + public void rewind() { + postToHandler(MessageHandler.MSG_REWIND); + } + + @Override + public void seekTo(long pos) { + postToHandler(MessageHandler.MSG_SEEK_TO, pos); + } + + @Override + public void rate(@Nullable RatingCompat rating) { + postToHandler(MessageHandler.MSG_RATE, rating); + } + + @Override + public void rateWithExtras(@Nullable RatingCompat rating, @Nullable Bundle extras) { + postToHandler(MessageHandler.MSG_RATE_EXTRA, rating, extras); + } + + @Override + public void setPlaybackSpeed(float speed) { + postToHandler(MessageHandler.MSG_SET_PLAYBACK_SPEED, speed); + } + + @Override + public void setCaptioningEnabled(boolean enabled) { + postToHandler(MessageHandler.MSG_SET_CAPTIONING_ENABLED, enabled); + } + + @Override + public void setRepeatMode(int repeatMode) { + postToHandler(MessageHandler.MSG_SET_REPEAT_MODE, repeatMode); + } + + @Override + public void setShuffleModeEnabledRemoved(boolean enabled) { + // Do nothing. + } + + @Override + public void setShuffleMode(int shuffleMode) { + postToHandler(MessageHandler.MSG_SET_SHUFFLE_MODE, shuffleMode); + } + + @Override + public void sendCustomAction(@Nullable String action, @Nullable Bundle args) + throws RemoteException { + postToHandler(MessageHandler.MSG_CUSTOM_ACTION, action, args); + } + + @Nullable + @Override + public MediaMetadataCompat getMetadata() { + MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); + return mediaSessionImpl != null ? mediaSessionImpl.mMetadata : null; + } + + @Nullable + @Override + public PlaybackStateCompat getPlaybackState() { + MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); + if (mediaSessionImpl == null) { + return null; + } + PlaybackStateCompat state; + MediaMetadataCompat metadata; + synchronized (mediaSessionImpl.mLock) { + state = mediaSessionImpl.mState; + metadata = mediaSessionImpl.mMetadata; + } + return getStateWithUpdatedPosition(state, metadata); + } + + @Nullable + @Override + public List getQueue() { + MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); + if (mediaSessionImpl == null) { + return null; + } + synchronized (mediaSessionImpl.mLock) { + return mediaSessionImpl.mQueue; + } + } + + @Override + public void addQueueItem(@Nullable MediaDescriptionCompat description) { + postToHandler(MessageHandler.MSG_ADD_QUEUE_ITEM, description); + } + + @Override + public void addQueueItemAt(@Nullable MediaDescriptionCompat description, int index) { + postToHandler(MessageHandler.MSG_ADD_QUEUE_ITEM_AT, description, index, /* extras= */ null); + } + + @Override + public void removeQueueItem(@Nullable MediaDescriptionCompat description) { + postToHandler(MessageHandler.MSG_REMOVE_QUEUE_ITEM, description); + } + + @Override + public void removeQueueItemAt(int index) { + postToHandler(MessageHandler.MSG_REMOVE_QUEUE_ITEM_AT, index); + } + + @Nullable + @Override + public CharSequence getQueueTitle() { + MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); + return mediaSessionImpl != null ? mediaSessionImpl.mQueueTitle : null; + } + + @Nullable + @Override + public Bundle getExtras() { + MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); + if (mediaSessionImpl == null) { + return null; + } + synchronized (mediaSessionImpl.mLock) { + return mediaSessionImpl.mExtras; + } + } + + @Override + @RatingCompat.Style + public int getRatingType() { + MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); + return mediaSessionImpl != null ? mediaSessionImpl.mRatingType : RatingCompat.RATING_NONE; + } + + @Override + public boolean isCaptioningEnabled() { + MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); + return mediaSessionImpl != null && mediaSessionImpl.mCaptioningEnabled; + } + + @Override + @PlaybackStateCompat.RepeatMode + public int getRepeatMode() { + MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); + return mediaSessionImpl != null + ? mediaSessionImpl.mRepeatMode + : PlaybackStateCompat.REPEAT_MODE_INVALID; + } + + @Override + public boolean isShuffleModeEnabledRemoved() { + return false; + } + + @Override + @PlaybackStateCompat.ShuffleMode + public int getShuffleMode() { + MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); + return mediaSessionImpl != null + ? mediaSessionImpl.mShuffleMode + : PlaybackStateCompat.SHUFFLE_MODE_INVALID; + } + + @Override + public boolean isTransportControlEnabled() { + // All sessions should support transport control commands. + return true; + } + + void postToHandler(int what) { + postToHandler(what, /* obj= */ null, /* arg1= */ 0, /* extras= */ null); + } + + void postToHandler(int what, int arg1) { + postToHandler(what, /* obj= */ null, arg1, /* extras= */ null); + } + + void postToHandler(int what, @Nullable Object obj) { + postToHandler(what, obj, /* arg1= */ 0, /* extras= */ null); + } + + void postToHandler(int what, @Nullable Object obj, @Nullable Bundle extras) { + postToHandler(what, obj, /* arg1= */ 0, extras); + } + + void postToHandler(int what, @Nullable Object obj, int arg1, @Nullable Bundle extras) { + MediaSessionImplBase mediaSessionImpl = mMediaSessionImplRef.get(); + if (mediaSessionImpl != null) { + mediaSessionImpl.postToHandler(what, arg1, /* arg2= */ 0, obj, extras); + } + } + } + + private static final class Command { + public final String command; + @Nullable public final Bundle extras; + @Nullable public final ResultReceiver stub; + + public Command(String command, @Nullable Bundle extras, @Nullable ResultReceiver stub) { + this.command = command; + this.extras = extras; + this.stub = stub; + } + } + + class MessageHandler extends Handler { + // Next ID: 33 + private static final int MSG_COMMAND = 1; + private static final int MSG_ADJUST_VOLUME = 2; + private static final int MSG_PREPARE = 3; + private static final int MSG_PREPARE_MEDIA_ID = 4; + private static final int MSG_PREPARE_SEARCH = 5; + private static final int MSG_PREPARE_URI = 6; + private static final int MSG_PLAY = 7; + private static final int MSG_PLAY_MEDIA_ID = 8; + private static final int MSG_PLAY_SEARCH = 9; + private static final int MSG_PLAY_URI = 10; + private static final int MSG_SKIP_TO_ITEM = 11; + private static final int MSG_PAUSE = 12; + private static final int MSG_STOP = 13; + private static final int MSG_NEXT = 14; + private static final int MSG_PREVIOUS = 15; + private static final int MSG_FAST_FORWARD = 16; + private static final int MSG_REWIND = 17; + private static final int MSG_SEEK_TO = 18; + private static final int MSG_RATE = 19; + private static final int MSG_RATE_EXTRA = 31; + private static final int MSG_SET_PLAYBACK_SPEED = 32; + private static final int MSG_CUSTOM_ACTION = 20; + private static final int MSG_MEDIA_BUTTON = 21; + private static final int MSG_SET_VOLUME = 22; + private static final int MSG_SET_REPEAT_MODE = 23; + private static final int MSG_ADD_QUEUE_ITEM = 25; + private static final int MSG_ADD_QUEUE_ITEM_AT = 26; + private static final int MSG_REMOVE_QUEUE_ITEM = 27; + private static final int MSG_REMOVE_QUEUE_ITEM_AT = 28; + private static final int MSG_SET_CAPTIONING_ENABLED = 29; + private static final int MSG_SET_SHUFFLE_MODE = 30; + + // KeyEvent constants only available on API 11+ + private static final int KEYCODE_MEDIA_PAUSE = 127; + private static final int KEYCODE_MEDIA_PLAY = 126; + + public MessageHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + MediaSessionCompat.Callback cb = mCallback; + if (cb == null) { + return; + } + + Bundle data = msg.getData(); + ensureClassLoader(data); + setCurrentControllerInfo( + new RemoteUserInfo( + data.getString(DATA_CALLING_PACKAGE), + data.getInt(DATA_CALLING_PID), + data.getInt(DATA_CALLING_UID))); + + Bundle extras = data.getBundle(DATA_EXTRAS); + ensureClassLoader(extras); + + try { + switch (msg.what) { + case MSG_COMMAND: + Command cmd = (Command) msg.obj; + cb.onCommand(cmd.command, cmd.extras, cmd.stub); + break; + case MSG_MEDIA_BUTTON: + KeyEvent keyEvent = (KeyEvent) msg.obj; + Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); + intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent); + // Let the Callback handle events first before using the default + // behavior + if (!cb.onMediaButtonEvent(intent)) { + onMediaButtonEvent(keyEvent, cb); + } + break; + case MSG_PREPARE: + cb.onPrepare(); + break; + case MSG_PREPARE_MEDIA_ID: + cb.onPrepareFromMediaId((String) msg.obj, extras); + break; + case MSG_PREPARE_SEARCH: + cb.onPrepareFromSearch((String) msg.obj, extras); + break; + case MSG_PREPARE_URI: + cb.onPrepareFromUri((Uri) msg.obj, extras); + break; + case MSG_PLAY: + cb.onPlay(); + break; + case MSG_PLAY_MEDIA_ID: + cb.onPlayFromMediaId((String) msg.obj, extras); + break; + case MSG_PLAY_SEARCH: + cb.onPlayFromSearch((String) msg.obj, extras); + break; + case MSG_PLAY_URI: + cb.onPlayFromUri((Uri) msg.obj, extras); + break; + case MSG_SKIP_TO_ITEM: + cb.onSkipToQueueItem((Long) msg.obj); + break; + case MSG_PAUSE: + cb.onPause(); + break; + case MSG_STOP: + cb.onStop(); + break; + case MSG_NEXT: + cb.onSkipToNext(); + break; + case MSG_PREVIOUS: + cb.onSkipToPrevious(); + break; + case MSG_FAST_FORWARD: + cb.onFastForward(); + break; + case MSG_REWIND: + cb.onRewind(); + break; + case MSG_SEEK_TO: + cb.onSeekTo((Long) msg.obj); + break; + case MSG_RATE: + cb.onSetRating((RatingCompat) msg.obj); + break; + case MSG_RATE_EXTRA: + cb.onSetRating((RatingCompat) msg.obj, extras); + break; + case MSG_SET_PLAYBACK_SPEED: + cb.onSetPlaybackSpeed((Float) msg.obj); + break; + case MSG_CUSTOM_ACTION: + cb.onCustomAction((String) msg.obj, extras); + break; + case MSG_ADD_QUEUE_ITEM: + cb.onAddQueueItem((MediaDescriptionCompat) msg.obj); + break; + case MSG_ADD_QUEUE_ITEM_AT: + cb.onAddQueueItem((MediaDescriptionCompat) msg.obj, msg.arg1); + break; + case MSG_REMOVE_QUEUE_ITEM: + cb.onRemoveQueueItem((MediaDescriptionCompat) msg.obj); + break; + case MSG_REMOVE_QUEUE_ITEM_AT: + if (mQueue != null) { + QueueItem item = + (msg.arg1 >= 0 && msg.arg1 < mQueue.size()) ? mQueue.get(msg.arg1) : null; + if (item != null) { + cb.onRemoveQueueItem(item.getDescription()); + } + } + break; + case MSG_ADJUST_VOLUME: + adjustVolume(msg.arg1, 0); + break; + case MSG_SET_VOLUME: + setVolumeTo(msg.arg1, 0); + break; + case MSG_SET_CAPTIONING_ENABLED: + cb.onSetCaptioningEnabled((boolean) msg.obj); + break; + case MSG_SET_REPEAT_MODE: + cb.onSetRepeatMode(msg.arg1); + break; + case MSG_SET_SHUFFLE_MODE: + cb.onSetShuffleMode(msg.arg1); + break; + } + } finally { + setCurrentControllerInfo(null); + } + } + + private void onMediaButtonEvent(@Nullable KeyEvent ke, MediaSessionCompat.Callback cb) { + if (ke == null || ke.getAction() != KeyEvent.ACTION_DOWN) { + return; + } + long validActions = mState == null ? 0 : mState.getActions(); + switch (ke.getKeyCode()) { + // Note KeyEvent.KEYCODE_MEDIA_PLAY is API 11+ + case KEYCODE_MEDIA_PLAY: + if ((validActions & PlaybackStateCompat.ACTION_PLAY) != 0) { + cb.onPlay(); + } + break; + // Note KeyEvent.KEYCODE_MEDIA_PAUSE is API 11+ + case KEYCODE_MEDIA_PAUSE: + if ((validActions & PlaybackStateCompat.ACTION_PAUSE) != 0) { + cb.onPause(); + } + break; + case KeyEvent.KEYCODE_MEDIA_NEXT: + if ((validActions & PlaybackStateCompat.ACTION_SKIP_TO_NEXT) != 0) { + cb.onSkipToNext(); + } + break; + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + if ((validActions & PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS) != 0) { + cb.onSkipToPrevious(); + } + break; + case KeyEvent.KEYCODE_MEDIA_STOP: + if ((validActions & PlaybackStateCompat.ACTION_STOP) != 0) { + cb.onStop(); + } + break; + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: + if ((validActions & PlaybackStateCompat.ACTION_FAST_FORWARD) != 0) { + cb.onFastForward(); + } + break; + case KeyEvent.KEYCODE_MEDIA_REWIND: + if ((validActions & PlaybackStateCompat.ACTION_REWIND) != 0) { + cb.onRewind(); + } + break; + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + case KeyEvent.KEYCODE_HEADSETHOOK: + Log.w(TAG, "KEYCODE_MEDIA_PLAY_PAUSE and KEYCODE_HEADSETHOOK are handled" + " already"); + break; + } + } + } + } + + static class MediaSessionImplApi18 extends MediaSessionImplBase { + private static boolean sIsMbrPendingIntentSupported = true; + + MediaSessionImplApi18( + Context context, + String tag, + ComponentName mbrComponent, + @Nullable PendingIntent mbrIntent, + @Nullable VersionedParcelable session2Token, + @Nullable Bundle sessionInfo) { + super(context, tag, mbrComponent, mbrIntent, session2Token, sessionInfo); + } + + @SuppressWarnings("argument.type.incompatible") + @Override + public void setCallback(@Nullable Callback callback, @Nullable Handler handler) { + super.setCallback(callback, handler); + if (callback == null) { + mRcc.setPlaybackPositionUpdateListener(null); + } else { + RemoteControlClient.OnPlaybackPositionUpdateListener listener = + new RemoteControlClient.OnPlaybackPositionUpdateListener() { + @Override + public void onPlaybackPositionUpdate(long newPositionMs) { + postToHandler(MessageHandler.MSG_SEEK_TO, -1, -1, newPositionMs, null); + } + }; + mRcc.setPlaybackPositionUpdateListener(listener); + } + } + + @Override + void setRccState(PlaybackStateCompat state) { + long position = state.getPosition(); + float speed = state.getPlaybackSpeed(); + long updateTime = state.getLastPositionUpdateTime(); + long currTime = SystemClock.elapsedRealtime(); + if (state.getState() == PlaybackStateCompat.STATE_PLAYING && position > 0) { + long diff = 0; + if (updateTime > 0) { + diff = currTime - updateTime; + if (speed > 0 && speed != 1f) { + diff = (long) (diff * speed); + } + } + position += diff; + } + mRcc.setPlaybackState(getRccStateFromState(state.getState()), position, speed); + } + + @Override + int getRccTransportControlFlagsFromActions(long actions) { + int transportControlFlags = super.getRccTransportControlFlagsFromActions(actions); + if ((actions & PlaybackStateCompat.ACTION_SEEK_TO) != 0) { + transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE; + } + return transportControlFlags; + } + + @Override + void registerMediaButtonEventReceiver(PendingIntent mbrIntent, ComponentName mbrComponent) { + // Some Android implementations are not able to register a media button event receiver + // using a PendingIntent but need a ComponentName instead. These will raise a + // NullPointerException. + if (sIsMbrPendingIntentSupported) { + try { + mAudioManager.registerMediaButtonEventReceiver(mbrIntent); + } catch (NullPointerException e) { + Log.w( + TAG, + "Unable to register media button event receiver with " + + "PendingIntent, falling back to ComponentName."); + sIsMbrPendingIntentSupported = false; + } + } + + if (!sIsMbrPendingIntentSupported) { + super.registerMediaButtonEventReceiver(mbrIntent, mbrComponent); + } + } + + @Override + void unregisterMediaButtonEventReceiver(PendingIntent mbrIntent, ComponentName mbrComponent) { + if (sIsMbrPendingIntentSupported) { + mAudioManager.unregisterMediaButtonEventReceiver(mbrIntent); + } else { + super.unregisterMediaButtonEventReceiver(mbrIntent, mbrComponent); + } + } + } + + static class MediaSessionImplApi19 extends MediaSessionImplApi18 { + MediaSessionImplApi19( + Context context, + String tag, + ComponentName mbrComponent, + @Nullable PendingIntent mbrIntent, + @Nullable VersionedParcelable session2Token, + @Nullable Bundle sessionInfo) { + super(context, tag, mbrComponent, mbrIntent, session2Token, sessionInfo); + } + + @SuppressWarnings("argument.type.incompatible") + @Override + public void setCallback(@Nullable Callback callback, @Nullable Handler handler) { + super.setCallback(callback, handler); + if (callback == null) { + mRcc.setMetadataUpdateListener(null); + } else { + RemoteControlClient.OnMetadataUpdateListener listener = + new RemoteControlClient.OnMetadataUpdateListener() { + @Override + public void onMetadataUpdate(int key, Object newValue) { + if (key == MediaMetadataEditor.RATING_KEY_BY_USER && newValue instanceof Rating) { + postToHandler( + MessageHandler.MSG_RATE, -1, -1, RatingCompat.fromRating(newValue), null); + } + } + }; + mRcc.setMetadataUpdateListener(listener); + } + } + + @Override + int getRccTransportControlFlagsFromActions(long actions) { + int transportControlFlags = super.getRccTransportControlFlagsFromActions(actions); + if ((actions & PlaybackStateCompat.ACTION_SET_RATING) != 0) { + transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_RATING; + } + return transportControlFlags; + } + + @Override + @SuppressWarnings({"deprecation", "argument.type.incompatible"}) + RemoteControlClient.MetadataEditor buildRccMetadata(@Nullable Bundle metadata) { + RemoteControlClient.MetadataEditor editor = super.buildRccMetadata(metadata); + long actions = mState == null ? 0 : mState.getActions(); + if ((actions & PlaybackStateCompat.ACTION_SET_RATING) != 0) { + editor.addEditableKey(RemoteControlClient.MetadataEditor.RATING_KEY_BY_USER); + } + + if (metadata == null) { + return editor; + } + if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_YEAR)) { + editor.putLong( + MediaMetadataRetriever.METADATA_KEY_YEAR, + metadata.getLong(MediaMetadataCompat.METADATA_KEY_YEAR)); + } + if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_RATING)) { + // Do not remove casting here. Without this, a crash will happen in API 19. + ((MediaMetadataEditor) editor) + .putObject( + MediaMetadataEditor.RATING_KEY_BY_OTHERS, + LegacyParcelableUtil.convert( + metadata.getParcelable(MediaMetadataCompat.METADATA_KEY_RATING), + RatingCompat.CREATOR)); + } + if (metadata.containsKey(MediaMetadataCompat.METADATA_KEY_USER_RATING)) { + // Do not remove casting here. Without this, a crash will happen in API 19. + ((MediaMetadataEditor) editor) + .putObject( + MediaMetadataEditor.RATING_KEY_BY_USER, + LegacyParcelableUtil.convert( + metadata.getParcelable(MediaMetadataCompat.METADATA_KEY_USER_RATING), + RatingCompat.CREATOR)); + } + return editor; + } + } + + @RequiresApi(21) + static class MediaSessionImplApi21 implements MediaSessionImpl { + final MediaSession mSessionFwk; + final ExtraSession mExtraSession; + final Token mToken; + final Object mLock = new Object(); + @Nullable Bundle mSessionInfo; + + boolean mDestroyed = false; + final RemoteCallbackList mExtraControllerCallbacks = + new RemoteCallbackList<>(); + + @Nullable PlaybackStateCompat mPlaybackState; + @Nullable List mQueue; + @Nullable MediaMetadataCompat mMetadata; + @RatingCompat.Style int mRatingType; + boolean mCaptioningEnabled; + @PlaybackStateCompat.RepeatMode int mRepeatMode; + @PlaybackStateCompat.ShuffleMode int mShuffleMode; + + @Nullable + @GuardedBy("mLock") + Callback mCallback; + + @Nullable + @GuardedBy("mLock") + RegistrationCallbackHandler mRegistrationCallbackHandler; + + @Nullable + @GuardedBy("mLock") + RemoteUserInfo mRemoteUserInfo; + + // Sharing this in constructor + @SuppressWarnings({ + "method.invocation.invalid", + "assignment.type.incompatible", + "argument.type.incompatible" + }) + MediaSessionImplApi21( + Context context, + String tag, + @Nullable VersionedParcelable session2Token, + @Nullable Bundle sessionInfo) { + mSessionFwk = createFwkMediaSession(context, tag, sessionInfo); + mExtraSession = new ExtraSession(/* mediaSessionImpl= */ this); + mToken = new Token(mSessionFwk.getSessionToken(), mExtraSession, session2Token); + mSessionInfo = sessionInfo; + // For backward compatibility, these flags are always set. + setFlags(FLAG_HANDLES_MEDIA_BUTTONS | FLAG_HANDLES_TRANSPORT_CONTROLS); + } + + // Sharing this in constructor + @SuppressWarnings({ + "method.invocation.invalid", + "assignment.type.incompatible", + "argument.type.incompatible" + }) + MediaSessionImplApi21(Object mediaSession) { + if (!(mediaSession instanceof MediaSession)) { + throw new IllegalArgumentException("mediaSession is not a valid MediaSession object"); + } + mSessionFwk = (MediaSession) mediaSession; + mExtraSession = new ExtraSession(/* mediaSessionImpl= */ this); + mToken = new Token(mSessionFwk.getSessionToken(), mExtraSession); + mSessionInfo = null; + // For backward compatibility, these flags are always set. + setFlags(FLAG_HANDLES_MEDIA_BUTTONS | FLAG_HANDLES_TRANSPORT_CONTROLS); + } + + public MediaSession createFwkMediaSession( + Context context, String tag, @Nullable Bundle sessionInfo) { + return new MediaSession(context, tag); + } + + @Override + public void setCallback(@Nullable Callback callback, @Nullable Handler handler) { + synchronized (mLock) { + mCallback = callback; + mSessionFwk.setCallback(callback == null ? null : callback.mCallbackFwk, handler); + if (callback != null) { + callback.setSessionImpl(this, handler); + } + } + } + + @Override + public void setRegistrationCallback(@Nullable RegistrationCallback callback, Handler handler) { + synchronized (mLock) { + if (mRegistrationCallbackHandler != null) { + mRegistrationCallbackHandler.removeCallbacksAndMessages(null); + } + if (callback != null) { + mRegistrationCallbackHandler = + new RegistrationCallbackHandler(handler.getLooper(), callback); + } else { + mRegistrationCallbackHandler = null; + } + } + } + + @SuppressLint("WrongConstant") + @Override + public void setFlags(@SessionFlags int flags) { + // For backward compatibility, always set these deprecated flags. + mSessionFwk.setFlags(flags | FLAG_HANDLES_MEDIA_BUTTONS | FLAG_HANDLES_TRANSPORT_CONTROLS); + } + + @Override + public void setPlaybackToLocal(int stream) { + // TODO update APIs to use support version of AudioAttributes + AudioAttributes.Builder bob = new AudioAttributes.Builder(); + bob.setLegacyStreamType(stream); + mSessionFwk.setPlaybackToLocal(bob.build()); + } + + @Override + public void setPlaybackToRemote(VolumeProviderCompat volumeProvider) { + mSessionFwk.setPlaybackToRemote((VolumeProvider) volumeProvider.getVolumeProvider()); + } + + @Override + public void setActive(boolean active) { + mSessionFwk.setActive(active); + } + + @Override + public boolean isActive() { + return mSessionFwk.isActive(); + } + + @Override + public void sendSessionEvent(String event, @Nullable Bundle extras) { + if (android.os.Build.VERSION.SDK_INT < 23) { + synchronized (mLock) { + int size = mExtraControllerCallbacks.beginBroadcast(); + for (int i = size - 1; i >= 0; i--) { + IMediaControllerCallback cb = mExtraControllerCallbacks.getBroadcastItem(i); + try { + cb.onEvent(event, extras); + } catch (RemoteException e) { + } + } + mExtraControllerCallbacks.finishBroadcast(); + } + } + mSessionFwk.sendSessionEvent(event, extras); + } + + @Override + public void release() { + mDestroyed = true; + mExtraControllerCallbacks.kill(); + if (Build.VERSION.SDK_INT == 27) { + // This is a workaround for framework MediaSession's bug in API 27. + try { + @SuppressLint({"PrivateApi", "DiscouragedPrivateApi"}) + Field callback = mSessionFwk.getClass().getDeclaredField("mCallback"); + callback.setAccessible(true); + Handler handler = (Handler) callback.get(mSessionFwk); + if (handler != null) { + handler.removeCallbacksAndMessages(null); + } + } catch (Exception e) { + Log.w(TAG, "Exception happened while accessing MediaSession.mCallback.", e); + } + } + // Prevent from receiving callbacks from released session. + mSessionFwk.setCallback(null); + mExtraSession.release(); + mSessionFwk.release(); + } + + @Override + public Token getSessionToken() { + return mToken; + } + + @Override + public void setPlaybackState(PlaybackStateCompat state) { + mPlaybackState = state; + synchronized (mLock) { + int size = mExtraControllerCallbacks.beginBroadcast(); + for (int i = size - 1; i >= 0; i--) { + IMediaControllerCallback cb = mExtraControllerCallbacks.getBroadcastItem(i); + try { + cb.onPlaybackStateChanged(state); + } catch (RemoteException e) { + } + } + mExtraControllerCallbacks.finishBroadcast(); + } + mSessionFwk.setPlaybackState(state == null ? null : (PlaybackState) state.getPlaybackState()); + } + + @Nullable + @Override + public PlaybackStateCompat getPlaybackState() { + return mPlaybackState; + } + + @Override + public void setMetadata(@Nullable MediaMetadataCompat metadata) { + mMetadata = metadata; + mSessionFwk.setMetadata( + metadata == null ? null : (MediaMetadata) metadata.getMediaMetadata()); + } + + @Override + public void setSessionActivity(PendingIntent pi) { + mSessionFwk.setSessionActivity(pi); + } + + @Override + public void setMediaButtonReceiver(@Nullable PendingIntent mbr) { + mSessionFwk.setMediaButtonReceiver(mbr); + } + + @Override + public void setQueue(@Nullable List queue) { + mQueue = queue; + if (queue == null) { + mSessionFwk.setQueue(null); + return; + } + ArrayList queueItemFwks = new ArrayList<>(queue.size()); + for (QueueItem item : queue) { + queueItemFwks.add((MediaSession.QueueItem) checkNotNull(item.getQueueItem())); + } + mSessionFwk.setQueue(queueItemFwks); + } + + @Override + public void setQueueTitle(CharSequence title) { + mSessionFwk.setQueueTitle(title); + } + + @Override + public void setRatingType(@RatingCompat.Style int type) { + mRatingType = type; + } + + @Override + public void setCaptioningEnabled(boolean enabled) { + if (mCaptioningEnabled != enabled) { + mCaptioningEnabled = enabled; + synchronized (mLock) { + int size = mExtraControllerCallbacks.beginBroadcast(); + for (int i = size - 1; i >= 0; i--) { + IMediaControllerCallback cb = mExtraControllerCallbacks.getBroadcastItem(i); + try { + cb.onCaptioningEnabledChanged(enabled); + } catch (RemoteException e) { + } + } + mExtraControllerCallbacks.finishBroadcast(); + } + } + } + + @Override + public void setRepeatMode(@PlaybackStateCompat.RepeatMode int repeatMode) { + if (mRepeatMode != repeatMode) { + mRepeatMode = repeatMode; + synchronized (mLock) { + int size = mExtraControllerCallbacks.beginBroadcast(); + for (int i = size - 1; i >= 0; i--) { + IMediaControllerCallback cb = mExtraControllerCallbacks.getBroadcastItem(i); + try { + cb.onRepeatModeChanged(repeatMode); + } catch (RemoteException e) { + } + } + mExtraControllerCallbacks.finishBroadcast(); + } + } + } + + @Override + public void setShuffleMode(@PlaybackStateCompat.ShuffleMode int shuffleMode) { + if (mShuffleMode != shuffleMode) { + mShuffleMode = shuffleMode; + synchronized (mLock) { + int size = mExtraControllerCallbacks.beginBroadcast(); + for (int i = size - 1; i >= 0; i--) { + IMediaControllerCallback cb = mExtraControllerCallbacks.getBroadcastItem(i); + try { + cb.onShuffleModeChanged(shuffleMode); + } catch (RemoteException e) { + } + } + mExtraControllerCallbacks.finishBroadcast(); + } + } + } + + @Override + public void setExtras(@Nullable Bundle extras) { + mSessionFwk.setExtras(extras); + } + + @Nullable + @Override + public Object getMediaSession() { + return mSessionFwk; + } + + @Nullable + @Override + public Object getRemoteControlClient() { + // Note: When this returns something, {@link MediaSessionCompatCallbackTest} and + // {@link #setCurrentUserInfoOverride} should be also updated. + return null; + } + + @Override + public void setCurrentControllerInfo(@Nullable RemoteUserInfo remoteUserInfo) { + synchronized (mLock) { + mRemoteUserInfo = remoteUserInfo; + } + } + + @Nullable + @Override + public String getCallingPackage() { + if (android.os.Build.VERSION.SDK_INT < 24) { + return null; + } else { + try { + Method getCallingPackageMethod = mSessionFwk.getClass().getMethod("getCallingPackage"); + return (String) getCallingPackageMethod.invoke(mSessionFwk); + } catch (Exception e) { + Log.e(TAG, "Cannot execute MediaSession.getCallingPackage()", e); + } + return null; + } + } + + @Nullable + @Override + public RemoteUserInfo getCurrentControllerInfo() { + synchronized (mLock) { + return mRemoteUserInfo; + } + } + + @Nullable + @Override + public Callback getCallback() { + synchronized (mLock) { + return mCallback; + } + } + + private static class ExtraSession extends IMediaSession.Stub { + + private final AtomicReference mMediaSessionImplRef; + + ExtraSession(MediaSessionImplApi21 mediaSessionImpl) { + mMediaSessionImplRef = new AtomicReference<>(mediaSessionImpl); + } + + /** Clears the reference to the containing component in order to enable garbage collection. */ + @SuppressWarnings("argument.type.incompatible") // Resetting variable to null + public void release() { + mMediaSessionImplRef.set(null); + } + + @Override + public void sendCommand( + @Nullable String command, @Nullable Bundle args, @Nullable ResultReceiverWrapper cb) { + // Will not be called. + throw new AssertionError(); + } + + @Override + public boolean sendMediaButton(@Nullable KeyEvent mediaButton) { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void registerCallbackListener(@Nullable IMediaControllerCallback cb) { + MediaSessionImplApi21 mediaSessionImpl = mMediaSessionImplRef.get(); + if (mediaSessionImpl == null || cb == null) { + return; + } + int callingPid = Binder.getCallingPid(); + int callingUid = Binder.getCallingUid(); + RemoteUserInfo info = + new RemoteUserInfo(RemoteUserInfo.LEGACY_CONTROLLER, callingPid, callingUid); + mediaSessionImpl.mExtraControllerCallbacks.register(cb, info); + synchronized (mediaSessionImpl.mLock) { + if (mediaSessionImpl.mRegistrationCallbackHandler != null) { + mediaSessionImpl.mRegistrationCallbackHandler.postCallbackRegistered( + callingPid, callingUid); + } + } + } + + @Override + public void unregisterCallbackListener(@Nullable IMediaControllerCallback cb) { + MediaSessionImplApi21 mediaSessionImpl = mMediaSessionImplRef.get(); + if (mediaSessionImpl == null || cb == null) { + return; + } + mediaSessionImpl.mExtraControllerCallbacks.unregister(cb); + + int callingPid = Binder.getCallingPid(); + int callingUid = Binder.getCallingUid(); + synchronized (mediaSessionImpl.mLock) { + if (mediaSessionImpl.mRegistrationCallbackHandler != null) { + mediaSessionImpl.mRegistrationCallbackHandler.postCallbackUnregistered( + callingPid, callingUid); + } + } + } + + @Override + public String getPackageName() { + // Will not be called. + throw new AssertionError(); + } + + @Nullable + @Override + public Bundle getSessionInfo() { + MediaSessionImplApi21 mediaSessionImpl = mMediaSessionImplRef.get(); + return mediaSessionImpl.mSessionInfo == null + ? null + : new Bundle(mediaSessionImpl.mSessionInfo); + } + + @Override + public String getTag() { + // Will not be called. + throw new AssertionError(); + } + + @Override + public PendingIntent getLaunchPendingIntent() { + // Will not be called. + throw new AssertionError(); + } + + @Override + @SessionFlags + public long getFlags() { + // Will not be called. + throw new AssertionError(); + } + + @Override + public ParcelableVolumeInfo getVolumeAttributes() { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void adjustVolume(int direction, int flags, @Nullable String packageName) { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void setVolumeTo(int value, int flags, @Nullable String packageName) { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void prepare() throws RemoteException { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void prepareFromMediaId(@Nullable String mediaId, @Nullable Bundle extras) { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void prepareFromSearch(@Nullable String query, @Nullable Bundle extras) { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void prepareFromUri(@Nullable Uri uri, @Nullable Bundle extras) { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void play() throws RemoteException { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void playFromMediaId(@Nullable String mediaId, @Nullable Bundle extras) { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void playFromSearch(@Nullable String query, @Nullable Bundle extras) { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void playFromUri(@Nullable Uri uri, @Nullable Bundle extras) { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void skipToQueueItem(long id) { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void pause() { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void stop() { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void next() { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void previous() { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void fastForward() { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void rewind() { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void seekTo(long pos) { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void rate(@Nullable RatingCompat rating) { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void rateWithExtras(@Nullable RatingCompat rating, @Nullable Bundle extras) { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void setPlaybackSpeed(float speed) { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void setCaptioningEnabled(boolean enabled) { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void setRepeatMode(int repeatMode) { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void setShuffleModeEnabledRemoved(boolean enabled) { + // Do nothing. + } + + @Override + public void setShuffleMode(int shuffleMode) throws RemoteException { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void sendCustomAction(@Nullable String action, @Nullable Bundle args) + throws RemoteException { + // Will not be called. + throw new AssertionError(); + } + + @Override + public MediaMetadataCompat getMetadata() { + // Will not be called. + throw new AssertionError(); + } + + @Nullable + @Override + public PlaybackStateCompat getPlaybackState() { + MediaSessionImplApi21 mediaSessionImpl = mMediaSessionImplRef.get(); + if (mediaSessionImpl != null) { + return getStateWithUpdatedPosition( + mediaSessionImpl.mPlaybackState, mediaSessionImpl.mMetadata); + } else { + return null; + } + } + + @Nullable + @Override + public List getQueue() { + // Will not be called. + return null; + } + + @Override + public void addQueueItem(@Nullable MediaDescriptionCompat descriptionCompat) { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void addQueueItemAt(@Nullable MediaDescriptionCompat descriptionCompat, int index) { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void removeQueueItem(@Nullable MediaDescriptionCompat description) { + // Will not be called. + throw new AssertionError(); + } + + @Override + public void removeQueueItemAt(int index) { + // Will not be called. + throw new AssertionError(); + } + + @Override + public CharSequence getQueueTitle() { + // Will not be called. + throw new AssertionError(); + } + + @Override + public Bundle getExtras() { + // Will not be called. + throw new AssertionError(); + } + + @Override + @RatingCompat.Style + public int getRatingType() { + MediaSessionImplApi21 mediaSessionImpl = mMediaSessionImplRef.get(); + return mediaSessionImpl != null ? mediaSessionImpl.mRatingType : RatingCompat.RATING_NONE; + } + + @Override + public boolean isCaptioningEnabled() { + MediaSessionImplApi21 mediaSessionImpl = mMediaSessionImplRef.get(); + return mediaSessionImpl != null && mediaSessionImpl.mCaptioningEnabled; + } + + @Override + @PlaybackStateCompat.RepeatMode + public int getRepeatMode() { + MediaSessionImplApi21 mediaSessionImpl = mMediaSessionImplRef.get(); + return mediaSessionImpl != null + ? mediaSessionImpl.mRepeatMode + : PlaybackStateCompat.REPEAT_MODE_INVALID; + } + + @Override + public boolean isShuffleModeEnabledRemoved() { + return false; + } + + @Override + @PlaybackStateCompat.ShuffleMode + public int getShuffleMode() { + MediaSessionImplApi21 mediaSessionImpl = mMediaSessionImplRef.get(); + return mediaSessionImpl != null + ? mediaSessionImpl.mShuffleMode + : PlaybackStateCompat.SHUFFLE_MODE_INVALID; + } + + @Override + public boolean isTransportControlEnabled() { + // Will not be called. + throw new AssertionError(); + } + } + } + + @RequiresApi(22) + static class MediaSessionImplApi22 extends MediaSessionImplApi21 { + MediaSessionImplApi22( + Context context, + String tag, + @Nullable VersionedParcelable session2Token, + @Nullable Bundle sessionInfo) { + super(context, tag, session2Token, sessionInfo); + } + + MediaSessionImplApi22(Object mediaSession) { + super(mediaSession); + } + + @Override + public void setRatingType(@RatingCompat.Style int type) { + mSessionFwk.setRatingType(type); + } + } + + @RequiresApi(28) + static class MediaSessionImplApi28 extends MediaSessionImplApi22 { + MediaSessionImplApi28( + Context context, + String tag, + @Nullable VersionedParcelable session2Token, + @Nullable Bundle sessionInfo) { + super(context, tag, session2Token, sessionInfo); + } + + MediaSessionImplApi28(Object mediaSession) { + super(mediaSession); + } + + @Override + public void setCurrentControllerInfo(@Nullable RemoteUserInfo remoteUserInfo) { + // No-op. {@link MediaSession#getCurrentControllerInfo} would work. + } + + @Nullable + @Override + public final RemoteUserInfo getCurrentControllerInfo() { + android.media.session.MediaSessionManager.RemoteUserInfo info = + ((MediaSession) mSessionFwk).getCurrentControllerInfo(); + return new RemoteUserInfo(info); + } + } + + @RequiresApi(29) + static class MediaSessionImplApi29 extends MediaSessionImplApi28 { + MediaSessionImplApi29( + Context context, + String tag, + @Nullable VersionedParcelable session2Token, + @Nullable Bundle sessionInfo) { + super(context, tag, session2Token, sessionInfo); + } + + MediaSessionImplApi29(Object mediaSession) { + super(mediaSession); + mSessionInfo = ((MediaSession) mediaSession).getController().getSessionInfo(); + } + + @Override + public MediaSession createFwkMediaSession( + Context context, String tag, @Nullable Bundle sessionInfo) { + return new MediaSession(context, tag, sessionInfo); + } + } + + static final class RegistrationCallbackHandler extends Handler { + private static final int MSG_CALLBACK_REGISTERED = 1001; + private static final int MSG_CALLBACK_UNREGISTERED = 1002; + + private final RegistrationCallback mCallback; + + RegistrationCallbackHandler(Looper looper, RegistrationCallback callback) { + super(looper); + mCallback = callback; + } + + @Override + public void handleMessage(Message msg) { + super.handleMessage(msg); + switch (msg.what) { + case MSG_CALLBACK_REGISTERED: + mCallback.onCallbackRegistered(msg.arg1, msg.arg2); + break; + case MSG_CALLBACK_UNREGISTERED: + mCallback.onCallbackUnregistered(msg.arg1, msg.arg2); + break; + } + } + + public void postCallbackRegistered(int callingPid, int callingUid) { + obtainMessage(MSG_CALLBACK_REGISTERED, callingPid, callingUid).sendToTarget(); + } + + public void postCallbackUnregistered(int callingPid, int callingUid) { + obtainMessage(MSG_CALLBACK_UNREGISTERED, callingPid, callingUid).sendToTarget(); + } + } +} diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/MediaSessionManager.java b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaSessionManager.java new file mode 100644 index 0000000000..48ed1fd936 --- /dev/null +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/MediaSessionManager.java @@ -0,0 +1,461 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.session.legacy; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY; + +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Process; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Log; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.RestrictTo; +import androidx.core.util.ObjectsCompat; +import androidx.media3.common.util.UnstableApi; + +/** + * Provides support for interacting with {@link MediaSessionCompat media sessions} that applications + * have published to express their ongoing media playback state. + * + * @see MediaSessionCompat + * @see MediaControllerCompat + */ +@UnstableApi +@RestrictTo(LIBRARY) +public final class MediaSessionManager { + static final String TAG = "MediaSessionManager"; + static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private static final Object sLock = new Object(); + @Nullable private static volatile MediaSessionManager sSessionManager; + + MediaSessionManagerImpl mImpl; + + /** + * Gets an instance of the media session manager associated with the context. + * + * @return The MediaSessionManager instance for this context. + */ + public static MediaSessionManager getSessionManager(Context context) { + if (context == null) { + throw new IllegalArgumentException("context cannot be null"); + } + synchronized (sLock) { + if (sSessionManager == null) { + sSessionManager = new MediaSessionManager(context.getApplicationContext()); + } + return sSessionManager; + } + } + + private MediaSessionManager(Context context) { + if (Build.VERSION.SDK_INT >= 28) { + mImpl = new MediaSessionManagerImplApi28(context); + } else if (Build.VERSION.SDK_INT >= 21) { + mImpl = new MediaSessionManagerImplApi21(context); + } else { + mImpl = new MediaSessionManagerImplBase(context); + } + } + + /** + * Checks whether the remote user is a trusted app. + * + *

An app is trusted if the app holds the android.Manifest.permission.MEDIA_CONTENT_CONTROL + * permission or has an enabled notification listener. + * + * @param userInfo The remote user info from either {@link + * MediaSessionCompat#getCurrentControllerInfo()} and {@link + * MediaBrowserServiceCompat#getCurrentBrowserInfo()}. + * @return {@code true} if the remote user is trusted and its package name matches with the UID. + * {@code false} otherwise. + */ + public boolean isTrustedForMediaControl(RemoteUserInfo userInfo) { + if (userInfo == null) { + throw new IllegalArgumentException("userInfo should not be null"); + } + return mImpl.isTrustedForMediaControl(userInfo.mImpl); + } + + Context getContext() { + return mImpl.getContext(); + } + + interface MediaSessionManagerImpl { + Context getContext(); + + boolean isTrustedForMediaControl(RemoteUserInfoImpl userInfo); + } + + interface RemoteUserInfoImpl { + String getPackageName(); + + int getPid(); + + int getUid(); + } + + /** + * Information of a remote user of {@link MediaSessionCompat} or {@link + * MediaBrowserServiceCompat}. This can be used to decide whether the remote user is trusted app, + * and also differentiate caller of {@link MediaSessionCompat} and {@link + * MediaBrowserServiceCompat} callbacks. + * + *

See {@link #equals(Object)} to take a look at how it differentiate media controller. + * + * @see #isTrustedForMediaControl(RemoteUserInfo) + */ + public static final class RemoteUserInfo { + /** + * Used by {@link #getPackageName()} when the session is connected to the legacy controller + * whose exact package name cannot be obtained. + */ + public static final String LEGACY_CONTROLLER = "android.media.session.MediaController"; + + /** Represents an unknown pid of an application. */ + public static final int UNKNOWN_PID = -1; + + /** Represents an unknown uid of an application. */ + public static final int UNKNOWN_UID = -1; + + RemoteUserInfoImpl mImpl; + + /** + * Public constructor. + * + *

Can be used for {@link MediaSessionManager#isTrustedForMediaControl(RemoteUserInfo)}}. + * + * @param packageName package name of the remote user + * @param pid pid of the remote user + * @param uid uid of the remote user + * @throws IllegalArgumentException if package name is empty + */ + public RemoteUserInfo(@Nullable String packageName, int pid, int uid) { + if (packageName == null) { + throw new NullPointerException("package shouldn't be null"); + } else if (TextUtils.isEmpty(packageName)) { + throw new IllegalArgumentException("packageName should be nonempty"); + } + if (Build.VERSION.SDK_INT >= 28) { + mImpl = new MediaSessionManagerImplApi28.RemoteUserInfoImplApi28(packageName, pid, uid); + } else { + // Note: We need to include IBinder to distinguish controllers in a process. + mImpl = new MediaSessionManagerImplBase.RemoteUserInfoImplBase(packageName, pid, uid); + } + } + + /** + * Public constructor for internal uses. + * + *

Internal code MUST use this on SDK >= 28 to distinguish individual RemoteUserInfos in a + * process. + * + * @param remoteUserInfo Framework RemoteUserInfo + * @throws IllegalArgumentException if package name is empty + */ + @RequiresApi(28) + public RemoteUserInfo(android.media.session.MediaSessionManager.RemoteUserInfo remoteUserInfo) { + // Framework RemoteUserInfo doesn't ensure non-null nor non-empty package name, + // so ensure package name here instead. + String packageName = + MediaSessionManagerImplApi28.RemoteUserInfoImplApi28.getPackageName(remoteUserInfo); + if (packageName == null) { + throw new NullPointerException("package shouldn't be null"); + } else if (TextUtils.isEmpty(packageName)) { + throw new IllegalArgumentException("packageName should be nonempty"); + } + mImpl = new MediaSessionManagerImplApi28.RemoteUserInfoImplApi28(remoteUserInfo); + } + + /** + * @return package name of the controller. Can be {@link #LEGACY_CONTROLLER} if the package name + * cannot be obtained. + */ + public String getPackageName() { + return mImpl.getPackageName(); + } + + /** + * @return pid of the controller. Can be a negative value if the pid cannot be obtained. + */ + public int getPid() { + return mImpl.getPid(); + } + + /** + * @return uid of the controller. Can be a negative value if the uid cannot be obtained. + */ + public int getUid() { + return mImpl.getUid(); + } + + /** + * Returns equality of two RemoteUserInfo by comparing their package name, UID, and PID. + * + *

On P and before (API ≤ 28), two RemoteUserInfo objects equal if following conditions + * are met: + * + *

    + *
  1. UID and package name are the same + *
  2. One of the RemoteUserInfo's PID is UNKNOWN_PID or both of RemoteUserInfo's PID are the + * same + *
+ * + * @param obj the reference object with which to compare. + * @return {@code true} if equals, {@code false} otherwise + */ + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof RemoteUserInfo)) { + return false; + } + return mImpl.equals(((RemoteUserInfo) obj).mImpl); + } + + @Override + public int hashCode() { + return mImpl.hashCode(); + } + } + + private static class MediaSessionManagerImplBase + implements MediaSessionManager.MediaSessionManagerImpl { + private static final String TAG = MediaSessionManager.TAG; + private static final boolean DEBUG = MediaSessionManager.DEBUG; + + private static final String PERMISSION_STATUS_BAR_SERVICE = + "android.permission.STATUS_BAR_SERVICE"; + private static final String PERMISSION_MEDIA_CONTENT_CONTROL = + "android.permission.MEDIA_CONTENT_CONTROL"; + private static final String ENABLED_NOTIFICATION_LISTENERS = "enabled_notification_listeners"; + + Context mContext; + ContentResolver mContentResolver; + + MediaSessionManagerImplBase(Context context) { + mContext = context; + mContentResolver = mContext.getContentResolver(); + } + + @Override + public Context getContext() { + return mContext; + } + + @Override + @SuppressWarnings("deprecation") + public boolean isTrustedForMediaControl(MediaSessionManager.RemoteUserInfoImpl userInfo) { + try { + ApplicationInfo applicationInfo = + mContext.getPackageManager().getApplicationInfo(userInfo.getPackageName(), 0); + if (applicationInfo == null) { + return false; + } + } catch (PackageManager.NameNotFoundException e) { + if (DEBUG) { + Log.d(TAG, "Package " + userInfo.getPackageName() + " doesn't exist"); + } + return false; + } + return isPermissionGranted(userInfo, PERMISSION_STATUS_BAR_SERVICE) + || isPermissionGranted(userInfo, PERMISSION_MEDIA_CONTENT_CONTROL) + || userInfo.getUid() == Process.SYSTEM_UID + || isEnabledNotificationListener(userInfo); + } + + private boolean isPermissionGranted( + MediaSessionManager.RemoteUserInfoImpl userInfo, String permission) { + if (userInfo.getPid() < 0) { + // This may happen for the MediaBrowserServiceCompat#onGetRoot(). + return mContext.getPackageManager().checkPermission(permission, userInfo.getPackageName()) + == PackageManager.PERMISSION_GRANTED; + } + return mContext.checkPermission(permission, userInfo.getPid(), userInfo.getUid()) + == PackageManager.PERMISSION_GRANTED; + } + + /** + * This checks if the component is an enabled notification listener for the specified user. + * Enabled components may only operate on behalf of the user they're running as. + * + * @return True if the component is enabled, false otherwise + */ + @SuppressWarnings("StringSplitter") + boolean isEnabledNotificationListener(MediaSessionManager.RemoteUserInfoImpl userInfo) { + final String enabledNotifListeners = + Settings.Secure.getString(mContentResolver, ENABLED_NOTIFICATION_LISTENERS); + if (enabledNotifListeners != null) { + final String[] components = enabledNotifListeners.split(":"); + for (int i = 0; i < components.length; i++) { + final ComponentName component = ComponentName.unflattenFromString(components[i]); + if (component != null) { + if (component.getPackageName().equals(userInfo.getPackageName())) { + return true; + } + } + } + } + return false; + } + + static class RemoteUserInfoImplBase implements MediaSessionManager.RemoteUserInfoImpl { + private String mPackageName; + private int mPid; + private int mUid; + + RemoteUserInfoImplBase(String packageName, int pid, int uid) { + mPackageName = packageName; + mPid = pid; + mUid = uid; + } + + @Override + public String getPackageName() { + return mPackageName; + } + + @Override + public int getPid() { + return mPid; + } + + @Override + public int getUid() { + return mUid; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof RemoteUserInfoImplBase)) { + return false; + } + RemoteUserInfoImplBase otherUserInfo = (RemoteUserInfoImplBase) obj; + if (mPid < 0 || otherUserInfo.mPid < 0) { + // Only compare package name and UID when PID is unknown. + return TextUtils.equals(mPackageName, otherUserInfo.mPackageName) + && mUid == otherUserInfo.mUid; + } + return TextUtils.equals(mPackageName, otherUserInfo.mPackageName) + && mPid == otherUserInfo.mPid + && mUid == otherUserInfo.mUid; + } + + @Override + public int hashCode() { + return ObjectsCompat.hash(mPackageName, mUid); + } + } + } + + @RequiresApi(21) + private static class MediaSessionManagerImplApi21 extends MediaSessionManagerImplBase { + MediaSessionManagerImplApi21(Context context) { + super(context); + mContext = context; + } + + @Override + public boolean isTrustedForMediaControl(MediaSessionManager.RemoteUserInfoImpl userInfo) { + + return hasMediaControlPermission(userInfo) || super.isTrustedForMediaControl(userInfo); + } + + /** Checks the caller has android.Manifest.permission.MEDIA_CONTENT_CONTROL permission. */ + private boolean hasMediaControlPermission(MediaSessionManager.RemoteUserInfoImpl userInfo) { + return getContext() + .checkPermission( + android.Manifest.permission.MEDIA_CONTENT_CONTROL, + userInfo.getPid(), + userInfo.getUid()) + == PackageManager.PERMISSION_GRANTED; + } + } + + @RequiresApi(28) + private static final class MediaSessionManagerImplApi28 extends MediaSessionManagerImplApi21 { + @Nullable android.media.session.MediaSessionManager mObject; + + MediaSessionManagerImplApi28(Context context) { + super(context); + mObject = + (android.media.session.MediaSessionManager) + context.getSystemService(Context.MEDIA_SESSION_SERVICE); + } + + @Override + public boolean isTrustedForMediaControl(MediaSessionManager.RemoteUserInfoImpl userInfo) { + // Don't use framework's isTrustedForMediaControl(). + // In P, framework's isTrustedForMediaControl() checks whether the UID, PID, + // and package name match. In MediaSession/MediaController, Context#getPackageName() is + // used by MediaController to tell MediaSession the package name. + // However, UID, PID and Context#getPackageName() may not match if a activity/service runs + // on the another app's process by specifying android:process in the AndroidManifest.xml. + // In that case, this check will always fail. + // Alternative way is to use Context#getOpPackageName() for sending the package name, + // but it's hidden so we cannot use it. + return super.isTrustedForMediaControl(userInfo); + } + + /** + * This extends {@link RemoteUserInfoImplBase} on purpose not to use frameworks' equals() and + * hashCode() implementation for two reasons: + * + *

1. To override PID checks when one of them are unknown. PID can be unknown between + * MediaBrowserCompat / MediaBrowserServiceCompat 2. To skip checking hidden binder. Framework's + * {@link android.media.session.MediaSessionManager.RemoteUserInfo} also checks internal binder + * to distinguish multiple {@link android.media.session.MediaController} and {@link + * android.media.browse.MediaBrowser} in a process. However, when the binders in both + * RemoteUserInfos are {@link null}, framework's equal() specially handles the case and returns + * {@code false}. This cause two issues that we need to workaround. Issue a) RemoteUserInfos + * created by key events are considered as all different. issue b) RemoteUserInfos created with + * public constructors are considers as all different. + */ + @RequiresApi(28) + private static final class RemoteUserInfoImplApi28 extends RemoteUserInfoImplBase { + final android.media.session.MediaSessionManager.RemoteUserInfo mObject; + + RemoteUserInfoImplApi28(String packageName, int pid, int uid) { + super(packageName, pid, uid); + mObject = + new android.media.session.MediaSessionManager.RemoteUserInfo(packageName, pid, uid); + } + + RemoteUserInfoImplApi28( + android.media.session.MediaSessionManager.RemoteUserInfo remoteUserInfo) { + super(remoteUserInfo.getPackageName(), remoteUserInfo.getPid(), remoteUserInfo.getUid()); + mObject = remoteUserInfo; + } + + static String getPackageName( + android.media.session.MediaSessionManager.RemoteUserInfo remoteUserInfo) { + return remoteUserInfo.getPackageName(); + } + } + } +} diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/ParcelableVolumeInfo.java b/libraries/session/src/main/java/androidx/media3/session/legacy/ParcelableVolumeInfo.java new file mode 100644 index 0000000000..993fc3b6ae --- /dev/null +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/ParcelableVolumeInfo.java @@ -0,0 +1,83 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.session.legacy; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY; + +import android.annotation.SuppressLint; +import android.os.Parcel; +import android.os.Parcelable; +import androidx.annotation.RestrictTo; +import androidx.media3.common.util.UnstableApi; + +/** + * Convenience class for passing information about the audio configuration of a {@link + * MediaSessionCompat}. + */ +@UnstableApi +@RestrictTo(LIBRARY) +@SuppressLint("BanParcelableUsage") +public class ParcelableVolumeInfo implements Parcelable { + public int volumeType; + public int audioStream; + public int controlType; + public int maxVolume; + public int currentVolume; + + public ParcelableVolumeInfo( + int volumeType, int audioStream, int controlType, int maxVolume, int currentVolume) { + this.volumeType = volumeType; + this.audioStream = audioStream; + this.controlType = controlType; + this.maxVolume = maxVolume; + this.currentVolume = currentVolume; + } + + public ParcelableVolumeInfo(Parcel from) { + volumeType = from.readInt(); + controlType = from.readInt(); + maxVolume = from.readInt(); + currentVolume = from.readInt(); + audioStream = from.readInt(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(volumeType); + dest.writeInt(controlType); + dest.writeInt(maxVolume); + dest.writeInt(currentVolume); + dest.writeInt(audioStream); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public ParcelableVolumeInfo createFromParcel(Parcel in) { + return new ParcelableVolumeInfo(in); + } + + @Override + public ParcelableVolumeInfo[] newArray(int size) { + return new ParcelableVolumeInfo[size]; + } + }; +} diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/PlaybackStateCompat.java b/libraries/session/src/main/java/androidx/media3/session/legacy/PlaybackStateCompat.java new file mode 100644 index 0000000000..db371d82ea --- /dev/null +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/PlaybackStateCompat.java @@ -0,0 +1,1555 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.session.legacy; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY; +import static androidx.media3.common.util.Assertions.checkNotNull; + +import android.annotation.SuppressLint; +import android.media.session.PlaybackState; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.text.TextUtils; +import android.view.KeyEvent; +import androidx.annotation.DoNotInline; +import androidx.annotation.IntDef; +import androidx.annotation.LongDef; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.RestrictTo; +import androidx.media3.common.util.UnstableApi; +import com.google.common.collect.ImmutableList; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; + +/** + * Playback state for a {@link MediaSessionCompat}. This includes a state like {@link + * PlaybackStateCompat#STATE_PLAYING}, the current playback position, and the current control + * capabilities. + */ +@UnstableApi +@RestrictTo(LIBRARY) +@SuppressLint("BanParcelableUsage") +public final class PlaybackStateCompat implements Parcelable { + + /** */ + @LongDef( + flag = true, + value = { + ACTION_STOP, + ACTION_PAUSE, + ACTION_PLAY, + ACTION_REWIND, + ACTION_SKIP_TO_PREVIOUS, + ACTION_SKIP_TO_NEXT, + ACTION_FAST_FORWARD, + ACTION_SET_RATING, + ACTION_SEEK_TO, + ACTION_PLAY_PAUSE, + ACTION_PLAY_FROM_MEDIA_ID, + ACTION_PLAY_FROM_SEARCH, + ACTION_SKIP_TO_QUEUE_ITEM, + ACTION_PLAY_FROM_URI, + ACTION_PREPARE, + ACTION_PREPARE_FROM_MEDIA_ID, + ACTION_PREPARE_FROM_SEARCH, + ACTION_PREPARE_FROM_URI, + ACTION_SET_REPEAT_MODE, + ACTION_SET_SHUFFLE_MODE, + ACTION_SET_CAPTIONING_ENABLED, + ACTION_SET_PLAYBACK_SPEED + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Actions {} + + /** */ + @LongDef({ + ACTION_STOP, + ACTION_PAUSE, + ACTION_PLAY, + ACTION_REWIND, + ACTION_SKIP_TO_PREVIOUS, + ACTION_SKIP_TO_NEXT, + ACTION_FAST_FORWARD, + ACTION_PLAY_PAUSE + }) + @Retention(RetentionPolicy.SOURCE) + public @interface MediaKeyAction {} + + /** + * Indicates this session supports the stop command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_STOP = 1 << 0; + + /** + * Indicates this session supports the pause command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_PAUSE = 1 << 1; + + /** + * Indicates this session supports the play command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_PLAY = 1 << 2; + + /** + * Indicates this session supports the rewind command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_REWIND = 1 << 3; + + /** + * Indicates this session supports the previous command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_SKIP_TO_PREVIOUS = 1 << 4; + + /** + * Indicates this session supports the next command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_SKIP_TO_NEXT = 1 << 5; + + /** + * Indicates this session supports the fast forward command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_FAST_FORWARD = 1 << 6; + + /** + * Indicates this session supports the set rating command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_SET_RATING = 1 << 7; + + /** + * Indicates this session supports the seek to command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_SEEK_TO = 1 << 8; + + /** + * Indicates this session supports the play/pause toggle command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_PLAY_PAUSE = 1 << 9; + + /** + * Indicates this session supports the play from media id command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_PLAY_FROM_MEDIA_ID = 1 << 10; + + /** + * Indicates this session supports the play from search command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_PLAY_FROM_SEARCH = 1 << 11; + + /** + * Indicates this session supports the skip to queue item command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_SKIP_TO_QUEUE_ITEM = 1 << 12; + + /** + * Indicates this session supports the play from URI command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_PLAY_FROM_URI = 1 << 13; + + /** + * Indicates this session supports the prepare command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_PREPARE = 1 << 14; + + /** + * Indicates this session supports the prepare from media id command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_PREPARE_FROM_MEDIA_ID = 1 << 15; + + /** + * Indicates this session supports the prepare from search command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_PREPARE_FROM_SEARCH = 1 << 16; + + /** + * Indicates this session supports the prepare from URI command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_PREPARE_FROM_URI = 1 << 17; + + /** + * Indicates this session supports the set repeat mode command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_SET_REPEAT_MODE = 1 << 18; + + /** + * Indicates this session supports the set shuffle mode enabled command. + * + * @see Builder#setActions(long) + * @deprecated Use {@link #ACTION_SET_SHUFFLE_MODE} instead. + */ + @Deprecated public static final long ACTION_SET_SHUFFLE_MODE_ENABLED = 1 << 19; + + /** + * Indicates this session supports the set captioning enabled command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_SET_CAPTIONING_ENABLED = 1 << 20; + + /** + * Indicates this session supports the set shuffle mode command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_SET_SHUFFLE_MODE = 1 << 21; + + /** + * Indicates this session supports the set playback speed command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_SET_PLAYBACK_SPEED = 1 << 22; + + /** */ + @IntDef({ + STATE_NONE, + STATE_STOPPED, + STATE_PAUSED, + STATE_PLAYING, + STATE_FAST_FORWARDING, + STATE_REWINDING, + STATE_BUFFERING, + STATE_ERROR, + STATE_CONNECTING, + STATE_SKIPPING_TO_PREVIOUS, + STATE_SKIPPING_TO_NEXT, + STATE_SKIPPING_TO_QUEUE_ITEM + }) + @Retention(RetentionPolicy.SOURCE) + public @interface State {} + + /** + * This is the default playback state and indicates that no media has been added yet, or the + * performer has been reset and has no content to play. + * + * @see Builder#setState + */ + public static final int STATE_NONE = 0; + + /** + * State indicating this item is currently stopped. + * + * @see Builder#setState + */ + public static final int STATE_STOPPED = 1; + + /** + * State indicating this item is currently paused. + * + * @see Builder#setState + */ + public static final int STATE_PAUSED = 2; + + /** + * State indicating this item is currently playing. + * + * @see Builder#setState + */ + public static final int STATE_PLAYING = 3; + + /** + * State indicating this item is currently fast forwarding. + * + * @see Builder#setState + */ + public static final int STATE_FAST_FORWARDING = 4; + + /** + * State indicating this item is currently rewinding. + * + * @see Builder#setState + */ + public static final int STATE_REWINDING = 5; + + /** + * State indicating this item is currently buffering and will begin playing when enough data has + * buffered. + * + * @see Builder#setState + */ + public static final int STATE_BUFFERING = 6; + + /** + * State indicating this item is currently in an error state. The error code should also be set + * when entering this state. + * + * @see Builder#setState + * @see Builder#setErrorMessage(int, CharSequence) + */ + public static final int STATE_ERROR = 7; + + /** + * State indicating the class doing playback is currently connecting to a route. Depending on the + * implementation you may return to the previous state when the connection finishes or enter + * {@link #STATE_NONE}. If the connection failed {@link #STATE_ERROR} should be used. + * + *

On devices earlier than API 21, this will appear as {@link #STATE_BUFFERING} + * + * @see Builder#setState + */ + public static final int STATE_CONNECTING = 8; + + /** + * State indicating the player is currently skipping to the previous item. + * + * @see Builder#setState + */ + public static final int STATE_SKIPPING_TO_PREVIOUS = 9; + + /** + * State indicating the player is currently skipping to the next item. + * + * @see Builder#setState + */ + public static final int STATE_SKIPPING_TO_NEXT = 10; + + /** + * State indicating the player is currently skipping to a specific item in the queue. + * + *

On devices earlier than API 21, this will appear as {@link #STATE_SKIPPING_TO_NEXT} + * + * @see Builder#setState + */ + public static final int STATE_SKIPPING_TO_QUEUE_ITEM = 11; + + /** Use this value for the position to indicate the position is not known. */ + public static final long PLAYBACK_POSITION_UNKNOWN = -1; + + /** */ + @IntDef({ + REPEAT_MODE_INVALID, + REPEAT_MODE_NONE, + REPEAT_MODE_ONE, + REPEAT_MODE_ALL, + REPEAT_MODE_GROUP + }) + @Retention(RetentionPolicy.SOURCE) + public @interface RepeatMode {} + + /** + * {@code MediaControllerCompat.TransportControls#getRepeatMode()} returns this value when the + * session is not ready for providing its repeat mode. + */ + public static final int REPEAT_MODE_INVALID = -1; + + /** + * Use this value with {@link MediaControllerCompat.TransportControls#setRepeatMode} to indicate + * that the playback will be stopped at the end of the playing media list. + */ + public static final int REPEAT_MODE_NONE = 0; + + /** + * Use this value with {@link MediaControllerCompat.TransportControls#setRepeatMode} to indicate + * that the playback of the current playing media item will be repeated. + */ + public static final int REPEAT_MODE_ONE = 1; + + /** + * Use this value with {@link MediaControllerCompat.TransportControls#setRepeatMode} to indicate + * that the playback of the playing media list will be repeated. + */ + public static final int REPEAT_MODE_ALL = 2; + + /** + * Use this value with {@link MediaControllerCompat.TransportControls#setRepeatMode} to indicate + * that the playback of the playing media group will be repeated. A group is a logical block of + * media items which is specified in the section 5.7 of the Bluetooth AVRCP 1.6. + */ + public static final int REPEAT_MODE_GROUP = 3; + + /** */ + @IntDef({SHUFFLE_MODE_INVALID, SHUFFLE_MODE_NONE, SHUFFLE_MODE_ALL, SHUFFLE_MODE_GROUP}) + @Retention(RetentionPolicy.SOURCE) + public @interface ShuffleMode {} + + /** + * {@code MediaControllerCompat.TransportControls#getShuffleMode()} returns this value when the + * session is not ready for providing its shuffle mode. + */ + public static final int SHUFFLE_MODE_INVALID = -1; + + /** + * Use this value with {@link MediaControllerCompat.TransportControls#setShuffleMode} to indicate + * that the media list will be played in order. + */ + public static final int SHUFFLE_MODE_NONE = 0; + + /** + * Use this value with {@link MediaControllerCompat.TransportControls#setShuffleMode} to indicate + * that the media list will be played in shuffled order. + */ + public static final int SHUFFLE_MODE_ALL = 1; + + /** + * Use this value with {@link MediaControllerCompat.TransportControls#setShuffleMode} to indicate + * that the media group will be played in shuffled order. A group is a logical block of media + * items which is specified in the section 5.7 of the Bluetooth AVRCP 1.6. + */ + public static final int SHUFFLE_MODE_GROUP = 2; + + @IntDef({ + ERROR_CODE_UNKNOWN_ERROR, + ERROR_CODE_APP_ERROR, + ERROR_CODE_NOT_SUPPORTED, + ERROR_CODE_AUTHENTICATION_EXPIRED, + ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED, + ERROR_CODE_CONCURRENT_STREAM_LIMIT, + ERROR_CODE_PARENTAL_CONTROL_RESTRICTED, + ERROR_CODE_NOT_AVAILABLE_IN_REGION, + ERROR_CODE_CONTENT_ALREADY_PLAYING, + ERROR_CODE_SKIP_LIMIT_REACHED, + ERROR_CODE_ACTION_ABORTED, + ERROR_CODE_END_OF_QUEUE + }) + @Retention(RetentionPolicy.SOURCE) + private @interface ErrorCode {} + + /** + * This is the default error code and indicates that none of the other error codes applies. The + * error code should be set when entering {@link #STATE_ERROR}. + */ + public static final int ERROR_CODE_UNKNOWN_ERROR = 0; + + /** + * Error code when the application state is invalid to fulfill the request. The error code should + * be set when entering {@link #STATE_ERROR}. + */ + public static final int ERROR_CODE_APP_ERROR = 1; + + /** + * Error code when the request is not supported by the application. The error code should be set + * when entering {@link #STATE_ERROR}. + */ + public static final int ERROR_CODE_NOT_SUPPORTED = 2; + + /** + * Error code when the request cannot be performed because authentication has expired. The error + * code should be set when entering {@link #STATE_ERROR}. + */ + public static final int ERROR_CODE_AUTHENTICATION_EXPIRED = 3; + + /** + * Error code when a premium account is required for the request to succeed. The error code should + * be set when entering {@link #STATE_ERROR}. + */ + public static final int ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED = 4; + + /** + * Error code when too many concurrent streams are detected. The error code should be set when + * entering {@link #STATE_ERROR}. + */ + public static final int ERROR_CODE_CONCURRENT_STREAM_LIMIT = 5; + + /** + * Error code when the content is blocked due to parental controls. The error code should be set + * when entering {@link #STATE_ERROR}. + */ + public static final int ERROR_CODE_PARENTAL_CONTROL_RESTRICTED = 6; + + /** + * Error code when the content is blocked due to being regionally unavailable. The error code + * should be set when entering {@link #STATE_ERROR}. + */ + public static final int ERROR_CODE_NOT_AVAILABLE_IN_REGION = 7; + + /** + * Error code when the requested content is already playing. The error code should be set when + * entering {@link #STATE_ERROR}. + */ + public static final int ERROR_CODE_CONTENT_ALREADY_PLAYING = 8; + + /** + * Error code when the application cannot skip any more songs because skip limit is reached. The + * error code should be set when entering {@link #STATE_ERROR}. + */ + public static final int ERROR_CODE_SKIP_LIMIT_REACHED = 9; + + /** + * Error code when the action is interrupted due to some external event. The error code should be + * set when entering {@link #STATE_ERROR}. + */ + public static final int ERROR_CODE_ACTION_ABORTED = 10; + + /** + * Error code when the playback navigation (previous, next) is not possible because the queue was + * exhausted. The error code should be set when entering {@link #STATE_ERROR}. + */ + public static final int ERROR_CODE_END_OF_QUEUE = 11; + + // KeyEvent constants only available on API 11+ + private static final int KEYCODE_MEDIA_PAUSE = 127; + private static final int KEYCODE_MEDIA_PLAY = 126; + + /** + * Translates a given action into a matched key code defined in {@link KeyEvent}. The given action + * should be one of the following: + * + *

    + *
  • {@link PlaybackStateCompat#ACTION_PLAY} + *
  • {@link PlaybackStateCompat#ACTION_PAUSE} + *
  • {@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT} + *
  • {@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS} + *
  • {@link PlaybackStateCompat#ACTION_STOP} + *
  • {@link PlaybackStateCompat#ACTION_FAST_FORWARD} + *
  • {@link PlaybackStateCompat#ACTION_REWIND} + *
  • {@link PlaybackStateCompat#ACTION_PLAY_PAUSE} + *
+ * + * @param action The action to be translated. + * @return the key code matched to the given action. + */ + public static int toKeyCode(@MediaKeyAction long action) { + if (action == ACTION_PLAY) { + return KEYCODE_MEDIA_PLAY; + } else if (action == ACTION_PAUSE) { + return KEYCODE_MEDIA_PAUSE; + } else if (action == ACTION_SKIP_TO_NEXT) { + return KeyEvent.KEYCODE_MEDIA_NEXT; + } else if (action == ACTION_SKIP_TO_PREVIOUS) { + return KeyEvent.KEYCODE_MEDIA_PREVIOUS; + } else if (action == ACTION_STOP) { + return KeyEvent.KEYCODE_MEDIA_STOP; + } else if (action == ACTION_FAST_FORWARD) { + return KeyEvent.KEYCODE_MEDIA_FAST_FORWARD; + } else if (action == ACTION_REWIND) { + return KeyEvent.KEYCODE_MEDIA_REWIND; + } else if (action == ACTION_PLAY_PAUSE) { + return KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE; + } + return KeyEvent.KEYCODE_UNKNOWN; + } + + final int mState; + final long mPosition; + final long mBufferedPosition; + final float mSpeed; + final long mActions; + final int mErrorCode; + @Nullable final CharSequence mErrorMessage; + final long mUpdateTime; + List mCustomActions; + final long mActiveItemId; + @Nullable final Bundle mExtras; + + @Nullable private PlaybackState mStateFwk; + + PlaybackStateCompat( + int state, + long position, + long bufferedPosition, + float rate, + long actions, + int errorCode, + @Nullable CharSequence errorMessage, + long updateTime, + @Nullable List customActions, + long activeItemId, + @Nullable Bundle extras) { + mState = state; + mPosition = position; + mBufferedPosition = bufferedPosition; + mSpeed = rate; + mActions = actions; + mErrorCode = errorCode; + mErrorMessage = errorMessage; + mUpdateTime = updateTime; + mCustomActions = customActions == null ? ImmutableList.of() : new ArrayList<>(customActions); + mActiveItemId = activeItemId; + mExtras = extras; + } + + PlaybackStateCompat(Parcel in) { + mState = in.readInt(); + mPosition = in.readLong(); + mSpeed = in.readFloat(); + mUpdateTime = in.readLong(); + mBufferedPosition = in.readLong(); + mActions = in.readLong(); + mErrorMessage = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + List actions = in.createTypedArrayList(CustomAction.CREATOR); + mCustomActions = actions == null ? ImmutableList.of() : actions; + mActiveItemId = in.readLong(); + mExtras = in.readBundle(MediaSessionCompat.class.getClassLoader()); + // New attributes should be added at the end for backward compatibility. + mErrorCode = in.readInt(); + } + + @Override + public String toString() { + StringBuilder bob = new StringBuilder("PlaybackState {"); + bob.append("state=").append(mState); + bob.append(", position=").append(mPosition); + bob.append(", buffered position=").append(mBufferedPosition); + bob.append(", speed=").append(mSpeed); + bob.append(", updated=").append(mUpdateTime); + bob.append(", actions=").append(mActions); + bob.append(", error code=").append(mErrorCode); + bob.append(", error message=").append(mErrorMessage); + bob.append(", custom actions=").append(mCustomActions); + bob.append(", active item id=").append(mActiveItemId); + bob.append("}"); + return bob.toString(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mState); + dest.writeLong(mPosition); + dest.writeFloat(mSpeed); + dest.writeLong(mUpdateTime); + dest.writeLong(mBufferedPosition); + dest.writeLong(mActions); + TextUtils.writeToParcel(mErrorMessage, dest, flags); + dest.writeTypedList(mCustomActions); + dest.writeLong(mActiveItemId); + dest.writeBundle(mExtras); + // New attributes should be added at the end for backward compatibility. + dest.writeInt(mErrorCode); + } + + /** + * Get the current state of playback. One of the following: + * + *
    + *
  • {@link PlaybackStateCompat#STATE_NONE} + *
  • {@link PlaybackStateCompat#STATE_STOPPED} + *
  • {@link PlaybackStateCompat#STATE_PLAYING} + *
  • {@link PlaybackStateCompat#STATE_PAUSED} + *
  • {@link PlaybackStateCompat#STATE_FAST_FORWARDING} + *
  • {@link PlaybackStateCompat#STATE_REWINDING} + *
  • {@link PlaybackStateCompat#STATE_BUFFERING} + *
  • {@link PlaybackStateCompat#STATE_ERROR} + *
  • {@link PlaybackStateCompat#STATE_CONNECTING} + *
  • {@link PlaybackStateCompat#STATE_SKIPPING_TO_PREVIOUS} + *
  • {@link PlaybackStateCompat#STATE_SKIPPING_TO_NEXT} + *
  • {@link PlaybackStateCompat#STATE_SKIPPING_TO_QUEUE_ITEM} + *
+ */ + @State + public int getState() { + return mState; + } + + /** Get the playback position in ms at last position update time. */ + public long getPosition() { + return mPosition; + } + + /** + * Get the elapsed real time at which position was last updated. If the position has never been + * set this will return 0; + * + * @return The last time the position was updated. + */ + public long getLastPositionUpdateTime() { + return mUpdateTime; + } + + /** + * Get the current playback position in ms. + * + * @param timeDiff Only used for testing, otherwise it should be null. + * @return The current playback position in ms + */ + public long getCurrentPosition(Long timeDiff) { + long expectedPosition = + mPosition + + (long) + (mSpeed + * ((timeDiff != null) + ? timeDiff + : SystemClock.elapsedRealtime() - mUpdateTime)); + return Math.max(0, expectedPosition); + } + + /** + * Get the current buffered position in ms. This is the farthest playback point that can be + * reached from the current position using only buffered content. + */ + public long getBufferedPosition() { + return mBufferedPosition; + } + + /** + * Get the current playback speed as a multiple of normal playback. This should be negative when + * rewinding. A value of 1 means normal playback and 0 means paused. + * + * @return The current speed of playback. + */ + public float getPlaybackSpeed() { + return mSpeed; + } + + /** + * Get the current actions available on this session. This should use a bitmask of the available + * actions. + * + *
    + *
  • {@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS} + *
  • {@link PlaybackStateCompat#ACTION_REWIND} + *
  • {@link PlaybackStateCompat#ACTION_PLAY} + *
  • {@link PlaybackStateCompat#ACTION_PLAY_PAUSE} + *
  • {@link PlaybackStateCompat#ACTION_PAUSE} + *
  • {@link PlaybackStateCompat#ACTION_STOP} + *
  • {@link PlaybackStateCompat#ACTION_FAST_FORWARD} + *
  • {@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT} + *
  • {@link PlaybackStateCompat#ACTION_SEEK_TO} + *
  • {@link PlaybackStateCompat#ACTION_SET_RATING} + *
  • {@link PlaybackStateCompat#ACTION_PLAY_FROM_MEDIA_ID} + *
  • {@link PlaybackStateCompat#ACTION_PLAY_FROM_SEARCH} + *
  • {@link PlaybackStateCompat#ACTION_SKIP_TO_QUEUE_ITEM} + *
  • {@link PlaybackStateCompat#ACTION_PLAY_FROM_URI} + *
  • {@link PlaybackStateCompat#ACTION_PREPARE} + *
  • {@link PlaybackStateCompat#ACTION_PREPARE_FROM_MEDIA_ID} + *
  • {@link PlaybackStateCompat#ACTION_PREPARE_FROM_SEARCH} + *
  • {@link PlaybackStateCompat#ACTION_PREPARE_FROM_URI} + *
  • {@link PlaybackStateCompat#ACTION_SET_REPEAT_MODE} + *
  • {@link PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE} + *
  • {@link PlaybackStateCompat#ACTION_SET_CAPTIONING_ENABLED} + *
  • {@link PlaybackStateCompat#ACTION_SET_PLAYBACK_SPEED} + *
+ */ + @Actions + public long getActions() { + return mActions; + } + + /** Get the list of custom actions. */ + @Nullable + public List getCustomActions() { + return mCustomActions; + } + + /** + * Get the error code. This should be set when the state is {@link + * PlaybackStateCompat#STATE_ERROR}. + * + * @see #ERROR_CODE_UNKNOWN_ERROR + * @see #ERROR_CODE_APP_ERROR + * @see #ERROR_CODE_NOT_SUPPORTED + * @see #ERROR_CODE_AUTHENTICATION_EXPIRED + * @see #ERROR_CODE_PREMIUM_ACCOUNT_REQUIRED + * @see #ERROR_CODE_CONCURRENT_STREAM_LIMIT + * @see #ERROR_CODE_PARENTAL_CONTROL_RESTRICTED + * @see #ERROR_CODE_NOT_AVAILABLE_IN_REGION + * @see #ERROR_CODE_CONTENT_ALREADY_PLAYING + * @see #ERROR_CODE_SKIP_LIMIT_REACHED + * @see #ERROR_CODE_ACTION_ABORTED + * @see #ERROR_CODE_END_OF_QUEUE + * @see #getErrorMessage() + */ + @ErrorCode + public int getErrorCode() { + return mErrorCode; + } + + /** + * Get the user readable optional error message. This may be set when the state is {@link + * PlaybackStateCompat#STATE_ERROR}. + * + * @see #getErrorCode() + */ + @Nullable + public CharSequence getErrorMessage() { + return mErrorMessage; + } + + /** + * Get the id of the currently active item in the queue. If there is no queue or a queue is not + * supported by the session this will be {@link MediaSessionCompat.QueueItem#UNKNOWN_ID}. + * + * @return The id of the currently active item in the queue or {@link + * MediaSessionCompat.QueueItem#UNKNOWN_ID}. + */ + public long getActiveQueueItemId() { + return mActiveItemId; + } + + /** + * Get any custom extras that were set on this playback state. + * + * @return The extras for this state or null. + */ + @Nullable + public Bundle getExtras() { + return mExtras; + } + + /** + * Creates an instance from a framework {@link android.media.session.PlaybackState} object. + * + *

This method is only supported on API 21+. + * + * @param stateObj A {@link android.media.session.PlaybackState} object, or null if none. + * @return An equivalent {@link PlaybackStateCompat} object, or null if none. + */ + @Nullable + public static PlaybackStateCompat fromPlaybackState(@Nullable Object stateObj) { + if (stateObj != null && Build.VERSION.SDK_INT >= 21) { + PlaybackState stateFwk = (PlaybackState) stateObj; + List customActionFwks = Api21Impl.getCustomActions(stateFwk); + List customActions = null; + if (customActionFwks != null) { + customActions = new ArrayList<>(customActionFwks.size()); + for (Object customActionFwk : customActionFwks) { + if (customActionFwk == null) { + continue; + } + customActions.add(CustomAction.fromCustomAction(customActionFwk)); + } + } + Bundle extras; + if (Build.VERSION.SDK_INT >= 22) { + extras = Api22Impl.getExtras(stateFwk); + MediaSessionCompat.ensureClassLoader(extras); + } else { + extras = null; + } + PlaybackStateCompat stateCompat = + new PlaybackStateCompat( + Api21Impl.getState(stateFwk), + Api21Impl.getPosition(stateFwk), + Api21Impl.getBufferedPosition(stateFwk), + Api21Impl.getPlaybackSpeed(stateFwk), + Api21Impl.getActions(stateFwk), + ERROR_CODE_UNKNOWN_ERROR, + Api21Impl.getErrorMessage(stateFwk), + Api21Impl.getLastPositionUpdateTime(stateFwk), + customActions, + Api21Impl.getActiveQueueItemId(stateFwk), + extras); + stateCompat.mStateFwk = stateFwk; + return stateCompat; + } else { + return null; + } + } + + /** + * Gets the underlying framework {@link android.media.session.PlaybackState} object. + * + *

This method is only supported on API 21+. + * + * @return An equivalent {@link android.media.session.PlaybackState} object, or null if none. + */ + @Nullable + public Object getPlaybackState() { + if (mStateFwk == null && Build.VERSION.SDK_INT >= 21) { + PlaybackState.Builder builder = Api21Impl.createBuilder(); + Api21Impl.setState(builder, mState, mPosition, mSpeed, mUpdateTime); + Api21Impl.setBufferedPosition(builder, mBufferedPosition); + Api21Impl.setActions(builder, mActions); + Api21Impl.setErrorMessage(builder, mErrorMessage); + for (PlaybackStateCompat.CustomAction customAction : mCustomActions) { + PlaybackState.CustomAction action = + (PlaybackState.CustomAction) customAction.getCustomAction(); + if (action != null) { + Api21Impl.addCustomAction(builder, action); + } + } + Api21Impl.setActiveQueueItemId(builder, mActiveItemId); + if (Build.VERSION.SDK_INT >= 22) { + Api22Impl.setExtras(builder, mExtras); + } + mStateFwk = Api21Impl.build(builder); + } + return mStateFwk; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public PlaybackStateCompat createFromParcel(Parcel in) { + return new PlaybackStateCompat(in); + } + + @Override + public PlaybackStateCompat[] newArray(int size) { + return new PlaybackStateCompat[size]; + } + }; + + /** + * {@link PlaybackStateCompat.CustomAction CustomActions} can be used to extend the capabilities + * of the standard transport controls by exposing app specific actions to {@link + * MediaControllerCompat Controllers}. + */ + public static final class CustomAction implements Parcelable { + private final String mAction; + private final CharSequence mName; + private final int mIcon; + @Nullable private final Bundle mExtras; + + @Nullable private PlaybackState.CustomAction mCustomActionFwk; + + /** Use {@link PlaybackStateCompat.CustomAction.Builder#build()}. */ + CustomAction(String action, CharSequence name, int icon, @Nullable Bundle extras) { + mAction = action; + mName = name; + mIcon = icon; + mExtras = extras; + } + + CustomAction(Parcel in) { + mAction = checkNotNull(in.readString()); + mName = checkNotNull(TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in)); + mIcon = in.readInt(); + mExtras = in.readBundle(MediaSessionCompat.class.getClassLoader()); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mAction); + TextUtils.writeToParcel(mName, dest, flags); + dest.writeInt(mIcon); + dest.writeBundle(mExtras); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * Creates an instance from a framework {@link android.media.session.PlaybackState.CustomAction} + * object. + * + *

This method is only supported on API 21+. + * + * @param customActionObj A {@link android.media.session.PlaybackState.CustomAction} object, or + * null if none. + * @return An equivalent {@link PlaybackStateCompat.CustomAction} object, or null if none. + */ + @RequiresApi(21) + public static PlaybackStateCompat.CustomAction fromCustomAction(Object customActionObj) { + PlaybackState.CustomAction customActionFwk = (PlaybackState.CustomAction) customActionObj; + Bundle extras = Api21Impl.getExtras(customActionFwk); + MediaSessionCompat.ensureClassLoader(extras); + PlaybackStateCompat.CustomAction customActionCompat = + new PlaybackStateCompat.CustomAction( + Api21Impl.getAction(customActionFwk), + Api21Impl.getName(customActionFwk), + Api21Impl.getIcon(customActionFwk), + extras); + customActionCompat.mCustomActionFwk = customActionFwk; + return customActionCompat; + } + + /** + * Gets the underlying framework {@link android.media.session.PlaybackState.CustomAction} + * object. + * + *

This method is only supported on API 21+. + * + * @return An equivalent {@link android.media.session.PlaybackState.CustomAction} object, or + * null if none. + */ + @Nullable + public Object getCustomAction() { + if (mCustomActionFwk != null || Build.VERSION.SDK_INT < 21) { + return mCustomActionFwk; + } + + PlaybackState.CustomAction.Builder builder = + Api21Impl.createCustomActionBuilder(mAction, mName, mIcon); + Api21Impl.setExtras(builder, mExtras); + return Api21Impl.build(builder); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + + @Override + public PlaybackStateCompat.CustomAction createFromParcel(Parcel p) { + return new PlaybackStateCompat.CustomAction(p); + } + + @Override + public PlaybackStateCompat.CustomAction[] newArray(int size) { + return new PlaybackStateCompat.CustomAction[size]; + } + }; + + /** + * Returns the action of the {@link CustomAction}. + * + * @return The action of the {@link CustomAction}. + */ + public String getAction() { + return mAction; + } + + /** + * Returns the display name of this action. e.g. "Favorite" + * + * @return The display name of this {@link CustomAction}. + */ + public CharSequence getName() { + return mName; + } + + /** + * Returns the resource id of the icon in the {@link MediaSessionCompat Session's} package. + * + * @return The resource id of the icon in the {@link MediaSessionCompat Session's} package. + */ + public int getIcon() { + return mIcon; + } + + /** + * Returns extras which provide additional application-specific information about the action, or + * null if none. These arguments are meant to be consumed by a {@link MediaControllerCompat} if + * it knows how to handle them. + * + * @return Optional arguments for the {@link CustomAction}. + */ + @Nullable + public Bundle getExtras() { + return mExtras; + } + + @Override + public String toString() { + return "Action:" + "mName='" + mName + ", mIcon=" + mIcon + ", mExtras=" + mExtras; + } + + /** Builder for {@link CustomAction} objects. */ + public static final class Builder { + private final String mAction; + private final CharSequence mName; + private final int mIcon; + @Nullable private Bundle mExtras; + + /** + * Creates a {@link CustomAction} builder with the id, name, and icon set. + * + * @param action The action of the {@link CustomAction}. + * @param name The display name of the {@link CustomAction}. This name will be displayed along + * side the action if the UI supports it. + * @param icon The icon resource id of the {@link CustomAction}. This resource id must be in + * the same package as the {@link MediaSessionCompat}. It will be displayed with the + * custom action if the UI supports it. + */ + public Builder(String action, CharSequence name, int icon) { + if (TextUtils.isEmpty(action)) { + throw new IllegalArgumentException("You must specify an action to build a CustomAction"); + } + if (TextUtils.isEmpty(name)) { + throw new IllegalArgumentException("You must specify a name to build a CustomAction"); + } + if (icon == 0) { + throw new IllegalArgumentException( + "You must specify an icon resource id to build a CustomAction"); + } + mAction = action; + mName = name; + mIcon = icon; + } + + /** + * Set optional extras for the {@link CustomAction}. These extras are meant to be consumed by + * a {@link MediaControllerCompat} if it knows how to handle them. Keys should be fully + * qualified (e.g. "com.example.MY_ARG") to avoid collisions. + * + * @param extras Optional extras for the {@link CustomAction}. + * @return this. + */ + public Builder setExtras(@Nullable Bundle extras) { + mExtras = extras; + return this; + } + + /** + * Build and return the {@link CustomAction} instance with the specified values. + * + * @return A new {@link CustomAction} instance. + */ + public CustomAction build() { + return new CustomAction(mAction, mName, mIcon, mExtras); + } + } + } + + /** Builder for {@link PlaybackStateCompat} objects. */ + public static final class Builder { + private final List mCustomActions = new ArrayList<>(); + + private int mState; + private long mPosition; + private long mBufferedPosition; + private float mRate; + private long mActions; + private int mErrorCode; + @Nullable private CharSequence mErrorMessage; + private long mUpdateTime; + private long mActiveItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID; + @Nullable private Bundle mExtras; + + /** Create an empty Builder. */ + public Builder() {} + + /** + * Create a Builder using a {@link PlaybackStateCompat} instance to set the initial values. + * + * @param source The playback state to copy. + */ + public Builder(PlaybackStateCompat source) { + mState = source.mState; + mPosition = source.mPosition; + mRate = source.mSpeed; + mUpdateTime = source.mUpdateTime; + mBufferedPosition = source.mBufferedPosition; + mActions = source.mActions; + mErrorCode = source.mErrorCode; + mErrorMessage = source.mErrorMessage; + if (source.mCustomActions != null) { + mCustomActions.addAll(source.mCustomActions); + } + mActiveItemId = source.mActiveItemId; + mExtras = source.mExtras; + } + + /** + * Set the current state of playback. + * + *

The position must be in ms and indicates the current playback position within the track. + * If the position is unknown use {@link #PLAYBACK_POSITION_UNKNOWN}. + * + *

The rate is a multiple of normal playback and should be 0 when paused and negative when + * rewinding. Normal playback rate is 1.0. + * + *

The state must be one of the following: + * + *

    + *
  • {@link PlaybackStateCompat#STATE_NONE} + *
  • {@link PlaybackStateCompat#STATE_STOPPED} + *
  • {@link PlaybackStateCompat#STATE_PLAYING} + *
  • {@link PlaybackStateCompat#STATE_PAUSED} + *
  • {@link PlaybackStateCompat#STATE_FAST_FORWARDING} + *
  • {@link PlaybackStateCompat#STATE_REWINDING} + *
  • {@link PlaybackStateCompat#STATE_BUFFERING} + *
  • {@link PlaybackStateCompat#STATE_ERROR} + *
  • {@link PlaybackStateCompat#STATE_CONNECTING} + *
  • {@link PlaybackStateCompat#STATE_SKIPPING_TO_PREVIOUS} + *
  • {@link PlaybackStateCompat#STATE_SKIPPING_TO_NEXT} + *
  • {@link PlaybackStateCompat#STATE_SKIPPING_TO_QUEUE_ITEM} + *
+ * + * @param state The current state of playback. + * @param position The position in the current track in ms. + * @param playbackSpeed The current rate of playback as a multiple of normal playback. + */ + public Builder setState(@State int state, long position, float playbackSpeed) { + return setState(state, position, playbackSpeed, SystemClock.elapsedRealtime()); + } + + /** + * Set the current state of playback. + * + *

The position must be in ms and indicates the current playback position within the track. + * If the position is unknown use {@link #PLAYBACK_POSITION_UNKNOWN}. + * + *

The rate is a multiple of normal playback and should be 0 when paused and negative when + * rewinding. Normal playback rate is 1.0. + * + *

The state must be one of the following: + * + *

    + *
  • {@link PlaybackStateCompat#STATE_NONE} + *
  • {@link PlaybackStateCompat#STATE_STOPPED} + *
  • {@link PlaybackStateCompat#STATE_PLAYING} + *
  • {@link PlaybackStateCompat#STATE_PAUSED} + *
  • {@link PlaybackStateCompat#STATE_FAST_FORWARDING} + *
  • {@link PlaybackStateCompat#STATE_REWINDING} + *
  • {@link PlaybackStateCompat#STATE_BUFFERING} + *
  • {@link PlaybackStateCompat#STATE_ERROR} + *
  • {@link PlaybackStateCompat#STATE_CONNECTING} + *
  • {@link PlaybackStateCompat#STATE_SKIPPING_TO_PREVIOUS} + *
  • {@link PlaybackStateCompat#STATE_SKIPPING_TO_NEXT} + *
  • {@link PlaybackStateCompat#STATE_SKIPPING_TO_QUEUE_ITEM} + *
+ * + * @param state The current state of playback. + * @param position The position in the current item in ms. + * @param playbackSpeed The current speed of playback as a multiple of normal playback. + * @param updateTime The time in the {@link SystemClock#elapsedRealtime} timebase that the + * position was updated at. + * @return this + */ + public Builder setState(@State int state, long position, float playbackSpeed, long updateTime) { + mState = state; + mPosition = position; + mUpdateTime = updateTime; + mRate = playbackSpeed; + return this; + } + + /** + * Set the current buffered position in ms. This is the farthest playback point that can be + * reached from the current position using only buffered content. + * + * @return this + */ + public Builder setBufferedPosition(long bufferPosition) { + mBufferedPosition = bufferPosition; + return this; + } + + /** + * Set the current capabilities available on this session. This should use a bitmask of the + * available capabilities. + * + *
    + *
  • {@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS} + *
  • {@link PlaybackStateCompat#ACTION_REWIND} + *
  • {@link PlaybackStateCompat#ACTION_PLAY} + *
  • {@link PlaybackStateCompat#ACTION_PLAY_PAUSE} + *
  • {@link PlaybackStateCompat#ACTION_PAUSE} + *
  • {@link PlaybackStateCompat#ACTION_STOP} + *
  • {@link PlaybackStateCompat#ACTION_FAST_FORWARD} + *
  • {@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT} + *
  • {@link PlaybackStateCompat#ACTION_SEEK_TO} + *
  • {@link PlaybackStateCompat#ACTION_SET_RATING} + *
  • {@link PlaybackStateCompat#ACTION_PLAY_FROM_MEDIA_ID} + *
  • {@link PlaybackStateCompat#ACTION_PLAY_FROM_SEARCH} + *
  • {@link PlaybackStateCompat#ACTION_SKIP_TO_QUEUE_ITEM} + *
  • {@link PlaybackStateCompat#ACTION_PLAY_FROM_URI} + *
  • {@link PlaybackStateCompat#ACTION_PREPARE} + *
  • {@link PlaybackStateCompat#ACTION_PREPARE_FROM_MEDIA_ID} + *
  • {@link PlaybackStateCompat#ACTION_PREPARE_FROM_SEARCH} + *
  • {@link PlaybackStateCompat#ACTION_PREPARE_FROM_URI} + *
  • {@link PlaybackStateCompat#ACTION_SET_REPEAT_MODE} + *
  • {@link PlaybackStateCompat#ACTION_SET_SHUFFLE_MODE} + *
  • {@link PlaybackStateCompat#ACTION_SET_CAPTIONING_ENABLED} + *
  • {@link PlaybackStateCompat#ACTION_SET_PLAYBACK_SPEED} + *
+ * + * @return this + */ + public Builder setActions(@Actions long capabilities) { + mActions = capabilities; + return this; + } + + /** + * Add a custom action to the playback state. Actions can be used to expose additional + * functionality to {@link MediaControllerCompat Controllers} beyond what is offered by the + * standard transport controls. + * + *

e.g. start a radio station based on the current item or skip ahead by 30 seconds. + * + * @param action An identifier for this action. It can be sent back to the {@link + * MediaSessionCompat} through {@link + * MediaControllerCompat.TransportControls#sendCustomAction(String, Bundle)}. + * @param name The display name for the action. If text is shown with the action or used for + * accessibility, this is what should be used. + * @param icon The resource action of the icon that should be displayed for the action. The + * resource should be in the package of the {@link MediaSessionCompat}. + * @return this + */ + public Builder addCustomAction(String action, String name, int icon) { + return addCustomAction(new PlaybackStateCompat.CustomAction(action, name, icon, null)); + } + + /** + * Add a custom action to the playback state. Actions can be used to expose additional + * functionality to {@link MediaControllerCompat Controllers} beyond what is offered by the + * standard transport controls. + * + *

An example of an action would be to start a radio station based on the current item or to + * skip ahead by 30 seconds. + * + * @param customAction The custom action to add to the {@link PlaybackStateCompat}. + * @return this + */ + public Builder addCustomAction(PlaybackStateCompat.CustomAction customAction) { + if (customAction == null) { + throw new IllegalArgumentException( + "You may not add a null CustomAction to PlaybackStateCompat"); + } + mCustomActions.add(customAction); + return this; + } + + /** + * Set the active item in the play queue by specifying its id. The default value is {@link + * MediaSessionCompat.QueueItem#UNKNOWN_ID} + * + * @param id The id of the active item. + * @return this + */ + public Builder setActiveQueueItemId(long id) { + mActiveItemId = id; + return this; + } + + /** + * Set a user readable error message. This should be set when the state is {@link + * PlaybackStateCompat#STATE_ERROR}. + * + * @return this + * @deprecated Use {@link #setErrorMessage(int, CharSequence)} instead. + */ + @Deprecated + public Builder setErrorMessage(@Nullable CharSequence errorMessage) { + mErrorMessage = errorMessage; + return this; + } + + /** + * Set the error code with an optional user readable error message. This should be set when the + * state is {@link PlaybackStateCompat#STATE_ERROR}. + * + * @param errorCode The errorCode to set. + * @param errorMessage The user readable error message. Can be null. + * @return this + */ + public Builder setErrorMessage(@ErrorCode int errorCode, @Nullable CharSequence errorMessage) { + mErrorCode = errorCode; + mErrorMessage = errorMessage; + return this; + } + + /** + * Set any custom extras to be included with the playback state. + * + * @param extras The extras to include. + * @return this + */ + public Builder setExtras(@Nullable Bundle extras) { + mExtras = extras; + return this; + } + + /** Creates the playback state object. */ + public PlaybackStateCompat build() { + return new PlaybackStateCompat( + mState, + mPosition, + mBufferedPosition, + mRate, + mActions, + mErrorCode, + mErrorMessage, + mUpdateTime, + mCustomActions, + mActiveItemId, + mExtras); + } + } + + @RequiresApi(21) + private static class Api21Impl { + private Api21Impl() {} + + @DoNotInline + static PlaybackState.Builder createBuilder() { + return new PlaybackState.Builder(); + } + + @DoNotInline + static void setState( + PlaybackState.Builder builder, + int state, + long position, + float playbackSpeed, + long updateTime) { + builder.setState(state, position, playbackSpeed, updateTime); + } + + @DoNotInline + static void setBufferedPosition(PlaybackState.Builder builder, long bufferedPosition) { + builder.setBufferedPosition(bufferedPosition); + } + + @DoNotInline + static void setActions(PlaybackState.Builder builder, long actions) { + builder.setActions(actions); + } + + @SuppressWarnings("argument.type.incompatible") // Platform class not annotated as nullable + @DoNotInline + static void setErrorMessage(PlaybackState.Builder builder, @Nullable CharSequence error) { + builder.setErrorMessage(error); + } + + @DoNotInline + static void addCustomAction( + PlaybackState.Builder builder, PlaybackState.CustomAction customAction) { + builder.addCustomAction(customAction); + } + + @DoNotInline + static void setActiveQueueItemId(PlaybackState.Builder builder, long id) { + builder.setActiveQueueItemId(id); + } + + @DoNotInline + static List getCustomActions(PlaybackState state) { + return state.getCustomActions(); + } + + @DoNotInline + static PlaybackState build(PlaybackState.Builder builder) { + return builder.build(); + } + + @DoNotInline + static int getState(PlaybackState state) { + return state.getState(); + } + + @DoNotInline + static long getPosition(PlaybackState state) { + return state.getPosition(); + } + + @DoNotInline + static long getBufferedPosition(PlaybackState state) { + return state.getBufferedPosition(); + } + + @DoNotInline + static float getPlaybackSpeed(PlaybackState state) { + return state.getPlaybackSpeed(); + } + + @DoNotInline + static long getActions(PlaybackState state) { + return state.getActions(); + } + + @Nullable + @DoNotInline + static CharSequence getErrorMessage(PlaybackState state) { + return state.getErrorMessage(); + } + + @DoNotInline + static long getLastPositionUpdateTime(PlaybackState state) { + return state.getLastPositionUpdateTime(); + } + + @DoNotInline + static long getActiveQueueItemId(PlaybackState state) { + return state.getActiveQueueItemId(); + } + + @DoNotInline + static PlaybackState.CustomAction.Builder createCustomActionBuilder( + String action, CharSequence name, int icon) { + return new PlaybackState.CustomAction.Builder(action, name, icon); + } + + @SuppressWarnings("argument.type.incompatible") // Platform class not annotated as nullable + @DoNotInline + static void setExtras(PlaybackState.CustomAction.Builder builder, @Nullable Bundle extras) { + builder.setExtras(extras); + } + + @DoNotInline + static PlaybackState.CustomAction build(PlaybackState.CustomAction.Builder builder) { + return builder.build(); + } + + @Nullable + @DoNotInline + static Bundle getExtras(PlaybackState.CustomAction customAction) { + return customAction.getExtras(); + } + + @DoNotInline + static String getAction(PlaybackState.CustomAction customAction) { + return customAction.getAction(); + } + + @DoNotInline + static CharSequence getName(PlaybackState.CustomAction customAction) { + return customAction.getName(); + } + + @DoNotInline + static int getIcon(PlaybackState.CustomAction customAction) { + return customAction.getIcon(); + } + } + + @RequiresApi(22) + private static class Api22Impl { + private Api22Impl() {} + + @SuppressWarnings("argument.type.incompatible") // Platform class not annotated as nullable + @DoNotInline + static void setExtras(PlaybackState.Builder builder, @Nullable Bundle extras) { + builder.setExtras(extras); + } + + @Nullable + @DoNotInline + static Bundle getExtras(PlaybackState state) { + return state.getExtras(); + } + } +} diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/RatingCompat.java b/libraries/session/src/main/java/androidx/media3/session/legacy/RatingCompat.java new file mode 100644 index 0000000000..23a50dbf81 --- /dev/null +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/RatingCompat.java @@ -0,0 +1,401 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.session.legacy; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY; +import static androidx.media3.common.util.Assertions.checkNotNull; + +import android.annotation.SuppressLint; +import android.media.Rating; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import androidx.annotation.RestrictTo; +import androidx.media3.common.util.UnstableApi; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A class to encapsulate rating information used as content metadata. A rating is defined by its + * rating style (see {@link #RATING_HEART}, {@link #RATING_THUMB_UP_DOWN}, {@link #RATING_3_STARS}, + * {@link #RATING_4_STARS}, {@link #RATING_5_STARS} or {@link #RATING_PERCENTAGE}) and the actual + * rating value (which may be defined as "unrated"), both of which are defined when the rating + * instance is constructed through one of the factory methods. + */ +@UnstableApi +@RestrictTo(LIBRARY) +@SuppressLint("BanParcelableUsage") +public final class RatingCompat implements Parcelable { + private static final String TAG = "Rating"; + + /** */ + @IntDef({ + RATING_NONE, + RATING_HEART, + RATING_THUMB_UP_DOWN, + RATING_3_STARS, + RATING_4_STARS, + RATING_5_STARS, + RATING_PERCENTAGE + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Style {} + + /** */ + @IntDef({RATING_3_STARS, RATING_4_STARS, RATING_5_STARS}) + @Retention(RetentionPolicy.SOURCE) + public @interface StarStyle {} + + /** + * Indicates a rating style is not supported. A Rating will never have this type, but can be used + * by other classes to indicate they do not support Rating. + */ + public static final int RATING_NONE = 0; + + /** + * A rating style with a single degree of rating, "heart" vs "no heart". Can be used to indicate + * the content referred to is a favorite (or not). + */ + public static final int RATING_HEART = 1; + + /** A rating style for "thumb up" vs "thumb down". */ + public static final int RATING_THUMB_UP_DOWN = 2; + + /** A rating style with 0 to 3 stars. */ + public static final int RATING_3_STARS = 3; + + /** A rating style with 0 to 4 stars. */ + public static final int RATING_4_STARS = 4; + + /** A rating style with 0 to 5 stars. */ + public static final int RATING_5_STARS = 5; + + /** A rating style expressed as a percentage. */ + public static final int RATING_PERCENTAGE = 6; + + private static final float RATING_NOT_RATED = -1.0f; + + private final int mRatingStyle; + private final float mRatingValue; + + @Nullable private Object mRatingObj; // framework Rating object + + RatingCompat(@Style int ratingStyle, float rating) { + mRatingStyle = ratingStyle; + mRatingValue = rating; + } + + @Override + public String toString() { + return "Rating:style=" + + mRatingStyle + + " rating=" + + (mRatingValue < 0.0f ? "unrated" : String.valueOf(mRatingValue)); + } + + @Override + public int describeContents() { + return mRatingStyle; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mRatingStyle); + dest.writeFloat(mRatingValue); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + /** + * Rebuilds a Rating previously stored with writeToParcel(). + * + * @param p Parcel object to read the Rating from + * @return a new Rating created from the data in the parcel + */ + @Override + public RatingCompat createFromParcel(Parcel p) { + return new RatingCompat(p.readInt(), p.readFloat()); + } + + @Override + public RatingCompat[] newArray(int size) { + return new RatingCompat[size]; + } + }; + + /** + * Return a Rating instance with no rating. Create and return a new Rating instance with no rating + * known for the given rating style. + * + * @param ratingStyle one of {@link #RATING_HEART}, {@link #RATING_THUMB_UP_DOWN}, {@link + * #RATING_3_STARS}, {@link #RATING_4_STARS}, {@link #RATING_5_STARS}, or {@link + * #RATING_PERCENTAGE}. + * @return null if an invalid rating style is passed, a new Rating instance otherwise. + */ + @Nullable + public static RatingCompat newUnratedRating(@Style int ratingStyle) { + switch (ratingStyle) { + case RATING_HEART: + case RATING_THUMB_UP_DOWN: + case RATING_3_STARS: + case RATING_4_STARS: + case RATING_5_STARS: + case RATING_PERCENTAGE: + return new RatingCompat(ratingStyle, RATING_NOT_RATED); + default: + return null; + } + } + + /** + * Return a Rating instance with a heart-based rating. Create and return a new Rating instance + * with a rating style of {@link #RATING_HEART}, and a heart-based rating. + * + * @param hasHeart true for a "heart selected" rating, false for "heart unselected". + * @return a new Rating instance. + */ + public static RatingCompat newHeartRating(boolean hasHeart) { + return new RatingCompat(RATING_HEART, hasHeart ? 1.0f : 0.0f); + } + + /** + * Return a Rating instance with a thumb-based rating. Create and return a new Rating instance + * with a {@link #RATING_THUMB_UP_DOWN} rating style, and a "thumb up" or "thumb down" rating. + * + * @param thumbIsUp true for a "thumb up" rating, false for "thumb down". + * @return a new Rating instance. + */ + public static RatingCompat newThumbRating(boolean thumbIsUp) { + return new RatingCompat(RATING_THUMB_UP_DOWN, thumbIsUp ? 1.0f : 0.0f); + } + + /** + * Return a Rating instance with a star-based rating. Create and return a new Rating instance with + * one of the star-base rating styles and the given integer or fractional number of stars. Non + * integer values can for instance be used to represent an average rating value, which might not + * be an integer number of stars. + * + * @param starRatingStyle one of {@link #RATING_3_STARS}, {@link #RATING_4_STARS}, {@link + * #RATING_5_STARS}. + * @param starRating a number ranging from 0.0f to 3.0f, 4.0f or 5.0f according to the rating + * style. + * @return null if the rating style is invalid, or the rating is out of range, a new Rating + * instance otherwise. + */ + @Nullable + public static RatingCompat newStarRating(@StarStyle int starRatingStyle, float starRating) { + float maxRating = -1.0f; + switch (starRatingStyle) { + case RATING_3_STARS: + maxRating = 3.0f; + break; + case RATING_4_STARS: + maxRating = 4.0f; + break; + case RATING_5_STARS: + maxRating = 5.0f; + break; + default: + Log.e(TAG, "Invalid rating style (" + starRatingStyle + ") for a star rating"); + return null; + } + if ((starRating < 0.0f) || (starRating > maxRating)) { + Log.e(TAG, "Trying to set out of range star-based rating"); + return null; + } + return new RatingCompat(starRatingStyle, starRating); + } + + /** + * Return a Rating instance with a percentage-based rating. Create and return a new Rating + * instance with a {@link #RATING_PERCENTAGE} rating style, and a rating of the given percentage. + * + * @param percent the value of the rating + * @return null if the rating is out of range, a new Rating instance otherwise. + */ + @Nullable + public static RatingCompat newPercentageRating(float percent) { + if ((percent < 0.0f) || (percent > 100.0f)) { + Log.e(TAG, "Invalid percentage-based rating value"); + return null; + } else { + return new RatingCompat(RATING_PERCENTAGE, percent); + } + } + + /** + * Return whether there is a rating value available. + * + * @return true if the instance was not created with {@link #newUnratedRating(int)}. + */ + public boolean isRated() { + return mRatingValue >= 0.0f; + } + + /** + * Return the rating style. + * + * @return one of {@link #RATING_HEART}, {@link #RATING_THUMB_UP_DOWN}, {@link #RATING_3_STARS}, + * {@link #RATING_4_STARS}, {@link #RATING_5_STARS}, or {@link #RATING_PERCENTAGE}. + */ + @Style + public int getRatingStyle() { + return mRatingStyle; + } + + /** + * Return whether the rating is "heart selected". + * + * @return true if the rating is "heart selected", false if the rating is "heart unselected", if + * the rating style is not {@link #RATING_HEART} or if it is unrated. + */ + public boolean hasHeart() { + if (mRatingStyle != RATING_HEART) { + return false; + } else { + return (mRatingValue == 1.0f); + } + } + + /** + * Return whether the rating is "thumb up". + * + * @return true if the rating is "thumb up", false if the rating is "thumb down", if the rating + * style is not {@link #RATING_THUMB_UP_DOWN} or if it is unrated. + */ + public boolean isThumbUp() { + if (mRatingStyle != RATING_THUMB_UP_DOWN) { + return false; + } else { + return (mRatingValue == 1.0f); + } + } + + /** + * Return the star-based rating value. + * + * @return a rating value greater or equal to 0.0f, or a negative value if the rating style is not + * star-based, or if it is unrated. + */ + public float getStarRating() { + switch (mRatingStyle) { + case RATING_3_STARS: + case RATING_4_STARS: + case RATING_5_STARS: + if (isRated()) { + return mRatingValue; + } + // fall through + default: + return -1.0f; + } + } + + /** + * Return the percentage-based rating value. + * + * @return a rating value greater or equal to 0.0f, or a negative value if the rating style is not + * percentage-based, or if it is unrated. + */ + public float getPercentRating() { + if ((mRatingStyle != RATING_PERCENTAGE) || !isRated()) { + return -1.0f; + } else { + return mRatingValue; + } + } + + /** + * Creates an instance from a framework {@link android.media.Rating} object. + * + *

This method is only supported on API 19+. + * + * @param ratingObj A {@link android.media.Rating} object, or null if none. + * @return An equivalent {@link RatingCompat} object, or null if none. + */ + @Nullable + @SuppressLint("WrongConstant") + public static RatingCompat fromRating(@Nullable Object ratingObj) { + if (ratingObj != null) { + final int ratingStyle = ((Rating) ratingObj).getRatingStyle(); + final RatingCompat rating; + if (((Rating) ratingObj).isRated()) { + switch (ratingStyle) { + case Rating.RATING_HEART: + rating = newHeartRating(((Rating) ratingObj).hasHeart()); + break; + case Rating.RATING_THUMB_UP_DOWN: + rating = newThumbRating(((Rating) ratingObj).isThumbUp()); + break; + case Rating.RATING_3_STARS: + case Rating.RATING_4_STARS: + case Rating.RATING_5_STARS: + rating = newStarRating(ratingStyle, ((Rating) ratingObj).getStarRating()); + break; + case Rating.RATING_PERCENTAGE: + rating = newPercentageRating(((Rating) ratingObj).getPercentRating()); + break; + default: + return null; + } + } else { + rating = newUnratedRating(ratingStyle); + } + checkNotNull(rating).mRatingObj = ratingObj; + return rating; + } else { + return null; + } + } + + /** + * Gets the underlying framework {@link android.media.Rating} object. + * + *

This method is only supported on API 19+. + * + * @return An equivalent {@link android.media.Rating} object, or null if none. + */ + @Nullable + public Object getRating() { + if (mRatingObj == null) { + if (isRated()) { + switch (mRatingStyle) { + case RATING_HEART: + mRatingObj = Rating.newHeartRating(hasHeart()); + break; + case RATING_THUMB_UP_DOWN: + mRatingObj = Rating.newThumbRating(isThumbUp()); + break; + case RATING_3_STARS: + case RATING_4_STARS: + case RATING_5_STARS: + mRatingObj = Rating.newStarRating(mRatingStyle, getStarRating()); + break; + case RATING_PERCENTAGE: + mRatingObj = Rating.newPercentageRating(getPercentRating()); + break; + default: + return null; + } + } else { + mRatingObj = Rating.newUnratedRating(mRatingStyle); + } + } + return mRatingObj; + } +} diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/VolumeProviderCompat.java b/libraries/session/src/main/java/androidx/media3/session/legacy/VolumeProviderCompat.java new file mode 100644 index 0000000000..5f4cf6ce4b --- /dev/null +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/VolumeProviderCompat.java @@ -0,0 +1,233 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.session.legacy; + +import static androidx.annotation.RestrictTo.Scope.LIBRARY; + +import android.media.VolumeProvider; +import android.os.Build; +import androidx.annotation.DoNotInline; +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.RestrictTo; +import androidx.media3.common.util.UnstableApi; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Handles requests to adjust or set the volume on a session. This is also used to push volume + * updates back to the session after a request has been handled. You can set a volume provider on a + * session by calling {@link MediaSessionCompat#setPlaybackToRemote}. + */ +@UnstableApi +@RestrictTo(LIBRARY) +public abstract class VolumeProviderCompat { + + /** */ + @IntDef({VOLUME_CONTROL_FIXED, VOLUME_CONTROL_RELATIVE, VOLUME_CONTROL_ABSOLUTE}) + @Retention(RetentionPolicy.SOURCE) + public @interface ControlType {} + + /** The volume is fixed and can not be modified. Requests to change volume should be ignored. */ + public static final int VOLUME_CONTROL_FIXED = 0; + + /** + * The volume control uses relative adjustment via {@link #onAdjustVolume(int)}. Attempts to set + * the volume to a specific value should be ignored. + */ + public static final int VOLUME_CONTROL_RELATIVE = 1; + + /** + * The volume control uses an absolute value. It may be adjusted using {@link + * #onAdjustVolume(int)} or set directly using {@link #onSetVolumeTo(int)}. + */ + public static final int VOLUME_CONTROL_ABSOLUTE = 2; + + private final int mControlType; + private final int mMaxVolume; + @Nullable private final String mControlId; + private int mCurrentVolume; + @Nullable private Callback mCallback; + + @Nullable private VolumeProvider mVolumeProviderFwk; + + /** + * Create a new volume provider for handling volume events. You must specify the type of volume + * control and the maximum volume that can be used. + * + * @param volumeControl The method for controlling volume that is used by this provider. + * @param maxVolume The maximum allowed volume. + * @param currentVolume The current volume. + */ + public VolumeProviderCompat(@ControlType int volumeControl, int maxVolume, int currentVolume) { + this(volumeControl, maxVolume, currentVolume, null); + } + + /** + * Create a new volume provider for handling volume events. You must specify the type of volume + * control and the maximum volume that can be used. + * + * @param volumeControl The method for controlling volume that is used by this provider. + * @param maxVolume The maximum allowed volume. + * @param currentVolume The current volume. + * @param volumeControlId The volume control ID of this provider. + */ + public VolumeProviderCompat( + @ControlType int volumeControl, + int maxVolume, + int currentVolume, + @Nullable String volumeControlId) { + mControlType = volumeControl; + mMaxVolume = maxVolume; + mCurrentVolume = currentVolume; + mControlId = volumeControlId; + } + + /** + * Get the current volume of the provider. + * + * @return The current volume. + */ + public final int getCurrentVolume() { + return mCurrentVolume; + } + + /** + * Get the volume control type that this volume provider uses. + * + * @return The volume control type for this volume provider + */ + @ControlType + public final int getVolumeControl() { + return mControlType; + } + + /** + * Get the maximum volume this provider allows. + * + * @return The max allowed volume. + */ + public final int getMaxVolume() { + return mMaxVolume; + } + + /** + * Set the current volume and notify the system that the volume has been changed. + * + * @param currentVolume The current volume of the output. + */ + public final void setCurrentVolume(int currentVolume) { + mCurrentVolume = currentVolume; + if (Build.VERSION.SDK_INT >= 21) { + VolumeProvider volumeProviderFwk = (VolumeProvider) getVolumeProvider(); + Api21Impl.setCurrentVolume(volumeProviderFwk, currentVolume); + } + if (mCallback != null) { + mCallback.onVolumeChanged(this); + } + } + + /** + * Gets the volume control ID. It can be used to identify which volume provider is used by the + * session. + * + * @return the volume control ID or {@code null} if it isn't set. + */ + @Nullable + public final String getVolumeControlId() { + return mControlId; + } + + /** + * Override to handle requests to set the volume of the current output. + * + * @param volume The volume to set the output to. + */ + public void onSetVolumeTo(int volume) {} + + /** + * Override to handle requests to adjust the volume of the current output. + * + * @param direction The direction to adjust the volume in. + */ + public void onAdjustVolume(int direction) {} + + /** + * Sets a callback to receive volume changes. + * + *

Used internally by the support library. + */ + public void setCallback(@Nullable Callback callback) { + mCallback = callback; + } + + /** + * Gets the underlying framework {@link android.media.VolumeProvider} object. + * + *

This method is only supported on API 21+. + * + * @return An equivalent {@link android.media.VolumeProvider} object, or null if none. + */ + @RequiresApi(21) + public Object getVolumeProvider() { + if (mVolumeProviderFwk == null) { + if (Build.VERSION.SDK_INT >= 30) { + mVolumeProviderFwk = + new VolumeProvider(mControlType, mMaxVolume, mCurrentVolume, mControlId) { + @Override + public void onSetVolumeTo(int volume) { + VolumeProviderCompat.this.onSetVolumeTo(volume); + } + + @Override + public void onAdjustVolume(int direction) { + VolumeProviderCompat.this.onAdjustVolume(direction); + } + }; + } else { + mVolumeProviderFwk = + new VolumeProvider(mControlType, mMaxVolume, mCurrentVolume) { + @Override + public void onSetVolumeTo(int volume) { + VolumeProviderCompat.this.onSetVolumeTo(volume); + } + + @Override + public void onAdjustVolume(int direction) { + VolumeProviderCompat.this.onAdjustVolume(direction); + } + }; + } + } + return mVolumeProviderFwk; + } + + /** Listens for changes to the volume. */ + public abstract static class Callback { + public abstract void onVolumeChanged(VolumeProviderCompat volumeProvider); + } + + @RequiresApi(21) + private static class Api21Impl { + private Api21Impl() {} + + @DoNotInline + static void setCurrentVolume(VolumeProvider volumeProvider, int currentVolume) { + volumeProvider.setCurrentVolume(currentVolume); + } + } +} diff --git a/libraries/session/src/main/java/androidx/media3/session/legacy/package-info.java b/libraries/session/src/main/java/androidx/media3/session/legacy/package-info.java new file mode 100644 index 0000000000..9fef70fd6d --- /dev/null +++ b/libraries/session/src/main/java/androidx/media3/session/legacy/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package androidx.media3.session.legacy; + +import androidx.media3.common.util.NonNullApi; diff --git a/libraries/session/src/main/res/layout/media3_notification_media_action.xml b/libraries/session/src/main/res/layout/media3_notification_media_action.xml new file mode 100644 index 0000000000..c06f54d708 --- /dev/null +++ b/libraries/session/src/main/res/layout/media3_notification_media_action.xml @@ -0,0 +1,25 @@ + + + + diff --git a/libraries/session/src/main/res/layout/media3_notification_media_cancel_action.xml b/libraries/session/src/main/res/layout/media3_notification_media_cancel_action.xml new file mode 100644 index 0000000000..f853d0ad66 --- /dev/null +++ b/libraries/session/src/main/res/layout/media3_notification_media_cancel_action.xml @@ -0,0 +1,28 @@ + + + + diff --git a/libraries/session/src/main/res/layout/media3_notification_template_big_media.xml b/libraries/session/src/main/res/layout/media3_notification_template_big_media.xml new file mode 100644 index 0000000000..bbe6228d12 --- /dev/null +++ b/libraries/session/src/main/res/layout/media3_notification_template_big_media.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + diff --git a/libraries/session/src/main/res/layout/media3_notification_template_big_media_custom.xml b/libraries/session/src/main/res/layout/media3_notification_template_big_media_custom.xml new file mode 100644 index 0000000000..19e9f3d2de --- /dev/null +++ b/libraries/session/src/main/res/layout/media3_notification_template_big_media_custom.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + diff --git a/libraries/session/src/main/res/layout/media3_notification_template_big_media_narrow.xml b/libraries/session/src/main/res/layout/media3_notification_template_big_media_narrow.xml new file mode 100644 index 0000000000..161913f073 --- /dev/null +++ b/libraries/session/src/main/res/layout/media3_notification_template_big_media_narrow.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + diff --git a/libraries/session/src/main/res/layout/media3_notification_template_big_media_narrow_custom.xml b/libraries/session/src/main/res/layout/media3_notification_template_big_media_narrow_custom.xml new file mode 100644 index 0000000000..3161756339 --- /dev/null +++ b/libraries/session/src/main/res/layout/media3_notification_template_big_media_narrow_custom.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libraries/session/src/main/res/layout/media3_notification_template_lines_media.xml b/libraries/session/src/main/res/layout/media3_notification_template_lines_media.xml new file mode 100644 index 0000000000..62aa29c2f2 --- /dev/null +++ b/libraries/session/src/main/res/layout/media3_notification_template_lines_media.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + diff --git a/libraries/session/src/main/res/layout/media3_notification_template_media.xml b/libraries/session/src/main/res/layout/media3_notification_template_media.xml new file mode 100644 index 0000000000..e1c6145b35 --- /dev/null +++ b/libraries/session/src/main/res/layout/media3_notification_template_media.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + diff --git a/libraries/session/src/main/res/layout/media3_notification_template_media_custom.xml b/libraries/session/src/main/res/layout/media3_notification_template_media_custom.xml new file mode 100644 index 0000000000..3bf7b0805b --- /dev/null +++ b/libraries/session/src/main/res/layout/media3_notification_template_media_custom.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + diff --git a/libraries/session/src/main/res/values-v21/styles.xml b/libraries/session/src/main/res/values-v21/styles.xml new file mode 100644 index 0000000000..a4b06a49b3 --- /dev/null +++ b/libraries/session/src/main/res/values-v21/styles.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/libraries/session/src/main/res/values-v24/styles.xml b/libraries/session/src/main/res/values-v24/styles.xml new file mode 100644 index 0000000000..ced2b25d81 --- /dev/null +++ b/libraries/session/src/main/res/values-v24/styles.xml @@ -0,0 +1,25 @@ + + + + +