Java tutorial
/* * Copyright 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package androidx.media; import static android.support.v4.media.MediaMetadataCompat.METADATA_KEY_DURATION; import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; import static androidx.media.MediaConstants2.ARGUMENT_ALLOWED_COMMANDS; import static androidx.media.MediaConstants2.ARGUMENT_ARGUMENTS; import static androidx.media.MediaConstants2.ARGUMENT_BUFFERING_STATE; import static androidx.media.MediaConstants2.ARGUMENT_COMMAND_BUTTONS; import static androidx.media.MediaConstants2.ARGUMENT_COMMAND_CODE; import static androidx.media.MediaConstants2.ARGUMENT_CUSTOM_COMMAND; import static androidx.media.MediaConstants2.ARGUMENT_ERROR_CODE; import static androidx.media.MediaConstants2.ARGUMENT_EXTRAS; import static androidx.media.MediaConstants2.ARGUMENT_ICONTROLLER_CALLBACK; import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ID; import static androidx.media.MediaConstants2.ARGUMENT_MEDIA_ITEM; import static androidx.media.MediaConstants2.ARGUMENT_PACKAGE_NAME; import static androidx.media.MediaConstants2.ARGUMENT_PID; import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_INFO; import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_SPEED; import static androidx.media.MediaConstants2.ARGUMENT_PLAYBACK_STATE_COMPAT; import static androidx.media.MediaConstants2.ARGUMENT_PLAYER_STATE; import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST; import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST_INDEX; import static androidx.media.MediaConstants2.ARGUMENT_PLAYLIST_METADATA; import static androidx.media.MediaConstants2.ARGUMENT_QUERY; import static androidx.media.MediaConstants2.ARGUMENT_RATING; import static androidx.media.MediaConstants2.ARGUMENT_REPEAT_MODE; import static androidx.media.MediaConstants2.ARGUMENT_RESULT_RECEIVER; import static androidx.media.MediaConstants2.ARGUMENT_ROUTE_BUNDLE; import static androidx.media.MediaConstants2.ARGUMENT_SEEK_POSITION; import static androidx.media.MediaConstants2.ARGUMENT_SHUFFLE_MODE; import static androidx.media.MediaConstants2.ARGUMENT_UID; import static androidx.media.MediaConstants2.ARGUMENT_URI; import static androidx.media.MediaConstants2.ARGUMENT_VOLUME; import static androidx.media.MediaConstants2.ARGUMENT_VOLUME_DIRECTION; import static androidx.media.MediaConstants2.ARGUMENT_VOLUME_FLAGS; import static androidx.media.MediaConstants2.CONNECT_RESULT_CONNECTED; import static androidx.media.MediaConstants2.CONNECT_RESULT_DISCONNECTED; import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_BY_COMMAND_CODE; import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_BY_CUSTOM_COMMAND; import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_CONNECT; import static androidx.media.MediaConstants2.CONTROLLER_COMMAND_DISCONNECT; import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED; import static androidx.media.MediaConstants2.SESSION_EVENT_ON_BUFFERING_STATE_CHAGNED; import static androidx.media.MediaConstants2.SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED; import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ERROR; import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED; import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED; import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYER_STATE_CHANGED; import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_CHANGED; import static androidx.media.MediaConstants2.SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED; import static androidx.media.MediaConstants2.SESSION_EVENT_ON_REPEAT_MODE_CHANGED; import static androidx.media.MediaConstants2.SESSION_EVENT_ON_ROUTES_INFO_CHANGED; import static androidx.media.MediaConstants2.SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED; import static androidx.media.MediaConstants2.SESSION_EVENT_SEND_CUSTOM_COMMAND; import static androidx.media.MediaConstants2.SESSION_EVENT_SET_CUSTOM_LAYOUT; import static androidx.media.MediaPlayerBase.BUFFERING_STATE_UNKNOWN; import static androidx.media.MediaPlayerBase.UNKNOWN_TIME; import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PAUSE; import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PLAY; import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_PREPARE; import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_RESET; import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SEEK_TO; import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYBACK_SET_SPEED; import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_ADD_ITEM; import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REMOVE_ITEM; import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_REPLACE_ITEM; import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST; import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_LIST_METADATA; import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE; import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE; import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM; import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM; import static androidx.media.SessionCommand2.COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM; import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_FAST_FORWARD; import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID; import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_SEARCH; import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PLAY_FROM_URI; import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID; import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH; import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_PREPARE_FROM_URI; import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_REWIND; import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SELECT_ROUTE; import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SET_RATING; import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO; import static androidx.media.SessionCommand2.COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO; import static androidx.media.SessionCommand2.COMMAND_CODE_VOLUME_ADJUST_VOLUME; import static androidx.media.SessionCommand2.COMMAND_CODE_VOLUME_SET_VOLUME; import android.annotation.TargetApi; import android.app.PendingIntent; import android.content.Context; import android.media.AudioManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Process; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.SystemClock; import android.support.v4.media.MediaBrowserCompat; import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.session.MediaControllerCompat; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; import android.util.Log; import androidx.annotation.GuardedBy; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; import androidx.media.MediaPlaylistAgent.RepeatMode; import androidx.media.MediaPlaylistAgent.ShuffleMode; import androidx.media.MediaSession2.CommandButton; import androidx.media.MediaSession2.ControllerInfo; import androidx.media.MediaSession2.ErrorCode; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; import java.util.concurrent.Executor; /** * Allows an app to interact with an active {@link MediaSession2} in any status. Media buttons and * other commands can be sent to the session. * <p> * When you're done, use {@link #close()} to clean up resources. This also helps session service * to be destroyed when there's no controller associated with it. * <p> * When controlling {@link MediaSession2}, the controller will be available immediately after * the creation. * <p> * MediaController2 objects are thread-safe. * <p> * @see MediaSession2 */ @TargetApi(Build.VERSION_CODES.KITKAT) public class MediaController2 implements AutoCloseable { /** * @hide */ @RestrictTo(LIBRARY_GROUP) @IntDef({ AudioManager.ADJUST_LOWER, AudioManager.ADJUST_RAISE, AudioManager.ADJUST_SAME, AudioManager.ADJUST_MUTE, AudioManager.ADJUST_UNMUTE, AudioManager.ADJUST_TOGGLE_MUTE }) @Retention(RetentionPolicy.SOURCE) public @interface VolumeDirection { } /** * @hide */ @RestrictTo(LIBRARY_GROUP) @IntDef(value = { AudioManager.FLAG_SHOW_UI, AudioManager.FLAG_ALLOW_RINGER_MODES, AudioManager.FLAG_PLAY_SOUND, AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE, AudioManager.FLAG_VIBRATE }, flag = true) @Retention(RetentionPolicy.SOURCE) public @interface VolumeFlags { } /** * Interface for listening to change in activeness of the {@link MediaSession2}. It's * active if and only if it has set a player. */ public abstract static class ControllerCallback { /** * Called when the controller is successfully connected to the session. The controller * becomes available afterwards. * * @param controller the controller for this event * @param allowedCommands commands that's allowed by the session. */ public void onConnected(@NonNull MediaController2 controller, @NonNull SessionCommandGroup2 allowedCommands) { } /** * Called when the session refuses the controller or the controller is disconnected from * the session. The controller becomes unavailable afterwards and the callback wouldn't * be called. * <p> * It will be also called after the {@link #close()}, so you can put clean up code here. * You don't need to call {@link #close()} after this. * * @param controller the controller for this event */ public void onDisconnected(@NonNull MediaController2 controller) { } /** * Called when the session set the custom layout through the * {@link MediaSession2#setCustomLayout(ControllerInfo, List)}. * <p> * Can be called before {@link #onConnected(MediaController2, SessionCommandGroup2)} * is called. * * @param controller the controller for this event * @param layout */ public void onCustomLayoutChanged(@NonNull MediaController2 controller, @NonNull List<CommandButton> layout) { } /** * Called when the session has changed anything related with the {@link PlaybackInfo}. * * @param controller the controller for this event * @param info new playback info */ public void onPlaybackInfoChanged(@NonNull MediaController2 controller, @NonNull PlaybackInfo info) { } /** * Called when the allowed commands are changed by session. * * @param controller the controller for this event * @param commands newly allowed commands */ public void onAllowedCommandsChanged(@NonNull MediaController2 controller, @NonNull SessionCommandGroup2 commands) { } /** * Called when the session sent a custom command. * * @param controller the controller for this event * @param command * @param args * @param receiver */ public void onCustomCommand(@NonNull MediaController2 controller, @NonNull SessionCommand2 command, @Nullable Bundle args, @Nullable ResultReceiver receiver) { } /** * Called when the player state is changed. * * @param controller the controller for this event * @param state */ public void onPlayerStateChanged(@NonNull MediaController2 controller, int state) { } /** * Called when playback speed is changed. * * @param controller the controller for this event * @param speed speed */ public void onPlaybackSpeedChanged(@NonNull MediaController2 controller, float speed) { } /** * Called to report buffering events for a data source. * <p> * Use {@link #getBufferedPosition()} for current buffering position. * * @param controller the controller for this event * @param item the media item for which buffering is happening. * @param state the new buffering state. */ public void onBufferingStateChanged(@NonNull MediaController2 controller, @NonNull MediaItem2 item, @MediaPlayerBase.BuffState int state) { } /** * Called to indicate that seeking is completed. * * @param controller the controller for this event. * @param position the previous seeking request. */ public void onSeekCompleted(@NonNull MediaController2 controller, long position) { } /** * Called when a error from * * @param controller the controller for this event * @param errorCode error code * @param extras extra information */ public void onError(@NonNull MediaController2 controller, @ErrorCode int errorCode, @Nullable Bundle extras) { } /** * Called when the player's currently playing item is changed * <p> * When it's called, you should invalidate previous playback information and wait for later * callbacks. * * @param controller the controller for this event * @param item new item * @see #onBufferingStateChanged(MediaController2, MediaItem2, int) */ public void onCurrentMediaItemChanged(@NonNull MediaController2 controller, @NonNull MediaItem2 item) { } /** * Called when a playlist is changed. * * @param controller the controller for this event * @param list new playlist * @param metadata new metadata */ public void onPlaylistChanged(@NonNull MediaController2 controller, @NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) { } /** * Called when a playlist metadata is changed. * * @param controller the controller for this event * @param metadata new metadata */ public void onPlaylistMetadataChanged(@NonNull MediaController2 controller, @Nullable MediaMetadata2 metadata) { } /** * Called when the shuffle mode is changed. * * @param controller the controller for this event * @param shuffleMode repeat mode * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP */ public void onShuffleModeChanged(@NonNull MediaController2 controller, @MediaPlaylistAgent.ShuffleMode int shuffleMode) { } /** * Called when the repeat mode is changed. * * @param controller the controller for this event * @param repeatMode repeat mode * @see MediaPlaylistAgent#REPEAT_MODE_NONE * @see MediaPlaylistAgent#REPEAT_MODE_ONE * @see MediaPlaylistAgent#REPEAT_MODE_ALL * @see MediaPlaylistAgent#REPEAT_MODE_GROUP */ public void onRepeatModeChanged(@NonNull MediaController2 controller, @MediaPlaylistAgent.RepeatMode int repeatMode) { } /** * Called when a property of the indicated media route has changed. * * @param controller the controller for this event * @param routes The list of Bundle from MediaRouteDescriptor.asBundle(). * See MediaRouteDescriptor.fromBundle(Bundle bundle) to get * MediaRouteDescriptor object from the {@code routes} */ public void onRoutesInfoChanged(@NonNull MediaController2 controller, @Nullable List<Bundle> routes) { } } /** * Holds information about the the way volume is handled for this session. */ // The same as MediaController.PlaybackInfo public static final class PlaybackInfo { private static final String KEY_PLAYBACK_TYPE = "android.media.audio_info.playback_type"; private static final String KEY_CONTROL_TYPE = "android.media.audio_info.control_type"; private static final String KEY_MAX_VOLUME = "android.media.audio_info.max_volume"; private static final String KEY_CURRENT_VOLUME = "android.media.audio_info.current_volume"; private static final String KEY_AUDIO_ATTRIBUTES = "android.media.audio_info.audio_attrs"; private final int mPlaybackType; private final int mControlType; private final int mMaxVolume; private final int mCurrentVolume; private final AudioAttributesCompat mAudioAttrsCompat; /** * The session uses remote playback. */ public static final int PLAYBACK_TYPE_REMOTE = 2; /** * The session uses local playback. */ public static final int PLAYBACK_TYPE_LOCAL = 1; PlaybackInfo(int playbackType, AudioAttributesCompat attrs, int controlType, int max, int current) { mPlaybackType = playbackType; mAudioAttrsCompat = attrs; mControlType = controlType; mMaxVolume = max; mCurrentVolume = current; } /** * Get the type of playback which affects volume handling. One of: * <ul> * <li>{@link #PLAYBACK_TYPE_LOCAL}</li> * <li>{@link #PLAYBACK_TYPE_REMOTE}</li> * </ul> * * @return The type of playback this session is using. */ public int getPlaybackType() { return mPlaybackType; } /** * Get the audio attributes for this session. The attributes will affect * volume handling for the session. When the volume type is * {@link #PLAYBACK_TYPE_REMOTE} these may be ignored by the * remote volume handler. * * @return The attributes for this session. */ public AudioAttributesCompat getAudioAttributes() { return mAudioAttrsCompat; } /** * Get the type of volume control that can be used. One of: * <ul> * <li>{@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}</li> * <li>{@link VolumeProviderCompat#VOLUME_CONTROL_RELATIVE}</li> * <li>{@link VolumeProviderCompat#VOLUME_CONTROL_FIXED}</li> * </ul> * * @return The type of volume control that may be used with this session. */ public int getControlType() { return mControlType; } /** * Get the maximum volume that may be set for this session. * * @return The maximum allowed volume where this session is playing. */ public int getMaxVolume() { return mMaxVolume; } /** * Get the current volume for this session. * * @return The current volume where this session is playing. */ public int getCurrentVolume() { return mCurrentVolume; } Bundle toBundle() { Bundle bundle = new Bundle(); bundle.putInt(KEY_PLAYBACK_TYPE, mPlaybackType); bundle.putInt(KEY_CONTROL_TYPE, mControlType); bundle.putInt(KEY_MAX_VOLUME, mMaxVolume); bundle.putInt(KEY_CURRENT_VOLUME, mCurrentVolume); if (mAudioAttrsCompat != null) { bundle.putParcelable(KEY_AUDIO_ATTRIBUTES, MediaUtils2.toAudioAttributesBundle(mAudioAttrsCompat)); } return bundle; } static PlaybackInfo createPlaybackInfo(int playbackType, AudioAttributesCompat attrs, int controlType, int max, int current) { return new PlaybackInfo(playbackType, attrs, controlType, max, current); } static PlaybackInfo fromBundle(Bundle bundle) { if (bundle == null) { return null; } final int volumeType = bundle.getInt(KEY_PLAYBACK_TYPE); final int volumeControl = bundle.getInt(KEY_CONTROL_TYPE); final int maxVolume = bundle.getInt(KEY_MAX_VOLUME); final int currentVolume = bundle.getInt(KEY_CURRENT_VOLUME); final AudioAttributesCompat attrs = MediaUtils2 .fromAudioAttributesBundle(bundle.getBundle(KEY_AUDIO_ATTRIBUTES)); return createPlaybackInfo(volumeType, attrs, volumeControl, maxVolume, currentVolume); } } private final class ControllerCompatCallback extends MediaControllerCompat.Callback { @Override public void onSessionReady() { sendCommand(CONTROLLER_COMMAND_CONNECT, new ResultReceiver(mHandler) { @Override protected void onReceiveResult(int resultCode, Bundle resultData) { if (!mHandlerThread.isAlive()) { return; } switch (resultCode) { case CONNECT_RESULT_CONNECTED: onConnectedNotLocked(resultData); break; case CONNECT_RESULT_DISCONNECTED: mCallback.onDisconnected(MediaController2.this); close(); break; } } }); } @Override public void onSessionDestroyed() { close(); } @Override public void onPlaybackStateChanged(PlaybackStateCompat state) { synchronized (mLock) { mPlaybackStateCompat = state; } } @Override public void onMetadataChanged(MediaMetadataCompat metadata) { synchronized (mLock) { mMediaMetadataCompat = metadata; } } @Override public void onSessionEvent(String event, Bundle extras) { switch (event) { case SESSION_EVENT_ON_ALLOWED_COMMANDS_CHANGED: { SessionCommandGroup2 allowedCommands = SessionCommandGroup2 .fromBundle(extras.getBundle(ARGUMENT_ALLOWED_COMMANDS)); synchronized (mLock) { mAllowedCommands = allowedCommands; } mCallback.onAllowedCommandsChanged(MediaController2.this, allowedCommands); break; } case SESSION_EVENT_ON_PLAYER_STATE_CHANGED: { int playerState = extras.getInt(ARGUMENT_PLAYER_STATE); synchronized (mLock) { mPlayerState = playerState; } mCallback.onPlayerStateChanged(MediaController2.this, playerState); break; } case SESSION_EVENT_ON_CURRENT_MEDIA_ITEM_CHANGED: { MediaItem2 item = MediaItem2.fromBundle(extras.getBundle(ARGUMENT_MEDIA_ITEM)); if (item == null) { return; } synchronized (mLock) { mCurrentMediaItem = item; } mCallback.onCurrentMediaItemChanged(MediaController2.this, item); break; } case SESSION_EVENT_ON_ERROR: { int errorCode = extras.getInt(ARGUMENT_ERROR_CODE); Bundle errorExtras = extras.getBundle(ARGUMENT_EXTRAS); mCallback.onError(MediaController2.this, errorCode, errorExtras); break; } case SESSION_EVENT_ON_ROUTES_INFO_CHANGED: { List<Bundle> routes = MediaUtils2.toBundleList(extras.getParcelableArray(ARGUMENT_ROUTE_BUNDLE)); mCallback.onRoutesInfoChanged(MediaController2.this, routes); break; } case SESSION_EVENT_ON_PLAYLIST_CHANGED: { MediaMetadata2 playlistMetadata = MediaMetadata2 .fromBundle(extras.getBundle(ARGUMENT_PLAYLIST_METADATA)); List<MediaItem2> playlist = MediaUtils2 .fromMediaItem2ParcelableArray(extras.getParcelableArray(ARGUMENT_PLAYLIST)); synchronized (mLock) { mPlaylist = playlist; mPlaylistMetadata = playlistMetadata; } mCallback.onPlaylistChanged(MediaController2.this, playlist, playlistMetadata); break; } case SESSION_EVENT_ON_PLAYLIST_METADATA_CHANGED: { MediaMetadata2 playlistMetadata = MediaMetadata2 .fromBundle(extras.getBundle(ARGUMENT_PLAYLIST_METADATA)); synchronized (mLock) { mPlaylistMetadata = playlistMetadata; } mCallback.onPlaylistMetadataChanged(MediaController2.this, playlistMetadata); break; } case SESSION_EVENT_ON_REPEAT_MODE_CHANGED: { int repeatMode = extras.getInt(ARGUMENT_REPEAT_MODE); synchronized (mLock) { mRepeatMode = repeatMode; } mCallback.onRepeatModeChanged(MediaController2.this, repeatMode); break; } case SESSION_EVENT_ON_SHUFFLE_MODE_CHANGED: { int shuffleMode = extras.getInt(ARGUMENT_SHUFFLE_MODE); synchronized (mLock) { mShuffleMode = shuffleMode; } mCallback.onShuffleModeChanged(MediaController2.this, shuffleMode); break; } case SESSION_EVENT_SEND_CUSTOM_COMMAND: { Bundle commandBundle = extras.getBundle(ARGUMENT_CUSTOM_COMMAND); if (commandBundle == null) { return; } SessionCommand2 command = SessionCommand2.fromBundle(commandBundle); Bundle args = extras.getBundle(ARGUMENT_ARGUMENTS); ResultReceiver receiver = extras.getParcelable(ARGUMENT_RESULT_RECEIVER); mCallback.onCustomCommand(MediaController2.this, command, args, receiver); break; } case SESSION_EVENT_SET_CUSTOM_LAYOUT: { List<CommandButton> layout = MediaUtils2 .fromCommandButtonParcelableArray(extras.getParcelableArray(ARGUMENT_COMMAND_BUTTONS)); if (layout == null) { return; } mCallback.onCustomLayoutChanged(MediaController2.this, layout); break; } case SESSION_EVENT_ON_PLAYBACK_INFO_CHANGED: { PlaybackInfo info = PlaybackInfo.fromBundle(extras.getBundle(ARGUMENT_PLAYBACK_INFO)); if (info == null) { return; } synchronized (mLock) { mPlaybackInfo = info; } mCallback.onPlaybackInfoChanged(MediaController2.this, info); break; } case SESSION_EVENT_ON_PLAYBACK_SPEED_CHANGED: { PlaybackStateCompat state = extras.getParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT); if (state == null) { return; } synchronized (mLock) { mPlaybackStateCompat = state; } mCallback.onPlaybackSpeedChanged(MediaController2.this, state.getPlaybackSpeed()); break; } case SESSION_EVENT_ON_BUFFERING_STATE_CHAGNED: { MediaItem2 item = MediaItem2.fromBundle(extras.getBundle(ARGUMENT_MEDIA_ITEM)); int bufferingState = extras.getInt(ARGUMENT_BUFFERING_STATE); if (item == null) { return; } synchronized (mLock) { mBufferingState = bufferingState; } mCallback.onBufferingStateChanged(MediaController2.this, item, bufferingState); break; } } } } private static final String TAG = "MediaController2"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); // Note: Using {@code null} doesn't helpful here because MediaBrowserServiceCompat always wraps // the rootHints so it becomes non-null. static final Bundle sDefaultRootExtras = new Bundle(); static { sDefaultRootExtras.putBoolean(MediaConstants2.ROOT_EXTRA_DEFAULT, true); } private final Context mContext; private final Object mLock = new Object(); private final SessionToken2 mToken; private final ControllerCallback mCallback; private final Executor mCallbackExecutor; private final IBinder.DeathRecipient mDeathRecipient; private final HandlerThread mHandlerThread; private final Handler mHandler; @GuardedBy("mLock") private MediaBrowserCompat mBrowserCompat; @GuardedBy("mLock") private boolean mIsReleased; @GuardedBy("mLock") private List<MediaItem2> mPlaylist; @GuardedBy("mLock") private MediaMetadata2 mPlaylistMetadata; @GuardedBy("mLock") private @RepeatMode int mRepeatMode; @GuardedBy("mLock") private @ShuffleMode int mShuffleMode; @GuardedBy("mLock") private int mPlayerState; @GuardedBy("mLock") private MediaItem2 mCurrentMediaItem; @GuardedBy("mLock") private int mBufferingState; @GuardedBy("mLock") private PlaybackInfo mPlaybackInfo; @GuardedBy("mLock") private SessionCommandGroup2 mAllowedCommands; // Media 1.0 variables @GuardedBy("mLock") private MediaControllerCompat mControllerCompat; @GuardedBy("mLock") private ControllerCompatCallback mControllerCompatCallback; @GuardedBy("mLock") private PlaybackStateCompat mPlaybackStateCompat; @GuardedBy("mLock") private MediaMetadataCompat mMediaMetadataCompat; // Assignment should be used with the lock hold, but should be used without a lock to prevent // potential deadlock. @GuardedBy("mLock") private volatile boolean mConnected; /** * Create a {@link MediaController2} from the {@link SessionToken2}. * This connects to the session and may wake up the service if it's not available. * * @param context Context * @param token token to connect to * @param executor executor to run callbacks on. * @param callback controller callback to receive changes in */ public MediaController2(@NonNull Context context, @NonNull SessionToken2 token, @NonNull Executor executor, @NonNull ControllerCallback callback) { super(); if (context == null) { throw new IllegalArgumentException("context shouldn't be null"); } if (token == null) { throw new IllegalArgumentException("token shouldn't be null"); } if (callback == null) { throw new IllegalArgumentException("callback shouldn't be null"); } if (executor == null) { throw new IllegalArgumentException("executor shouldn't be null"); } mContext = context; mHandlerThread = new HandlerThread("MediaController2_Thread"); mHandlerThread.start(); mHandler = new Handler(mHandlerThread.getLooper()); mToken = token; mCallback = callback; mCallbackExecutor = executor; mDeathRecipient = new IBinder.DeathRecipient() { @Override public void binderDied() { MediaController2.this.close(); } }; initialize(); } /** * Release this object, and disconnect from the session. After this, callbacks wouldn't be * received. */ @Override public void close() { if (DEBUG) { //Log.d(TAG, "release from " + mToken, new IllegalStateException()); } synchronized (mLock) { if (mIsReleased) { // Prevent re-enterance from the ControllerCallback.onDisconnected() return; } mHandler.removeCallbacksAndMessages(null); mHandlerThread.quitSafely(); mIsReleased = true; // Send command before the unregister callback to use mIControllerCallback in the // callback. sendCommand(CONTROLLER_COMMAND_DISCONNECT); if (mControllerCompat != null) { mControllerCompat.unregisterCallback(mControllerCompatCallback); } if (mBrowserCompat != null) { mBrowserCompat.disconnect(); mBrowserCompat = null; } if (mControllerCompat != null) { mControllerCompat.unregisterCallback(mControllerCompatCallback); mControllerCompat = null; } mConnected = false; } mCallbackExecutor.execute(new Runnable() { @Override public void run() { mCallback.onDisconnected(MediaController2.this); } }); } /** * @return token */ public @NonNull SessionToken2 getSessionToken() { return mToken; } /** * Returns whether this class is connected to active {@link MediaSession2} or not. */ public boolean isConnected() { synchronized (mLock) { return mConnected; } } /** * Requests that the player starts or resumes playback. */ public void play() { synchronized (mLock) { if (!mConnected) { Log.w(TAG, "Session isn't active", new IllegalStateException()); return; } sendCommand(COMMAND_CODE_PLAYBACK_PLAY); } } /** * Requests that the player pauses playback. */ public void pause() { synchronized (mLock) { if (!mConnected) { Log.w(TAG, "Session isn't active", new IllegalStateException()); return; } sendCommand(COMMAND_CODE_PLAYBACK_PAUSE); } } /** * Requests that the player be reset to its uninitialized state. */ public void reset() { synchronized (mLock) { if (!mConnected) { Log.w(TAG, "Session isn't active", new IllegalStateException()); return; } sendCommand(COMMAND_CODE_PLAYBACK_RESET); } } /** * Request that the player prepare its playback. In other words, other sessions can continue * to play during the preparation of this session. This method can be used to speed up the * start of the playback. Once the preparation is done, the session will change its playback * state to {@link MediaPlayerBase#PLAYER_STATE_PAUSED}. Afterwards, {@link #play} can be called * to start playback. */ public void prepare() { synchronized (mLock) { if (!mConnected) { Log.w(TAG, "Session isn't active", new IllegalStateException()); return; } sendCommand(COMMAND_CODE_PLAYBACK_PREPARE); } } /** * Start fast forwarding. If playback is already fast forwarding this * may increase the rate. */ public void fastForward() { synchronized (mLock) { if (!mConnected) { Log.w(TAG, "Session isn't active", new IllegalStateException()); return; } sendCommand(COMMAND_CODE_SESSION_FAST_FORWARD); } } /** * Start rewinding. If playback is already rewinding this may increase * the rate. */ public void rewind() { synchronized (mLock) { if (!mConnected) { Log.w(TAG, "Session isn't active", new IllegalStateException()); return; } sendCommand(COMMAND_CODE_SESSION_REWIND); } } /** * Move to a new location in the media stream. * * @param pos Position to move to, in milliseconds. */ public void seekTo(long pos) { synchronized (mLock) { if (!mConnected) { Log.w(TAG, "Session isn't active", new IllegalStateException()); return; } Bundle args = new Bundle(); args.putLong(ARGUMENT_SEEK_POSITION, pos); sendCommand(COMMAND_CODE_PLAYBACK_SEEK_TO, args); } } /** * @hide */ @RestrictTo(LIBRARY_GROUP) public void skipForward() { // To match with KEYCODE_MEDIA_SKIP_FORWARD } /** * @hide */ @RestrictTo(LIBRARY_GROUP) public void skipBackward() { // To match with KEYCODE_MEDIA_SKIP_BACKWARD } /** * Request that the player start playback for a specific media id. * * @param mediaId The id of the requested media. * @param extras Optional extras that can include extra information about the media item * to be played. */ public void playFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) { synchronized (mLock) { if (!mConnected) { Log.w(TAG, "Session isn't active", new IllegalStateException()); return; } Bundle args = new Bundle(); args.putString(ARGUMENT_MEDIA_ID, mediaId); args.putBundle(ARGUMENT_EXTRAS, extras); sendCommand(COMMAND_CODE_SESSION_PLAY_FROM_MEDIA_ID, args); } } /** * Request that the player start playback for a specific search query. * * @param query The search query. Should not be an empty string. * @param extras Optional extras that can include extra information about the query. */ public void playFromSearch(@NonNull String query, @Nullable Bundle extras) { synchronized (mLock) { if (!mConnected) { Log.w(TAG, "Session isn't active", new IllegalStateException()); return; } Bundle args = new Bundle(); args.putString(ARGUMENT_QUERY, query); args.putBundle(ARGUMENT_EXTRAS, extras); sendCommand(COMMAND_CODE_SESSION_PLAY_FROM_SEARCH, args); } } /** * Request that the player start playback for a specific {@link Uri}. * * @param uri The URI of the requested media. * @param extras Optional extras that can include extra information about the media item * to be played. */ public void playFromUri(@NonNull Uri uri, @Nullable Bundle extras) { synchronized (mLock) { if (!mConnected) { Log.w(TAG, "Session isn't active", new IllegalStateException()); return; } Bundle args = new Bundle(); args.putParcelable(ARGUMENT_URI, uri); args.putBundle(ARGUMENT_EXTRAS, extras); sendCommand(COMMAND_CODE_SESSION_PLAY_FROM_URI, args); } } /** * Request that the player prepare playback for a specific media id. In other words, other * sessions can continue to play during the preparation of this session. This method can be * used to speed up the start of the playback. Once the preparation is done, the session * will change its playback state to {@link MediaPlayerBase#PLAYER_STATE_PAUSED}. Afterwards, * {@link #play} can be called to start playback. If the preparation is not needed, * {@link #playFromMediaId} can be directly called without this method. * * @param mediaId The id of the requested media. * @param extras Optional extras that can include extra information about the media item * to be prepared. */ public void prepareFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) { synchronized (mLock) { if (!mConnected) { Log.w(TAG, "Session isn't active", new IllegalStateException()); return; } Bundle args = new Bundle(); args.putString(ARGUMENT_MEDIA_ID, mediaId); args.putBundle(ARGUMENT_EXTRAS, extras); sendCommand(COMMAND_CODE_SESSION_PREPARE_FROM_MEDIA_ID, args); } } /** * Request that the player prepare playback for a specific search query. * In other words, other sessions can continue to play during the preparation of this session. * This method can be used to speed up the start of the playback. * Once the preparation is done, the session will change its playback state to * {@link MediaPlayerBase#PLAYER_STATE_PAUSED}. Afterwards, * {@link #play} can be called to start playback. If the preparation is not needed, * {@link #playFromSearch} can be directly called without this method. * * @param query The search query. Should not be an empty string. * @param extras Optional extras that can include extra information about the query. */ public void prepareFromSearch(@NonNull String query, @Nullable Bundle extras) { synchronized (mLock) { if (!mConnected) { Log.w(TAG, "Session isn't active", new IllegalStateException()); return; } Bundle args = new Bundle(); args.putString(ARGUMENT_QUERY, query); args.putBundle(ARGUMENT_EXTRAS, extras); sendCommand(COMMAND_CODE_SESSION_PREPARE_FROM_SEARCH, args); } } /** * Request that the player prepare playback for a specific {@link Uri}. In other words, * other sessions can continue to play during the preparation of this session. This method * can be used to speed up the start of the playback. Once the preparation is done, the * session will change its playback state to {@link MediaPlayerBase#PLAYER_STATE_PAUSED}. * Afterwards, {@link #play} can be called to start playback. If the preparation is not needed, * {@link #playFromUri} can be directly called without this method. * * @param uri The URI of the requested media. * @param extras Optional extras that can include extra information about the media item * to be prepared. */ public void prepareFromUri(@NonNull Uri uri, @Nullable Bundle extras) { synchronized (mLock) { if (!mConnected) { Log.w(TAG, "Session isn't active", new IllegalStateException()); return; } Bundle args = new Bundle(); args.putParcelable(ARGUMENT_URI, uri); args.putBundle(ARGUMENT_EXTRAS, extras); sendCommand(COMMAND_CODE_SESSION_PREPARE_FROM_URI, args); } } /** * Set the volume of the output this session is playing on. The command will be ignored if it * does not support {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}. * <p> * If the session is local playback, this changes the device's volume with the stream that * session's player is using. Flags will be specified for the {@link AudioManager}. * <p> * If the session is remote player (i.e. session has set volume provider), its volume provider * will receive this request instead. * * @see #getPlaybackInfo() * @param value The value to set it to, between 0 and the reported max. * @param flags flags from {@link AudioManager} to include with the volume request for local * playback */ public void setVolumeTo(int value, @VolumeFlags int flags) { synchronized (mLock) { if (!mConnected) { Log.w(TAG, "Session isn't active", new IllegalStateException()); return; } Bundle args = new Bundle(); args.putInt(ARGUMENT_VOLUME, value); args.putInt(ARGUMENT_VOLUME_FLAGS, flags); sendCommand(COMMAND_CODE_VOLUME_SET_VOLUME, args); } } /** * Adjust the volume of the output this session is playing on. The direction * must be one of {@link AudioManager#ADJUST_LOWER}, * {@link AudioManager#ADJUST_RAISE}, or {@link AudioManager#ADJUST_SAME}. * <p> * The command will be ignored if the session does not support * {@link VolumeProviderCompat#VOLUME_CONTROL_RELATIVE} or * {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}. * <p> * If the session is local playback, this changes the device's volume with the stream that * session's player is using. Flags will be specified for the {@link AudioManager}. * <p> * If the session is remote player (i.e. session has set volume provider), its volume provider * will receive this request instead. * * @see #getPlaybackInfo() * @param direction The direction to adjust the volume in. * @param flags flags from {@link AudioManager} to include with the volume request for local * playback */ public void adjustVolume(@VolumeDirection int direction, @VolumeFlags int flags) { synchronized (mLock) { if (!mConnected) { Log.w(TAG, "Session isn't active", new IllegalStateException()); return; } Bundle args = new Bundle(); args.putInt(ARGUMENT_VOLUME_DIRECTION, direction); args.putInt(ARGUMENT_VOLUME_FLAGS, flags); sendCommand(COMMAND_CODE_VOLUME_ADJUST_VOLUME, args); } } /** * Get an intent for launching UI associated with this session if one exists. * * @return A {@link PendingIntent} to launch UI or null. */ public @Nullable PendingIntent getSessionActivity() { synchronized (mLock) { if (!mConnected) { Log.w(TAG, "Session isn't active", new IllegalStateException()); return null; } return mControllerCompat.getSessionActivity(); } } /** * Get the lastly cached player state from * {@link ControllerCallback#onPlayerStateChanged(MediaController2, int)}. * * @return player state */ public int getPlayerState() { synchronized (mLock) { return mPlayerState; } } /** * Gets the duration of the current media item, or {@link MediaPlayerBase#UNKNOWN_TIME} if * unknown. * @return the duration in ms, or {@link MediaPlayerBase#UNKNOWN_TIME}. */ public long getDuration() { synchronized (mLock) { if (mMediaMetadataCompat != null && mMediaMetadataCompat.containsKey(METADATA_KEY_DURATION)) { return mMediaMetadataCompat.getLong(METADATA_KEY_DURATION); } } return MediaPlayerBase.UNKNOWN_TIME; } /** * Gets the current playback position. * <p> * This returns the calculated value of the position, based on the difference between the * update time and current time. * * @return position */ public long getCurrentPosition() { synchronized (mLock) { if (!mConnected) { Log.w(TAG, "Session isn't active", new IllegalStateException()); return UNKNOWN_TIME; } if (mPlaybackStateCompat != null) { long timeDiff = SystemClock.elapsedRealtime() - mPlaybackStateCompat.getLastPositionUpdateTime(); long expectedPosition = mPlaybackStateCompat.getPosition() + (long) (mPlaybackStateCompat.getPlaybackSpeed() * timeDiff); return Math.max(0, expectedPosition); } return UNKNOWN_TIME; } } /** * Get the lastly cached playback speed from * {@link ControllerCallback#onPlaybackSpeedChanged(MediaController2, float)}. * * @return speed the lastly cached playback speed, or 0.0f if unknown. */ public float getPlaybackSpeed() { synchronized (mLock) { if (!mConnected) { Log.w(TAG, "Session isn't active", new IllegalStateException()); return 0f; } return (mPlaybackStateCompat == null) ? 0f : mPlaybackStateCompat.getPlaybackSpeed(); } } /** * Set the playback speed. */ public void setPlaybackSpeed(float speed) { synchronized (mLock) { if (!mConnected) { Log.w(TAG, "Session isn't active", new IllegalStateException()); return; } Bundle args = new Bundle(); args.putFloat(ARGUMENT_PLAYBACK_SPEED, speed); sendCommand(COMMAND_CODE_PLAYBACK_SET_SPEED, args); } } /** * Gets the current buffering state of the player. * During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already * buffered. * @return the buffering state. */ public @MediaPlayerBase.BuffState int getBufferingState() { synchronized (mLock) { if (!mConnected) { Log.w(TAG, "Session isn't active", new IllegalStateException()); return BUFFERING_STATE_UNKNOWN; } return mBufferingState; } } /** * Gets the lastly cached buffered position from the session when * {@link ControllerCallback#onBufferingStateChanged(MediaController2, MediaItem2, int)} is * called. * * @return buffering position in millis, or {@link MediaPlayerBase#UNKNOWN_TIME} if unknown. */ public long getBufferedPosition() { synchronized (mLock) { if (!mConnected) { Log.w(TAG, "Session isn't active", new IllegalStateException()); return UNKNOWN_TIME; } return (mPlaybackStateCompat == null) ? UNKNOWN_TIME : mPlaybackStateCompat.getBufferedPosition(); } } /** * Get the current playback info for this session. * * @return The current playback info or null. */ public @Nullable PlaybackInfo getPlaybackInfo() { synchronized (mLock) { return mPlaybackInfo; } } /** * Rate the media. This will cause the rating to be set for the current user. * The rating style must follow the user rating style from the session. * You can get the rating style from the session through the * {@link MediaMetadata2#getRating(String)} with the key * {@link MediaMetadata2#METADATA_KEY_USER_RATING}. * <p> * If the user rating was {@code null}, the media item does not accept setting user rating. * * @param mediaId The id of the media * @param rating The rating to set */ public void setRating(@NonNull String mediaId, @NonNull Rating2 rating) { synchronized (mLock) { if (!mConnected) { Log.w(TAG, "Session isn't active", new IllegalStateException()); return; } Bundle args = new Bundle(); args.putString(ARGUMENT_MEDIA_ID, mediaId); args.putBundle(ARGUMENT_RATING, rating.toBundle()); sendCommand(COMMAND_CODE_SESSION_SET_RATING, args); } } /** * Send custom command to the session * * @param command custom command * @param args optional argument * @param cb optional result receiver */ public void sendCustomCommand(@NonNull SessionCommand2 command, @Nullable Bundle args, @Nullable ResultReceiver cb) { synchronized (mLock) { if (!mConnected) { Log.w(TAG, "Session isn't active", new IllegalStateException()); return; } Bundle bundle = new Bundle(); bundle.putBundle(ARGUMENT_CUSTOM_COMMAND, command.toBundle()); bundle.putBundle(ARGUMENT_ARGUMENTS, args); sendCommand(CONTROLLER_COMMAND_BY_CUSTOM_COMMAND, bundle, cb); } } /** * Returns the cached playlist from {@link ControllerCallback#onPlaylistChanged}. * <p> * This list may differ with the list that was specified with * {@link #setPlaylist(List, MediaMetadata2)} depending on the {@link MediaPlaylistAgent} * implementation. Use media items returned here for other playlist agent APIs such as * {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)}. * * @return playlist. Can be {@code null} if the playlist hasn't set nor controller doesn't have * enough permission. * @see SessionCommand2#COMMAND_CODE_PLAYLIST_GET_LIST */ public @Nullable List<MediaItem2> getPlaylist() { synchronized (mLock) { return mPlaylist; } } /** * Sets the playlist. * <p> * Even when the playlist is successfully set, use the playlist returned from * {@link #getPlaylist()} for playlist APIs such as {@link #skipToPlaylistItem(MediaItem2)}. * Otherwise the session in the remote process can't distinguish between media items. * * @param list playlist * @param metadata metadata of the playlist * @see #getPlaylist() * @see ControllerCallback#onPlaylistChanged */ public void setPlaylist(@NonNull List<MediaItem2> list, @Nullable MediaMetadata2 metadata) { if (list == null) { throw new IllegalArgumentException("list shouldn't be null"); } Bundle args = new Bundle(); args.putParcelableArray(ARGUMENT_PLAYLIST, MediaUtils2.toMediaItem2ParcelableArray(list)); args.putBundle(ARGUMENT_PLAYLIST_METADATA, metadata == null ? null : metadata.toBundle()); sendCommand(COMMAND_CODE_PLAYLIST_SET_LIST, args); } /** * Updates the playlist metadata * * @param metadata metadata of the playlist */ public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) { Bundle args = new Bundle(); args.putBundle(ARGUMENT_PLAYLIST_METADATA, metadata == null ? null : metadata.toBundle()); sendCommand(COMMAND_CODE_PLAYLIST_SET_LIST_METADATA, args); } /** * Gets the lastly cached playlist playlist metadata either from * {@link ControllerCallback#onPlaylistMetadataChanged or * {@link ControllerCallback#onPlaylistChanged}. * * @return metadata metadata of the playlist, or null if none is set */ public @Nullable MediaMetadata2 getPlaylistMetadata() { synchronized (mLock) { return mPlaylistMetadata; } } /** * Adds the media item to the playlist at position index. Index equals or greater than * the current playlist size (e.g. {@link Integer#MAX_VALUE}) will add the item at the end of * the playlist. * <p> * This will not change the currently playing media item. * If index is less than or equal to the current index of the playlist, * the current index of the playlist will be incremented correspondingly. * * @param index the index you want to add * @param item the media item you want to add */ public void addPlaylistItem(int index, @NonNull MediaItem2 item) { Bundle args = new Bundle(); args.putInt(ARGUMENT_PLAYLIST_INDEX, index); args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle()); sendCommand(COMMAND_CODE_PLAYLIST_ADD_ITEM, args); } /** * Removes the media item at index in the playlist. *<p> * If the item is the currently playing item of the playlist, current playback * will be stopped and playback moves to next source in the list. * * @param item the media item you want to add */ public void removePlaylistItem(@NonNull MediaItem2 item) { Bundle args = new Bundle(); args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle()); sendCommand(COMMAND_CODE_PLAYLIST_REMOVE_ITEM, args); } /** * Replace the media item at index in the playlist. This can be also used to update metadata of * an item. * * @param index the index of the item to replace * @param item the new item */ public void replacePlaylistItem(int index, @NonNull MediaItem2 item) { Bundle args = new Bundle(); args.putInt(ARGUMENT_PLAYLIST_INDEX, index); args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle()); sendCommand(COMMAND_CODE_PLAYLIST_REPLACE_ITEM, args); } /** * Get the lastly cached current item from * {@link ControllerCallback#onCurrentMediaItemChanged(MediaController2, MediaItem2)}. * * @return the currently playing item, or null if unknown. */ public MediaItem2 getCurrentMediaItem() { synchronized (mLock) { return mCurrentMediaItem; } } /** * Skips to the previous item in the playlist. * <p> * This calls {@link MediaPlaylistAgent#skipToPreviousItem()}. */ public void skipToPreviousItem() { sendCommand(COMMAND_CODE_PLAYLIST_SKIP_TO_PREV_ITEM); } /** * Skips to the next item in the playlist. * <p> * This calls {@link MediaPlaylistAgent#skipToNextItem()}. */ public void skipToNextItem() { sendCommand(COMMAND_CODE_PLAYLIST_SKIP_TO_NEXT_ITEM); } /** * Skips to the item in the playlist. * <p> * This calls {@link MediaPlaylistAgent#skipToPlaylistItem(MediaItem2)}. * * @param item The item in the playlist you want to play */ public void skipToPlaylistItem(@NonNull MediaItem2 item) { Bundle args = new Bundle(); args.putBundle(ARGUMENT_MEDIA_ITEM, item.toBundle()); sendCommand(COMMAND_CODE_PLAYLIST_SKIP_TO_PLAYLIST_ITEM, args); } /** * Gets the cached repeat mode from the {@link ControllerCallback#onRepeatModeChanged}. * * @return repeat mode * @see MediaPlaylistAgent#REPEAT_MODE_NONE * @see MediaPlaylistAgent#REPEAT_MODE_ONE * @see MediaPlaylistAgent#REPEAT_MODE_ALL * @see MediaPlaylistAgent#REPEAT_MODE_GROUP */ public @RepeatMode int getRepeatMode() { synchronized (mLock) { return mRepeatMode; } } /** * Sets the repeat mode. * * @param repeatMode repeat mode * @see MediaPlaylistAgent#REPEAT_MODE_NONE * @see MediaPlaylistAgent#REPEAT_MODE_ONE * @see MediaPlaylistAgent#REPEAT_MODE_ALL * @see MediaPlaylistAgent#REPEAT_MODE_GROUP */ public void setRepeatMode(@RepeatMode int repeatMode) { Bundle args = new Bundle(); args.putInt(ARGUMENT_REPEAT_MODE, repeatMode); sendCommand(COMMAND_CODE_PLAYLIST_SET_REPEAT_MODE, args); } /** * Gets the cached shuffle mode from the {@link ControllerCallback#onShuffleModeChanged}. * * @return The shuffle mode * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP */ public @ShuffleMode int getShuffleMode() { synchronized (mLock) { return mShuffleMode; } } /** * Sets the shuffle mode. * * @param shuffleMode The shuffle mode * @see MediaPlaylistAgent#SHUFFLE_MODE_NONE * @see MediaPlaylistAgent#SHUFFLE_MODE_ALL * @see MediaPlaylistAgent#SHUFFLE_MODE_GROUP */ public void setShuffleMode(@ShuffleMode int shuffleMode) { Bundle args = new Bundle(); args.putInt(ARGUMENT_SHUFFLE_MODE, shuffleMode); sendCommand(COMMAND_CODE_PLAYLIST_SET_SHUFFLE_MODE, args); } /** * Queries for information about the routes currently known. */ public void subscribeRoutesInfo() { sendCommand(COMMAND_CODE_SESSION_SUBSCRIBE_ROUTES_INFO); } /** * Unsubscribes for changes to the routes. * <p> * The {@link ControllerCallback#onRoutesInfoChanged callback} will no longer be invoked for * the routes once this method returns. * </p> */ public void unsubscribeRoutesInfo() { sendCommand(COMMAND_CODE_SESSION_UNSUBSCRIBE_ROUTES_INFO); } /** * Selects the specified route. * * @param route The route to select. */ public void selectRoute(@NonNull Bundle route) { if (route == null) { throw new IllegalArgumentException("route shouldn't be null"); } Bundle args = new Bundle(); args.putBundle(ARGUMENT_ROUTE_BUNDLE, route); sendCommand(COMMAND_CODE_SESSION_SELECT_ROUTE, args); } // Should be used without a lock to prevent potential deadlock. void onConnectedNotLocked(Bundle data) { // is enough or should we pass it while connecting? final SessionCommandGroup2 allowedCommands = SessionCommandGroup2 .fromBundle(data.getBundle(ARGUMENT_ALLOWED_COMMANDS)); final int playerState = data.getInt(ARGUMENT_PLAYER_STATE); final int bufferingState = data.getInt(ARGUMENT_BUFFERING_STATE); final PlaybackStateCompat playbackStateCompat = data.getParcelable(ARGUMENT_PLAYBACK_STATE_COMPAT); final int repeatMode = data.getInt(ARGUMENT_REPEAT_MODE); final int shuffleMode = data.getInt(ARGUMENT_SHUFFLE_MODE); final List<MediaItem2> playlist = MediaUtils2 .fromMediaItem2ParcelableArray(data.getParcelableArray(ARGUMENT_PLAYLIST)); final MediaItem2 currentMediaItem = MediaItem2.fromBundle(data.getBundle(ARGUMENT_MEDIA_ITEM)); final PlaybackInfo playbackInfo = PlaybackInfo.fromBundle(data.getBundle(ARGUMENT_PLAYBACK_INFO)); final MediaMetadata2 metadata = MediaMetadata2.fromBundle(data.getBundle(ARGUMENT_PLAYLIST_METADATA)); if (DEBUG) { Log.d(TAG, "onConnectedNotLocked sessionCompatToken=" + mToken.getSessionCompatToken() + ", allowedCommands=" + allowedCommands); } boolean close = false; try { synchronized (mLock) { if (mIsReleased) { return; } if (mConnected) { Log.e(TAG, "Cannot be notified about the connection result many times." + " Probably a bug or malicious app."); close = true; return; } mAllowedCommands = allowedCommands; mPlayerState = playerState; mBufferingState = bufferingState; mPlaybackStateCompat = playbackStateCompat; mRepeatMode = repeatMode; mShuffleMode = shuffleMode; mPlaylist = playlist; mCurrentMediaItem = currentMediaItem; mPlaylistMetadata = metadata; mConnected = true; mPlaybackInfo = playbackInfo; } mCallbackExecutor.execute(new Runnable() { @Override public void run() { // Note: We may trigger ControllerCallbacks with the initial values // But it's hard to define the order of the controller callbacks // Only notify about the mCallback.onConnected(MediaController2.this, allowedCommands); } }); } finally { if (close) { // Trick to call release() without holding the lock, to prevent potential deadlock // with the developer's custom lock within the ControllerCallback.onDisconnected(). close(); } } } private void initialize() { if (mToken.getType() == SessionToken2.TYPE_SESSION) { synchronized (mLock) { mBrowserCompat = null; } connectToSession(mToken.getSessionCompatToken()); } else { connectToService(); } } private void connectToSession(MediaSessionCompat.Token sessionCompatToken) { MediaControllerCompat controllerCompat = null; try { controllerCompat = new MediaControllerCompat(mContext, sessionCompatToken); } catch (RemoteException e) { e.printStackTrace(); } synchronized (mLock) { mControllerCompat = controllerCompat; mControllerCompatCallback = new ControllerCompatCallback(); mControllerCompat.registerCallback(mControllerCompatCallback, mHandler); } if (controllerCompat.isSessionReady()) { sendCommand(CONTROLLER_COMMAND_CONNECT, new ResultReceiver(mHandler) { @Override protected void onReceiveResult(int resultCode, Bundle resultData) { if (!mHandlerThread.isAlive()) { return; } switch (resultCode) { case CONNECT_RESULT_CONNECTED: onConnectedNotLocked(resultData); break; case CONNECT_RESULT_DISCONNECTED: mCallback.onDisconnected(MediaController2.this); close(); break; } } }); } } private void connectToService() { synchronized (mLock) { mBrowserCompat = new MediaBrowserCompat(mContext, mToken.getComponentName(), new ConnectionCallback(), sDefaultRootExtras); mBrowserCompat.connect(); } } private void sendCommand(int commandCode) { sendCommand(commandCode, null); } private void sendCommand(int commandCode, Bundle args) { if (args == null) { args = new Bundle(); } args.putInt(ARGUMENT_COMMAND_CODE, commandCode); sendCommand(CONTROLLER_COMMAND_BY_COMMAND_CODE, args, null); } private void sendCommand(String command) { sendCommand(command, null, null); } private void sendCommand(String command, ResultReceiver receiver) { sendCommand(command, null, receiver); } private void sendCommand(String command, Bundle args, ResultReceiver receiver) { if (args == null) { args = new Bundle(); } MediaControllerCompat controller; ControllerCompatCallback callback; synchronized (mLock) { controller = mControllerCompat; callback = mControllerCompatCallback; } args.putBinder(ARGUMENT_ICONTROLLER_CALLBACK, callback.getIControllerCallback().asBinder()); args.putString(ARGUMENT_PACKAGE_NAME, mContext.getPackageName()); args.putInt(ARGUMENT_UID, Process.myUid()); args.putInt(ARGUMENT_PID, Process.myPid()); controller.sendCommand(command, args, receiver); } @NonNull Context getContext() { return mContext; } @NonNull ControllerCallback getCallback() { return mCallback; } @NonNull Executor getCallbackExecutor() { return mCallbackExecutor; } @Nullable MediaBrowserCompat getBrowserCompat() { synchronized (mLock) { return mBrowserCompat; } } private class ConnectionCallback extends MediaBrowserCompat.ConnectionCallback { @Override public void onConnected() { MediaBrowserCompat browser = getBrowserCompat(); if (browser != null) { connectToSession(browser.getSessionToken()); } else if (DEBUG) { Log.d(TAG, "Controller is closed prematually", new IllegalStateException()); } } @Override public void onConnectionSuspended() { close(); } @Override public void onConnectionFailed() { close(); } } }