/*
 * 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.media.session;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.PendingIntent;
import android.content.Context;
import android.content.pm.ParceledListSlice;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.MediaMetadata;
import android.media.Rating;
import android.media.VolumeProvider;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;

Allows an app to interact with an ongoing media session. Media buttons and other commands can be sent to the session. A callback may be registered to receive updates from the session, such as metadata and play state changes.

A MediaController can be created through MediaSessionManager if you hold the "android.permission.MEDIA_CONTENT_CONTROL" permission or are an enabled notification listener or by getting a Token directly from the session owner.

MediaController objects are thread-safe.

/** * Allows an app to interact with an ongoing media session. Media buttons and * other commands can be sent to the session. A callback may be registered to * receive updates from the session, such as metadata and play state changes. * <p> * A MediaController can be created through {@link MediaSessionManager} if you * hold the "android.permission.MEDIA_CONTENT_CONTROL" permission or are an * enabled notification listener or by getting a {@link MediaSession.Token} * directly from the session owner. * <p> * MediaController objects are thread-safe. */
public final class MediaController { private static final String TAG = "MediaController"; private static final int MSG_EVENT = 1; private static final int MSG_UPDATE_PLAYBACK_STATE = 2; private static final int MSG_UPDATE_METADATA = 3; private static final int MSG_UPDATE_VOLUME = 4; private static final int MSG_UPDATE_QUEUE = 5; private static final int MSG_UPDATE_QUEUE_TITLE = 6; private static final int MSG_UPDATE_EXTRAS = 7; private static final int MSG_DESTROYED = 8; private final ISessionController mSessionBinder; private final MediaSession.Token mToken; private final Context mContext; private final CallbackStub mCbStub = new CallbackStub(this); private final ArrayList<MessageHandler> mCallbacks = new ArrayList<MessageHandler>(); private final Object mLock = new Object(); private boolean mCbRegistered = false; private String mPackageName; private String mTag; private final TransportControls mTransportControls;
Call for creating a MediaController directly from a binder. Should only be used by framework code.
@hide
/** * Call for creating a MediaController directly from a binder. Should only * be used by framework code. * * @hide */
public MediaController(Context context, ISessionController sessionBinder) { if (sessionBinder == null) { throw new IllegalArgumentException("Session token cannot be null"); } if (context == null) { throw new IllegalArgumentException("Context cannot be null"); } mSessionBinder = sessionBinder; mTransportControls = new TransportControls(); mToken = new MediaSession.Token(sessionBinder); mContext = context; }
Create a new MediaController from a session's token.
Params:
  • context – The caller's context.
  • token – The token for the session.
/** * Create a new MediaController from a session's token. * * @param context The caller's context. * @param token The token for the session. */
public MediaController(@NonNull Context context, @NonNull MediaSession.Token token) { this(context, token.getBinder()); }
Get a TransportControls instance to send transport actions to the associated session.
Returns:A transport controls instance.
/** * Get a {@link TransportControls} instance to send transport actions to * the associated session. * * @return A transport controls instance. */
public @NonNull TransportControls getTransportControls() { return mTransportControls; }
Send the specified media button event to the session. Only media keys can be sent by this method, other keys will be ignored.
Params:
  • keyEvent – The media button event to dispatch.
Returns:true if the event was sent to the session, false otherwise.
/** * Send the specified media button event to the session. Only media keys can * be sent by this method, other keys will be ignored. * * @param keyEvent The media button event to dispatch. * @return true if the event was sent to the session, false otherwise. */
public boolean dispatchMediaButtonEvent(@NonNull KeyEvent keyEvent) { return dispatchMediaButtonEventInternal(false, keyEvent); }
Dispatches the media button event as system service to the session. This only effects the MediaSession.getCurrentControllerInfo() and doesn't bypass any permission check done by the system service.

Should be only called by the PhoneWindow when the foreground activity didn't consume the key from the hardware devices.

Params:
  • keyEvent – media key event
Returns:true if the event was sent to the session, false otherwise
@hide
/** * Dispatches the media button event as system service to the session. This only effects the * {@link MediaSession.Callback#getCurrentControllerInfo()} and doesn't bypass any permission * check done by the system service. * <p> * Should be only called by the {@link com.android.internal.policy.PhoneWindow} when the * foreground activity didn't consume the key from the hardware devices. * * @param keyEvent media key event * @return {@code true} if the event was sent to the session, {@code false} otherwise * @hide */
public boolean dispatchMediaButtonEventAsSystemService(@NonNull KeyEvent keyEvent) { return dispatchMediaButtonEventInternal(true, keyEvent); } private boolean dispatchMediaButtonEventInternal(boolean asSystemService, @NonNull KeyEvent keyEvent) { if (keyEvent == null) { throw new IllegalArgumentException("KeyEvent may not be null"); } if (!KeyEvent.isMediaKey(keyEvent.getKeyCode())) { return false; } try { return mSessionBinder.sendMediaButton(mContext.getPackageName(), mCbStub, asSystemService, keyEvent); } catch (RemoteException e) { // System is dead. =( } return false; }
Dispatches the volume button event as system service to the session. This only effects the MediaSession.getCurrentControllerInfo() and doesn't bypass any permission check done by the system service.

Should be only called by the PhoneWindow when the foreground activity didn't consume the key from the hardware devices.

Params:
  • keyEvent – volume key event
@hide
/** * Dispatches the volume button event as system service to the session. This only effects the * {@link MediaSession.Callback#getCurrentControllerInfo()} and doesn't bypass any permission * check done by the system service. * <p> * Should be only called by the {@link com.android.internal.policy.PhoneWindow} when the * foreground activity didn't consume the key from the hardware devices. * * @param keyEvent volume key event * @hide */
public void dispatchVolumeButtonEventAsSystemService(@NonNull KeyEvent keyEvent) { switch (keyEvent.getAction()) { case KeyEvent.ACTION_DOWN: { int direction = 0; switch (keyEvent.getKeyCode()) { case KeyEvent.KEYCODE_VOLUME_UP: direction = AudioManager.ADJUST_RAISE; break; case KeyEvent.KEYCODE_VOLUME_DOWN: direction = AudioManager.ADJUST_LOWER; break; case KeyEvent.KEYCODE_VOLUME_MUTE: direction = AudioManager.ADJUST_TOGGLE_MUTE; break; } try { mSessionBinder.adjustVolume(mContext.getPackageName(), mCbStub, true, direction, AudioManager.FLAG_SHOW_UI); } catch (RemoteException e) { Log.wtf(TAG, "Error calling adjustVolumeBy", e); } } case KeyEvent.ACTION_UP: { final int flags = AudioManager.FLAG_PLAY_SOUND | AudioManager.FLAG_VIBRATE | AudioManager.FLAG_FROM_KEY; try { mSessionBinder.adjustVolume(mContext.getPackageName(), mCbStub, true, 0, flags); } catch (RemoteException e) { Log.wtf(TAG, "Error calling adjustVolumeBy", e); } } } }
Get the current playback state for this session.
Returns:The current PlaybackState or null
/** * Get the current playback state for this session. * * @return The current PlaybackState or null */
public @Nullable PlaybackState getPlaybackState() { try { return mSessionBinder.getPlaybackState(); } catch (RemoteException e) { Log.wtf(TAG, "Error calling getPlaybackState.", e); return null; } }
Get the current metadata for this session.
Returns:The current MediaMetadata or null.
/** * Get the current metadata for this session. * * @return The current MediaMetadata or null. */
public @Nullable MediaMetadata getMetadata() { try { return mSessionBinder.getMetadata(); } catch (RemoteException e) { Log.wtf(TAG, "Error calling getMetadata.", e); return null; } }
Get the current play queue for this session if one is set. If you only care about the current item getMetadata() should be used.
Returns:The current play queue or null.
/** * Get the current play queue for this session if one is set. If you only * care about the current item {@link #getMetadata()} should be used. * * @return The current play queue or null. */
public @Nullable List<MediaSession.QueueItem> getQueue() { try { ParceledListSlice queue = mSessionBinder.getQueue(); if (queue != null) { return queue.getList(); } } catch (RemoteException e) { Log.wtf(TAG, "Error calling getQueue.", e); } return null; }
Get the queue title for this session.
/** * Get the queue title for this session. */
public @Nullable CharSequence getQueueTitle() { try { return mSessionBinder.getQueueTitle(); } catch (RemoteException e) { Log.wtf(TAG, "Error calling getQueueTitle", e); } return null; }
Get the extras for this session.
/** * Get the extras for this session. */
public @Nullable Bundle getExtras() { try { return mSessionBinder.getExtras(); } catch (RemoteException e) { Log.wtf(TAG, "Error calling getExtras", e); } return null; }
Returns:The supported rating type
/** * Get the rating type supported by the session. One of: * <ul> * <li>{@link Rating#RATING_NONE}</li> * <li>{@link Rating#RATING_HEART}</li> * <li>{@link Rating#RATING_THUMB_UP_DOWN}</li> * <li>{@link Rating#RATING_3_STARS}</li> * <li>{@link Rating#RATING_4_STARS}</li> * <li>{@link Rating#RATING_5_STARS}</li> * <li>{@link Rating#RATING_PERCENTAGE}</li> * </ul> * * @return The supported rating type */
public int getRatingType() { try { return mSessionBinder.getRatingType(); } catch (RemoteException e) { Log.wtf(TAG, "Error calling getRatingType.", e); return Rating.RATING_NONE; } }
Get the flags for this session. Flags are defined in MediaSession.
Returns:The current set of flags for the session.
/** * Get the flags for this session. Flags are defined in {@link MediaSession}. * * @return The current set of flags for the session. */
public @MediaSession.SessionFlags long getFlags() { try { return mSessionBinder.getFlags(); } catch (RemoteException e) { Log.wtf(TAG, "Error calling getFlags.", e); } return 0; }
Get the current playback info for this session.
Returns:The current playback info or null.
/** * Get the current playback info for this session. * * @return The current playback info or null. */
public @Nullable PlaybackInfo getPlaybackInfo() { try { ParcelableVolumeInfo result = mSessionBinder.getVolumeAttributes(); return new PlaybackInfo(result.volumeType, result.audioAttrs, result.controlType, result.maxVolume, result.currentVolume); } catch (RemoteException e) { Log.wtf(TAG, "Error calling getAudioInfo.", e); } return null; }
Get an intent for launching UI associated with this session if one exists.
Returns:A PendingIntent to launch UI or null.
/** * 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() { try { return mSessionBinder.getLaunchPendingIntent(); } catch (RemoteException e) { Log.wtf(TAG, "Error calling getPendingIntent.", e); } return null; }
Get the token for the session this is connected to.
Returns:The token for the connected session.
/** * Get the token for the session this is connected to. * * @return The token for the connected session. */
public @NonNull MediaSession.Token getSessionToken() { return mToken; }
Set the volume of the output this session is playing on. The command will be ignored if it does not support VolumeProvider.VOLUME_CONTROL_ABSOLUTE. The flags in AudioManager may be used to affect the handling.
Params:
  • value – The value to set it to, between 0 and the reported max.
  • flags – Flags from AudioManager to include with the volume request.
See Also:
/** * Set the volume of the output this session is playing on. The command will * be ignored if it does not support * {@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}. The flags in * {@link AudioManager} may be used to affect the handling. * * @see #getPlaybackInfo() * @param value The value to set it to, between 0 and the reported max. * @param flags Flags from {@link AudioManager} to include with the volume * request. */
public void setVolumeTo(int value, int flags) { try { mSessionBinder.setVolumeTo(mContext.getPackageName(), mCbStub, value, flags); } catch (RemoteException e) { Log.wtf(TAG, "Error calling setVolumeTo.", e); } }
Adjust the volume of the output this session is playing on. The direction must be one of AudioManager.ADJUST_LOWER, AudioManager.ADJUST_RAISE, or AudioManager.ADJUST_SAME. The command will be ignored if the session does not support VolumeProvider.VOLUME_CONTROL_RELATIVE or VolumeProvider.VOLUME_CONTROL_ABSOLUTE. The flags in AudioManager may be used to affect the handling.
Params:
  • direction – The direction to adjust the volume in.
  • flags – Any flags to pass with the command.
See Also:
/** * 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}. * The command will be ignored if the session does not support * {@link VolumeProvider#VOLUME_CONTROL_RELATIVE} or * {@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}. The flags in * {@link AudioManager} may be used to affect the handling. * * @see #getPlaybackInfo() * @param direction The direction to adjust the volume in. * @param flags Any flags to pass with the command. */
public void adjustVolume(int direction, int flags) { try { mSessionBinder.adjustVolume(mContext.getPackageName(), mCbStub, false, direction, flags); } catch (RemoteException e) { Log.wtf(TAG, "Error calling adjustVolumeBy.", e); } }
Registers a callback to receive updates from the Session. Updates will be posted on the caller's thread.
Params:
  • callback – The callback object, must not be null.
/** * Registers a callback to receive updates from the Session. Updates will be * posted on the caller's thread. * * @param callback The callback object, must not be null. */
public void registerCallback(@NonNull Callback callback) { registerCallback(callback, null); }
Registers a callback to receive updates from the session. Updates will be posted on the specified handler's thread.
Params:
  • callback – The callback object, must not be null.
  • handler – The handler to post updates on. If null the callers thread will be used.
/** * Registers a callback to receive updates from the session. Updates will be * posted on the specified handler's thread. * * @param callback The callback object, must not be null. * @param handler The handler to post updates on. If null the callers thread * will be used. */
public void registerCallback(@NonNull Callback callback, @Nullable Handler handler) { if (callback == null) { throw new IllegalArgumentException("callback must not be null"); } if (handler == null) { handler = new Handler(); } synchronized (mLock) { addCallbackLocked(callback, handler); } }
Unregisters the specified callback. If an update has already been posted you may still receive it after calling this method.
Params:
  • callback – The callback to remove.
/** * Unregisters the specified callback. If an update has already been posted * you may still receive it after calling this method. * * @param callback The callback to remove. */
public void unregisterCallback(@NonNull Callback callback) { if (callback == null) { throw new IllegalArgumentException("callback must not be null"); } synchronized (mLock) { removeCallbackLocked(callback); } }
Sends a generic command to the session. It is up to the session creator to decide what commands and parameters they will support. As such, commands should only be sent to sessions that the controller owns.
Params:
  • command – The command to send
  • args – Any parameters to include with the command
  • cb – The callback to receive the result on
/** * Sends a generic command to the session. It is up to the session creator * to decide what commands and parameters they will support. As such, * commands should only be sent to sessions that the controller owns. * * @param command The command to send * @param args Any parameters to include with the command * @param cb The callback to receive the result on */
public void sendCommand(@NonNull String command, @Nullable Bundle args, @Nullable ResultReceiver cb) { if (TextUtils.isEmpty(command)) { throw new IllegalArgumentException("command cannot be null or empty"); } try { mSessionBinder.sendCommand(mContext.getPackageName(), mCbStub, command, args, cb); } catch (RemoteException e) { Log.d(TAG, "Dead object in sendCommand.", e); } }
Get the session owner's package name.
Returns:The package name of of the session owner.
/** * Get the session owner's package name. * * @return The package name of of the session owner. */
public String getPackageName() { if (mPackageName == null) { try { mPackageName = mSessionBinder.getPackageName(); } catch (RemoteException e) { Log.d(TAG, "Dead object in getPackageName.", e); } } return mPackageName; }
Get the session's tag for debugging purposes.
Returns:The session's tag.
@hide
/** * Get the session's tag for debugging purposes. * * @return The session's tag. * @hide */
public String getTag() { if (mTag == null) { try { mTag = mSessionBinder.getTag(); } catch (RemoteException e) { Log.d(TAG, "Dead object in getTag.", e); } } return mTag; } /* * @hide */ ISessionController getSessionBinder() { return mSessionBinder; }
@hide
/** * @hide */
public boolean controlsSameSession(MediaController other) { if (other == null) return false; return mSessionBinder.asBinder() == other.getSessionBinder().asBinder(); } private void addCallbackLocked(Callback cb, Handler handler) { if (getHandlerForCallbackLocked(cb) != null) { Log.w(TAG, "Callback is already added, ignoring"); return; } MessageHandler holder = new MessageHandler(handler.getLooper(), cb); mCallbacks.add(holder); holder.mRegistered = true; if (!mCbRegistered) { try { mSessionBinder.registerCallbackListener(mContext.getPackageName(), mCbStub); mCbRegistered = true; } catch (RemoteException e) { Log.e(TAG, "Dead object in registerCallback", e); } } } private boolean removeCallbackLocked(Callback cb) { boolean success = false; for (int i = mCallbacks.size() - 1; i >= 0; i--) { MessageHandler handler = mCallbacks.get(i); if (cb == handler.mCallback) { mCallbacks.remove(i); success = true; handler.mRegistered = false; } } if (mCbRegistered && mCallbacks.size() == 0) { try { mSessionBinder.unregisterCallbackListener(mCbStub); } catch (RemoteException e) { Log.e(TAG, "Dead object in removeCallbackLocked"); } mCbRegistered = false; } return success; } private MessageHandler getHandlerForCallbackLocked(Callback cb) { if (cb == null) { throw new IllegalArgumentException("Callback cannot be null"); } for (int i = mCallbacks.size() - 1; i >= 0; i--) { MessageHandler handler = mCallbacks.get(i); if (cb == handler.mCallback) { return handler; } } return null; } private final void postMessage(int what, Object obj, Bundle data) { synchronized (mLock) { for (int i = mCallbacks.size() - 1; i >= 0; i--) { mCallbacks.get(i).post(what, obj, data); } } }
Callback for receiving updates from the session. A Callback can be registered using registerCallback.
/** * Callback for receiving updates from the session. A Callback can be * registered using {@link #registerCallback}. */
public static abstract class Callback {
Override to handle the session being destroyed. The session is no longer valid after this call and calls to it will be ignored.
/** * Override to handle the session being destroyed. The session is no * longer valid after this call and calls to it will be ignored. */
public void onSessionDestroyed() { }
Override to handle custom events sent by the session owner without a specified interface. Controllers should only handle these for sessions they own.
Params:
  • event – The event from the session.
  • extras – Optional parameters for the event, may be null.
/** * Override to handle custom events sent by the session owner without a * specified interface. Controllers should only handle these for * sessions they own. * * @param event The event from the session. * @param extras Optional parameters for the event, may be null. */
public void onSessionEvent(@NonNull String event, @Nullable Bundle extras) { }
Override to handle changes in playback state.
Params:
  • state – The new playback state of the session
/** * Override to handle changes in playback state. * * @param state The new playback state of the session */
public void onPlaybackStateChanged(@Nullable PlaybackState state) { }
Override to handle changes to the current metadata.
Params:
  • metadata – The current metadata for the session or null if none.
See Also:
/** * Override to handle changes to the current metadata. * * @param metadata The current metadata for the session or null if none. * @see MediaMetadata */
public void onMetadataChanged(@Nullable MediaMetadata metadata) { }
Override to handle changes to items in the queue.
Params:
  • queue – A list of items in the current play queue. It should include the currently playing item as well as previous and upcoming items if applicable.
See Also:
/** * Override to handle changes to items in the queue. * * @param queue A list of items in the current play queue. It should * include the currently playing item as well as previous and * upcoming items if applicable. * @see MediaSession.QueueItem */
public void onQueueChanged(@Nullable List<MediaSession.QueueItem> queue) { }
Override to handle changes to the queue title.
Params:
  • title – The title that should be displayed along with the play queue such as "Now Playing". May be null if there is no such title.
/** * Override to handle changes to the queue title. * * @param title The title that should be displayed along with the play queue such as * "Now Playing". May be null if there is no such title. */
public void onQueueTitleChanged(@Nullable CharSequence title) { }
Override to handle changes to the MediaSession extras.
Params:
  • extras – The extras that can include other information associated with the MediaSession.
/** * Override to handle changes to the {@link MediaSession} extras. * * @param extras The extras that can include other information associated with the * {@link MediaSession}. */
public void onExtrasChanged(@Nullable Bundle extras) { }
Override to handle changes to the audio info.
Params:
  • info – The current audio info for this session.
/** * Override to handle changes to the audio info. * * @param info The current audio info for this session. */
public void onAudioInfoChanged(PlaybackInfo info) { } }
Interface for controlling media playback on a session. This allows an app to send media transport commands to the session.
/** * Interface for controlling media playback on a session. This allows an app * to send media transport commands to the session. */
public final class TransportControls { private static final String TAG = "TransportController"; private TransportControls() { }
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 PlaybackState.STATE_PAUSED. Afterwards, play can be called to start playback.
/** * 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 PlaybackState#STATE_PAUSED}. Afterwards, {@link #play} can be called to * start playback. */
public void prepare() { try { mSessionBinder.prepare(mContext.getPackageName(), mCbStub); } catch (RemoteException e) { Log.wtf(TAG, "Error calling prepare.", e); } }
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 PlaybackState.STATE_PAUSED. Afterwards, play can be called to start playback. If the preparation is not needed, playFromMediaId can be directly called without this method.
Params:
  • mediaId – The id of the requested media.
  • extras – Optional extras that can include extra information about the media item to be prepared.
/** * 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 PlaybackState#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(String mediaId, Bundle extras) { if (TextUtils.isEmpty(mediaId)) { throw new IllegalArgumentException( "You must specify a non-empty String for prepareFromMediaId."); } try { mSessionBinder.prepareFromMediaId(mContext.getPackageName(), mCbStub, mediaId, extras); } catch (RemoteException e) { Log.wtf(TAG, "Error calling prepare(" + mediaId + ").", e); } }
Request that the player prepare playback for a specific search query. An empty or null query should be treated as a request to prepare any music. 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 PlaybackState.STATE_PAUSED. Afterwards, play can be called to start playback. If the preparation is not needed, playFromSearch can be directly called without this method.
Params:
  • query – The search query.
  • extras – Optional extras that can include extra information about the query.
/** * Request that the player prepare playback for a specific search query. An empty or null * query should be treated as a request to prepare any music. 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 PlaybackState#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. * @param extras Optional extras that can include extra information * about the query. */
public void prepareFromSearch(String query, Bundle extras) { if (query == null) { // This is to remain compatible with // INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH query = ""; } try { mSessionBinder.prepareFromSearch(mContext.getPackageName(), mCbStub, query, extras); } catch (RemoteException e) { Log.wtf(TAG, "Error calling prepare(" + query + ").", e); } }
Request that the player prepare playback for a specific 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 PlaybackState.STATE_PAUSED. Afterwards, play can be called to start playback. If the preparation is not needed, playFromUri can be directly called without this method.
Params:
  • uri – The URI of the requested media.
  • extras – Optional extras that can include extra information about the media item to be prepared.
/** * 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 PlaybackState#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(Uri uri, Bundle extras) { if (uri == null || Uri.EMPTY.equals(uri)) { throw new IllegalArgumentException( "You must specify a non-empty Uri for prepareFromUri."); } try { mSessionBinder.prepareFromUri(mContext.getPackageName(), mCbStub, uri, extras); } catch (RemoteException e) { Log.wtf(TAG, "Error calling prepare(" + uri + ").", e); } }
Request that the player start its playback at its current position.
/** * Request that the player start its playback at its current position. */
public void play() { try { mSessionBinder.play(mContext.getPackageName(), mCbStub); } catch (RemoteException e) { Log.wtf(TAG, "Error calling play.", e); } }
Request that the player start playback for a specific media id.
Params:
  • mediaId – The id of the requested media.
  • extras – Optional extras that can include extra information about the media item to be played.
/** * 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(String mediaId, Bundle extras) { if (TextUtils.isEmpty(mediaId)) { throw new IllegalArgumentException( "You must specify a non-empty String for playFromMediaId."); } try { mSessionBinder.playFromMediaId(mContext.getPackageName(), mCbStub, mediaId, extras); } catch (RemoteException e) { Log.wtf(TAG, "Error calling play(" + mediaId + ").", e); } }
Request that the player start playback for a specific search query. An empty or null query should be treated as a request to play any music.
Params:
  • query – The search query.
  • extras – Optional extras that can include extra information about the query.
/** * Request that the player start playback for a specific search query. * An empty or null query should be treated as a request to play any * music. * * @param query The search query. * @param extras Optional extras that can include extra information * about the query. */
public void playFromSearch(String query, Bundle extras) { if (query == null) { // This is to remain compatible with // INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH query = ""; } try { mSessionBinder.playFromSearch(mContext.getPackageName(), mCbStub, query, extras); } catch (RemoteException e) { Log.wtf(TAG, "Error calling play(" + query + ").", e); } }
Request that the player start playback for a specific Uri.
Params:
  • uri – The URI of the requested media.
  • extras – Optional extras that can include extra information about the media item to be played.
/** * 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(Uri uri, Bundle extras) { if (uri == null || Uri.EMPTY.equals(uri)) { throw new IllegalArgumentException( "You must specify a non-empty Uri for playFromUri."); } try { mSessionBinder.playFromUri(mContext.getPackageName(), mCbStub, uri, extras); } catch (RemoteException e) { Log.wtf(TAG, "Error calling play(" + uri + ").", e); } }
Play an item with a specific id in the play queue. If you specify an id that is not in the play queue, the behavior is undefined.
/** * Play an item with a specific id in the play queue. If you specify an * id that is not in the play queue, the behavior is undefined. */
public void skipToQueueItem(long id) { try { mSessionBinder.skipToQueueItem(mContext.getPackageName(), mCbStub, id); } catch (RemoteException e) { Log.wtf(TAG, "Error calling skipToItem(" + id + ").", e); } }
Request that the player pause its playback and stay at its current position.
/** * Request that the player pause its playback and stay at its current * position. */
public void pause() { try { mSessionBinder.pause(mContext.getPackageName(), mCbStub); } catch (RemoteException e) { Log.wtf(TAG, "Error calling pause.", e); } }
Request that the player stop its playback; it may clear its state in whatever way is appropriate.
/** * Request that the player stop its playback; it may clear its state in * whatever way is appropriate. */
public void stop() { try { mSessionBinder.stop(mContext.getPackageName(), mCbStub); } catch (RemoteException e) { Log.wtf(TAG, "Error calling stop.", e); } }
Move to a new location in the media stream.
Params:
  • pos – Position to move to, in milliseconds.
/** * Move to a new location in the media stream. * * @param pos Position to move to, in milliseconds. */
public void seekTo(long pos) { try { mSessionBinder.seekTo(mContext.getPackageName(), mCbStub, pos); } catch (RemoteException e) { Log.wtf(TAG, "Error calling seekTo.", e); } }
Start fast forwarding. If playback is already fast forwarding this may increase the rate.
/** * Start fast forwarding. If playback is already fast forwarding this * may increase the rate. */
public void fastForward() { try { mSessionBinder.fastForward(mContext.getPackageName(), mCbStub); } catch (RemoteException e) { Log.wtf(TAG, "Error calling fastForward.", e); } }
Skip to the next item.
/** * Skip to the next item. */
public void skipToNext() { try { mSessionBinder.next(mContext.getPackageName(), mCbStub); } catch (RemoteException e) { Log.wtf(TAG, "Error calling next.", e); } }
Start rewinding. If playback is already rewinding this may increase the rate.
/** * Start rewinding. If playback is already rewinding this may increase * the rate. */
public void rewind() { try { mSessionBinder.rewind(mContext.getPackageName(), mCbStub); } catch (RemoteException e) { Log.wtf(TAG, "Error calling rewind.", e); } }
Skip to the previous item.
/** * Skip to the previous item. */
public void skipToPrevious() { try { mSessionBinder.previous(mContext.getPackageName(), mCbStub); } catch (RemoteException e) { Log.wtf(TAG, "Error calling previous.", e); } }
Rate the current content. This will cause the rating to be set for the current user. The Rating type must match the type returned by MediaController.getRatingType().
Params:
  • rating – The rating to set for the current content
/** * Rate the current content. This will cause the rating to be set for * the current user. The Rating type must match the type returned by * {@link #getRatingType()}. * * @param rating The rating to set for the current content */
public void setRating(Rating rating) { try { mSessionBinder.rate(mContext.getPackageName(), mCbStub, rating); } catch (RemoteException e) { Log.wtf(TAG, "Error calling rate.", e); } }
Send a custom action back for the MediaSession to perform.
Params:
  • customAction – The action to perform.
  • args – Optional arguments to supply to the MediaSession for this custom action.
/** * Send a custom action back for the {@link MediaSession} to perform. * * @param customAction The action to perform. * @param args Optional arguments to supply to the {@link MediaSession} for this * custom action. */
public void sendCustomAction(@NonNull PlaybackState.CustomAction customAction, @Nullable Bundle args) { if (customAction == null) { throw new IllegalArgumentException("CustomAction cannot be null."); } sendCustomAction(customAction.getAction(), args); }
Send the id and args from a custom action back for the MediaSession to perform.
Params:
See Also:
/** * Send the id and args from a custom action back for the {@link MediaSession} to perform. * * @see #sendCustomAction(PlaybackState.CustomAction action, Bundle args) * @param action The action identifier of the {@link PlaybackState.CustomAction} as * specified by the {@link MediaSession}. * @param args Optional arguments to supply to the {@link MediaSession} for this * custom action. */
public void sendCustomAction(@NonNull String action, @Nullable Bundle args) { if (TextUtils.isEmpty(action)) { throw new IllegalArgumentException("CustomAction cannot be null."); } try { mSessionBinder.sendCustomAction(mContext.getPackageName(), mCbStub, action, args); } catch (RemoteException e) { Log.d(TAG, "Dead object in sendCustomAction.", e); } } }
Holds information about the current playback and how audio is handled for this session.
/** * Holds information about the current playback and how audio is handled for * this session. */
public static final class PlaybackInfo {
The session uses remote playback.
/** * The session uses remote playback. */
public static final int PLAYBACK_TYPE_REMOTE = 2;
The session uses local playback.
/** * The session uses local playback. */
public static final int PLAYBACK_TYPE_LOCAL = 1; private final int mVolumeType; private final int mVolumeControl; private final int mMaxVolume; private final int mCurrentVolume; private final AudioAttributes mAudioAttrs;
@hide
/** * @hide */
public PlaybackInfo(int type, AudioAttributes attrs, int control, int max, int current) { mVolumeType = type; mAudioAttrs = attrs; mVolumeControl = control; mMaxVolume = max; mCurrentVolume = current; }
Get the type of playback which affects volume handling. One of:
Returns:The type of playback this session is using.
/** * 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 mVolumeType; }
Get the audio attributes for this session. The attributes will affect volume handling for the session. When the volume type is PLAYBACK_TYPE_REMOTE these may be ignored by the remote volume handler.
Returns:The attributes for this session.
/** * Get the audio attributes for this session. The attributes will affect * volume handling for the session. When the volume type is * {@link PlaybackInfo#PLAYBACK_TYPE_REMOTE} these may be ignored by the * remote volume handler. * * @return The attributes for this session. */
public AudioAttributes getAudioAttributes() { return mAudioAttrs; }
Returns:The type of volume control that may be used with this session.
/** * Get the type of volume control that can be used. One of: * <ul> * <li>{@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}</li> * <li>{@link VolumeProvider#VOLUME_CONTROL_RELATIVE}</li> * <li>{@link VolumeProvider#VOLUME_CONTROL_FIXED}</li> * </ul> * * @return The type of volume control that may be used with this * session. */
public int getVolumeControl() { return mVolumeControl; }
Get the maximum volume that may be set for this session.
Returns:The maximum allowed volume where this session is playing.
/** * 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.
Returns:The current volume where this session is playing.
/** * Get the current volume for this session. * * @return The current volume where this session is playing. */
public int getCurrentVolume() { return mCurrentVolume; } } private final static class CallbackStub extends ISessionControllerCallback.Stub { private final WeakReference<MediaController> mController; public CallbackStub(MediaController controller) { mController = new WeakReference<MediaController>(controller); } @Override public void onSessionDestroyed() { MediaController controller = mController.get(); if (controller != null) { controller.postMessage(MSG_DESTROYED, null, null); } } @Override public void onEvent(String event, Bundle extras) { MediaController controller = mController.get(); if (controller != null) { controller.postMessage(MSG_EVENT, event, extras); } } @Override public void onPlaybackStateChanged(PlaybackState state) { MediaController controller = mController.get(); if (controller != null) { controller.postMessage(MSG_UPDATE_PLAYBACK_STATE, state, null); } } @Override public void onMetadataChanged(MediaMetadata metadata) { MediaController controller = mController.get(); if (controller != null) { controller.postMessage(MSG_UPDATE_METADATA, metadata, null); } } @Override public void onQueueChanged(ParceledListSlice parceledQueue) { List<MediaSession.QueueItem> queue = parceledQueue == null ? null : parceledQueue .getList(); MediaController controller = mController.get(); if (controller != null) { controller.postMessage(MSG_UPDATE_QUEUE, queue, null); } } @Override public void onQueueTitleChanged(CharSequence title) { MediaController controller = mController.get(); if (controller != null) { controller.postMessage(MSG_UPDATE_QUEUE_TITLE, title, null); } } @Override public void onExtrasChanged(Bundle extras) { MediaController controller = mController.get(); if (controller != null) { controller.postMessage(MSG_UPDATE_EXTRAS, extras, null); } } @Override public void onVolumeInfoChanged(ParcelableVolumeInfo pvi) { MediaController controller = mController.get(); if (controller != null) { PlaybackInfo info = new PlaybackInfo(pvi.volumeType, pvi.audioAttrs, pvi.controlType, pvi.maxVolume, pvi.currentVolume); controller.postMessage(MSG_UPDATE_VOLUME, info, null); } } } private final static class MessageHandler extends Handler { private final MediaController.Callback mCallback; private boolean mRegistered = false; public MessageHandler(Looper looper, MediaController.Callback cb) { super(looper, null, true); mCallback = cb; } @Override public void handleMessage(Message msg) { if (!mRegistered) { return; } switch (msg.what) { case MSG_EVENT: mCallback.onSessionEvent((String) msg.obj, msg.getData()); break; case MSG_UPDATE_PLAYBACK_STATE: mCallback.onPlaybackStateChanged((PlaybackState) msg.obj); break; case MSG_UPDATE_METADATA: mCallback.onMetadataChanged((MediaMetadata) msg.obj); break; case MSG_UPDATE_QUEUE: mCallback.onQueueChanged((List<MediaSession.QueueItem>) msg.obj); break; case MSG_UPDATE_QUEUE_TITLE: mCallback.onQueueTitleChanged((CharSequence) msg.obj); break; case MSG_UPDATE_EXTRAS: mCallback.onExtrasChanged((Bundle) msg.obj); break; case MSG_UPDATE_VOLUME: mCallback.onAudioInfoChanged((PlaybackInfo) msg.obj); break; case MSG_DESTROYED: mCallback.onSessionDestroyed(); break; } } public void post(int what, Object obj, Bundle data) { Message msg = obtainMessage(what, obj); msg.setData(data); msg.sendToTarget(); } } }