/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.support.v4.media.session; import android.content.Context; import android.content.Intent; import android.media.AudioManager; import android.os.Bundle; import android.os.Handler; import android.os.Parcel; import android.os.Parcelable; import android.os.ResultReceiver; import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.RatingCompat; import android.support.v4.media.VolumeProviderCompat; import android.text.TextUtils; /** * Allows interaction with media controllers, volume keys, media buttons, and * transport controls. * <p> * A MediaSession should be created when an app wants to publish media playback * information or handle media keys. In general an app only needs one session * for all playback, though multiple sessions can be created to provide finer * grain controls of media. * <p> * Once a session is created the owner of the session may pass its * {@link #getSessionToken() session token} to other processes to allow them to * create a {@link MediaControllerCompat} to interact with the session. * <p> * To receive commands, media keys, and other events a {@link Callback} must be * set with {@link #setCallback(Callback)}. * <p> * When an app is finished performing playback it must call {@link #release()} * to clean up the session and notify any controllers. * <p> * MediaSession objects are thread safe. * <p> * This is a helper for accessing features in * {@link android.media.session.MediaSession} introduced after API level 4 in a * backwards compatible fashion. */ public class MediaSessionCompat { private final MediaSessionImpl mImpl; /** * Set this flag on the session to indicate that it can handle media button * events. */ 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}. */ public static final int FLAG_HANDLES_TRANSPORT_CONTROLS = 1 << 1; /** * Creates a new session. * * @param context The context. * @param tag A short name for debugging purposes. */ public MediaSessionCompat(Context context, String tag) { if (context == null) { throw new IllegalArgumentException("context must not be null"); } if (TextUtils.isEmpty(tag)) { throw new IllegalArgumentException("tag must not be null or empty"); } if (android.os.Build.VERSION.SDK_INT >= 21) { mImpl = new MediaSessionImplApi21(context, tag); } else { mImpl = new MediaSessionImplBase(); } } private MediaSessionCompat(MediaSessionImpl impl) { mImpl = impl; } /** * Add a callback to receive updates on for the MediaSession. This includes * media button and volume events. The caller's thread will be used to post * events. * * @param callback The callback object */ public void setCallback(Callback callback) { setCallback(callback, null); } /** * Set the callback to receive updates for the MediaSession. This includes * media button and volume events. Set the callback to null to stop * receiving events. * * @param callback The callback to receive updates on. * @param handler The handler that events should be posted on. */ public void setCallback(Callback callback, Handler handler) { mImpl.setCallback(callback, handler != null ? handler : new Handler()); } /** * Set any flags for the session. * * @param flags The flags to set for this session. */ public void setFlags(int flags) { mImpl.setFlags(flags); } /** * Set the stream this session is playing on. This will affect the system's * volume handling for this session. If {@link #setPlaybackToRemote} was * previously called it will stop receiving volume commands and the system * will begin sending volume changes to the appropriate stream. * <p> * By default sessions are on {@link AudioManager#STREAM_MUSIC}. * * @param stream The {@link AudioManager} stream this session is playing on. */ public void setPlaybackToLocal(int stream) { mImpl.setPlaybackToLocal(stream); } /** * Configure this session to use remote volume handling. This must be called * to receive volume button events, otherwise the system will adjust the * current stream volume for this session. If {@link #setPlaybackToLocal} * was previously called that stream will stop receiving volume changes for * this session. * * @param volumeProvider The provider that will handle volume changes. May * not be null. */ public void setPlaybackToRemote(VolumeProviderCompat volumeProvider) { if (volumeProvider == null) { throw new IllegalArgumentException("volumeProvider may not be null!"); } mImpl.setPlaybackToRemote(volumeProvider); } /** * 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) { mImpl.setActive(active); } /** * Get the current active state of this session. * * @return True if the session is active, false otherwise. */ public boolean isActive() { return mImpl.isActive(); } /** * 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(String event, Bundle extras) { if (TextUtils.isEmpty(event)) { throw new IllegalArgumentException("event cannot be null or empty"); } mImpl.sendSessionEvent(event, extras); } /** * This must be called when an app has finished performing playback. If * playback is expected to start again shortly the session can be left open, * but it must be released if your activity or service is being destroyed. */ public void release() { mImpl.release(); } /** * Retrieve a token object that can be used by apps to create a * {@link MediaControllerCompat} for interacting with this session. The owner of * the session is responsible for deciding how to distribute these tokens. * * @return A token that can be used to create a MediaController for this * session. */ public Token getSessionToken() { return mImpl.getSessionToken(); } /** * Update the current playback state. * * @param state The current state of playback */ public void setPlaybackState(PlaybackStateCompat state) { mImpl.setPlaybackState(state); } /** * Update the current metadata. New metadata can be created using * {@link android.media.MediaMetadata.Builder}. * * @param metadata The new metadata */ public void setMetadata(MediaMetadataCompat metadata) { mImpl.setMetadata(metadata); } /** * Gets the underlying framework {@link android.media.session.MediaSession} object. * <p> * This method is only supported on API 21+. * </p> * * @return The underlying {@link android.media.session.MediaSession} object, * or null if none. */ public Object getMediaSession() { return mImpl.getMediaSession(); } /** * Obtain a compat wrapper for an existing MediaSession. * * @param mediaSession The {@link android.media.session.MediaSession} to * wrap. * @return A compat wrapper for the provided session. */ public static MediaSessionCompat obtain(Object mediaSession) { return new MediaSessionCompat(new MediaSessionImplApi21(mediaSession)); } /** * Receives transport controls, media buttons, and commands from controllers * and the system. The callback may be set using {@link #setCallback}. */ public abstract static class Callback { final Object mCallbackObj; public Callback() { if (android.os.Build.VERSION.SDK_INT >= 21) { mCallbackObj = MediaSessionCompatApi21.createCallback(new StubApi21()); } else { mCallbackObj = null; } } /** * Called when a controller has sent a custom command to this session. * The owner of the session may handle custom commands but is not * required to. * * @param command The command name. * @param extras Optional parameters for the command, may be null. * @param cb A result receiver to which a result may be sent by the command, may be null. */ public void onCommand(String command, Bundle extras, ResultReceiver cb) { } /** * Override to handle media button events. * * @param mediaButtonEvent The media button event intent. * @return True if the event was handled, false otherwise. */ public boolean onMediaButtonEvent(Intent mediaButtonEvent) { return false; } /** * Override to handle requests to begin playback. */ public void onPlay() { } /** * 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(RatingCompat rating) { } private class StubApi21 implements MediaSessionCompatApi21.Callback { @Override public void onCommand(String command, Bundle extras, ResultReceiver cb) { Callback.this.onCommand(command, extras, cb); } @Override public boolean onMediaButtonEvent(Intent mediaButtonIntent) { return Callback.this.onMediaButtonEvent(mediaButtonIntent); } @Override public void onPlay() { Callback.this.onPlay(); } @Override public void onPause() { Callback.this.onPause(); } @Override public void onSkipToNext() { Callback.this.onSkipToNext(); } @Override public void onSkipToPrevious() { Callback.this.onSkipToPrevious(); } @Override public void onFastForward() { Callback.this.onFastForward(); } @Override public void onRewind() { Callback.this.onRewind(); } @Override public void onStop() { Callback.this.onStop(); } @Override public void onSeekTo(long pos) { Callback.this.onSeekTo(pos); } @Override public void onSetRating(Object ratingObj) { Callback.this.onSetRating(RatingCompat.fromRating(ratingObj)); } } } /** * Represents an ongoing session. This may be passed to apps by the session * owner to allow them to create a {@link MediaControllerCompat} to communicate with * the session. */ public static final class Token implements Parcelable { private final Parcelable mInner; Token(Parcelable inner) { mInner = inner; } @Override public int describeContents() { return mInner.describeContents(); } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeParcelable(mInner, flags); } /** * Gets the underlying framework {@link android.media.session.MediaSession.Token} object. * <p> * This method is only supported on API 21+. * </p> * * @return The underlying {@link android.media.session.MediaSession.Token} object, * or null if none. */ public Object getToken() { return mInner; } public static final Parcelable.Creator<Token> CREATOR = new Parcelable.Creator<Token>() { @Override public Token createFromParcel(Parcel in) { return new Token(in.readParcelable(null)); } @Override public Token[] newArray(int size) { return new Token[size]; } }; } interface MediaSessionImpl { void setCallback(Callback callback, Handler handler); void setFlags(int flags); void setPlaybackToLocal(int stream); void setPlaybackToRemote(VolumeProviderCompat volumeProvider); void setActive(boolean active); boolean isActive(); void sendSessionEvent(String event, Bundle extras); void release(); Token getSessionToken(); void setPlaybackState(PlaybackStateCompat state); void setMetadata(MediaMetadataCompat metadata); Object getMediaSession(); } // TODO: compatibility implementation static class MediaSessionImplBase implements MediaSessionImpl { @Override public void setCallback(Callback callback, Handler handler) { } @Override public void setFlags(int flags) { } @Override public void setPlaybackToLocal(int stream) { } @Override public void setPlaybackToRemote(VolumeProviderCompat volumeProvider) { } @Override public void setActive(boolean active) { } @Override public boolean isActive() { return false; } @Override public void sendSessionEvent(String event, Bundle extras) { } @Override public void release() { } @Override public Token getSessionToken() { return null; } @Override public void setPlaybackState(PlaybackStateCompat state) { } @Override public void setMetadata(MediaMetadataCompat metadata) { } @Override public Object getMediaSession() { return null; } } static class MediaSessionImplApi21 implements MediaSessionImpl { private final Object mSessionObj; private final Token mToken; public MediaSessionImplApi21(Context context, String tag) { mSessionObj = MediaSessionCompatApi21.createSession(context, tag); mToken = new Token(MediaSessionCompatApi21.getSessionToken(mSessionObj)); } public MediaSessionImplApi21(Object mediaSession) { mSessionObj = MediaSessionCompatApi21.verifySession(mediaSession); mToken = new Token(MediaSessionCompatApi21.getSessionToken(mSessionObj)); } @Override public void setCallback(Callback callback, Handler handler) { MediaSessionCompatApi21.setCallback(mSessionObj, callback.mCallbackObj, handler); } @Override public void setFlags(int flags) { MediaSessionCompatApi21.setFlags(mSessionObj, flags); } @Override public void setPlaybackToLocal(int stream) { MediaSessionCompatApi21.setPlaybackToLocal(mSessionObj, stream); } @Override public void setPlaybackToRemote(VolumeProviderCompat volumeProvider) { MediaSessionCompatApi21.setPlaybackToRemote(mSessionObj, volumeProvider.getVolumeProvider()); } @Override public void setActive(boolean active) { MediaSessionCompatApi21.setActive(mSessionObj, active); } @Override public boolean isActive() { return MediaSessionCompatApi21.isActive(mSessionObj); } @Override public void sendSessionEvent(String event, Bundle extras) { MediaSessionCompatApi21.sendSessionEvent(mSessionObj, event, extras); } @Override public void release() { MediaSessionCompatApi21.release(mSessionObj); } @Override public Token getSessionToken() { return mToken; } @Override public void setPlaybackState(PlaybackStateCompat state) { MediaSessionCompatApi21.setPlaybackState(mSessionObj, state.getPlaybackState()); } @Override public void setMetadata(MediaMetadataCompat metadata) { MediaSessionCompatApi21.setMetadata(mSessionObj, metadata.getMediaMetadata()); } @Override public Object getMediaSession() { return mSessionObj; } } }