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:
+ *
+ *
+ * - usage: "why" you are playing a sound, what is this sound used for. This is achieved with
+ * the "usage" information. Examples of usage are {@link #USAGE_MEDIA} and {@link
+ * #USAGE_ALARM}. These two examples are the closest to stream types, but more detailed use
+ * cases are available. Usage information is more expressive than a stream type, and allows
+ * certain platforms or routing policies to use this information for more refined volume or
+ * routing decisions. Usage is the most important information to supply in
+ * AudioAttributesCompat
and it is recommended to build any instance with this information
+ * supplied, see {@link AudioAttributesCompat.Builder} for exceptions.
+ * - content type: "what" you are playing. The content type expresses the general category of
+ * the content. This information is optional. But in case it is known (for instance {@link
+ * #CONTENT_TYPE_MOVIE} for a movie streaming service or {@link #CONTENT_TYPE_MUSIC} for a
+ * music playback application) this information might be used by the audio framework to
+ * selectively configure some audio post-processing blocks.
+ *
- flags: "how" is playback to be affected, see the flag definitions for the specific playback
+ * behaviors they control.
+ *
+ *
+ * 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 extends @NonNull Object> 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:
+ *
+ *
+ * - UID and package name are the same
+ *
- 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/libraries/session/src/main/res/values/colors.xml b/libraries/session/src/main/res/values/colors.xml
new file mode 100644
index 0000000000..e0dd8c965b
--- /dev/null
+++ b/libraries/session/src/main/res/values/colors.xml
@@ -0,0 +1,20 @@
+
+
+
+
+ #ff424242
+
diff --git a/libraries/session/src/main/res/values/colors_material.xml b/libraries/session/src/main/res/values/colors_material.xml
new file mode 100644
index 0000000000..13f01b63b2
--- /dev/null
+++ b/libraries/session/src/main/res/values/colors_material.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+ #ffffffff
+
+ #b3ffffff
+
diff --git a/libraries/session/src/main/res/values/config.xml b/libraries/session/src/main/res/values/config.xml
new file mode 100644
index 0000000000..1a048e9e51
--- /dev/null
+++ b/libraries/session/src/main/res/values/config.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+ 127
+
diff --git a/libraries/session/src/main/res/values/ids.xml b/libraries/session/src/main/res/values/ids.xml
new file mode 100644
index 0000000000..0dacdaedce
--- /dev/null
+++ b/libraries/session/src/main/res/values/ids.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
diff --git a/libraries/session/src/main/res/values/styles.xml b/libraries/session/src/main/res/values/styles.xml
new file mode 100644
index 0000000000..ec5bbc6e8d
--- /dev/null
+++ b/libraries/session/src/main/res/values/styles.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/libraries/session/src/test/java/androidx/media3/session/LegacyConversionsTest.java b/libraries/session/src/test/java/androidx/media3/session/LegacyConversionsTest.java
index 445386b8d8..7732395eb0 100644
--- a/libraries/session/src/test/java/androidx/media3/session/LegacyConversionsTest.java
+++ b/libraries/session/src/test/java/androidx/media3/session/LegacyConversionsTest.java
@@ -15,13 +15,13 @@
*/
package androidx.media3.session;
-import static android.support.v4.media.MediaBrowserCompat.MediaItem.FLAG_BROWSABLE;
-import static android.support.v4.media.MediaBrowserCompat.MediaItem.FLAG_PLAYABLE;
-import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_DURATION;
-import static androidx.media.utils.MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS;
import static androidx.media3.session.MediaConstants.EXTRAS_KEY_COMMAND_BUTTON_ICON_COMPAT;
import static androidx.media3.session.MediaConstants.EXTRAS_KEY_MEDIA_TYPE_COMPAT;
import static androidx.media3.session.MediaConstants.EXTRA_KEY_ROOT_CHILDREN_BROWSABLE_ONLY;
+import static androidx.media3.session.legacy.MediaBrowserCompat.MediaItem.FLAG_BROWSABLE;
+import static androidx.media3.session.legacy.MediaBrowserCompat.MediaItem.FLAG_PLAYABLE;
+import static androidx.media3.session.legacy.MediaConstants.BROWSER_ROOT_HINTS_KEY_ROOT_CHILDREN_SUPPORTED_FLAGS;
+import static androidx.media3.session.legacy.MediaMetadataCompat.METADATA_KEY_DURATION;
import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.fail;
@@ -32,17 +32,8 @@ import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Bundle;
import android.service.media.MediaBrowserService;
-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;
-import android.support.v4.media.session.PlaybackStateCompat;
import android.text.SpannedString;
import androidx.annotation.Nullable;
-import androidx.media.AudioAttributesCompat;
-import androidx.media.VolumeProviderCompat;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C;
import androidx.media3.common.HeartRating;
@@ -55,6 +46,15 @@ import androidx.media3.common.StarRating;
import androidx.media3.common.ThumbRating;
import androidx.media3.common.util.BitmapLoader;
import androidx.media3.datasource.DataSourceBitmapLoader;
+import androidx.media3.session.legacy.AudioAttributesCompat;
+import androidx.media3.session.legacy.MediaBrowserCompat;
+import androidx.media3.session.legacy.MediaControllerCompat;
+import androidx.media3.session.legacy.MediaDescriptionCompat;
+import androidx.media3.session.legacy.MediaMetadataCompat;
+import androidx.media3.session.legacy.MediaSessionCompat;
+import androidx.media3.session.legacy.PlaybackStateCompat;
+import androidx.media3.session.legacy.RatingCompat;
+import androidx.media3.session.legacy.VolumeProviderCompat;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
diff --git a/libraries/session/src/test/java/androidx/media3/session/MediaSessionUnitTest.java b/libraries/session/src/test/java/androidx/media3/session/MediaSessionUnitTest.java
index fe8e4d3bc4..7534a61cb5 100644
--- a/libraries/session/src/test/java/androidx/media3/session/MediaSessionUnitTest.java
+++ b/libraries/session/src/test/java/androidx/media3/session/MediaSessionUnitTest.java
@@ -19,8 +19,8 @@ import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static com.google.common.truth.Truth.assertThat;
import android.os.Bundle;
-import androidx.media.MediaSessionManager;
import androidx.media3.common.MediaLibraryInfo;
+import androidx.media3.session.legacy.MediaSessionManager;
import androidx.media3.test.utils.TestExoPlayerBuilder;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.After;
diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionTest.java
index 513a7e4ffb..3ed6b5fc9c 100644
--- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionTest.java
+++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaSessionTest.java
@@ -495,6 +495,38 @@ public class MediaSessionTest {
assertThat(player.seekPositionMs).isEqualTo(testSeekPositionMs);
}
+ /** Test {@link MediaSession#getSessionCompatToken()}. */
+ @Test
+ public void getPlatformToken_returnsCompatibleWithPlatformMediaController() throws Exception {
+ MediaSession session =
+ sessionTestRule.ensureReleaseAfterTest(
+ new MediaSession.Builder(context, player)
+ .setId("getPlatformToken_returnsCompatibleWithPlatformMediaController")
+ .setCallback(
+ new MediaSession.Callback() {
+ @Override
+ public MediaSession.ConnectionResult onConnect(
+ MediaSession session, ControllerInfo controller) {
+ if (TextUtils.equals(
+ getControllerCallerPackageName(controller),
+ controller.getPackageName())) {
+ return MediaSession.Callback.super.onConnect(session, controller);
+ }
+ return MediaSession.ConnectionResult.reject();
+ }
+ })
+ .build());
+ android.media.session.MediaSession.Token token = session.getPlatformToken();
+ android.media.session.MediaController platformController =
+ new android.media.session.MediaController(context, token);
+
+ long testSeekPositionMs = 1234;
+ platformController.getTransportControls().seekTo(testSeekPositionMs);
+
+ player.awaitMethodCalled(MockPlayer.METHOD_SEEK_TO, TIMEOUT_MS);
+ assertThat(player.seekPositionMs).isEqualTo(testSeekPositionMs);
+ }
+
@Test
public void getControllerVersion() throws Exception {
CountDownLatch connectedLatch = new CountDownLatch(1);
diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/legacy/AudioAttributesCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/legacy/AudioAttributesCompatTest.java
new file mode 100644
index 0000000000..0dc2fcbb21
--- /dev/null
+++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/legacy/AudioAttributesCompatTest.java
@@ -0,0 +1,182 @@
+/*
+ * 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 org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.IsEqual.equalTo;
+import static org.hamcrest.core.IsNot.not;
+
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.os.Build;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Test {@link AudioAttributesCompat}. */
+@RunWith(AndroidJUnit4.class)
+public class AudioAttributesCompatTest {
+ // some macros for conciseness
+ static AudioAttributesCompat.Builder mkBuilder(
+ @AudioAttributesCompat.AttributeContentType int type,
+ @AudioAttributesCompat.AttributeUsage int usage) {
+ return new AudioAttributesCompat.Builder().setContentType(type).setUsage(usage);
+ }
+
+ static AudioAttributesCompat.Builder mkBuilder(int legacyStream) {
+ return new AudioAttributesCompat.Builder().setLegacyStreamType(legacyStream);
+ }
+
+ // some objects we'll toss around
+ Object mMediaAA;
+ AudioAttributesCompat mMediaAAC,
+ mMediaLegacyAAC,
+ mMediaAACFromAA,
+ mNotificationAAC,
+ mNotificationLegacyAAC;
+
+ @Before
+ @SdkSuppress(minSdkVersion = 21)
+ public void setUpApi21() {
+ if (Build.VERSION.SDK_INT < 21) return;
+ mMediaAA =
+ new AudioAttributes.Builder()
+ .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+ .setUsage(AudioAttributes.USAGE_MEDIA)
+ .build();
+ mMediaAACFromAA = AudioAttributesCompat.wrap((AudioAttributes) mMediaAA);
+ }
+
+ @Before
+ public void setUp() {
+ mMediaAAC =
+ mkBuilder(AudioAttributesCompat.CONTENT_TYPE_MUSIC, AudioAttributesCompat.USAGE_MEDIA)
+ .build();
+ mMediaLegacyAAC = mkBuilder(AudioManager.STREAM_MUSIC).build();
+ mNotificationAAC =
+ mkBuilder(
+ AudioAttributesCompat.CONTENT_TYPE_SONIFICATION,
+ AudioAttributesCompat.USAGE_NOTIFICATION)
+ .build();
+ mNotificationLegacyAAC = mkBuilder(AudioManager.STREAM_NOTIFICATION).build();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 21)
+ public void testCreateWithAudioAttributesApi21() {
+ assertThat(mMediaAACFromAA, not(equalTo(null)));
+ assertThat((AudioAttributes) mMediaAACFromAA.unwrap(), equalTo(mMediaAA));
+ assertThat(
+ (AudioAttributes) mMediaAACFromAA.unwrap(),
+ equalTo(new AudioAttributes.Builder((AudioAttributes) mMediaAA).build()));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 21)
+ public void testEqualityApi21() {
+ assertThat("self equality", mMediaAACFromAA, equalTo(mMediaAACFromAA));
+ assertThat("different things", mMediaAACFromAA, not(equalTo(mNotificationAAC)));
+ }
+
+ @Test
+ public void testEquality() {
+ assertThat("self equality", mMediaAAC, equalTo(mMediaAAC));
+ assertThat(
+ "equal to clone", mMediaAAC, equalTo(new AudioAttributesCompat.Builder(mMediaAAC).build()));
+ assertThat("different things are different", mMediaAAC, not(equalTo(mNotificationAAC)));
+ assertThat("different things are different 2", mNotificationAAC, not(equalTo(mMediaAAC)));
+ assertThat(
+ "equal to clone 2",
+ mNotificationAAC,
+ equalTo(new AudioAttributesCompat.Builder(mNotificationAAC).build()));
+ }
+
+ @Test
+ public void testGetters() {
+ assertThat(mMediaAAC.getContentType(), equalTo(AudioAttributesCompat.CONTENT_TYPE_MUSIC));
+ assertThat(mMediaAAC.getUsage(), equalTo(AudioAttributesCompat.USAGE_MEDIA));
+ assertThat(mMediaAAC.getFlags(), equalTo(0));
+ }
+
+ @Test
+ public void testLegacyStreamTypeInference() {
+ assertThat(mMediaAAC.getLegacyStreamType(), equalTo(AudioManager.STREAM_MUSIC));
+ assertThat(mMediaLegacyAAC.getLegacyStreamType(), equalTo(AudioManager.STREAM_MUSIC));
+ assertThat(mNotificationAAC.getLegacyStreamType(), equalTo(AudioManager.STREAM_NOTIFICATION));
+ assertThat(
+ mNotificationLegacyAAC.getLegacyStreamType(), equalTo(AudioManager.STREAM_NOTIFICATION));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 21)
+ public void testLegacyStreamTypeInferenceApi21() {
+ assertThat(mMediaAACFromAA.getLegacyStreamType(), equalTo(AudioManager.STREAM_MUSIC));
+ }
+
+ @Test
+ public void testLegacyStreamTypeInferenceInLegacyMode() {
+ // the builders behave differently based on the value of this only-for-testing global
+ // so we need our very own objects inside this method
+ AudioAttributesCompat.setForceLegacyBehavior(true);
+
+ AudioAttributesCompat mediaAAC =
+ mkBuilder(AudioAttributesCompat.CONTENT_TYPE_MUSIC, AudioAttributesCompat.USAGE_MEDIA)
+ .build();
+ AudioAttributesCompat mediaLegacyAAC = mkBuilder(AudioManager.STREAM_MUSIC).build();
+
+ AudioAttributesCompat notificationAAC =
+ mkBuilder(
+ AudioAttributesCompat.CONTENT_TYPE_SONIFICATION,
+ AudioAttributesCompat.USAGE_NOTIFICATION)
+ .build();
+ AudioAttributesCompat notificationLegacyAAC =
+ mkBuilder(AudioManager.STREAM_NOTIFICATION).build();
+
+ assertThat(mediaAAC.getLegacyStreamType(), equalTo(AudioManager.STREAM_MUSIC));
+ assertThat(mediaLegacyAAC.getLegacyStreamType(), equalTo(AudioManager.STREAM_MUSIC));
+ assertThat(notificationAAC.getLegacyStreamType(), equalTo(AudioManager.STREAM_NOTIFICATION));
+ assertThat(
+ notificationLegacyAAC.getLegacyStreamType(), equalTo(AudioManager.STREAM_NOTIFICATION));
+ }
+
+ @Test
+ public void testUsageAndContentTypeInferredFromLegacyStreamType() {
+ AudioAttributesCompat alarmAAC = mkBuilder(AudioManager.STREAM_ALARM).build();
+ assertThat(alarmAAC.getUsage(), equalTo(AudioAttributesCompat.USAGE_ALARM));
+ assertThat(alarmAAC.getContentType(), equalTo(AudioAttributesCompat.CONTENT_TYPE_SONIFICATION));
+
+ AudioAttributesCompat musicAAC = mkBuilder(AudioManager.STREAM_MUSIC).build();
+ assertThat(musicAAC.getUsage(), equalTo(AudioAttributesCompat.USAGE_MEDIA));
+ assertThat(musicAAC.getContentType(), equalTo(AudioAttributesCompat.CONTENT_TYPE_MUSIC));
+
+ AudioAttributesCompat notificationAAC = mkBuilder(AudioManager.STREAM_NOTIFICATION).build();
+ assertThat(notificationAAC.getUsage(), equalTo(AudioAttributesCompat.USAGE_NOTIFICATION));
+ assertThat(
+ notificationAAC.getContentType(), equalTo(AudioAttributesCompat.CONTENT_TYPE_SONIFICATION));
+
+ AudioAttributesCompat voiceCallAAC = mkBuilder(AudioManager.STREAM_VOICE_CALL).build();
+ assertThat(voiceCallAAC.getUsage(), equalTo(AudioAttributesCompat.USAGE_VOICE_COMMUNICATION));
+ assertThat(voiceCallAAC.getContentType(), equalTo(AudioAttributesCompat.CONTENT_TYPE_SPEECH));
+ }
+
+ @After
+ public void cleanUp() {
+ AudioAttributesCompat.setForceLegacyBehavior(false);
+ }
+}
diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/legacy/MediaDescriptionCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/legacy/MediaDescriptionCompatTest.java
new file mode 100644
index 0000000000..5c7e18c4df
--- /dev/null
+++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/legacy/MediaDescriptionCompatTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+
+import android.net.Uri;
+import android.os.Bundle;
+import androidx.media3.test.session.common.TestUtils;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Tests for {@link MediaDescriptionCompat}. */
+@RunWith(AndroidJUnit4.class)
+public class MediaDescriptionCompatTest {
+
+ @SdkSuppress(minSdkVersion = 21)
+ @Test
+ public void roundTripViaFrameworkObject_returnsEqualMediaUriAndExtras() {
+ Uri mediaUri = Uri.parse("androidx://media/uri");
+ MediaDescriptionCompat originalDescription =
+ new MediaDescriptionCompat.Builder()
+ .setMediaUri(mediaUri)
+ .setExtras(createExtras())
+ .build();
+
+ MediaDescriptionCompat restoredDescription =
+ MediaDescriptionCompat.fromMediaDescription(originalDescription.getMediaDescription());
+
+ // Test second round-trip as MediaDescriptionCompat keeps an internal reference to a previously
+ // restored platform instance.
+ MediaDescriptionCompat restoredDescription2 =
+ MediaDescriptionCompat.fromMediaDescription(restoredDescription.getMediaDescription());
+
+ assertEquals(mediaUri, restoredDescription.getMediaUri());
+ TestUtils.equals(createExtras(), restoredDescription.getExtras());
+ assertEquals(mediaUri, restoredDescription2.getMediaUri());
+ TestUtils.equals(createExtras(), restoredDescription2.getExtras());
+ }
+
+ @SdkSuppress(minSdkVersion = 21)
+ @Test
+ public void getMediaDescription_withMediaUri_doesNotTouchExtras() {
+ MediaDescriptionCompat originalDescription =
+ new MediaDescriptionCompat.Builder()
+ .setMediaUri(Uri.EMPTY)
+ .setExtras(createExtras())
+ .build();
+ originalDescription.getMediaDescription();
+ TestUtils.equals(createExtras(), originalDescription.getExtras());
+ }
+
+ private static Bundle createExtras() {
+ Bundle extras = new Bundle();
+ extras.putString("key1", "value1");
+ extras.putString("key2", "value2");
+ return extras;
+ }
+}
diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/legacy/MediaItemTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/legacy/MediaItemTest.java
new file mode 100644
index 0000000000..aa54fa80f4
--- /dev/null
+++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/legacy/MediaItemTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.os.Parcel;
+import androidx.media3.session.legacy.MediaBrowserCompat.MediaItem;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Test {@link MediaItem}. */
+@RunWith(AndroidJUnit4.class)
+public class MediaItemTest {
+ private static final String DESCRIPTION = "test_description";
+ private static final String MEDIA_ID = "test_media_id";
+ private static final String TITLE = "test_title";
+ private static final String SUBTITLE = "test_subtitle";
+
+ @Test
+ public void testBrowsableMediaItem() {
+ MediaDescriptionCompat description =
+ new MediaDescriptionCompat.Builder()
+ .setDescription(DESCRIPTION)
+ .setMediaId(MEDIA_ID)
+ .setTitle(TITLE)
+ .setSubtitle(SUBTITLE)
+ .build();
+ MediaItem mediaItem = new MediaItem(description, MediaItem.FLAG_BROWSABLE);
+
+ assertEquals(description.toString(), mediaItem.getDescription().toString());
+ assertEquals(MEDIA_ID, mediaItem.getMediaId());
+ assertEquals(MediaItem.FLAG_BROWSABLE, mediaItem.getFlags());
+ assertTrue(mediaItem.isBrowsable());
+ assertFalse(mediaItem.isPlayable());
+ assertEquals(0, mediaItem.describeContents());
+
+ // Test writeToParcel
+ Parcel p = Parcel.obtain();
+ mediaItem.writeToParcel(p, 0);
+ p.setDataPosition(0);
+ assertEquals(mediaItem.getFlags(), p.readInt());
+ assertEquals(
+ description.toString(), MediaDescriptionCompat.CREATOR.createFromParcel(p).toString());
+ p.recycle();
+ }
+
+ @Test
+ public void testPlayableMediaItem() {
+ MediaDescriptionCompat description =
+ new MediaDescriptionCompat.Builder()
+ .setDescription(DESCRIPTION)
+ .setMediaId(MEDIA_ID)
+ .setTitle(TITLE)
+ .setSubtitle(SUBTITLE)
+ .build();
+ MediaItem mediaItem = new MediaItem(description, MediaItem.FLAG_PLAYABLE);
+
+ assertEquals(description.toString(), mediaItem.getDescription().toString());
+ assertEquals(MEDIA_ID, mediaItem.getMediaId());
+ assertEquals(MediaItem.FLAG_PLAYABLE, mediaItem.getFlags());
+ assertFalse(mediaItem.isBrowsable());
+ assertTrue(mediaItem.isPlayable());
+ assertEquals(0, mediaItem.describeContents());
+
+ // Test writeToParcel
+ Parcel p = Parcel.obtain();
+ mediaItem.writeToParcel(p, 0);
+ p.setDataPosition(0);
+ assertEquals(mediaItem.getFlags(), p.readInt());
+ assertEquals(
+ description.toString(), MediaDescriptionCompat.CREATOR.createFromParcel(p).toString());
+ p.recycle();
+ }
+}
diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/legacy/PlaybackStateCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/legacy/PlaybackStateCompatTest.java
new file mode 100644
index 0000000000..22373ed55b
--- /dev/null
+++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/legacy/PlaybackStateCompatTest.java
@@ -0,0 +1,316 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import java.util.ArrayList;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Test {@link PlaybackStateCompat}. */
+@RunWith(AndroidJUnit4.class)
+public class PlaybackStateCompatTest {
+
+ private static final long TEST_POSITION = 20000L;
+ private static final long TEST_BUFFERED_POSITION = 15000L;
+ private static final long TEST_UPDATE_TIME = 100000L;
+ private static final long TEST_ACTIONS =
+ PlaybackStateCompat.ACTION_PLAY
+ | PlaybackStateCompat.ACTION_STOP
+ | PlaybackStateCompat.ACTION_SEEK_TO;
+ private static final long TEST_QUEUE_ITEM_ID = 23L;
+ private static final float TEST_PLAYBACK_SPEED = 3.0f;
+ private static final float TEST_PLAYBACK_SPEED_ON_REWIND = -2.0f;
+ private static final float DELTA = 1e-7f;
+
+ private static final int TEST_ERROR_CODE = PlaybackStateCompat.ERROR_CODE_AUTHENTICATION_EXPIRED;
+ private static final String TEST_ERROR_MSG = "test-error-msg";
+ private static final String TEST_CUSTOM_ACTION = "test-custom-action";
+ private static final String TEST_CUSTOM_ACTION_NAME = "test-custom-action-name";
+ private static final int TEST_ICON_RESOURCE_ID = android.R.drawable.ic_media_next;
+
+ private static final String EXTRAS_KEY = "test-key";
+ private static final String EXTRAS_VALUE = "test-value";
+
+ /** Test default values of {@link PlaybackStateCompat}. */
+ @Test
+ public void testBuilder() {
+ PlaybackStateCompat state = new PlaybackStateCompat.Builder().build();
+
+ assertEquals(new ArrayList(), state.getCustomActions());
+ assertEquals(0, state.getState());
+ assertEquals(0L, state.getPosition());
+ assertEquals(0L, state.getBufferedPosition());
+ assertEquals(0.0f, state.getPlaybackSpeed(), DELTA);
+ assertEquals(0L, state.getActions());
+ assertEquals(0, state.getErrorCode());
+ assertNull(state.getErrorMessage());
+ assertEquals(0L, state.getLastPositionUpdateTime());
+ assertEquals(MediaSessionCompat.QueueItem.UNKNOWN_ID, state.getActiveQueueItemId());
+ assertNull(state.getExtras());
+ }
+
+ /**
+ * Test following setter methods of {@link PlaybackStateCompat.Builder}: {@link
+ * PlaybackStateCompat.Builder#setState(int, long, float)} {@link
+ * PlaybackStateCompat.Builder#setActions(long)} {@link
+ * PlaybackStateCompat.Builder#setActiveQueueItemId(long)} {@link
+ * PlaybackStateCompat.Builder#setBufferedPosition(long)} {@link
+ * PlaybackStateCompat.Builder#setErrorMessage(CharSequence)} {@link
+ * PlaybackStateCompat.Builder#setExtras(Bundle)}
+ */
+ @Test
+ @SuppressWarnings("deprecation")
+ public void testBuilder_setterMethods() {
+ Bundle extras = new Bundle();
+ extras.putString(EXTRAS_KEY, EXTRAS_VALUE);
+
+ PlaybackStateCompat state =
+ new PlaybackStateCompat.Builder()
+ .setState(PlaybackStateCompat.STATE_PLAYING, TEST_POSITION, TEST_PLAYBACK_SPEED)
+ .setActions(TEST_ACTIONS)
+ .setActiveQueueItemId(TEST_QUEUE_ITEM_ID)
+ .setBufferedPosition(TEST_BUFFERED_POSITION)
+ .setErrorMessage(TEST_ERROR_CODE, TEST_ERROR_MSG)
+ .setExtras(extras)
+ .build();
+ assertEquals(PlaybackStateCompat.STATE_PLAYING, state.getState());
+ assertEquals(TEST_POSITION, state.getPosition());
+ assertEquals(TEST_PLAYBACK_SPEED, state.getPlaybackSpeed(), DELTA);
+ assertEquals(TEST_ACTIONS, state.getActions());
+ assertEquals(TEST_QUEUE_ITEM_ID, state.getActiveQueueItemId());
+ assertEquals(TEST_BUFFERED_POSITION, state.getBufferedPosition());
+ assertEquals(TEST_ERROR_CODE, state.getErrorCode());
+ assertEquals(TEST_ERROR_MSG, state.getErrorMessage().toString());
+ assertNotNull(state.getExtras());
+ assertEquals(EXTRAS_VALUE, state.getExtras().get(EXTRAS_KEY));
+ }
+
+ /** Test {@link PlaybackStateCompat.Builder#setState(int, long, float, long)}. */
+ @Test
+ public void testBuilder_setStateWithUpdateTime() {
+ PlaybackStateCompat state =
+ new PlaybackStateCompat.Builder()
+ .setState(
+ PlaybackStateCompat.STATE_REWINDING,
+ TEST_POSITION,
+ TEST_PLAYBACK_SPEED_ON_REWIND,
+ TEST_UPDATE_TIME)
+ .build();
+ assertEquals(PlaybackStateCompat.STATE_REWINDING, state.getState());
+ assertEquals(TEST_POSITION, state.getPosition());
+ assertEquals(TEST_PLAYBACK_SPEED_ON_REWIND, state.getPlaybackSpeed(), DELTA);
+ assertEquals(TEST_UPDATE_TIME, state.getLastPositionUpdateTime());
+ }
+
+ /** Test {@link PlaybackStateCompat.Builder#addCustomAction(String, String, int)}. */
+ @Test
+ public void testBuilder_addCustomAction() {
+ ArrayList actions = new ArrayList<>();
+ PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
+
+ for (int i = 0; i < 5; i++) {
+ actions.add(
+ new PlaybackStateCompat.CustomAction.Builder(
+ TEST_CUSTOM_ACTION + i, TEST_CUSTOM_ACTION_NAME + i, TEST_ICON_RESOURCE_ID + i)
+ .build());
+ builder.addCustomAction(
+ TEST_CUSTOM_ACTION + i, TEST_CUSTOM_ACTION_NAME + i, TEST_ICON_RESOURCE_ID + i);
+ }
+
+ PlaybackStateCompat state = builder.build();
+ assertEquals(actions.size(), state.getCustomActions().size());
+ for (int i = 0; i < actions.size(); i++) {
+ assertCustomActionEquals(actions.get(i), state.getCustomActions().get(i));
+ }
+ }
+
+ /** Test {@link PlaybackStateCompat.Builder#addCustomAction(PlaybackStateCompat.CustomAction)}. */
+ @Test
+ public void testBuilder_addCustomActionWithCustomActionObject() {
+ Bundle extras = new Bundle();
+ extras.putString(EXTRAS_KEY, EXTRAS_VALUE);
+
+ ArrayList actions = new ArrayList<>();
+ PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder();
+
+ for (int i = 0; i < 5; i++) {
+ actions.add(
+ new PlaybackStateCompat.CustomAction.Builder(
+ TEST_CUSTOM_ACTION + i, TEST_CUSTOM_ACTION_NAME + i, TEST_ICON_RESOURCE_ID + i)
+ .setExtras(extras)
+ .build());
+ builder.addCustomAction(
+ new PlaybackStateCompat.CustomAction.Builder(
+ TEST_CUSTOM_ACTION + i, TEST_CUSTOM_ACTION_NAME + i, TEST_ICON_RESOURCE_ID + i)
+ .setExtras(extras)
+ .build());
+ }
+
+ PlaybackStateCompat state = builder.build();
+ assertEquals(actions.size(), state.getCustomActions().size());
+ for (int i = 0; i < actions.size(); i++) {
+ assertCustomActionEquals(actions.get(i), state.getCustomActions().get(i));
+ }
+ }
+
+ /** Test {@link PlaybackStateCompat#writeToParcel(Parcel, int)}. */
+ @Test
+ @SuppressWarnings("deprecation")
+ public void testWriteToParcel() {
+ Bundle extras = new Bundle();
+ extras.putString(EXTRAS_KEY, EXTRAS_VALUE);
+
+ PlaybackStateCompat.Builder builder =
+ new PlaybackStateCompat.Builder()
+ .setState(
+ PlaybackStateCompat.STATE_CONNECTING,
+ TEST_POSITION,
+ TEST_PLAYBACK_SPEED,
+ TEST_UPDATE_TIME)
+ .setActions(TEST_ACTIONS)
+ .setActiveQueueItemId(TEST_QUEUE_ITEM_ID)
+ .setBufferedPosition(TEST_BUFFERED_POSITION)
+ .setErrorMessage(TEST_ERROR_CODE, TEST_ERROR_MSG)
+ .setExtras(extras);
+
+ for (int i = 0; i < 5; i++) {
+ builder.addCustomAction(
+ new PlaybackStateCompat.CustomAction.Builder(
+ TEST_CUSTOM_ACTION + i, TEST_CUSTOM_ACTION_NAME + i, TEST_ICON_RESOURCE_ID + i)
+ .setExtras(extras)
+ .build());
+ }
+ PlaybackStateCompat state = builder.build();
+
+ Parcel parcel = Parcel.obtain();
+ state.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+
+ PlaybackStateCompat stateOut = PlaybackStateCompat.CREATOR.createFromParcel(parcel);
+ assertEquals(PlaybackStateCompat.STATE_CONNECTING, stateOut.getState());
+ assertEquals(TEST_POSITION, stateOut.getPosition());
+ assertEquals(TEST_PLAYBACK_SPEED, stateOut.getPlaybackSpeed(), DELTA);
+ assertEquals(TEST_UPDATE_TIME, stateOut.getLastPositionUpdateTime());
+ assertEquals(TEST_BUFFERED_POSITION, stateOut.getBufferedPosition());
+ assertEquals(TEST_ACTIONS, stateOut.getActions());
+ assertEquals(TEST_QUEUE_ITEM_ID, stateOut.getActiveQueueItemId());
+ assertEquals(TEST_ERROR_CODE, stateOut.getErrorCode());
+ assertEquals(TEST_ERROR_MSG, stateOut.getErrorMessage());
+ assertNotNull(stateOut.getExtras());
+ assertEquals(EXTRAS_VALUE, stateOut.getExtras().get(EXTRAS_KEY));
+
+ assertEquals(state.getCustomActions().size(), stateOut.getCustomActions().size());
+ for (int i = 0; i < state.getCustomActions().size(); i++) {
+ assertCustomActionEquals(state.getCustomActions().get(i), stateOut.getCustomActions().get(i));
+ }
+ parcel.recycle();
+ }
+
+ /** Test {@link PlaybackStateCompat#describeContents()}. */
+ @Test
+ public void testDescribeContents() {
+ assertEquals(0, new PlaybackStateCompat.Builder().build().describeContents());
+ }
+
+ /** Test {@link PlaybackStateCompat.CustomAction}. */
+ @Test
+ @SuppressWarnings("deprecation")
+ public void testCustomAction() {
+ Bundle extras = new Bundle();
+ extras.putString(EXTRAS_KEY, EXTRAS_VALUE);
+
+ // Test Builder/Getters
+ PlaybackStateCompat.CustomAction customAction =
+ new PlaybackStateCompat.CustomAction.Builder(
+ TEST_CUSTOM_ACTION, TEST_CUSTOM_ACTION_NAME, TEST_ICON_RESOURCE_ID)
+ .setExtras(extras)
+ .build();
+ assertEquals(TEST_CUSTOM_ACTION, customAction.getAction());
+ assertEquals(TEST_CUSTOM_ACTION_NAME, customAction.getName().toString());
+ assertEquals(TEST_ICON_RESOURCE_ID, customAction.getIcon());
+ assertEquals(EXTRAS_VALUE, customAction.getExtras().get(EXTRAS_KEY));
+
+ // Test describeContents
+ assertEquals(0, customAction.describeContents());
+
+ // Test writeToParcel
+ Parcel parcel = Parcel.obtain();
+ customAction.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+
+ assertCustomActionEquals(
+ customAction, PlaybackStateCompat.CustomAction.CREATOR.createFromParcel(parcel));
+ parcel.recycle();
+ }
+
+ /** Tests that each ACTION_* constant does not overlap. */
+ @Test
+ public void testActionConstantDoesNotOverlap() {
+ long[] actionConstants =
+ new long[] {
+ PlaybackStateCompat.ACTION_STOP,
+ PlaybackStateCompat.ACTION_PAUSE,
+ PlaybackStateCompat.ACTION_PLAY,
+ PlaybackStateCompat.ACTION_REWIND,
+ PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS,
+ PlaybackStateCompat.ACTION_SKIP_TO_NEXT,
+ PlaybackStateCompat.ACTION_FAST_FORWARD,
+ PlaybackStateCompat.ACTION_SET_RATING,
+ PlaybackStateCompat.ACTION_SEEK_TO,
+ PlaybackStateCompat.ACTION_PLAY_PAUSE,
+ PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID,
+ PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH,
+ PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM,
+ PlaybackStateCompat.ACTION_PLAY_FROM_URI,
+ PlaybackStateCompat.ACTION_PREPARE,
+ PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID,
+ PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH,
+ PlaybackStateCompat.ACTION_PREPARE_FROM_URI,
+ PlaybackStateCompat.ACTION_SET_REPEAT_MODE,
+ PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE,
+ PlaybackStateCompat.ACTION_SET_CAPTIONING_ENABLED,
+ PlaybackStateCompat.ACTION_SET_PLAYBACK_SPEED
+ };
+
+ // Check that the values are not overlapped.
+ for (int i = 0; i < actionConstants.length; i++) {
+ for (int j = i + 1; j < actionConstants.length; j++) {
+ assertEquals(0, actionConstants[i] & actionConstants[j]);
+ }
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ private void assertCustomActionEquals(
+ PlaybackStateCompat.CustomAction action1, PlaybackStateCompat.CustomAction action2) {
+ assertEquals(action1.getAction(), action2.getAction());
+ assertEquals(action1.getName(), action2.getName());
+ assertEquals(action1.getIcon(), action2.getIcon());
+
+ // To be the same, two extras should be both null or both not null.
+ assertEquals(action1.getExtras() != null, action2.getExtras() != null);
+ if (action1.getExtras() != null) {
+ assertEquals(action1.getExtras().get(EXTRAS_KEY), action2.getExtras().get(EXTRAS_KEY));
+ }
+ }
+}
diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/legacy/RemoteUserInfoTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/legacy/RemoteUserInfoTest.java
new file mode 100644
index 0000000000..55356e6a28
--- /dev/null
+++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/legacy/RemoteUserInfoTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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 org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Test of {@link MediaSessionManager.RemoteUserInfo} methods. */
+@RunWith(AndroidJUnit4.class)
+public class RemoteUserInfoTest {
+ @Test
+ public void testConstructor() {
+ String testPackageName = "com.media.test";
+ int testPid = 1000;
+ int testUid = 2000;
+ MediaSessionManager.RemoteUserInfo remoteUserInfo =
+ new MediaSessionManager.RemoteUserInfo(testPackageName, testPid, testUid);
+ assertEquals(testPackageName, remoteUserInfo.getPackageName());
+ assertEquals(testPid, remoteUserInfo.getPid());
+ assertEquals(testUid, remoteUserInfo.getUid());
+ }
+
+ @Test
+ public void testConstructor_withNullPackageName_throwsNPE() {
+ try {
+ MediaSessionManager.RemoteUserInfo remoteUserInfo =
+ new MediaSessionManager.RemoteUserInfo(null, 1000, 2000);
+ fail("null package name shouldn't be allowed");
+ } catch (NullPointerException e) {
+ // expected
+ } catch (Exception e) {
+ fail("unexpected exception " + e);
+ }
+ }
+
+ @Test
+ public void testConstructor_withEmptyPackageName_throwsIAE() {
+ try {
+ MediaSessionManager.RemoteUserInfo remoteUserInfo =
+ new MediaSessionManager.RemoteUserInfo("", 1000, 2000);
+ fail("empty package name shouldn't be allowed");
+ } catch (IllegalArgumentException e) {
+ // expected
+ } catch (Exception e) {
+ fail("unexpected exception " + e);
+ }
+ }
+}
diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaTestUtils.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaTestUtils.java
index 3eaa366f9c..d27e33617f 100644
--- a/libraries/test_session_current/src/main/java/androidx/media3/session/MediaTestUtils.java
+++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MediaTestUtils.java
@@ -241,8 +241,19 @@ public final class MediaTestUtils {
List list = new ArrayList<>();
for (int i = 0; i < mediaItems.size(); i++) {
MediaItem item = mediaItems.get(i);
- MediaDescriptionCompat description =
+ androidx.media3.session.legacy.MediaDescriptionCompat media3Description =
LegacyConversions.convertToMediaDescriptionCompat(item, null);
+ MediaDescriptionCompat description =
+ new MediaDescriptionCompat.Builder()
+ .setTitle(media3Description.getTitle())
+ .setSubtitle(media3Description.getSubtitle())
+ .setDescription(media3Description.getDescription())
+ .setIconUri(media3Description.getIconUri())
+ .setIconBitmap(media3Description.getIconBitmap())
+ .setMediaId(media3Description.getMediaId())
+ .setMediaUri(media3Description.getMediaUri())
+ .setExtras(media3Description.getExtras())
+ .build();
long id = LegacyConversions.convertToQueueItemId(i);
list.add(new MediaSessionCompat.QueueItem(description, id));
}