Java tutorial
/* * Copyright (C) 2014 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 android.support.v4.media.session; import static androidx.annotation.RestrictTo.Scope.LIBRARY; import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; import static androidx.media.MediaSessionManager.RemoteUserInfo.LEGACY_CONTROLLER; import static androidx.media.MediaSessionManager.RemoteUserInfo.UNKNOWN_PID; import static androidx.media.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.support.v4.media.MediaDescriptionCompat; import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.RatingCompat; import android.text.TextUtils; import android.util.Log; import android.util.TypedValue; import android.view.KeyEvent; import android.view.ViewConfiguration; import androidx.annotation.GuardedBy; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.RestrictTo; import androidx.core.app.BundleCompat; import androidx.media.MediaSessionManager; import androidx.media.MediaSessionManager.RemoteUserInfo; import androidx.media.VolumeProviderCompat; import androidx.media.session.MediaButtonReceiver; 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.Method; import java.util.ArrayList; import java.util.List; /** * Allows interaction with media controllers, volume keys, media buttons, and * transport controls. * <p> * 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. * <p> * 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. * <p> * To receive commands, media keys, and other events a {@link Callback} must be * set with {@link #setCallback(Callback)}. * <p> * When an app is finished performing playback it must call {@link #release()} * to clean up the session and notify any controllers. * <p> * MediaSessionCompat objects are not thread safe and all calls should be made * from the same thread. * <p> * This is a helper for accessing features in * {@link android.media.session.MediaSession} introduced after API level 4 in a * backwards compatible fashion. * * <div class="special reference"> * <h3>Developer Guides</h3> * <p>For information about building your media application, read the * <a href="{@docRoot}guide/topics/media-apps/index.html">Media Apps</a> developer guide.</p> * </div> */ public class MediaSessionCompat { static final String TAG = "MediaSessionCompat"; private final MediaSessionImpl mImpl; private final MediaControllerCompat mController; private final ArrayList<OnActiveChangeListener> mActiveListeners = new ArrayList<>(); /** * @hide */ @RestrictTo(LIBRARY_GROUP_PREFIX) @IntDef(flag = true, value = { FLAG_HANDLES_MEDIA_BUTTONS, FLAG_HANDLES_TRANSPORT_CONTROLS, FLAG_HANDLES_QUEUE_COMMANDS }) @Retention(RetentionPolicy.SOURCE) public @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: * <ul> * <li>{@link #MEDIA_ATTRIBUTE_ARTIST}</li> * <li>{@link #MEDIA_ATTRIBUTE_PLAYLIST}</li> * <li>{@link #MEDIA_ATTRIBUTE_ALBUM}</li> * </ul> */ 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. * * @hide */ @RestrictTo(LIBRARY) 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. * * @hide */ @RestrictTo(LIBRARY) public static final String ACTION_PREPARE = "android.support.v4.media.session.action.PREPARE"; /** * Custom action to invoke prepareFromMediaId() for the forward compatibility. * * @hide */ @RestrictTo(LIBRARY) 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. * * @hide */ @RestrictTo(LIBRARY) 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. * * @hide */ @RestrictTo(LIBRARY) 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. * * @hide */ @RestrictTo(LIBRARY) 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. * * @hide */ @RestrictTo(LIBRARY) 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. * * @hide */ @RestrictTo(LIBRARY) 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. * * @hide */ @RestrictTo(LIBRARY) public static final String ACTION_SET_RATING = "android.support.v4.media.session.action.SET_RATING"; /** * Custom action to invoke setPlaybackSpeed() with extra fields. * * @hide */ @RestrictTo(LIBRARY) 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. * * @hide */ @RestrictTo(LIBRARY) 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. * * @hide */ @RestrictTo(LIBRARY) 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. * * @hide */ @RestrictTo(LIBRARY) 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. * * @hide */ @RestrictTo(LIBRARY) 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. * * @hide */ @RestrictTo(LIBRARY) 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. * * @hide */ @RestrictTo(LIBRARY) 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. * * @hide */ @RestrictTo(LIBRARY) 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. * * @hide */ @RestrictTo(LIBRARY) 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. * * @hide */ @RestrictTo(LIBRARY) public static final String ACTION_ARGUMENT_SHUFFLE_MODE = "android.support.v4.media.session.action.ARGUMENT_SHUFFLE_MODE"; /** * @hide */ @RestrictTo(LIBRARY_GROUP_PREFIX) public static final String KEY_TOKEN = "android.support.v4.media.session.TOKEN"; /** * @hide */ @RestrictTo(LIBRARY) public static final String KEY_EXTRA_BINDER = "android.support.v4.media.session.EXTRA_BINDER"; /** * @hide */ @RestrictTo(LIBRARY_GROUP_PREFIX) 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. * <p> * The session will automatically be registered with the system but will not be published * until {@link #setActive(boolean) setActive(true)} is called. * </p><p> * 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. * </p> * @param context The context to use to create the session. * @param tag A short name for debugging purposes. */ public MediaSessionCompat(@NonNull Context context, @NonNull 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. * <p> * The session will automatically be registered with the system but will not be published * until {@link #setActive(boolean) setActive(true)} is called. * </p><p> * 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. * </p> * @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(@NonNull Context context, @NonNull 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. * <p> * The session will automatically be registered with the system but will not be published * until {@link #setActive(boolean) setActive(true)} is called. * </p><p> * 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. * </p> * 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(@NonNull Context context, @NonNull String tag, @Nullable ComponentName mbrComponent, @Nullable PendingIntent mbrIntent, @Nullable Bundle sessionInfo) { this(context, tag, mbrComponent, mbrIntent, sessionInfo, null /* session2Token */); } /** * @hide */ @RestrictTo(LIBRARY_GROUP_PREFIX) public MediaSessionCompat(@NonNull Context context, @NonNull 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, 0/* flags */); } if (doesBundleHaveCustomParcelable(sessionInfo)) { throw new IllegalArgumentException("sessionInfo shouldn't contain any custom " + "parcelables"); } if (android.os.Build.VERSION.SDK_INT >= 21) { MediaSession sessionFwk = createFwkMediaSession(context, tag, sessionInfo); if (android.os.Build.VERSION.SDK_INT >= 29) { mImpl = new MediaSessionImplApi29(sessionFwk, session2Token, sessionInfo); } else if (android.os.Build.VERSION.SDK_INT >= 28) { mImpl = new MediaSessionImplApi28(sessionFwk, session2Token, sessionInfo); } else { mImpl = new MediaSessionImplApi21(sessionFwk, session2Token, sessionInfo); } // Set default callback to respond to controllers' extra binder requests. setCallback(new Callback() { }); mImpl.setMediaButtonReceiver(mbrIntent); } else if (android.os.Build.VERSION.SDK_INT >= 19) { mImpl = new MediaSessionImplApi19(context, tag, mbrComponent, mbrIntent, sessionInfo); } else if (android.os.Build.VERSION.SDK_INT >= 18) { mImpl = new MediaSessionImplApi18(context, tag, mbrComponent, mbrIntent, sessionInfo); } else { mImpl = new MediaSessionImplBase(context, tag, mbrComponent, mbrIntent, 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); } } private MediaSessionCompat(Context context, MediaSessionImpl impl) { mImpl = impl; mController = new MediaControllerCompat(context, this); } @RequiresApi(21) private MediaSession createFwkMediaSession(Context context, String tag, Bundle sessionInfo) { if (android.os.Build.VERSION.SDK_INT >= 29) { return new MediaSession(context, tag, sessionInfo); } else { return new MediaSession(context, tag); } } /** * 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. * * @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. * * @param callback The callback to receive updates on. * @param handler The handler that events should be posted on. */ public void setCallback(Callback callback, Handler handler) { if (callback == null) { mImpl.setCallback(null, null); } else { mImpl.setCallback(callback, handler != null ? handler : new 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. * <p> * 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. * <p> * 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. * <p> * 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. * <p> * 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, 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. * <p> * 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 android.support.v4.media.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 android.support.v4.media.MediaMetadataCompat.Builder#putBitmap */ public void setMetadata(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. Specify null if there is no current play queue. * <p> * 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(List<QueueItem> queue) { 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: * <ul> * <li>{@link RatingCompat#RATING_NONE}</li> * <li>{@link RatingCompat#RATING_3_STARS}</li> * <li>{@link RatingCompat#RATING_4_STARS}</li> * <li>{@link RatingCompat#RATING_5_STARS}</li> * <li>{@link RatingCompat#RATING_HEART}</li> * <li>{@link RatingCompat#RATING_PERCENTAGE}</li> * <li>{@link RatingCompat#RATING_THUMB_UP_DOWN}</li> * </ul> */ 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. * <p> * 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 followings: * {@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. * <p> * 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 followings: * {@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(Bundle extras) { mImpl.setExtras(extras); } /** * Gets the underlying framework {@link android.media.session.MediaSession} * object. * <p> * This method is only supported on API 21+. * </p> * * @return The underlying {@link android.media.session.MediaSession} object, * or null if none. */ public Object getMediaSession() { return mImpl.getMediaSession(); } /** * Gets the underlying framework {@link android.media.RemoteControlClient} * object. * <p> * 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. */ public Object getRemoteControlClient() { return mImpl.getRemoteControlClient(); } /** * Gets the controller information who sent the current request. * <p> * Note: This is only valid while in a request callback, such as {@link Callback#onPlay}. * <p> * Note: From API 21 to 23, this method returns a dummy {@link RemoteUserInfo} which has * following values: * <ul> * <li>Package name is {@link MediaSessionManager.RemoteUserInfo#LEGACY_CONTROLLER}.</li> * <li>PID and UID will have negative values.</li> * </ul> * <p> * 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) */ @NonNull 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. * * @hide */ @RestrictTo(LIBRARY_GROUP_PREFIX) 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. * <p> * This method is only supported on API 21+. On API 20 and below, it returns null. * <p> * 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. */ public static MediaSessionCompat fromMediaSession(Context context, 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); } /** * Returns whether the given bundle includes non-framework Parcelables. * * @hide */ @RestrictTo(LIBRARY_GROUP_PREFIX) public static boolean doesBundleHaveCustomParcelable(@Nullable Bundle bundle) { if (bundle == null) { return false; } // Try writing the bundle to parcel, and read it with framework classloader. Parcel parcel = null; try { parcel = Parcel.obtain(); parcel.writeBundle(bundle); parcel.setDataPosition(0); Bundle out = parcel.readBundle(null); // Calling Bundle#isEmpty() will trigger Bundle#unparcel(). out.isEmpty(); } catch (BadParcelableException e) { Log.d(TAG, "Custom parcelable in sessionInfo.", e); return true; } finally { if (parcel != null) { parcel.recycle(); } } return false; } /** * A helper method for setting the class loader to {@link Bundle} objects. * * @hide */ @RestrictTo(LIBRARY_GROUP_PREFIX) public static void ensureClassLoader(@Nullable Bundle bundle) { if (bundle != null) { bundle.setClassLoader(MediaSessionCompat.class.getClassLoader()); } } @SuppressWarnings("WeakerAccess") /* synthetic access */ static PlaybackStateCompat getStateWithUpdatedPosition(PlaybackStateCompat state, 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}. */ public abstract static class Callback { final MediaSession.Callback mCallbackFwk; WeakReference<MediaSessionImpl> mSessionImpl; private CallbackHandler mCallbackHandler = null; private boolean mMediaPlayPauseKeyPending; public Callback() { if (android.os.Build.VERSION.SDK_INT >= 21) { mCallbackFwk = new MediaSessionCallbackApi21(); } else { mCallbackFwk = null; } } void setSessionImpl(MediaSessionImpl impl, Handler handler) { mSessionImpl = new WeakReference<MediaSessionImpl>(impl); if (mCallbackHandler != null) { mCallbackHandler.removeCallbacksAndMessages(null); } mCallbackHandler = 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, Bundle extras, ResultReceiver cb) { } /** * Override to handle media button events. * <p> * 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. */ 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 = mSessionImpl.get(); if (impl == null || mCallbackHandler == 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) { // Consider long-press as a single tap. handleMediaPlayPauseKeySingleTapIfPending(); } else if (mMediaPlayPauseKeyPending) { mCallbackHandler.removeMessages(CallbackHandler.MSG_MEDIA_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT); mMediaPlayPauseKeyPending = 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 { mMediaPlayPauseKeyPending = true; mCallbackHandler.sendMessageDelayed( mCallbackHandler.obtainMessage( CallbackHandler.MSG_MEDIA_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT, remoteUserInfo), ViewConfiguration.getDoubleTapTimeout()); } 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. handleMediaPlayPauseKeySingleTapIfPending(); break; } return false; } void handleMediaPlayPauseKeySingleTapIfPending() { if (!mMediaPlayPauseKeyPending) { return; } mMediaPlayPauseKeyPending = false; mCallbackHandler.removeMessages(CallbackHandler.MSG_MEDIA_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT); MediaSessionImpl impl = mSessionImpl.get(); if (impl == null) { return; } 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(String mediaId, 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(String query, 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(Uri uri, 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(String mediaId, 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(String query, Bundle extras) { } /** * Override to handle requests to play a specific media item represented by a URI. */ public void onPlayFromUri(Uri uri, 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(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(RatingCompat rating, 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)}. * <p> * 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. * <p> * 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. * <p> * 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, 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(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(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(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 = mSessionImpl.get(); if (impl == null) { return; } RemoteUserInfo info = (RemoteUserInfo) msg.obj; impl.setCurrentControllerInfo(info); handleMediaPlayPauseKeySingleTapIfPending(); impl.setCurrentControllerInfo(null); } } } @RequiresApi(21) private class MediaSessionCallbackApi21 extends MediaSession.Callback { MediaSessionCallbackApi21() { } @Override public void onCommand(String command, Bundle extras, ResultReceiver cb) { ensureClassLoader(extras); setCurrentControllerInfo(); try { if (command.equals(MediaControllerCompat.COMMAND_GET_EXTRA_BINDER)) { MediaSessionImplApi21 impl = (MediaSessionImplApi21) mSessionImpl.get(); if (impl != null) { Bundle result = new Bundle(); Token token = impl.getSessionToken(); IMediaSession extraBinder = token.getExtraBinder(); BundleCompat.putBinder(result, 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)) { Callback.this.onAddQueueItem((MediaDescriptionCompat) extras .getParcelable(MediaControllerCompat.COMMAND_ARGUMENT_MEDIA_DESCRIPTION)); } else if (command.equals(MediaControllerCompat.COMMAND_ADD_QUEUE_ITEM_AT)) { Callback.this.onAddQueueItem( (MediaDescriptionCompat) extras .getParcelable(MediaControllerCompat.COMMAND_ARGUMENT_MEDIA_DESCRIPTION), extras.getInt(MediaControllerCompat.COMMAND_ARGUMENT_INDEX)); } else if (command.equals(MediaControllerCompat.COMMAND_REMOVE_QUEUE_ITEM)) { Callback.this.onRemoveQueueItem((MediaDescriptionCompat) extras .getParcelable(MediaControllerCompat.COMMAND_ARGUMENT_MEDIA_DESCRIPTION)); } else if (command.equals(MediaControllerCompat.COMMAND_REMOVE_QUEUE_ITEM_AT)) { MediaSessionImplApi21 impl = (MediaSessionImplApi21) mSessionImpl.get(); if (impl != null && impl.mQueue != null) { int index = extras.getInt(MediaControllerCompat.COMMAND_ARGUMENT_INDEX, -1); QueueItem item = (index >= 0 && index < impl.mQueue.size()) ? impl.mQueue.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(); } @Override public boolean onMediaButtonEvent(Intent mediaButtonIntent) { setCurrentControllerInfo(); boolean result = Callback.this.onMediaButtonEvent(mediaButtonIntent); clearCurrentControllerInfo(); return result || super.onMediaButtonEvent(mediaButtonIntent); } @Override public void onPlay() { setCurrentControllerInfo(); Callback.this.onPlay(); clearCurrentControllerInfo(); } @Override public void onPlayFromMediaId(String mediaId, Bundle extras) { ensureClassLoader(extras); setCurrentControllerInfo(); Callback.this.onPlayFromMediaId(mediaId, extras); clearCurrentControllerInfo(); } @Override public void onPlayFromSearch(String search, Bundle extras) { ensureClassLoader(extras); setCurrentControllerInfo(); Callback.this.onPlayFromSearch(search, extras); clearCurrentControllerInfo(); } @RequiresApi(23) @Override public void onPlayFromUri(Uri uri, Bundle extras) { ensureClassLoader(extras); setCurrentControllerInfo(); Callback.this.onPlayFromUri(uri, extras); clearCurrentControllerInfo(); } @Override public void onSkipToQueueItem(long id) { setCurrentControllerInfo(); Callback.this.onSkipToQueueItem(id); clearCurrentControllerInfo(); } @Override public void onPause() { setCurrentControllerInfo(); Callback.this.onPause(); clearCurrentControllerInfo(); } @Override public void onSkipToNext() { setCurrentControllerInfo(); Callback.this.onSkipToNext(); clearCurrentControllerInfo(); } @Override public void onSkipToPrevious() { setCurrentControllerInfo(); Callback.this.onSkipToPrevious(); clearCurrentControllerInfo(); } @Override public void onFastForward() { setCurrentControllerInfo(); Callback.this.onFastForward(); clearCurrentControllerInfo(); } @Override public void onRewind() { setCurrentControllerInfo(); Callback.this.onRewind(); clearCurrentControllerInfo(); } @Override public void onStop() { setCurrentControllerInfo(); Callback.this.onStop(); clearCurrentControllerInfo(); } @Override public void onSeekTo(long pos) { setCurrentControllerInfo(); Callback.this.onSeekTo(pos); clearCurrentControllerInfo(); } @Override public void onSetRating(Rating ratingFwk) { setCurrentControllerInfo(); Callback.this.onSetRating(RatingCompat.fromRating(ratingFwk)); clearCurrentControllerInfo(); } public void onSetRating(Rating ratingFwk, Bundle extras) { // This method will not be called. } @Override public void onCustomAction(String action, Bundle extras) { ensureClassLoader(extras); setCurrentControllerInfo(); Bundle bundle = extras.getBundle(ACTION_ARGUMENT_EXTRAS); ensureClassLoader(bundle); if (action.equals(ACTION_PLAY_FROM_URI)) { Uri uri = extras.getParcelable(ACTION_ARGUMENT_URI); Callback.this.onPlayFromUri(uri, bundle); } else if (action.equals(ACTION_PREPARE)) { Callback.this.onPrepare(); } else if (action.equals(ACTION_PREPARE_FROM_MEDIA_ID)) { String mediaId = extras.getString(ACTION_ARGUMENT_MEDIA_ID); Callback.this.onPrepareFromMediaId(mediaId, bundle); } else if (action.equals(ACTION_PREPARE_FROM_SEARCH)) { String query = extras.getString(ACTION_ARGUMENT_QUERY); Callback.this.onPrepareFromSearch(query, bundle); } else if (action.equals(ACTION_PREPARE_FROM_URI)) { Uri uri = extras.getParcelable(ACTION_ARGUMENT_URI); Callback.this.onPrepareFromUri(uri, bundle); } else if (action.equals(ACTION_SET_CAPTIONING_ENABLED)) { boolean enabled = extras.getBoolean(ACTION_ARGUMENT_CAPTIONING_ENABLED); Callback.this.onSetCaptioningEnabled(enabled); } else if (action.equals(ACTION_SET_REPEAT_MODE)) { int repeatMode = extras.getInt(ACTION_ARGUMENT_REPEAT_MODE); Callback.this.onSetRepeatMode(repeatMode); } else if (action.equals(ACTION_SET_SHUFFLE_MODE)) { int shuffleMode = extras.getInt(ACTION_ARGUMENT_SHUFFLE_MODE); Callback.this.onSetShuffleMode(shuffleMode); } else if (action.equals(ACTION_SET_RATING)) { RatingCompat rating = extras.getParcelable(ACTION_ARGUMENT_RATING); Callback.this.onSetRating(rating, bundle); } else if (action.equals(ACTION_SET_PLAYBACK_SPEED)) { float speed = extras.getFloat(ACTION_ARGUMENT_PLAYBACK_SPEED, 1.0f); Callback.this.onSetPlaybackSpeed(speed); } else { Callback.this.onCustomAction(action, extras); } clearCurrentControllerInfo(); } void setCurrentControllerInfo() { if (Build.VERSION.SDK_INT >= 28) { // From API 28, this method has no effect since // MediaSessionImplApi28#getCurrentControllerInfo() returns controller info from // framework. return; } MediaSessionImpl sessionImpl = mSessionImpl != null ? mSessionImpl.get() : null; if (sessionImpl == null) { return; } String packageName = sessionImpl.getCallingPackage(); if (TextUtils.isEmpty(packageName)) { packageName = LEGACY_CONTROLLER; } sessionImpl.setCurrentControllerInfo(new RemoteUserInfo(packageName, UNKNOWN_PID, UNKNOWN_UID)); } void clearCurrentControllerInfo() { MediaSessionImpl sessionImpl = mSessionImpl != null ? mSessionImpl.get() : null; if (sessionImpl != null) { sessionImpl.setCurrentControllerInfo(null); } } @RequiresApi(24) @Override public void onPrepare() { setCurrentControllerInfo(); Callback.this.onPrepare(); clearCurrentControllerInfo(); } @RequiresApi(24) @Override public void onPrepareFromMediaId(String mediaId, Bundle extras) { ensureClassLoader(extras); setCurrentControllerInfo(); Callback.this.onPrepareFromMediaId(mediaId, extras); clearCurrentControllerInfo(); } @RequiresApi(24) @Override public void onPrepareFromSearch(String query, Bundle extras) { ensureClassLoader(extras); setCurrentControllerInfo(); Callback.this.onPrepareFromSearch(query, extras); clearCurrentControllerInfo(); } @RequiresApi(24) @Override public void onPrepareFromUri(Uri uri, Bundle extras) { ensureClassLoader(extras); setCurrentControllerInfo(); Callback.this.onPrepareFromUri(uri, extras); clearCurrentControllerInfo(); } @RequiresApi(29) @Override public void onSetPlaybackSpeed(float speed) { setCurrentControllerInfo(); Callback.this.onSetPlaybackSpeed(speed); clearCurrentControllerInfo(); } } } /** * 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 mInner; private IMediaSession mExtraBinder; private VersionedParcelable mSession2Token; Token(Object inner) { this(inner, null, null); } Token(Object inner, IMediaSession extraBinder) { this(inner, extraBinder, null); } Token(Object inner, IMediaSession extraBinder, VersionedParcelable session2Token) { mInner = inner; mExtraBinder = extraBinder; mSession2Token = session2Token; } /** * Creates a compat Token from a framework * {@link android.media.session.MediaSession.Token} object. * <p> * This method is only supported on * {@link android.os.Build.VERSION_CODES#LOLLIPOP} and later. * </p> * * @param token The framework token object. * @return A compat Token for use with {@link MediaControllerCompat}. */ 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. * <p> * This method is only supported on * {@link android.os.Build.VERSION_CODES#LOLLIPOP} and later. * </p> * * @param token The framework token object. * @param extraBinder The extra binder. * @return A compat Token for use with {@link MediaControllerCompat}. * @hide */ @RestrictTo(LIBRARY_GROUP_PREFIX) public static Token fromToken(Object token, IMediaSession extraBinder) { if (token != null && android.os.Build.VERSION.SDK_INT >= 21) { if (!(token instanceof MediaSession.Token)) { throw new IllegalArgumentException("token is not a valid MediaSession.Token object"); } return new Token(token, extraBinder); } return null; } @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(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. * <p> * This method is only supported on API 21+. * </p> * * @return The underlying {@link android.media.session.MediaSession.Token} object, * or null if none. */ public Object getToken() { return mInner; } /** * @hide */ @RestrictTo(LIBRARY_GROUP_PREFIX) public IMediaSession getExtraBinder() { return mExtraBinder; } /** * @hide */ @RestrictTo(LIBRARY_GROUP_PREFIX) public void setExtraBinder(IMediaSession extraBinder) { mExtraBinder = extraBinder; } /** * @hide */ @RestrictTo(LIBRARY_GROUP_PREFIX) public VersionedParcelable getSession2Token() { return mSession2Token; } /** * @hide */ @RestrictTo(LIBRARY_GROUP_PREFIX) public void setSession2Token(VersionedParcelable session2Token) { mSession2Token = session2Token; } /** * @hide */ @RestrictTo(LIBRARY_GROUP_PREFIX) public Bundle toBundle() { Bundle bundle = new Bundle(); bundle.putParcelable(KEY_TOKEN, this); if (mExtraBinder != null) { BundleCompat.putBinder(bundle, 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}. * @hide */ @RestrictTo(LIBRARY_GROUP_PREFIX) public static Token fromBundle(Bundle tokenBundle) { if (tokenBundle == null) { return null; } IMediaSession extraSession = IMediaSession.Stub .asInterface(BundleCompat.getBinder(tokenBundle, KEY_EXTRA_BINDER)); VersionedParcelable session2Token = ParcelUtils.getVersionedParcelable(tokenBundle, KEY_SESSION2_TOKEN); Token token = tokenBundle.getParcelable(KEY_TOKEN); return token == null ? null : new Token(token.mInner, extraSession, session2Token); } public static final Parcelable.Creator<Token> CREATOR = new Parcelable.Creator<Token>() { @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(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; 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(MediaSession.QueueItem queueItem, 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}. * <p> * On builds before {@link android.os.Build.VERSION_CODES#LOLLIPOP} null * is returned. * * @return The underlying * {@link android.media.session.MediaSession.QueueItem} or null. */ public Object getQueueItem() { if (mItemFwk != null || android.os.Build.VERSION.SDK_INT < 21) { return mItemFwk; } mItemFwk = new MediaSession.QueueItem((MediaDescription) mDescription.getMediaDescription(), mId); return mItemFwk; } /** * Creates an instance from a framework {@link android.media.session.MediaSession.QueueItem} * object. * <p> * This method is only supported on API 21+. On API 20 and below, it returns null. * </p> * * @param queueItem A {@link android.media.session.MediaSession.QueueItem} object. * @return An equivalent {@link QueueItem} object, or null if none. */ public static QueueItem fromQueueItem(Object queueItem) { if (queueItem == null || Build.VERSION.SDK_INT < 21) { return null; } MediaSession.QueueItem queueItemObj = (MediaSession.QueueItem) queueItem; Object descriptionObj = queueItemObj.getDescription(); MediaDescriptionCompat description = MediaDescriptionCompat.fromMediaDescription(descriptionObj); long id = queueItemObj.getQueueId(); return new QueueItem(queueItemObj, description, id); } /** * Creates a list of {@link QueueItem} objects from a framework * {@link android.media.session.MediaSession.QueueItem} object list. * <p> * This method is only supported on API 21+. On API 20 and below, it returns null. * </p> * * @param itemList A list of {@link android.media.session.MediaSession.QueueItem} objects. * @return An equivalent list of {@link QueueItem} objects, or null if none. */ public static List<QueueItem> fromQueueItemList(List<?> itemList) { if (itemList == null || Build.VERSION.SDK_INT < 21) { return null; } List<QueueItem> items = new ArrayList<>(); for (Object itemObj : itemList) { items.add(fromQueueItem(itemObj)); } return items; } public static final Creator<MediaSessionCompat.QueueItem> CREATOR = new Creator<MediaSessionCompat.QueueItem>() { @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 + " }"; } } /** * 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}. * * @hide */ @RestrictTo(LIBRARY) @SuppressLint("BanParcelableUsage") public static final class ResultReceiverWrapper implements Parcelable { ResultReceiver mResultReceiver; public ResultReceiverWrapper(@NonNull ResultReceiver resultReceiver) { mResultReceiver = resultReceiver; } ResultReceiverWrapper(Parcel in) { mResultReceiver = ResultReceiver.CREATOR.createFromParcel(in); } public static final Creator<ResultReceiverWrapper> CREATOR = new Creator<ResultReceiverWrapper>() { @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(Callback 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, Bundle extras); void release(); Token getSessionToken(); void setPlaybackState(PlaybackStateCompat state); PlaybackStateCompat getPlaybackState(); void setMetadata(MediaMetadataCompat metadata); void setSessionActivity(PendingIntent pi); void setMediaButtonReceiver(PendingIntent mbr); void setQueue(List<QueueItem> 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(Bundle extras); Object getMediaSession(); Object getRemoteControlClient(); String getCallingPackage(); RemoteUserInfo getCurrentControllerInfo(); // Internal only method void setCurrentControllerInfo(RemoteUserInfo remoteUserInfo); } 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; final String mPackageName; final Bundle mSessionInfo; final String mTag; final AudioManager mAudioManager; final RemoteControlClient mRcc; final Object mLock = new Object(); final RemoteCallbackList<IMediaControllerCallback> mControllerCallbacks = new RemoteCallbackList<>(); private MessageHandler mHandler; boolean mDestroyed = false; boolean mIsActive = false; volatile Callback mCallback; private RemoteUserInfo mRemoteUserInfo; // For backward compatibility, these flags are always set. @SessionFlags int mFlags = FLAG_HANDLES_MEDIA_BUTTONS | FLAG_HANDLES_TRANSPORT_CONTROLS; MediaMetadataCompat mMetadata; PlaybackStateCompat mState; PendingIntent mSessionActivity; List<QueueItem> mQueue; CharSequence mQueueTitle; @RatingCompat.Style int mRatingType; boolean mCaptioningEnabled; @PlaybackStateCompat.RepeatMode int mRepeatMode; @PlaybackStateCompat.ShuffleMode int mShuffleMode; Bundle mExtras; int mVolumeType; int mLocalStream; VolumeProviderCompat mVolumeProvider; private VolumeProviderCompat.Callback mVolumeCallback = new VolumeProviderCompat.Callback() { @Override public void onVolumeChanged(VolumeProviderCompat volumeProvider) { if (mVolumeProvider != volumeProvider) { return; } ParcelableVolumeInfo info = new ParcelableVolumeInfo(mVolumeType, mLocalStream, volumeProvider.getVolumeControl(), volumeProvider.getMaxVolume(), volumeProvider.getCurrentVolume()); sendVolumeInfoChanged(info); } }; public MediaSessionImplBase(Context context, String tag, ComponentName mbrComponent, PendingIntent mbrIntent, Bundle sessionInfo) { if (mbrComponent == null) { throw new IllegalArgumentException("MediaButtonReceiver component may not be null"); } mContext = context; mPackageName = context.getPackageName(); mSessionInfo = sessionInfo; mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); mTag = tag; mMediaButtonReceiverComponentName = mbrComponent; mMediaButtonReceiverIntent = mbrIntent; mStub = new MediaSessionStub(); mToken = new Token(mStub); mRatingType = RatingCompat.RATING_NONE; mVolumeType = MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_LOCAL; mLocalStream = AudioManager.STREAM_MUSIC; mRcc = new RemoteControlClient(mbrIntent); } @Override public void setCallback(Callback callback, Handler handler) { mCallback = callback; if (callback != null) { if (handler == null) { handler = new Handler(); } synchronized (mLock) { if (mHandler != null) { mHandler.removeCallbacksAndMessages(null); } mHandler = new MessageHandler(handler.getLooper()); mCallback.setSessionImpl(this, handler); } } } void postToHandler(int what, int arg1, int arg2, Object obj, 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, Bundle extras) { sendEvent(event, extras); } @Override public void release() { mIsActive = false; mDestroyed = true; updateMbrAndRcc(); sendSessionDestroyed(); } @Override public Token getSessionToken() { return mToken; } @Override public void setPlaybackState(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(state); // Set transport control flags mRcc.setTransportControlFlags(getRccTransportControlFlagsFromActions(state.getActions())); } } @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(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(); } RemoteControlClient.MetadataEditor buildRccMetadata(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(PendingIntent mbr) { // Do nothing, changing this is not supported before API 21. } @Override public void setQueue(List<QueueItem> queue) { mQueue = queue; sendQueue(queue); } @Override public void setQueueTitle(CharSequence title) { mQueueTitle = title; sendQueueTitle(title); } @Override public Object getMediaSession() { return null; } @Override public Object getRemoteControlClient() { return null; } @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(Bundle extras) { mExtras = extras; sendExtras(extras); } @Override public RemoteUserInfo getCurrentControllerInfo() { synchronized (mLock) { return mRemoteUserInfo; } } @Override public void setCurrentControllerInfo(RemoteUserInfo remoteUserInfo) { synchronized (mLock) { mRemoteUserInfo = remoteUserInfo; } } // 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) { 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() { 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, Bundle extras) { 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(PlaybackStateCompat state) { 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(MediaMetadataCompat metadata) { 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(List<QueueItem> queue) { 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) { 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) { 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) { 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) { 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(Bundle extras) { 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(); } class MediaSessionStub extends IMediaSession.Stub { @Override public void sendCommand(String command, Bundle args, ResultReceiverWrapper cb) { postToHandler(MessageHandler.MSG_COMMAND, new Command(command, args, cb == null ? null : cb.mResultReceiver)); } @Override public boolean sendMediaButton(KeyEvent mediaButton) { postToHandler(MessageHandler.MSG_MEDIA_BUTTON, mediaButton); return true; } @Override public void registerCallbackListener(IMediaControllerCallback cb) { // If this session is already destroyed tell the caller and // don't add them. if (mDestroyed) { try { cb.onSessionDestroyed(); } catch (Exception e) { // ignored } return; } final int uid = getCallingUid(); RemoteUserInfo info = new RemoteUserInfo(getPackageNameForUid(uid), getCallingPid(), getCallingUid()); mControllerCallbacks.register(cb, info); } @Override public void unregisterCallbackListener(IMediaControllerCallback cb) { mControllerCallbacks.unregister(cb); } @Override public String getPackageName() { // mPackageName is final so doesn't need synchronize block return mPackageName; } @Override public Bundle getSessionInfo() { // mSessionInfo is final so doesn't need synchronize block return mSessionInfo == null ? null : new Bundle(mSessionInfo); } @Override public String getTag() { // mTag is final so doesn't need synchronize block return mTag; } @Override public PendingIntent getLaunchPendingIntent() { synchronized (mLock) { return mSessionActivity; } } @Override @SessionFlags public long getFlags() { synchronized (mLock) { return mFlags; } } @Override public ParcelableVolumeInfo getVolumeAttributes() { int controlType; int max; int current; int stream; int volumeType; synchronized (mLock) { volumeType = mVolumeType; stream = mLocalStream; VolumeProviderCompat vp = mVolumeProvider; if (volumeType == MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE) { controlType = vp.getVolumeControl(); max = vp.getMaxVolume(); current = vp.getCurrentVolume(); } else { controlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE; max = mAudioManager.getStreamMaxVolume(stream); current = mAudioManager.getStreamVolume(stream); } } return new ParcelableVolumeInfo(volumeType, stream, controlType, max, current); } @Override public void adjustVolume(int direction, int flags, String packageName) { MediaSessionImplBase.this.adjustVolume(direction, flags); } @Override public void setVolumeTo(int value, int flags, String packageName) { MediaSessionImplBase.this.setVolumeTo(value, flags); } @Override public void prepare() throws RemoteException { postToHandler(MessageHandler.MSG_PREPARE); } @Override public void prepareFromMediaId(String mediaId, Bundle extras) throws RemoteException { postToHandler(MessageHandler.MSG_PREPARE_MEDIA_ID, mediaId, extras); } @Override public void prepareFromSearch(String query, Bundle extras) throws RemoteException { postToHandler(MessageHandler.MSG_PREPARE_SEARCH, query, extras); } @Override public void prepareFromUri(Uri uri, Bundle extras) throws RemoteException { postToHandler(MessageHandler.MSG_PREPARE_URI, uri, extras); } @Override public void play() throws RemoteException { postToHandler(MessageHandler.MSG_PLAY); } @Override public void playFromMediaId(String mediaId, Bundle extras) throws RemoteException { postToHandler(MessageHandler.MSG_PLAY_MEDIA_ID, mediaId, extras); } @Override public void playFromSearch(String query, Bundle extras) throws RemoteException { postToHandler(MessageHandler.MSG_PLAY_SEARCH, query, extras); } @Override public void playFromUri(Uri uri, Bundle extras) throws RemoteException { postToHandler(MessageHandler.MSG_PLAY_URI, uri, extras); } @Override public void skipToQueueItem(long id) { postToHandler(MessageHandler.MSG_SKIP_TO_ITEM, id); } @Override public void pause() throws RemoteException { postToHandler(MessageHandler.MSG_PAUSE); } @Override public void stop() throws RemoteException { postToHandler(MessageHandler.MSG_STOP); } @Override public void next() throws RemoteException { postToHandler(MessageHandler.MSG_NEXT); } @Override public void previous() throws RemoteException { postToHandler(MessageHandler.MSG_PREVIOUS); } @Override public void fastForward() throws RemoteException { postToHandler(MessageHandler.MSG_FAST_FORWARD); } @Override public void rewind() throws RemoteException { postToHandler(MessageHandler.MSG_REWIND); } @Override public void seekTo(long pos) throws RemoteException { postToHandler(MessageHandler.MSG_SEEK_TO, pos); } @Override public void rate(RatingCompat rating) throws RemoteException { postToHandler(MessageHandler.MSG_RATE, rating); } @Override public void rateWithExtras(RatingCompat rating, Bundle extras) throws RemoteException { postToHandler(MessageHandler.MSG_RATE_EXTRA, rating, extras); } @Override public void setPlaybackSpeed(float speed) throws RemoteException { postToHandler(MessageHandler.MSG_SET_PLAYBACK_SPEED, speed); } @Override public void setCaptioningEnabled(boolean enabled) throws RemoteException { postToHandler(MessageHandler.MSG_SET_CAPTIONING_ENABLED, enabled); } @Override public void setRepeatMode(int repeatMode) throws RemoteException { postToHandler(MessageHandler.MSG_SET_REPEAT_MODE, repeatMode); } @Override public void setShuffleModeEnabledRemoved(boolean enabled) throws RemoteException { // Do nothing. } @Override public void setShuffleMode(int shuffleMode) throws RemoteException { postToHandler(MessageHandler.MSG_SET_SHUFFLE_MODE, shuffleMode); } @Override public void sendCustomAction(String action, Bundle args) throws RemoteException { postToHandler(MessageHandler.MSG_CUSTOM_ACTION, action, args); } @Override public MediaMetadataCompat getMetadata() { return mMetadata; } @Override public PlaybackStateCompat getPlaybackState() { PlaybackStateCompat state; MediaMetadataCompat metadata; synchronized (mLock) { state = mState; metadata = mMetadata; } return getStateWithUpdatedPosition(state, metadata); } @Override public List<QueueItem> getQueue() { synchronized (mLock) { return mQueue; } } @Override public void addQueueItem(MediaDescriptionCompat description) { postToHandler(MessageHandler.MSG_ADD_QUEUE_ITEM, description); } @Override public void addQueueItemAt(MediaDescriptionCompat description, int index) { postToHandler(MessageHandler.MSG_ADD_QUEUE_ITEM_AT, description, index); } @Override public void removeQueueItem(MediaDescriptionCompat description) { postToHandler(MessageHandler.MSG_REMOVE_QUEUE_ITEM, description); } @Override public void removeQueueItemAt(int index) { postToHandler(MessageHandler.MSG_REMOVE_QUEUE_ITEM_AT, index); } @Override public CharSequence getQueueTitle() { return mQueueTitle; } @Override public Bundle getExtras() { synchronized (mLock) { return mExtras; } } @Override @RatingCompat.Style public int getRatingType() { return mRatingType; } @Override public boolean isCaptioningEnabled() { return mCaptioningEnabled; } @Override @PlaybackStateCompat.RepeatMode public int getRepeatMode() { return mRepeatMode; } @Override public boolean isShuffleModeEnabledRemoved() { return false; } @Override @PlaybackStateCompat.ShuffleMode public int getShuffleMode() { return mShuffleMode; } @Override public boolean isTransportControlEnabled() { // All sessions should support transport control commands. return true; } void postToHandler(int what) { MediaSessionImplBase.this.postToHandler(what, 0, 0, null, null); } void postToHandler(int what, int arg1) { MediaSessionImplBase.this.postToHandler(what, arg1, 0, null, null); } void postToHandler(int what, Object obj) { MediaSessionImplBase.this.postToHandler(what, 0, 0, obj, null); } void postToHandler(int what, Object obj, int arg1) { MediaSessionImplBase.this.postToHandler(what, arg1, 0, obj, null); } void postToHandler(int what, Object obj, Bundle extras) { MediaSessionImplBase.this.postToHandler(what, 0, 0, obj, extras); } } private static final class Command { public final String command; public final Bundle extras; public final ResultReceiver stub; public Command(String command, Bundle extras, 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(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; } } } } @RequiresApi(18) static class MediaSessionImplApi18 extends MediaSessionImplBase { private static boolean sIsMbrPendingIntentSupported = true; MediaSessionImplApi18(Context context, String tag, ComponentName mbrComponent, PendingIntent mbrIntent, Bundle sessionInfo) { super(context, tag, mbrComponent, mbrIntent, sessionInfo); } @Override public void setCallback(Callback callback, 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); } } } @RequiresApi(19) static class MediaSessionImplApi19 extends MediaSessionImplApi18 { MediaSessionImplApi19(Context context, String tag, ComponentName mbrComponent, PendingIntent mbrIntent, Bundle sessionInfo) { super(context, tag, mbrComponent, mbrIntent, sessionInfo); } @Override public void setCallback(Callback callback, 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 RemoteControlClient.MetadataEditor buildRccMetadata(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, metadata.getParcelable(MediaMetadataCompat.METADATA_KEY_RATING)); } 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, metadata.getParcelable(MediaMetadataCompat.METADATA_KEY_USER_RATING)); } return editor; } } @RequiresApi(21) static class MediaSessionImplApi21 implements MediaSessionImpl { final MediaSession mSessionFwk; final Token mToken; final Object mLock = new Object(); Bundle mSessionInfo; boolean mDestroyed = false; final RemoteCallbackList<IMediaControllerCallback> mExtraControllerCallbacks = new RemoteCallbackList<>(); PlaybackStateCompat mPlaybackState; List<QueueItem> mQueue; MediaMetadataCompat mMetadata; @RatingCompat.Style int mRatingType; boolean mCaptioningEnabled; @PlaybackStateCompat.RepeatMode int mRepeatMode; @PlaybackStateCompat.ShuffleMode int mShuffleMode; @GuardedBy("mLock") RemoteUserInfo mRemoteUserInfo; MediaSessionImplApi21(MediaSession sessionFwk, VersionedParcelable session2Token, Bundle sessionInfo) { mSessionFwk = sessionFwk; mToken = new Token(mSessionFwk.getSessionToken(), new ExtraSession(), session2Token); mSessionInfo = sessionInfo; // For backward compatibility, these flags are always set. setFlags(FLAG_HANDLES_MEDIA_BUTTONS | FLAG_HANDLES_TRANSPORT_CONTROLS); } MediaSessionImplApi21(Object mediaSession) { if (!(mediaSession instanceof MediaSession)) { throw new IllegalArgumentException("mediaSession is not a valid MediaSession object"); } mSessionFwk = (MediaSession) mediaSession; mToken = new Token(mSessionFwk.getSessionToken(), new ExtraSession()); mSessionInfo = null; // For backward compatibility, these flags are always set. setFlags(FLAG_HANDLES_MEDIA_BUTTONS | FLAG_HANDLES_TRANSPORT_CONTROLS); } @Override public void setCallback(Callback callback, Handler handler) { mSessionFwk.setCallback(callback == null ? null : callback.mCallbackFwk, handler); if (callback != null) { callback.setSessionImpl(this, handler); } } @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, Bundle extras) { if (android.os.Build.VERSION.SDK_INT < 23) { 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; mSessionFwk.release(); } @Override public Token getSessionToken() { return mToken; } @Override public void setPlaybackState(PlaybackStateCompat state) { mPlaybackState = state; 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()); } @Override public PlaybackStateCompat getPlaybackState() { return mPlaybackState; } @Override public void setMetadata(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(PendingIntent mbr) { mSessionFwk.setMediaButtonReceiver(mbr); } @Override public void setQueue(List<QueueItem> queue) { mQueue = queue; if (queue == null) { mSessionFwk.setQueue(null); return; } ArrayList<MediaSession.QueueItem> queueItemFwks = new ArrayList<>(); for (QueueItem item : queue) { queueItemFwks.add((MediaSession.QueueItem) item.getQueueItem()); } mSessionFwk.setQueue(queueItemFwks); } @Override public void setQueueTitle(CharSequence title) { mSessionFwk.setQueueTitle(title); } @Override public void setRatingType(@RatingCompat.Style int type) { if (android.os.Build.VERSION.SDK_INT < 22) { mRatingType = type; } else { mSessionFwk.setRatingType(type); } } @Override public void setCaptioningEnabled(boolean enabled) { if (mCaptioningEnabled != enabled) { mCaptioningEnabled = enabled; 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; 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; 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(Bundle extras) { mSessionFwk.setExtras(extras); } @Override public Object getMediaSession() { return mSessionFwk; } @Override public Object getRemoteControlClient() { // Note: When this returns somthing, {@link MediaSessionCompatCallbackTest} and // {@link #setCurrentUserInfoOverride} should be also updated. return null; } @Override public void setCurrentControllerInfo(RemoteUserInfo remoteUserInfo) { synchronized (mLock) { mRemoteUserInfo = remoteUserInfo; } } @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; } } @Override public RemoteUserInfo getCurrentControllerInfo() { synchronized (mLock) { return mRemoteUserInfo; } } class ExtraSession extends IMediaSession.Stub { @Override public void sendCommand(String command, Bundle args, ResultReceiverWrapper cb) { // Will not be called. throw new AssertionError(); } @Override public boolean sendMediaButton(KeyEvent mediaButton) { // Will not be called. throw new AssertionError(); } @Override public void registerCallbackListener(IMediaControllerCallback cb) { if (!mDestroyed) { RemoteUserInfo info = new RemoteUserInfo(RemoteUserInfo.LEGACY_CONTROLLER, getCallingPid(), getCallingUid()); mExtraControllerCallbacks.register(cb, info); } } @Override public void unregisterCallbackListener(IMediaControllerCallback cb) { mExtraControllerCallbacks.unregister(cb); } @Override public String getPackageName() { // Will not be called. throw new AssertionError(); } @Override public Bundle getSessionInfo() { return mSessionInfo == null ? null : new Bundle(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, String packageName) { // Will not be called. throw new AssertionError(); } @Override public void setVolumeTo(int value, int flags, 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(String mediaId, Bundle extras) throws RemoteException { // Will not be called. throw new AssertionError(); } @Override public void prepareFromSearch(String query, Bundle extras) throws RemoteException { // Will not be called. throw new AssertionError(); } @Override public void prepareFromUri(Uri uri, Bundle extras) throws RemoteException { // Will not be called. throw new AssertionError(); } @Override public void play() throws RemoteException { // Will not be called. throw new AssertionError(); } @Override public void playFromMediaId(String mediaId, Bundle extras) throws RemoteException { // Will not be called. throw new AssertionError(); } @Override public void playFromSearch(String query, Bundle extras) throws RemoteException { // Will not be called. throw new AssertionError(); } @Override public void playFromUri(Uri uri, Bundle extras) throws RemoteException { // Will not be called. throw new AssertionError(); } @Override public void skipToQueueItem(long id) { // Will not be called. throw new AssertionError(); } @Override public void pause() throws RemoteException { // Will not be called. throw new AssertionError(); } @Override public void stop() throws RemoteException { // Will not be called. throw new AssertionError(); } @Override public void next() throws RemoteException { // Will not be called. throw new AssertionError(); } @Override public void previous() throws RemoteException { // Will not be called. throw new AssertionError(); } @Override public void fastForward() throws RemoteException { // Will not be called. throw new AssertionError(); } @Override public void rewind() throws RemoteException { // Will not be called. throw new AssertionError(); } @Override public void seekTo(long pos) throws RemoteException { // Will not be called. throw new AssertionError(); } @Override public void rate(RatingCompat rating) throws RemoteException { // Will not be called. throw new AssertionError(); } @Override public void rateWithExtras(RatingCompat rating, Bundle extras) throws RemoteException { // Will not be called. throw new AssertionError(); } @Override public void setPlaybackSpeed(float speed) throws RemoteException { // Will not be called. throw new AssertionError(); } @Override public void setCaptioningEnabled(boolean enabled) throws RemoteException { // Will not be called. throw new AssertionError(); } @Override public void setRepeatMode(int repeatMode) throws RemoteException { // Will not be called. throw new AssertionError(); } @Override public void setShuffleModeEnabledRemoved(boolean enabled) throws RemoteException { // Do nothing. } @Override public void setShuffleMode(int shuffleMode) throws RemoteException { // Will not be called. throw new AssertionError(); } @Override public void sendCustomAction(String action, Bundle args) throws RemoteException { // Will not be called. throw new AssertionError(); } @Override public MediaMetadataCompat getMetadata() { // Will not be called. throw new AssertionError(); } @Override public PlaybackStateCompat getPlaybackState() { return getStateWithUpdatedPosition(mPlaybackState, mMetadata); } @Override public List<QueueItem> getQueue() { // Will not be called. return null; } @Override public void addQueueItem(MediaDescriptionCompat descriptionCompat) { // Will not be called. throw new AssertionError(); } @Override public void addQueueItemAt(MediaDescriptionCompat descriptionCompat, int index) { // Will not be called. throw new AssertionError(); } @Override public void removeQueueItem(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() { return mRatingType; } @Override public boolean isCaptioningEnabled() { return mCaptioningEnabled; } @Override @PlaybackStateCompat.RepeatMode public int getRepeatMode() { return mRepeatMode; } @Override public boolean isShuffleModeEnabledRemoved() { return false; } @Override @PlaybackStateCompat.ShuffleMode public int getShuffleMode() { return mShuffleMode; } @Override public boolean isTransportControlEnabled() { // Will not be called. throw new AssertionError(); } } } @RequiresApi(28) static class MediaSessionImplApi28 extends MediaSessionImplApi21 { MediaSessionImplApi28(MediaSession sessionFwk, VersionedParcelable session2Token, Bundle sessionInfo) { super(sessionFwk, session2Token, sessionInfo); } MediaSessionImplApi28(Object mediaSession) { super(mediaSession); } @Override public void setCurrentControllerInfo(RemoteUserInfo remoteUserInfo) { // No-op. {@link MediaSession#getCurrentControllerInfo} would work. } @Override @NonNull 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(MediaSession sessionFwk, VersionedParcelable session2Token, Bundle sessionInfo) { super(sessionFwk, session2Token, sessionInfo); } MediaSessionImplApi29(Object mediaSession) { super(mediaSession); mSessionInfo = ((MediaSession) mediaSession).getController().getSessionInfo(); } } }