Java tutorial
/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.media.session; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UnsupportedAppUsage; import android.app.Activity; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.pm.ParceledListSlice; import android.media.AudioAttributes; import android.media.MediaDescription; 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.Parcel; import android.os.Parcelable; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.UserHandle; import android.media.session.MediaSessionManager.RemoteUserInfo; import android.service.media.MediaBrowserService; import android.text.TextUtils; import android.util.Log; import android.util.Pair; import android.view.KeyEvent; import android.view.ViewConfiguration; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; import java.util.List; import java.util.Objects; /** * Allows interaction with media controllers, volume keys, media buttons, and * transport controls. * <p> * A MediaSession should be created when an app wants to publish media playback * information or handle media keys. In general an app only needs one session * for all playback, though multiple sessions can be created to provide finer * grain controls of media. * <p> * Once a session is created the owner of the session may pass its * {@link #getSessionToken() session token} to other processes to allow them to * create a {@link MediaController} to interact with the session. * <p> * To receive commands, media keys, and other events a {@link Callback} must be * set with {@link #setCallback(Callback)} and {@link #setActive(boolean) * setActive(true)} must be called. * <p> * When an app is finished performing playback it must call {@link #release()} * to clean up the session and notify any controllers. * <p> * MediaSession objects are thread safe. */ public final class MediaSession { private static final String TAG = "MediaSession"; /** * Set this flag on the session to indicate that it can handle media button * events. * @deprecated This flag is no longer used. All media sessions are expected to handle media * button events now. */ @Deprecated public static final int FLAG_HANDLES_MEDIA_BUTTONS = 1 << 0; /** * Set this flag on the session to indicate that it handles transport * control commands through its {@link Callback}. * @deprecated This flag is no longer used. All media sessions are expected to handle transport * controls now. */ @Deprecated public static final int FLAG_HANDLES_TRANSPORT_CONTROLS = 1 << 1; /** * System only flag for a session that needs to have priority over all other * sessions. This flag ensures this session will receive media button events * regardless of the current ordering in the system. * * @hide */ public static final int FLAG_EXCLUSIVE_GLOBAL_PRIORITY = 1 << 16; /** * @hide */ public static final int INVALID_UID = -1; /** * @hide */ public static final int INVALID_PID = -1; /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = { FLAG_HANDLES_MEDIA_BUTTONS, FLAG_HANDLES_TRANSPORT_CONTROLS, FLAG_EXCLUSIVE_GLOBAL_PRIORITY }) public @interface SessionFlags { } private final Object mLock = new Object(); private final int mMaxBitmapSize; private final MediaSession.Token mSessionToken; private final MediaController mController; private final ISession mBinder; private final CallbackStub mCbStub; // Do not change the name of mCallback. Support lib accesses this by using reflection. @UnsupportedAppUsage private CallbackMessageHandler mCallback; private VolumeProvider mVolumeProvider; private PlaybackState mPlaybackState; private boolean mActive = false; /** * Creates a new session. The session will automatically be registered with * the system but will not be published until {@link #setActive(boolean) * setActive(true)} is called. You must call {@link #release()} when * finished with the session. * * @param context The context to use to create the session. * @param tag A short name for debugging purposes. */ public MediaSession(@NonNull Context context, @NonNull String tag) { this(context, tag, UserHandle.myUserId()); } /** * Creates a new session as the specified user. To create a session as a * user other than your own you must hold the * {@link android.Manifest.permission#INTERACT_ACROSS_USERS_FULL} * permission. * * @param context The context to use to create the session. * @param tag A short name for debugging purposes. * @param userId The user id to create the session as. * @hide */ public MediaSession(@NonNull Context context, @NonNull String tag, int userId) { if (context == null) { throw new IllegalArgumentException("context cannot be null."); } if (TextUtils.isEmpty(tag)) { throw new IllegalArgumentException("tag cannot be null or empty"); } mMaxBitmapSize = context.getResources() .getDimensionPixelSize(com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize); mCbStub = new CallbackStub(this); MediaSessionManager manager = (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE); try { mBinder = manager.createSession(mCbStub, tag, userId); mSessionToken = new Token(mBinder.getController()); mController = new MediaController(context, mSessionToken); } catch (RemoteException e) { throw new RuntimeException("Remote error creating session.", e); } } /** * Set the callback to receive updates for the MediaSession. This includes * media button events and transport controls. The caller's thread will be * used to post updates. * <p> * Set the callback to null to stop receiving updates. * * @param callback The callback object */ public void setCallback(@Nullable Callback callback) { setCallback(callback, null); } /** * Set the callback to receive updates for the MediaSession. This includes * media button events and transport controls. * <p> * Set the callback to null to stop receiving updates. * * @param callback The callback to receive updates on. * @param handler The handler that events should be posted on. */ public void setCallback(@Nullable Callback callback, @Nullable Handler handler) { synchronized (mLock) { if (mCallback != null) { // We're updating the callback, clear the session from the old one. mCallback.mCallback.mSession = null; mCallback.removeCallbacksAndMessages(null); } if (callback == null) { mCallback = null; return; } if (handler == null) { handler = new Handler(); } callback.mSession = this; CallbackMessageHandler msgHandler = new CallbackMessageHandler(handler.getLooper(), callback); mCallback = msgHandler; } } /** * Set an intent for launching UI for this Session. This can be used as a * quick link to an ongoing media screen. The intent should be for an * activity that may be started using {@link Activity#startActivity(Intent)}. * * @param pi The intent to launch to show UI for this Session. */ public void setSessionActivity(@Nullable PendingIntent pi) { try { mBinder.setLaunchPendingIntent(pi); } catch (RemoteException e) { Log.wtf(TAG, "Failure in setLaunchPendingIntent.", e); } } /** * Set a pending intent for your media button receiver to allow restarting * playback after the session has been stopped. If your app is started in * this way an {@link Intent#ACTION_MEDIA_BUTTON} intent will be sent via * the pending intent. * * @param mbr The {@link PendingIntent} to send the media button event to. */ public void setMediaButtonReceiver(@Nullable PendingIntent mbr) { try { mBinder.setMediaButtonReceiver(mbr); } catch (RemoteException e) { Log.wtf(TAG, "Failure in setMediaButtonReceiver.", e); } } /** * Set any flags for the session. * * @param flags The flags to set for this session. */ public void setFlags(@SessionFlags int flags) { try { mBinder.setFlags(flags); } catch (RemoteException e) { Log.wtf(TAG, "Failure in setFlags.", e); } } /** * Set the attributes for this session's audio. This will affect the * system's volume handling for this session. If * {@link #setPlaybackToRemote} was previously called it will stop receiving * volume commands and the system will begin sending volume changes to the * appropriate stream. * <p> * By default sessions use attributes for media. * * @param attributes The {@link AudioAttributes} for this session's audio. */ public void setPlaybackToLocal(AudioAttributes attributes) { if (attributes == null) { throw new IllegalArgumentException("Attributes cannot be null for local playback."); } try { mBinder.setPlaybackToLocal(attributes); } catch (RemoteException e) { Log.wtf(TAG, "Failure in setPlaybackToLocal.", e); } } /** * Configure this session to use remote volume handling. This must be called * to receive volume button events, otherwise the system will adjust the * appropriate stream volume for this session. If * {@link #setPlaybackToLocal} was previously called the system will stop * handling volume changes for this session and pass them to the volume * provider instead. * * @param volumeProvider The provider that will handle volume changes. May * not be null. */ public void setPlaybackToRemote(@NonNull VolumeProvider volumeProvider) { if (volumeProvider == null) { throw new IllegalArgumentException("volumeProvider may not be null!"); } synchronized (mLock) { mVolumeProvider = volumeProvider; } volumeProvider.setCallback(new VolumeProvider.Callback() { @Override public void onVolumeChanged(VolumeProvider volumeProvider) { notifyRemoteVolumeChanged(volumeProvider); } }); try { mBinder.setPlaybackToRemote(volumeProvider.getVolumeControl(), volumeProvider.getMaxVolume()); mBinder.setCurrentVolume(volumeProvider.getCurrentVolume()); } catch (RemoteException e) { Log.wtf(TAG, "Failure in setPlaybackToRemote.", e); } } /** * Set if this session is currently active and ready to receive commands. If * set to false your session's controller may not be discoverable. You must * set the session to active before it can start receiving media button * events or transport commands. * * @param active Whether this session is active or not. */ public void setActive(boolean active) { if (mActive == active) { return; } try { mBinder.setActive(active); mActive = active; } catch (RemoteException e) { Log.wtf(TAG, "Failure in setActive.", e); } } /** * Get the current active state of this session. * * @return True if the session is active, false otherwise. */ public boolean isActive() { return mActive; } /** * Send a proprietary event to all MediaControllers listening to this * Session. It's up to the Controller/Session owner to determine the meaning * of any events. * * @param event The name of the event to send * @param extras Any extras included with the event */ public void sendSessionEvent(@NonNull String event, @Nullable Bundle extras) { if (TextUtils.isEmpty(event)) { throw new IllegalArgumentException("event cannot be null or empty"); } try { mBinder.sendEvent(event, extras); } catch (RemoteException e) { Log.wtf(TAG, "Error sending event", e); } } /** * This must be called when an app has finished performing playback. If * playback is expected to start again shortly the session can be left open, * but it must be released if your activity or service is being destroyed. */ public void release() { try { mBinder.destroy(); } catch (RemoteException e) { Log.wtf(TAG, "Error releasing session: ", e); } } /** * Retrieve a token object that can be used by apps to create a * {@link MediaController} for interacting with this session. The owner of * the session is responsible for deciding how to distribute these tokens. * * @return A token that can be used to create a MediaController for this * session */ public @NonNull Token getSessionToken() { return mSessionToken; } /** * Get a controller for this session. This is a convenience method to avoid * having to cache your own controller in process. * * @return A controller for this session. */ public @NonNull MediaController getController() { return mController; } /** * Update the current playback state. * * @param state The current state of playback */ public void setPlaybackState(@Nullable PlaybackState state) { mPlaybackState = state; try { mBinder.setPlaybackState(state); } catch (RemoteException e) { Log.wtf(TAG, "Dead object in setPlaybackState.", e); } } /** * Update the current metadata. New metadata can be created using * {@link android.media.MediaMetadata.Builder}. This operation may take time proportional to * the size of the bitmap to replace large bitmaps with a scaled down copy. * * @param metadata The new metadata * @see android.media.MediaMetadata.Builder#putBitmap */ public void setMetadata(@Nullable MediaMetadata metadata) { if (metadata != null) { metadata = (new MediaMetadata.Builder(metadata, mMaxBitmapSize)).build(); } try { mBinder.setMetadata(metadata); } catch (RemoteException e) { Log.wtf(TAG, "Dead object in setPlaybackState.", e); } } /** * Update the list of items in the play queue. It is an ordered list and * should contain the current item, and previous or upcoming items if they * exist. Specify null if there is no current play queue. * <p> * The queue should be of reasonable size. If the play queue is unbounded * within your app, it is better to send a reasonable amount in a sliding * window instead. * * @param queue A list of items in the play queue. */ public void setQueue(@Nullable List<QueueItem> queue) { try { mBinder.setQueue(queue == null ? null : new ParceledListSlice<QueueItem>(queue)); } catch (RemoteException e) { Log.wtf("Dead object in setQueue.", e); } } /** * Set the title of the play queue. The UI should display this title along * with the play queue itself. * e.g. "Play Queue", "Now Playing", or an album name. * * @param title The title of the play queue. */ public void setQueueTitle(@Nullable CharSequence title) { try { mBinder.setQueueTitle(title); } catch (RemoteException e) { Log.wtf("Dead object in setQueueTitle.", e); } } /** * Set the style of rating used by this session. Apps trying to set the * rating should use this style. Must be one of the following: * <ul> * <li>{@link Rating#RATING_NONE}</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_HEART}</li> * <li>{@link Rating#RATING_PERCENTAGE}</li> * <li>{@link Rating#RATING_THUMB_UP_DOWN}</li> * </ul> */ public void setRatingType(@Rating.Style int type) { try { mBinder.setRatingType(type); } catch (RemoteException e) { Log.e(TAG, "Error in setRatingType.", e); } } /** * Set some extras that can be associated with the {@link MediaSession}. No assumptions should * be made as to how a {@link MediaController} will handle these extras. * Keys should be fully qualified (e.g. com.example.MY_EXTRA) to avoid conflicts. * * @param extras The extras associated with the {@link MediaSession}. */ public void setExtras(@Nullable Bundle extras) { try { mBinder.setExtras(extras); } catch (RemoteException e) { Log.wtf("Dead object in setExtras.", e); } } /** * Gets the controller information who sent the current request. * <p> * Note: This is only valid while in a request callback, such as {@link Callback#onPlay}. * * @throws IllegalStateException If this method is called outside of {@link Callback} methods. * @see MediaSessionManager#isTrustedForMediaControl(RemoteUserInfo) */ public final @NonNull RemoteUserInfo getCurrentControllerInfo() { if (mCallback == null || mCallback.mCurrentControllerInfo == null) { throw new IllegalStateException("This should be called inside of MediaSession.Callback methods"); } return mCallback.mCurrentControllerInfo; } /** * Notify the system that the remote volume changed. * * @param provider The provider that is handling volume changes. * @hide */ public void notifyRemoteVolumeChanged(VolumeProvider provider) { synchronized (mLock) { if (provider == null || provider != mVolumeProvider) { Log.w(TAG, "Received update from stale volume provider"); return; } } try { mBinder.setCurrentVolume(provider.getCurrentVolume()); } catch (RemoteException e) { Log.e(TAG, "Error in notifyVolumeChanged", e); } } /** * Returns the name of the package that sent the last media button, transport control, or * command from controllers and the system. This is only valid while in a request callback, such * as {@link Callback#onPlay}. * * @hide */ @UnsupportedAppUsage public String getCallingPackage() { if (mCallback != null && mCallback.mCurrentControllerInfo != null) { return mCallback.mCurrentControllerInfo.getPackageName(); } return null; } private void dispatchPrepare(RemoteUserInfo caller) { postToCallback(caller, CallbackMessageHandler.MSG_PREPARE, null, null); } private void dispatchPrepareFromMediaId(RemoteUserInfo caller, String mediaId, Bundle extras) { postToCallback(caller, CallbackMessageHandler.MSG_PREPARE_MEDIA_ID, mediaId, extras); } private void dispatchPrepareFromSearch(RemoteUserInfo caller, String query, Bundle extras) { postToCallback(caller, CallbackMessageHandler.MSG_PREPARE_SEARCH, query, extras); } private void dispatchPrepareFromUri(RemoteUserInfo caller, Uri uri, Bundle extras) { postToCallback(caller, CallbackMessageHandler.MSG_PREPARE_URI, uri, extras); } private void dispatchPlay(RemoteUserInfo caller) { postToCallback(caller, CallbackMessageHandler.MSG_PLAY, null, null); } private void dispatchPlayFromMediaId(RemoteUserInfo caller, String mediaId, Bundle extras) { postToCallback(caller, CallbackMessageHandler.MSG_PLAY_MEDIA_ID, mediaId, extras); } private void dispatchPlayFromSearch(RemoteUserInfo caller, String query, Bundle extras) { postToCallback(caller, CallbackMessageHandler.MSG_PLAY_SEARCH, query, extras); } private void dispatchPlayFromUri(RemoteUserInfo caller, Uri uri, Bundle extras) { postToCallback(caller, CallbackMessageHandler.MSG_PLAY_URI, uri, extras); } private void dispatchSkipToItem(RemoteUserInfo caller, long id) { postToCallback(caller, CallbackMessageHandler.MSG_SKIP_TO_ITEM, id, null); } private void dispatchPause(RemoteUserInfo caller) { postToCallback(caller, CallbackMessageHandler.MSG_PAUSE, null, null); } private void dispatchStop(RemoteUserInfo caller) { postToCallback(caller, CallbackMessageHandler.MSG_STOP, null, null); } private void dispatchNext(RemoteUserInfo caller) { postToCallback(caller, CallbackMessageHandler.MSG_NEXT, null, null); } private void dispatchPrevious(RemoteUserInfo caller) { postToCallback(caller, CallbackMessageHandler.MSG_PREVIOUS, null, null); } private void dispatchFastForward(RemoteUserInfo caller) { postToCallback(caller, CallbackMessageHandler.MSG_FAST_FORWARD, null, null); } private void dispatchRewind(RemoteUserInfo caller) { postToCallback(caller, CallbackMessageHandler.MSG_REWIND, null, null); } private void dispatchSeekTo(RemoteUserInfo caller, long pos) { postToCallback(caller, CallbackMessageHandler.MSG_SEEK_TO, pos, null); } private void dispatchRate(RemoteUserInfo caller, Rating rating) { postToCallback(caller, CallbackMessageHandler.MSG_RATE, rating, null); } private void dispatchCustomAction(RemoteUserInfo caller, String action, Bundle args) { postToCallback(caller, CallbackMessageHandler.MSG_CUSTOM_ACTION, action, args); } private void dispatchMediaButton(RemoteUserInfo caller, Intent mediaButtonIntent) { postToCallback(caller, CallbackMessageHandler.MSG_MEDIA_BUTTON, mediaButtonIntent, null); } private void dispatchMediaButtonDelayed(RemoteUserInfo info, Intent mediaButtonIntent, long delay) { postToCallbackDelayed(info, CallbackMessageHandler.MSG_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT, mediaButtonIntent, null, delay); } private void dispatchAdjustVolume(RemoteUserInfo caller, int direction) { postToCallback(caller, CallbackMessageHandler.MSG_ADJUST_VOLUME, direction, null); } private void dispatchSetVolumeTo(RemoteUserInfo caller, int volume) { postToCallback(caller, CallbackMessageHandler.MSG_SET_VOLUME, volume, null); } private void dispatchCommand(RemoteUserInfo caller, String command, Bundle args, ResultReceiver resultCb) { Command cmd = new Command(command, args, resultCb); postToCallback(caller, CallbackMessageHandler.MSG_COMMAND, cmd, null); } private void postToCallback(RemoteUserInfo caller, int what, Object obj, Bundle data) { postToCallbackDelayed(caller, what, obj, data, 0); } private void postToCallbackDelayed(RemoteUserInfo caller, int what, Object obj, Bundle data, long delay) { synchronized (mLock) { if (mCallback != null) { mCallback.post(caller, what, obj, data, delay); } } } /** * Return true if this is considered an active playback state. * * @hide */ public static boolean isActiveState(int state) { switch (state) { case PlaybackState.STATE_FAST_FORWARDING: case PlaybackState.STATE_REWINDING: case PlaybackState.STATE_SKIPPING_TO_PREVIOUS: case PlaybackState.STATE_SKIPPING_TO_NEXT: case PlaybackState.STATE_BUFFERING: case PlaybackState.STATE_CONNECTING: case PlaybackState.STATE_PLAYING: return true; } return false; } /** * Represents an ongoing session. This may be passed to apps by the session * owner to allow them to create a {@link MediaController} to communicate with * the session. */ public static final class Token implements Parcelable { private ISessionController mBinder; /** * @hide */ public Token(ISessionController binder) { mBinder = binder; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeStrongBinder(mBinder.asBinder()); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((mBinder == null) ? 0 : mBinder.asBinder().hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Token other = (Token) obj; if (mBinder == null) { if (other.mBinder != null) return false; } else if (!mBinder.asBinder().equals(other.mBinder.asBinder())) return false; return true; } ISessionController getBinder() { return mBinder; } public static final Parcelable.Creator<Token> CREATOR = new Parcelable.Creator<Token>() { @Override public Token createFromParcel(Parcel in) { return new Token(ISessionController.Stub.asInterface(in.readStrongBinder())); } @Override public Token[] newArray(int size) { return new Token[size]; } }; } /** * Receives media buttons, transport controls, and commands from controllers * and the system. A callback may be set using {@link #setCallback}. */ public abstract static class Callback { private MediaSession mSession; private CallbackMessageHandler mHandler; private boolean mMediaPlayPauseKeyPending; public Callback() { } /** * Called when a controller has sent a command to this session. * The owner of the session may handle custom commands but is not * required to. * * @param command The command name. * @param args Optional parameters for the command, may be null. * @param cb A result receiver to which a result may be sent by the command, may be null. */ public void onCommand(@NonNull String command, @Nullable Bundle args, @Nullable ResultReceiver cb) { } /** * Called when a media button is pressed and this session has the * highest priority or a controller sends a media button event to the * session. The default behavior will call the relevant method if the * action for it was set. * <p> * The intent will be of type {@link Intent#ACTION_MEDIA_BUTTON} with a * KeyEvent in {@link Intent#EXTRA_KEY_EVENT} * * @param mediaButtonIntent an intent containing the KeyEvent as an * extra * @return True if the event was handled, false otherwise. */ public boolean onMediaButtonEvent(@NonNull Intent mediaButtonIntent) { if (mSession != null && mHandler != null && Intent.ACTION_MEDIA_BUTTON.equals(mediaButtonIntent.getAction())) { KeyEvent ke = mediaButtonIntent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); if (ke != null && ke.getAction() == KeyEvent.ACTION_DOWN) { PlaybackState state = mSession.mPlaybackState; long validActions = state == null ? 0 : state.getActions(); switch (ke.getKeyCode()) { case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: case KeyEvent.KEYCODE_HEADSETHOOK: if (ke.getRepeatCount() > 0) { // Consider long-press as a single tap. handleMediaPlayPauseKeySingleTapIfPending(); } else if (mMediaPlayPauseKeyPending) { // Consider double tap as the next. mHandler.removeMessages(CallbackMessageHandler.MSG_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT); mMediaPlayPauseKeyPending = false; if ((validActions & PlaybackState.ACTION_SKIP_TO_NEXT) != 0) { onSkipToNext(); } } else { mMediaPlayPauseKeyPending = true; mSession.dispatchMediaButtonDelayed(mSession.getCurrentControllerInfo(), mediaButtonIntent, ViewConfiguration.getDoubleTapTimeout()); } return true; default: // If another key is pressed within double tap timeout, consider the // pending play/pause as a single tap to handle media keys in order. handleMediaPlayPauseKeySingleTapIfPending(); break; } switch (ke.getKeyCode()) { case KeyEvent.KEYCODE_MEDIA_PLAY: if ((validActions & PlaybackState.ACTION_PLAY) != 0) { onPlay(); return true; } break; case KeyEvent.KEYCODE_MEDIA_PAUSE: if ((validActions & PlaybackState.ACTION_PAUSE) != 0) { onPause(); return true; } break; case KeyEvent.KEYCODE_MEDIA_NEXT: if ((validActions & PlaybackState.ACTION_SKIP_TO_NEXT) != 0) { onSkipToNext(); return true; } break; case KeyEvent.KEYCODE_MEDIA_PREVIOUS: if ((validActions & PlaybackState.ACTION_SKIP_TO_PREVIOUS) != 0) { onSkipToPrevious(); return true; } break; case KeyEvent.KEYCODE_MEDIA_STOP: if ((validActions & PlaybackState.ACTION_STOP) != 0) { onStop(); return true; } break; case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: if ((validActions & PlaybackState.ACTION_FAST_FORWARD) != 0) { onFastForward(); return true; } break; case KeyEvent.KEYCODE_MEDIA_REWIND: if ((validActions & PlaybackState.ACTION_REWIND) != 0) { onRewind(); return true; } break; } } } return false; } private void handleMediaPlayPauseKeySingleTapIfPending() { if (!mMediaPlayPauseKeyPending) { return; } mMediaPlayPauseKeyPending = false; mHandler.removeMessages(CallbackMessageHandler.MSG_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT); PlaybackState state = mSession.mPlaybackState; long validActions = state == null ? 0 : state.getActions(); boolean isPlaying = state != null && state.getState() == PlaybackState.STATE_PLAYING; boolean canPlay = (validActions & (PlaybackState.ACTION_PLAY_PAUSE | PlaybackState.ACTION_PLAY)) != 0; boolean canPause = (validActions & (PlaybackState.ACTION_PLAY_PAUSE | PlaybackState.ACTION_PAUSE)) != 0; if (isPlaying && canPause) { onPause(); } else if (!isPlaying && canPlay) { onPlay(); } } /** * Override to handle requests to prepare playback. During the preparation, a session should * not hold audio focus in order to allow other sessions play seamlessly. The state of * playback should be updated to {@link PlaybackState#STATE_PAUSED} after the preparation is * done. */ public void onPrepare() { } /** * Override to handle requests to prepare for playing a specific mediaId that was provided * by your app's {@link MediaBrowserService}. During the preparation, a session should not * hold audio focus in order to allow other sessions play seamlessly. The state of playback * should be updated to {@link PlaybackState#STATE_PAUSED} after the preparation is done. * The playback of the prepared content should start in the implementation of * {@link #onPlay}. Override {@link #onPlayFromMediaId} to handle requests for starting * playback without preparation. */ public void onPrepareFromMediaId(String mediaId, Bundle extras) { } /** * Override to handle requests to prepare playback from a search query. An empty query * indicates that the app may prepare any music. The implementation should attempt to make a * smart choice about what to play. During the preparation, a session should not hold audio * focus in order to allow other sessions play seamlessly. The state of playback should be * updated to {@link PlaybackState#STATE_PAUSED} after the preparation is done. The playback * of the prepared content should start in the implementation of {@link #onPlay}. Override * {@link #onPlayFromSearch} to handle requests for starting playback without preparation. */ public void onPrepareFromSearch(String query, Bundle extras) { } /** * Override to handle requests to prepare a specific media item represented by a URI. * During the preparation, a session should not hold audio focus in order to allow * other sessions play seamlessly. The state of playback should be updated to * {@link PlaybackState#STATE_PAUSED} after the preparation is done. * The playback of the prepared content should start in the implementation of * {@link #onPlay}. Override {@link #onPlayFromUri} to handle requests * for starting playback without preparation. */ public void onPrepareFromUri(Uri uri, Bundle extras) { } /** * Override to handle requests to begin playback. */ public void onPlay() { } /** * Override to handle requests to begin playback from a search query. An * empty query indicates that the app may play any music. The * implementation should attempt to make a smart choice about what to * play. */ public void onPlayFromSearch(String query, Bundle extras) { } /** * Override to handle requests to play a specific mediaId that was * provided by your app's {@link MediaBrowserService}. */ public void onPlayFromMediaId(String mediaId, Bundle extras) { } /** * Override to handle requests to play a specific media item represented by a URI. */ public void onPlayFromUri(Uri uri, Bundle extras) { } /** * Override to handle requests to play an item with a given id from the * play queue. */ public void onSkipToQueueItem(long id) { } /** * Override to handle requests to pause playback. */ public void onPause() { } /** * Override to handle requests to skip to the next media item. */ public void onSkipToNext() { } /** * Override to handle requests to skip to the previous media item. */ public void onSkipToPrevious() { } /** * Override to handle requests to fast forward. */ public void onFastForward() { } /** * Override to handle requests to rewind. */ public void onRewind() { } /** * Override to handle requests to stop playback. */ public void onStop() { } /** * Override to handle requests to seek to a specific position in ms. * * @param pos New position to move to, in milliseconds. */ public void onSeekTo(long pos) { } /** * Override to handle the item being rated. * * @param rating */ public void onSetRating(@NonNull Rating rating) { } /** * Called when a {@link MediaController} wants a {@link PlaybackState.CustomAction} to be * performed. * * @param action The action that was originally sent in the * {@link PlaybackState.CustomAction}. * @param extras Optional extras specified by the {@link MediaController}. */ public void onCustomAction(@NonNull String action, @Nullable Bundle extras) { } } /** * @hide */ public static class CallbackStub extends ISessionCallback.Stub { private WeakReference<MediaSession> mMediaSession; public CallbackStub(MediaSession session) { mMediaSession = new WeakReference<>(session); } private static RemoteUserInfo createRemoteUserInfo(String packageName, int pid, int uid, ISessionControllerCallback caller) { return new RemoteUserInfo(packageName, pid, uid, caller != null ? caller.asBinder() : null); } @Override public void onCommand(String packageName, int pid, int uid, ISessionControllerCallback caller, String command, Bundle args, ResultReceiver cb) { MediaSession session = mMediaSession.get(); if (session != null) { session.dispatchCommand(createRemoteUserInfo(packageName, pid, uid, caller), command, args, cb); } } @Override public void onMediaButton(String packageName, int pid, int uid, Intent mediaButtonIntent, int sequenceNumber, ResultReceiver cb) { MediaSession session = mMediaSession.get(); try { if (session != null) { session.dispatchMediaButton(createRemoteUserInfo(packageName, pid, uid, null), mediaButtonIntent); } } finally { if (cb != null) { cb.send(sequenceNumber, null); } } } @Override public void onMediaButtonFromController(String packageName, int pid, int uid, ISessionControllerCallback caller, Intent mediaButtonIntent) { MediaSession session = mMediaSession.get(); if (session != null) { session.dispatchMediaButton(createRemoteUserInfo(packageName, pid, uid, caller), mediaButtonIntent); } } @Override public void onPrepare(String packageName, int pid, int uid, ISessionControllerCallback caller) { MediaSession session = mMediaSession.get(); if (session != null) { session.dispatchPrepare(createRemoteUserInfo(packageName, pid, uid, caller)); } } @Override public void onPrepareFromMediaId(String packageName, int pid, int uid, ISessionControllerCallback caller, String mediaId, Bundle extras) { MediaSession session = mMediaSession.get(); if (session != null) { session.dispatchPrepareFromMediaId(createRemoteUserInfo(packageName, pid, uid, caller), mediaId, extras); } } @Override public void onPrepareFromSearch(String packageName, int pid, int uid, ISessionControllerCallback caller, String query, Bundle extras) { MediaSession session = mMediaSession.get(); if (session != null) { session.dispatchPrepareFromSearch(createRemoteUserInfo(packageName, pid, uid, caller), query, extras); } } @Override public void onPrepareFromUri(String packageName, int pid, int uid, ISessionControllerCallback caller, Uri uri, Bundle extras) { MediaSession session = mMediaSession.get(); if (session != null) { session.dispatchPrepareFromUri(createRemoteUserInfo(packageName, pid, uid, caller), uri, extras); } } @Override public void onPlay(String packageName, int pid, int uid, ISessionControllerCallback caller) { MediaSession session = mMediaSession.get(); if (session != null) { session.dispatchPlay(createRemoteUserInfo(packageName, pid, uid, caller)); } } @Override public void onPlayFromMediaId(String packageName, int pid, int uid, ISessionControllerCallback caller, String mediaId, Bundle extras) { MediaSession session = mMediaSession.get(); if (session != null) { session.dispatchPlayFromMediaId(createRemoteUserInfo(packageName, pid, uid, caller), mediaId, extras); } } @Override public void onPlayFromSearch(String packageName, int pid, int uid, ISessionControllerCallback caller, String query, Bundle extras) { MediaSession session = mMediaSession.get(); if (session != null) { session.dispatchPlayFromSearch(createRemoteUserInfo(packageName, pid, uid, caller), query, extras); } } @Override public void onPlayFromUri(String packageName, int pid, int uid, ISessionControllerCallback caller, Uri uri, Bundle extras) { MediaSession session = mMediaSession.get(); if (session != null) { session.dispatchPlayFromUri(createRemoteUserInfo(packageName, pid, uid, caller), uri, extras); } } @Override public void onSkipToTrack(String packageName, int pid, int uid, ISessionControllerCallback caller, long id) { MediaSession session = mMediaSession.get(); if (session != null) { session.dispatchSkipToItem(createRemoteUserInfo(packageName, pid, uid, caller), id); } } @Override public void onPause(String packageName, int pid, int uid, ISessionControllerCallback caller) { MediaSession session = mMediaSession.get(); if (session != null) { session.dispatchPause(createRemoteUserInfo(packageName, pid, uid, caller)); } } @Override public void onStop(String packageName, int pid, int uid, ISessionControllerCallback caller) { MediaSession session = mMediaSession.get(); if (session != null) { session.dispatchStop(createRemoteUserInfo(packageName, pid, uid, caller)); } } @Override public void onNext(String packageName, int pid, int uid, ISessionControllerCallback caller) { MediaSession session = mMediaSession.get(); if (session != null) { session.dispatchNext(createRemoteUserInfo(packageName, pid, uid, caller)); } } @Override public void onPrevious(String packageName, int pid, int uid, ISessionControllerCallback caller) { MediaSession session = mMediaSession.get(); if (session != null) { session.dispatchPrevious(createRemoteUserInfo(packageName, pid, uid, caller)); } } @Override public void onFastForward(String packageName, int pid, int uid, ISessionControllerCallback caller) { MediaSession session = mMediaSession.get(); if (session != null) { session.dispatchFastForward(createRemoteUserInfo(packageName, pid, uid, caller)); } } @Override public void onRewind(String packageName, int pid, int uid, ISessionControllerCallback caller) { MediaSession session = mMediaSession.get(); if (session != null) { session.dispatchRewind(createRemoteUserInfo(packageName, pid, uid, caller)); } } @Override public void onSeekTo(String packageName, int pid, int uid, ISessionControllerCallback caller, long pos) { MediaSession session = mMediaSession.get(); if (session != null) { session.dispatchSeekTo(createRemoteUserInfo(packageName, pid, uid, caller), pos); } } @Override public void onRate(String packageName, int pid, int uid, ISessionControllerCallback caller, Rating rating) { MediaSession session = mMediaSession.get(); if (session != null) { session.dispatchRate(createRemoteUserInfo(packageName, pid, uid, caller), rating); } } @Override public void onCustomAction(String packageName, int pid, int uid, ISessionControllerCallback caller, String action, Bundle args) { MediaSession session = mMediaSession.get(); if (session != null) { session.dispatchCustomAction(createRemoteUserInfo(packageName, pid, uid, caller), action, args); } } @Override public void onAdjustVolume(String packageName, int pid, int uid, ISessionControllerCallback caller, int direction) { MediaSession session = mMediaSession.get(); if (session != null) { session.dispatchAdjustVolume(createRemoteUserInfo(packageName, pid, uid, caller), direction); } } @Override public void onSetVolumeTo(String packageName, int pid, int uid, ISessionControllerCallback caller, int value) { MediaSession session = mMediaSession.get(); if (session != null) { session.dispatchSetVolumeTo(createRemoteUserInfo(packageName, pid, uid, caller), value); } } } /** * A single item that is part of the play queue. It contains a description * of the item and its id in the queue. */ public static final class QueueItem implements Parcelable { /** * This id is reserved. No items can be explicitly assigned this id. */ public static final int UNKNOWN_ID = -1; private final MediaDescription mDescription; @UnsupportedAppUsage private final long mId; /** * Create a new {@link MediaSession.QueueItem}. * * @param description The {@link MediaDescription} for this item. * @param id An identifier for this item. It must be unique within the * play queue and cannot be {@link #UNKNOWN_ID}. */ public QueueItem(MediaDescription description, long id) { if (description == null) { throw new IllegalArgumentException("Description cannot be null."); } if (id == UNKNOWN_ID) { throw new IllegalArgumentException("Id cannot be QueueItem.UNKNOWN_ID"); } mDescription = description; mId = id; } private QueueItem(Parcel in) { mDescription = MediaDescription.CREATOR.createFromParcel(in); mId = in.readLong(); } /** * Get the description for this item. */ public MediaDescription getDescription() { return mDescription; } /** * Get the queue id for this item. */ public long getQueueId() { return mId; } @Override public void writeToParcel(Parcel dest, int flags) { mDescription.writeToParcel(dest, flags); dest.writeLong(mId); } @Override public int describeContents() { return 0; } public static final Creator<MediaSession.QueueItem> CREATOR = new Creator<MediaSession.QueueItem>() { @Override public MediaSession.QueueItem createFromParcel(Parcel p) { return new MediaSession.QueueItem(p); } @Override public MediaSession.QueueItem[] newArray(int size) { return new MediaSession.QueueItem[size]; } }; @Override public String toString() { return "MediaSession.QueueItem {" + "Description=" + mDescription + ", Id=" + mId + " }"; } @Override public boolean equals(Object o) { if (o == null) { return false; } if (!(o instanceof QueueItem)) { return false; } final QueueItem item = (QueueItem) o; if (mId != item.mId) { return false; } if (!Objects.equals(mDescription, item.mDescription)) { return false; } return true; } } private static final class Command { public final String command; public final Bundle extras; public final ResultReceiver stub; public Command(String command, Bundle extras, ResultReceiver stub) { this.command = command; this.extras = extras; this.stub = stub; } } private class CallbackMessageHandler extends Handler { private static final int MSG_COMMAND = 1; private static final int MSG_MEDIA_BUTTON = 2; private static final int MSG_PREPARE = 3; private static final int MSG_PREPARE_MEDIA_ID = 4; private static final int MSG_PREPARE_SEARCH = 5; private static final int MSG_PREPARE_URI = 6; private static final int MSG_PLAY = 7; private static final int MSG_PLAY_MEDIA_ID = 8; private static final int MSG_PLAY_SEARCH = 9; private static final int MSG_PLAY_URI = 10; private static final int MSG_SKIP_TO_ITEM = 11; private static final int MSG_PAUSE = 12; private static final int MSG_STOP = 13; private static final int MSG_NEXT = 14; private static final int MSG_PREVIOUS = 15; private static final int MSG_FAST_FORWARD = 16; private static final int MSG_REWIND = 17; private static final int MSG_SEEK_TO = 18; private static final int MSG_RATE = 19; private static final int MSG_CUSTOM_ACTION = 20; private static final int MSG_ADJUST_VOLUME = 21; private static final int MSG_SET_VOLUME = 22; private static final int MSG_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT = 23; private MediaSession.Callback mCallback; private RemoteUserInfo mCurrentControllerInfo; public CallbackMessageHandler(Looper looper, MediaSession.Callback callback) { super(looper, null, true); mCallback = callback; mCallback.mHandler = this; } public void post(RemoteUserInfo caller, int what, Object obj, Bundle data, long delayMs) { Pair<RemoteUserInfo, Object> objWithCaller = Pair.create(caller, obj); Message msg = obtainMessage(what, objWithCaller); msg.setData(data); if (delayMs > 0) { sendMessageDelayed(msg, delayMs); } else { sendMessage(msg); } } @Override public void handleMessage(Message msg) { mCurrentControllerInfo = ((Pair<RemoteUserInfo, Object>) msg.obj).first; VolumeProvider vp; Object obj = ((Pair<RemoteUserInfo, Object>) msg.obj).second; switch (msg.what) { case MSG_COMMAND: Command cmd = (Command) obj; mCallback.onCommand(cmd.command, cmd.extras, cmd.stub); break; case MSG_MEDIA_BUTTON: mCallback.onMediaButtonEvent((Intent) obj); break; case MSG_PREPARE: mCallback.onPrepare(); break; case MSG_PREPARE_MEDIA_ID: mCallback.onPrepareFromMediaId((String) obj, msg.getData()); break; case MSG_PREPARE_SEARCH: mCallback.onPrepareFromSearch((String) obj, msg.getData()); break; case MSG_PREPARE_URI: mCallback.onPrepareFromUri((Uri) obj, msg.getData()); break; case MSG_PLAY: mCallback.onPlay(); break; case MSG_PLAY_MEDIA_ID: mCallback.onPlayFromMediaId((String) obj, msg.getData()); break; case MSG_PLAY_SEARCH: mCallback.onPlayFromSearch((String) obj, msg.getData()); break; case MSG_PLAY_URI: mCallback.onPlayFromUri((Uri) obj, msg.getData()); break; case MSG_SKIP_TO_ITEM: mCallback.onSkipToQueueItem((Long) obj); break; case MSG_PAUSE: mCallback.onPause(); break; case MSG_STOP: mCallback.onStop(); break; case MSG_NEXT: mCallback.onSkipToNext(); break; case MSG_PREVIOUS: mCallback.onSkipToPrevious(); break; case MSG_FAST_FORWARD: mCallback.onFastForward(); break; case MSG_REWIND: mCallback.onRewind(); break; case MSG_SEEK_TO: mCallback.onSeekTo((Long) obj); break; case MSG_RATE: mCallback.onSetRating((Rating) obj); break; case MSG_CUSTOM_ACTION: mCallback.onCustomAction((String) obj, msg.getData()); break; case MSG_ADJUST_VOLUME: synchronized (mLock) { vp = mVolumeProvider; } if (vp != null) { vp.onAdjustVolume((int) obj); } break; case MSG_SET_VOLUME: synchronized (mLock) { vp = mVolumeProvider; } if (vp != null) { vp.onSetVolumeTo((int) obj); } break; case MSG_PLAY_PAUSE_KEY_DOUBLE_TAP_TIMEOUT: mCallback.handleMediaPlayPauseKeySingleTapIfPending(); break; } mCurrentControllerInfo = null; } } }