diff options
24 files changed, 3159 insertions, 315 deletions
diff --git a/api/current.txt b/api/current.txt index 9df5c9e..eb6de96 100644 --- a/api/current.txt +++ b/api/current.txt @@ -13816,6 +13816,7 @@ package android.media { field public static final int RATING_4_STARS = 4; // 0x4 field public static final int RATING_5_STARS = 5; // 0x5 field public static final int RATING_HEART = 1; // 0x1 + field public static final int RATING_NONE = 0; // 0x0 field public static final int RATING_PERCENTAGE = 6; // 0x6 field public static final int RATING_THUMB_UP_DOWN = 2; // 0x2 } @@ -14456,34 +14457,76 @@ package android.media.effect { package android.media.session { public final class MediaController { - ctor public MediaController(android.media.session.MediaSessionToken); method public void addCallback(android.media.session.MediaController.Callback); method public void addCallback(android.media.session.MediaController.Callback, android.os.Handler); + method public static android.media.session.MediaController fromToken(android.media.session.MediaSessionToken); + method public android.media.session.TransportController getTransportController(); method public void removeCallback(android.media.session.MediaController.Callback); - method public void sendCommand(java.lang.String, android.os.Bundle); + method public void sendCommand(java.lang.String, android.os.Bundle, android.os.ResultReceiver); method public void sendMediaButton(int); } public static abstract class MediaController.Callback { ctor public MediaController.Callback(); method public void onEvent(java.lang.String, android.os.Bundle); - method public void onMetadataUpdate(android.os.Bundle); - method public void onPlaybackStateChange(int); method public void onRouteChanged(android.os.Bundle); } + public final class MediaMetadata implements android.os.Parcelable { + method public int describeContents(); + method public android.graphics.Bitmap getBitmap(java.lang.String); + method public long getLong(java.lang.String); + method public android.media.Rating getRating(java.lang.String); + method public java.lang.String getString(java.lang.String); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + field public static final java.lang.String METADATA_KEY_ALBUM = "android.media.metadata.ALBUM"; + field public static final java.lang.String METADATA_KEY_ALBUM_ART = "android.media.metadata.ALBUM_ART"; + field public static final java.lang.String METADATA_KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST"; + field public static final java.lang.String METADATA_KEY_ALBUM_ART_URI = "android.media.metadata.ALBUM_ART_URI"; + field public static final java.lang.String METADATA_KEY_ART = "android.media.metadata.ART"; + field public static final java.lang.String METADATA_KEY_ARTIST = "android.media.metadata.ARTIST"; + field public static final java.lang.String METADATA_KEY_ART_URI = "android.media.metadata.ART_URI"; + field public static final java.lang.String METADATA_KEY_AUTHOR = "android.media.metadata.AUTHOR"; + field public static final java.lang.String METADATA_KEY_COMPOSER = "android.media.metadata.COMPOSER"; + field public static final java.lang.String METADATA_KEY_DATE = "android.media.metadata.DATE"; + field public static final java.lang.String METADATA_KEY_DISC_NUMBER = "android.media.metadata.DISC_NUMBER"; + field public static final java.lang.String METADATA_KEY_DURATION = "android.media.metadata.DURATION"; + field public static final java.lang.String METADATA_KEY_GENRE = "android.media.metadata.GENRE"; + field public static final java.lang.String METADATA_KEY_NUM_TRACKS = "android.media.metadata.NUM_TRACKS"; + field public static final java.lang.String METADATA_KEY_RATING = "android.media.metadata.RATING"; + field public static final java.lang.String METADATA_KEY_TITLE = "android.media.metadata.TITLE"; + field public static final java.lang.String METADATA_KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER"; + field public static final java.lang.String METADATA_KEY_USER_RATING = "android.media.metadata.USER_RATING"; + field public static final java.lang.String METADATA_KEY_WRITER = "android.media.metadata.WRITER"; + field public static final java.lang.String METADATA_KEY_YEAR = "android.media.metadata.YEAR"; + } + + public static final class MediaMetadata.Builder { + ctor public MediaMetadata.Builder(); + ctor public MediaMetadata.Builder(android.media.session.MediaMetadata); + method public android.media.session.MediaMetadata build(); + method public android.media.session.MediaMetadata.Builder putBitmap(java.lang.String, android.graphics.Bitmap); + method public android.media.session.MediaMetadata.Builder putLong(java.lang.String, long); + method public android.media.session.MediaMetadata.Builder putRating(java.lang.String, android.media.Rating); + method public android.media.session.MediaMetadata.Builder putString(java.lang.String, java.lang.String); + } + public final class MediaSession { method public void addCallback(android.media.session.MediaSession.Callback); method public void addCallback(android.media.session.MediaSession.Callback, android.os.Handler); method public android.media.session.MediaSessionToken getSessionToken(); + method public android.media.session.TransportPerformer getTransportPerformer(); + method public void publish(); method public void release(); method public void removeCallback(android.media.session.MediaSession.Callback); - method public void setPlaybackState(int); + method public void sendEvent(java.lang.String, android.os.Bundle); + method public android.media.session.TransportPerformer setTransportPerformerEnabled(); } public static abstract class MediaSession.Callback { ctor public MediaSession.Callback(); - method public void onCommand(java.lang.String, android.os.Bundle); + method public void onCommand(java.lang.String, android.os.Bundle, android.os.ResultReceiver); method public void onMediaButton(android.content.Intent); method public void onRequestRouteChange(android.os.Bundle); } @@ -14499,6 +14542,137 @@ package android.media.session { field public static final android.os.Parcelable.Creator CREATOR; } + public final class PlaybackState implements android.os.Parcelable { + ctor public PlaybackState(); + ctor public PlaybackState(android.media.session.PlaybackState); + method public int describeContents(); + method public long getActions(); + method public long getBufferPosition(); + method public java.lang.String getErrorMessage(); + method public long getPosition(); + method public float getSpeed(); + method public int getState(); + method public void setActions(long); + method public void setBufferPosition(long); + method public void setErrorMessage(java.lang.String); + method public void setPosition(long); + method public void setSpeed(float); + method public void setState(int); + method public void writeToParcel(android.os.Parcel, int); + field public static final long ACTION_FASTFORWARD = 64L; // 0x40L + field public static final long ACTION_NEXT_ITEM = 32L; // 0x20L + field public static final long ACTION_PAUSE = 2L; // 0x2L + field public static final long ACTION_PLAY = 4L; // 0x4L + field public static final long ACTION_PREVIOUS_ITEM = 16L; // 0x10L + field public static final long ACTION_RATING = 128L; // 0x80L + field public static final long ACTION_REWIND = 8L; // 0x8L + field public static final long ACTION_SEEK_TO = 256L; // 0x100L + field public static final long ACTION_STOP = 1L; // 0x1L + field public static final android.os.Parcelable.Creator CREATOR; + field public static final int PLAYSTATE_BUFFERING = 6; // 0x6 + field public static final int PLAYSTATE_ERROR = 7; // 0x7 + field public static final int PLAYSTATE_FAST_FORWARDING = 4; // 0x4 + field public static final int PLAYSTATE_NONE = 0; // 0x0 + field public static final int PLAYSTATE_PAUSED = 2; // 0x2 + field public static final int PLAYSTATE_PLAYING = 3; // 0x3 + field public static final int PLAYSTATE_REWINDING = 5; // 0x5 + field public static final int PLAYSTATE_STOPPED = 1; // 0x1 + } + + public final class RouteInterface { + method public void addListener(android.media.session.RouteInterface.EventListener); + method public void addListener(android.media.session.RouteInterface.EventListener, android.os.Handler); + method public void removeListener(android.media.session.RouteInterface.EventListener); + method public void sendCommand(java.lang.String, android.os.Bundle, android.os.ResultReceiver); + } + + public static abstract class RouteInterface.EventListener { + ctor public RouteInterface.EventListener(); + method public abstract void onEvent(java.lang.String, android.os.Bundle); + } + + public static abstract class RouteInterface.Stub { + ctor public RouteInterface.Stub(); + method public abstract java.lang.String getName(); + method public abstract void onCommand(java.lang.String, android.os.Bundle, android.os.ResultReceiver); + method public final void sendEvent(android.media.session.MediaSession, java.lang.String, android.os.Bundle); + } + + public final class RouteTransportControls { + method public void addListener(android.media.session.RouteTransportControls.Listener); + method public void addListener(android.media.session.RouteTransportControls.Listener, android.os.Handler); + method public void fastForward(float); + method public static android.media.session.RouteTransportControls from(android.media.session.MediaController); + method public void getCapabilities(android.os.ResultReceiver); + method public void getCurrentPosition(android.os.ResultReceiver); + method public void pause(); + method public void play(); + method public void removeListener(android.media.session.RouteTransportControls.Listener); + field public static final java.lang.String NAME = "android.media.session.RouteTransportControls"; + } + + public static abstract class RouteTransportControls.Listener { + ctor public RouteTransportControls.Listener(); + method public void onMetadataUpdate(android.os.Bundle); + method public void onPlaybackStateChange(int); + } + + public static abstract class RouteTransportControls.Stub extends android.media.session.RouteInterface.Stub { + ctor public RouteTransportControls.Stub(android.media.session.MediaSession); + method public void fastForward(float); + method public long getCapabilities(); + method public long getCurrentPosition(); + method public java.lang.String getName(); + method public void onCommand(java.lang.String, android.os.Bundle, android.os.ResultReceiver); + method public final void updatePlaybackState(int); + } + + public final class TransportController { + method public void addStateListener(android.media.session.TransportController.TransportStateListener); + method public void addStateListener(android.media.session.TransportController.TransportStateListener, android.os.Handler); + method public void fastForward(); + method public android.media.session.MediaMetadata getMetadata(); + method public android.media.session.PlaybackState getPlaybackState(); + method public int getRatingType(); + method public void next(); + method public void pause(); + method public void play(); + method public void previous(); + method public void rate(android.media.Rating); + method public void removeStateListener(android.media.session.TransportController.TransportStateListener); + method public void rewind(); + method public void seekTo(long); + method public void stop(); + } + + public static abstract class TransportController.TransportStateListener { + ctor public TransportController.TransportStateListener(); + method public void onMetadataChanged(android.media.session.MediaMetadata); + method public void onPlaybackStateChanged(android.media.session.PlaybackState); + } + + public final class TransportPerformer { + method public void addListener(android.media.session.TransportPerformer.Listener); + method public void addListener(android.media.session.TransportPerformer.Listener, android.os.Handler); + method public void removeListener(android.media.session.TransportPerformer.Listener); + method public final void setMetadata(android.media.session.MediaMetadata); + method public final void setPlaybackState(android.media.session.PlaybackState); + } + + public static abstract class TransportPerformer.Listener { + ctor public TransportPerformer.Listener(); + method public void onFastForward(); + method public void onNext(); + method public void onPause(); + method public void onPlay(); + method public void onPrevious(); + method public void onRate(android.media.Rating); + method public void onRewind(); + method public void onRouteFocusChange(int); + method public void onSeekTo(long); + method public void onStop(); + } + } package android.mtp { diff --git a/media/java/android/media/Rating.java b/media/java/android/media/Rating.java index b94db18..f4fbe2c 100644 --- a/media/java/android/media/Rating.java +++ b/media/java/android/media/Rating.java @@ -29,8 +29,13 @@ import android.util.Log; * through one of the factory methods. */ public final class Rating implements Parcelable { - private final static String TAG = "Rating"; + /** + * Indicates a rating style is not supported. A Rating will never have this + * type, but can be used by other classes to indicate they do not support + * Rating. + */ + public final static int RATING_NONE = 0; /** * A rating style with a single degree of rating, "heart" vs "no heart". Can be used to diff --git a/media/java/android/media/session/IMediaController.aidl b/media/java/android/media/session/IMediaController.aidl index 8ca0e45..d34e973 100644 --- a/media/java/android/media/session/IMediaController.aidl +++ b/media/java/android/media/session/IMediaController.aidl @@ -16,9 +16,12 @@ package android.media.session; import android.content.Intent; +import android.media.Rating; import android.media.session.IMediaControllerCallback; +import android.media.session.MediaMetadata; +import android.media.session.PlaybackState; import android.os.Bundle; -import android.os.IBinder; +import android.os.ResultReceiver; import android.view.KeyEvent; /** @@ -26,9 +29,23 @@ import android.view.KeyEvent; * @hide */ interface IMediaController { - void sendCommand(String command, in Bundle extras); + void sendCommand(String command, in Bundle extras, in ResultReceiver cb); void sendMediaButton(in KeyEvent mediaButton); void registerCallbackListener(in IMediaControllerCallback cb); void unregisterCallbackListener(in IMediaControllerCallback cb); - int getPlaybackState(); + boolean isTransportControlEnabled(); + + // These commands are for the TransportController + void play(); + void pause(); + void stop(); + void next(); + void previous(); + void fastForward(); + void rewind(); + void seekTo(long pos); + void rate(in Rating rating); + MediaMetadata getMetadata(); + PlaybackState getPlaybackState(); + int getRatingType(); }
\ No newline at end of file diff --git a/media/java/android/media/session/IMediaControllerCallback.aidl b/media/java/android/media/session/IMediaControllerCallback.aidl index 3aa0ee4..3651f1b 100644 --- a/media/java/android/media/session/IMediaControllerCallback.aidl +++ b/media/java/android/media/session/IMediaControllerCallback.aidl @@ -15,6 +15,8 @@ package android.media.session; +import android.media.session.MediaMetadata; +import android.media.session.PlaybackState; import android.os.Bundle; /** @@ -22,7 +24,9 @@ import android.os.Bundle; */ oneway interface IMediaControllerCallback { void onEvent(String event, in Bundle extras); - void onMetadataUpdate(in Bundle metadata); - void onPlaybackUpdate(int newState); void onRouteChanged(in Bundle route); + + // These callbacks are for the TransportController + void onPlaybackStateChanged(in PlaybackState state); + void onMetadataChanged(in MediaMetadata metadata); }
\ No newline at end of file diff --git a/media/java/android/media/session/IMediaSession.aidl b/media/java/android/media/session/IMediaSession.aidl index 19f7092..aed7641 100644 --- a/media/java/android/media/session/IMediaSession.aidl +++ b/media/java/android/media/session/IMediaSession.aidl @@ -16,6 +16,8 @@ package android.media.session; import android.media.session.IMediaController; +import android.media.session.MediaMetadata; +import android.media.session.PlaybackState; import android.os.Bundle; /** @@ -23,11 +25,17 @@ import android.os.Bundle; * @hide */ interface IMediaSession { - void sendEvent(in Bundle data); - IMediaController getMediaSessionToken(); - void setPlaybackState(int state); - void setMetadata(in Bundle metadata); + void sendEvent(String event, in Bundle data); + IMediaController getMediaController(); + void setTransportPerformerEnabled(); void setRouteState(in Bundle routeState); void setRoute(in Bundle mediaRouteDescriptor); + List<String> getSupportedInterfaces(); + void publish(); void destroy(); + + // These commands are for the TransportPerformer + void setMetadata(in MediaMetadata metadata); + void setPlaybackState(in PlaybackState state); + void setRatingType(int type); }
\ No newline at end of file diff --git a/media/java/android/media/session/IMediaSessionCallback.aidl b/media/java/android/media/session/IMediaSessionCallback.aidl index eb5f222..7c183e0 100644 --- a/media/java/android/media/session/IMediaSessionCallback.aidl +++ b/media/java/android/media/session/IMediaSessionCallback.aidl @@ -15,15 +15,27 @@ package android.media.session; +import android.media.Rating; import android.content.Intent; import android.os.Bundle; -import android.os.IBinder; +import android.os.ResultReceiver; /** * @hide */ oneway interface IMediaSessionCallback { - void onCommand(String command, in Bundle extras); + void onCommand(String command, in Bundle extras, in ResultReceiver cb); void onMediaButton(in Intent mediaRequestIntent); void onRequestRouteChange(in Bundle route); + + // These callbacks are for the TransportPerformer + void onPlay(); + void onPause(); + void onStop(); + void onNext(); + void onPrevious(); + void onFastForward(); + void onRewind(); + void onSeekTo(long pos); + void onRate(in Rating rating); }
\ No newline at end of file diff --git a/media/java/android/media/session/MediaController.java b/media/java/android/media/session/MediaController.java index 09de859..afd8b11 100644 --- a/media/java/android/media/session/MediaController.java +++ b/media/java/android/media/session/MediaController.java @@ -16,20 +16,17 @@ package android.media.session; -import android.content.Intent; -import android.media.session.IMediaController; -import android.media.session.IMediaControllerCallback; -import android.media.MediaMetadataRetriever; -import android.media.RemoteControlClient; 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; /** @@ -46,58 +43,62 @@ import java.util.ArrayList; public final class MediaController { private static final String TAG = "MediaController"; - private static final int MESSAGE_EVENT = 1; + private static final int MSG_EVENT = 1; private static final int MESSAGE_PLAYBACK_STATE = 2; private static final int MESSAGE_METADATA = 3; - private static final int MESSAGE_ROUTE = 4; - - private static final String KEY_EVENT = "event"; - private static final String KEY_EXTRAS = "extras"; + private static final int MSG_ROUTE = 4; private final IMediaController mSessionBinder; - private final CallbackStub mCbStub = new CallbackStub(); - private final ArrayList<Callback> mCbs = new ArrayList<Callback>(); + 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 TransportController mTransportController; + + private MediaController(IMediaController sessionBinder) { + mSessionBinder = sessionBinder; + } + /** - * If you have a {@link MediaSessionToken} from the owner of the session a - * controller can be created directly. It is up to the session creator to - * handle token distribution if desired. - * - * @see MediaSession#getSessionToken() - * @param token A token from the creator of the session + * @hide */ - public MediaController(MediaSessionToken token) { - mSessionBinder = token.getBinder(); + public static MediaController fromBinder(IMediaController sessionBinder) { + MediaController controller = new MediaController(sessionBinder); + try { + controller.mSessionBinder.registerCallbackListener(controller.mCbStub); + if (controller.mSessionBinder.isTransportControlEnabled()) { + controller.mTransportController = new TransportController(sessionBinder); + } + } catch (RemoteException e) { + Log.wtf(TAG, "MediaController created with expired token", e); + controller = null; + } + return controller; } /** - * @hide + * Get a new MediaController for a MediaSessionToken. If successful the + * controller returned will be connected to the session that generated the + * token. + * + * @param token The session token to use + * @return A controller for the session or null */ - public MediaController(IMediaController sessionBinder) { - mSessionBinder = sessionBinder; + public static MediaController fromToken(MediaSessionToken token) { + return fromBinder(token.getBinder()); } /** - * 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. + * Get a TransportController if the session supports it. If it is not + * supported null will be returned. * - * @param command The command to send - * @param params Any parameters to include with the command + * @return A TransportController or null */ - public void sendCommand(String command, Bundle params) { - if (TextUtils.isEmpty(command)) { - throw new IllegalArgumentException("command cannot be null or empty"); - } - try { - mSessionBinder.sendCommand(command, params); - } catch (RemoteException e) { - Log.d(TAG, "Dead object in sendCommand.", e); - } + public TransportController getTransportController() { + return mTransportController; } /** @@ -133,10 +134,10 @@ public final class MediaController { /** * Adds a callback to receive updates from the session. Updates will be - * posted on the specified handler. + * posted on the specified handler's thread. * * @param cb Cannot be null. - * @param handler The handler to post updates on, if null the callers thread + * @param handler The handler to post updates on. If null the callers thread * will be used */ public void addCallback(Callback cb, Handler handler) { @@ -160,6 +161,26 @@ public final class MediaController { } } + /** + * 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 params Any parameters to include with the command + * @param cb The callback to receive the result on + */ + public void sendCommand(String command, Bundle params, ResultReceiver cb) { + if (TextUtils.isEmpty(command)) { + throw new IllegalArgumentException("command cannot be null or empty"); + } + try { + mSessionBinder.sendCommand(command, params, cb); + } catch (RemoteException e) { + Log.d(TAG, "Dead object in sendCommand.", e); + } + } + /* * @hide */ @@ -174,14 +195,13 @@ public final class MediaController { if (handler == null) { throw new IllegalArgumentException("Handler cannot be null"); } - if (mCbs.contains(cb)) { + if (getHandlerForCallbackLocked(cb) != null) { Log.w(TAG, "Callback is already added, ignoring"); return; } - cb.setHandler(handler); - mCbs.add(cb); + MessageHandler holder = new MessageHandler(handler.getLooper(), cb); + mCallbacks.add(holder); - // Only register one cb binder, track callbacks internally and notify if (!mCbRegistered) { try { mSessionBinder.registerCallbackListener(mCbStub); @@ -192,56 +212,58 @@ public final class MediaController { } } - private void removeCallbackLocked(Callback cb) { + private boolean removeCallbackLocked(Callback cb) { if (cb == null) { throw new IllegalArgumentException("Callback cannot be null"); } - mCbs.remove(cb); - - if (mCbs.size() == 0 && mCbRegistered) { - try { - mSessionBinder.unregisterCallbackListener(mCbStub); - } catch (RemoteException e) { - Log.d(TAG, "Dead object in unregisterCallback", e); + for (int i = mCallbacks.size() - 1; i >= 0; i--) { + MessageHandler handler = mCallbacks.get(i); + if (cb == handler.mCallback) { + mCallbacks.remove(i); + return true; } - mCbRegistered = false; } + return false; } - private void pushOnEventLocked(String event, Bundle extras) { - for (int i = mCbs.size() - 1; i >= 0; i--) { - mCbs.get(i).postEvent(event, extras); + private MessageHandler getHandlerForCallbackLocked(Callback cb) { + if (cb == null) { + throw new IllegalArgumentException("Callback cannot be null"); } - } - - private void pushOnMetadataUpdateLocked(Bundle metadata) { - for (int i = mCbs.size() - 1; i >= 0; i--) { - mCbs.get(i).postMetadataUpdate(metadata); + for (int i = mCallbacks.size() - 1; i >= 0; i--) { + MessageHandler handler = mCallbacks.get(i); + if (cb == handler.mCallback) { + return handler; + } } + return null; } - private void pushOnPlaybackUpdateLocked(int newState) { - for (int i = mCbs.size() - 1; i >= 0; i--) { - mCbs.get(i).postPlaybackStateChange(newState); + private void postEvent(String event, Bundle extras) { + synchronized (mLock) { + for (int i = mCallbacks.size() - 1; i >= 0; i--) { + mCallbacks.get(i).post(MSG_EVENT, event, extras); + } } } - private void pushOnRouteChangedLocked(Bundle routeDescriptor) { - for (int i = mCbs.size() - 1; i >= 0; i--) { - mCbs.get(i).postRouteChanged(routeDescriptor); + private void postRouteChanged(Bundle routeDescriptor) { + synchronized (mLock) { + for (int i = mCallbacks.size() - 1; i >= 0; i--) { + mCallbacks.get(i).post(MSG_ROUTE, null, routeDescriptor); + } } } /** - * MediaSession callbacks will be posted on the thread that created the - * Callback object. + * Callback for receiving updates on from the session. A Callback can be + * registered using {@link #addCallback} */ public static abstract class Callback { - private Handler mHandler; - /** - * Override to handle custom events sent by the session owner. - * Controllers should only handle these for sessions they own. + * 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 */ @@ -249,119 +271,83 @@ public final class MediaController { } /** - * Override to handle updates to the playback state. Valid values are in - * {@link RemoteControlClient}. TODO put playstate values somewhere more - * generic. - * - * @param state - */ - public void onPlaybackStateChange(int state) { - } - - /** - * Override to handle metadata changes for this session's media. The - * default supported fields are those in {@link MediaMetadataRetriever}. - * - * @param metadata - */ - public void onMetadataUpdate(Bundle metadata) { - } - - /** * Override to handle route changes for this session. * * @param route */ public void onRouteChanged(Bundle route) { } + } - private void setHandler(Handler handler) { - mHandler = new MessageHandler(handler.getLooper(), this); - } - - private void postEvent(String event, Bundle extras) { - Bundle eventBundle = new Bundle(); - eventBundle.putString(KEY_EVENT, event); - eventBundle.putBundle(KEY_EXTRAS, extras); - Message msg = mHandler.obtainMessage(MESSAGE_EVENT, eventBundle); - mHandler.sendMessage(msg); - } - - private void postPlaybackStateChange(final int state) { - Message msg = mHandler.obtainMessage(MESSAGE_PLAYBACK_STATE, state, 0); - mHandler.sendMessage(msg); - } - - private void postMetadataUpdate(final Bundle metadata) { - Message msg = mHandler.obtainMessage(MESSAGE_METADATA, metadata); - mHandler.sendMessage(msg); - } + private final static class CallbackStub extends IMediaControllerCallback.Stub { + private final WeakReference<MediaController> mController; - private void postRouteChanged(final Bundle descriptor) { - Message msg = mHandler.obtainMessage(MESSAGE_ROUTE, descriptor); - mHandler.sendMessage(msg); + public CallbackStub(MediaController controller) { + mController = new WeakReference<MediaController>(controller); } - } - - private final class CallbackStub extends IMediaControllerCallback.Stub { @Override - public void onEvent(String event, Bundle extras) throws RemoteException { - synchronized (mLock) { - pushOnEventLocked(event, extras); + public void onEvent(String event, Bundle extras) { + MediaController controller = mController.get(); + if (controller != null) { + controller.postEvent(event, extras); } } @Override - public void onMetadataUpdate(Bundle metadata) throws RemoteException { - synchronized (mLock) { - pushOnMetadataUpdateLocked(metadata); + public void onRouteChanged(Bundle mediaRouteDescriptor) { + MediaController controller = mController.get(); + if (controller != null) { + controller.postRouteChanged(mediaRouteDescriptor); } } @Override - public void onPlaybackUpdate(final int newState) throws RemoteException { - synchronized (mLock) { - pushOnPlaybackUpdateLocked(newState); + public void onPlaybackStateChanged(PlaybackState state) { + MediaController controller = mController.get(); + if (controller != null) { + TransportController tc = controller.getTransportController(); + if (tc != null) { + tc.postPlaybackStateChanged(state); + } } } @Override - public void onRouteChanged(Bundle mediaRouteDescriptor) throws RemoteException { - synchronized (mLock) { - pushOnRouteChangedLocked(mediaRouteDescriptor); + public void onMetadataChanged(MediaMetadata metadata) { + MediaController controller = mController.get(); + if (controller != null) { + TransportController tc = controller.getTransportController(); + if (tc != null) { + tc.postMetadataChanged(metadata); + } } } } private final static class MessageHandler extends Handler { - private final MediaController.Callback mCb; + private final MediaController.Callback mCallback; public MessageHandler(Looper looper, MediaController.Callback cb) { - super(looper); - mCb = cb; + super(looper, null, true); + mCallback = cb; } @Override public void handleMessage(Message msg) { switch (msg.what) { - case MESSAGE_EVENT: - Bundle eventBundle = (Bundle) msg.obj; - String event = eventBundle.getString(KEY_EVENT); - Bundle extras = eventBundle.getBundle(KEY_EXTRAS); - mCb.onEvent(event, extras); - break; - case MESSAGE_PLAYBACK_STATE: - mCb.onPlaybackStateChange(msg.arg1); - break; - case MESSAGE_METADATA: - mCb.onMetadataUpdate((Bundle) msg.obj); + case MSG_EVENT: + mCallback.onEvent((String) msg.obj, msg.getData()); break; - case MESSAGE_ROUTE: - mCb.onRouteChanged((Bundle) msg.obj); + case MSG_ROUTE: + mCallback.onRouteChanged(msg.getData()); } } + + public void post(int what, Object obj, Bundle data) { + obtainMessage(what, obj).sendToTarget(); + } } } diff --git a/media/java/android/media/session/MediaMetadata.aidl b/media/java/android/media/session/MediaMetadata.aidl new file mode 100644 index 0000000..4431d9d --- /dev/null +++ b/media/java/android/media/session/MediaMetadata.aidl @@ -0,0 +1,18 @@ +/* Copyright 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; + +parcelable MediaMetadata; diff --git a/media/java/android/media/session/MediaMetadata.java b/media/java/android/media/session/MediaMetadata.java new file mode 100644 index 0000000..e2330f7 --- /dev/null +++ b/media/java/android/media/session/MediaMetadata.java @@ -0,0 +1,404 @@ +/* + * 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.graphics.Bitmap; +import android.media.Rating; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.ArrayMap; +import android.util.Log; + +/** + * Contains metadata about an item, such as the title, artist, etc. + */ +public final class MediaMetadata implements Parcelable { + private static final String TAG = "MediaMetadata"; + + /** + * The title of the media. + */ + public static final String METADATA_KEY_TITLE = "android.media.metadata.TITLE"; + + /** + * The artist of the media. + */ + public static final String METADATA_KEY_ARTIST = "android.media.metadata.ARTIST"; + + /** + * The duration of the media in ms. A duration of 0 is the default. + */ + public static final String METADATA_KEY_DURATION = "android.media.metadata.DURATION"; + + /** + * The album title for the media. + */ + public static final String METADATA_KEY_ALBUM = "android.media.metadata.ALBUM"; + + /** + * The author of the media. + */ + public static final String METADATA_KEY_AUTHOR = "android.media.metadata.AUTHOR"; + + /** + * The writer of the media. + */ + public static final String METADATA_KEY_WRITER = "android.media.metadata.WRITER"; + + /** + * The composer of the media. + */ + public static final String METADATA_KEY_COMPOSER = "android.media.metadata.COMPOSER"; + + /** + * The date the media was created or published as TODO determine format. + */ + public static final String METADATA_KEY_DATE = "android.media.metadata.DATE"; + + /** + * The year the media was created or published as a numeric String. + */ + public static final String METADATA_KEY_YEAR = "android.media.metadata.YEAR"; + + /** + * The genre of the media. + */ + public static final String METADATA_KEY_GENRE = "android.media.metadata.GENRE"; + + /** + * The track number for the media. + */ + public static final String METADATA_KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER"; + + /** + * The number of tracks in the media's original source. + */ + public static final String METADATA_KEY_NUM_TRACKS = "android.media.metadata.NUM_TRACKS"; + + /** + * The disc number for the media's original source. + */ + public static final String METADATA_KEY_DISC_NUMBER = "android.media.metadata.DISC_NUMBER"; + + /** + * The artist for the album of the media's original source. + */ + public static final String METADATA_KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST"; + + /** + * The artwork for the media as a {@link Bitmap}. + */ + public static final String METADATA_KEY_ART = "android.media.metadata.ART"; + + /** + * The artwork for the media as a Uri style String. + */ + public static final String METADATA_KEY_ART_URI = "android.media.metadata.ART_URI"; + + /** + * The artwork for the album of the media's original source as a + * {@link Bitmap}. + */ + public static final String METADATA_KEY_ALBUM_ART = "android.media.metadata.ALBUM_ART"; + + /** + * The artwork for the album of the media's original source as a Uri style + * String. + */ + public static final String METADATA_KEY_ALBUM_ART_URI = "android.media.metadata.ALBUM_ART_URI"; + + /** + * The user's rating for the media. + * + * @see Rating + */ + public static final String METADATA_KEY_USER_RATING = "android.media.metadata.USER_RATING"; + + /** + * The overall rating for the media. + * + * @see Rating + */ + public static final String METADATA_KEY_RATING = "android.media.metadata.RATING"; + + private static final int METADATA_TYPE_INVALID = -1; + private static final int METADATA_TYPE_LONG = 0; + private static final int METADATA_TYPE_STRING = 1; + private static final int METADATA_TYPE_BITMAP = 2; + private static final int METADATA_TYPE_RATING = 3; + private static final ArrayMap<String, Integer> METADATA_KEYS_TYPE; + + static { + METADATA_KEYS_TYPE = new ArrayMap<String, Integer>(); + METADATA_KEYS_TYPE.put(METADATA_KEY_TITLE, METADATA_TYPE_STRING); + METADATA_KEYS_TYPE.put(METADATA_KEY_ARTIST, METADATA_TYPE_STRING); + METADATA_KEYS_TYPE.put(METADATA_KEY_DURATION, METADATA_TYPE_LONG); + METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM, METADATA_TYPE_STRING); + METADATA_KEYS_TYPE.put(METADATA_KEY_AUTHOR, METADATA_TYPE_STRING); + METADATA_KEYS_TYPE.put(METADATA_KEY_WRITER, METADATA_TYPE_STRING); + METADATA_KEYS_TYPE.put(METADATA_KEY_COMPOSER, METADATA_TYPE_STRING); + METADATA_KEYS_TYPE.put(METADATA_KEY_DATE, METADATA_TYPE_STRING); + METADATA_KEYS_TYPE.put(METADATA_KEY_YEAR, METADATA_TYPE_STRING); + METADATA_KEYS_TYPE.put(METADATA_KEY_GENRE, METADATA_TYPE_STRING); + METADATA_KEYS_TYPE.put(METADATA_KEY_TRACK_NUMBER, METADATA_TYPE_LONG); + METADATA_KEYS_TYPE.put(METADATA_KEY_NUM_TRACKS, METADATA_TYPE_LONG); + METADATA_KEYS_TYPE.put(METADATA_KEY_DISC_NUMBER, METADATA_TYPE_LONG); + METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ARTIST, METADATA_TYPE_STRING); + METADATA_KEYS_TYPE.put(METADATA_KEY_ART, METADATA_TYPE_BITMAP); + METADATA_KEYS_TYPE.put(METADATA_KEY_ART_URI, METADATA_TYPE_STRING); + METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ART, METADATA_TYPE_BITMAP); + METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ART_URI, METADATA_TYPE_STRING); + METADATA_KEYS_TYPE.put(METADATA_KEY_USER_RATING, METADATA_TYPE_RATING); + METADATA_KEYS_TYPE.put(METADATA_KEY_RATING, METADATA_TYPE_RATING); + } + private final Bundle mBundle; + + private MediaMetadata(Bundle bundle) { + mBundle = new Bundle(bundle); + } + + private MediaMetadata(Parcel in) { + mBundle = in.readBundle(); + } + + /** + * Returns the value associated with the given key, or null if no mapping of + * the desired type exists for the given key or a null value is explicitly + * associated with the key. + * + * @param key The key the value is stored under + * @return a String value, or null + */ + public String getString(String key) { + return mBundle.getString(key); + } + + /** + * Returns the value associated with the given key, or 0L if no long exists + * for the given key. + * + * @param key The key the value is stored under + * @return a long value + */ + public long getLong(String key) { + return mBundle.getLong(key); + } + + /** + * Return a {@link Rating} for the given key or null if no rating exists for + * the given key. + * + * @param key The key the value is stored under + * @return A {@link Rating} or null + */ + public Rating getRating(String key) { + Rating rating = null; + try { + rating = mBundle.getParcelable(key); + } catch (Exception e) { + // ignore, value was not a bitmap + Log.d(TAG, "Failed to retrieve a key as Rating.", e); + } + return rating; + } + + /** + * Return a {@link Bitmap} for the given key or null if no bitmap exists for + * the given key. + * + * @param key The key the value is stored under + * @return A {@link Bitmap} or null + */ + public Bitmap getBitmap(String key) { + Bitmap bmp = null; + try { + bmp = mBundle.getParcelable(key); + } catch (Exception e) { + // ignore, value was not a bitmap + Log.d(TAG, "Failed to retrieve a key as Bitmap.", e); + } + return bmp; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeBundle(mBundle); + } + + public static final Parcelable.Creator<MediaMetadata> CREATOR + = new Parcelable.Creator<MediaMetadata>() { + @Override + public MediaMetadata createFromParcel(Parcel in) { + return new MediaMetadata(in); + } + + @Override + public MediaMetadata[] newArray(int size) { + return new MediaMetadata[size]; + } + }; + + /** + * Use to build MediaMetadata objects. The system defined metadata keys must + * use the appropriate data type. + */ + public static final class Builder { + private final Bundle mBundle; + + /** + * Create an empty Builder. Any field that should be included in the + * {@link MediaMetadata} must be added. + */ + public Builder() { + mBundle = new Bundle(); + } + + /** + * Create a Builder using a {@link MediaMetadata} instance to set the + * initial values. All fields in the source metadata will be included in + * the new metadata. Fields can be overwritten by adding the same key. + * + * @param source + */ + public Builder(MediaMetadata source) { + mBundle = new Bundle(source.mBundle); + } + + /** + * Put a String value into the metadata. Custom keys may be used, but if + * the METADATA_KEYs defined in this class are used they may only be one + * of the following: + * <ul> + * <li>{@link #METADATA_KEY_TITLE}</li> + * <li>{@link #METADATA_KEY_ARTIST}</li> + * <li>{@link #METADATA_KEY_ALBUM}</li> + * <li>{@link #METADATA_KEY_AUTHOR}</li> + * <li>{@link #METADATA_KEY_WRITER}</li> + * <li>{@link #METADATA_KEY_COMPOSER}</li> + * <li>{@link #METADATA_KEY_DATE}</li> + * <li>{@link #METADATA_KEY_YEAR}</li> + * <li>{@link #METADATA_KEY_GENRE}</li> + * <li>{@link #METADATA_KEY_ALBUM_ARTIST}</li>li> + * <li>{@link #METADATA_KEY_ART_URI}</li>li> + * <li>{@link #METADATA_KEY_ALBUM_ART_URI}</li> + * </ul> + * + * @param key The key for referencing this value + * @param value The String value to store + * @return The Builder to allow chaining + */ + public Builder putString(String key, String value) { + if (METADATA_KEYS_TYPE.containsKey(key)) { + if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_STRING) { + throw new IllegalArgumentException("The " + key + + " key cannot be used to put a String"); + } + } + mBundle.putString(key, value); + return this; + } + + /** + * Put a long value into the metadata. Custom keys may be used, but if + * the METADATA_KEYs defined in this class are used they may only be one + * of the following: + * <ul> + * <li>{@link #METADATA_KEY_DURATION}</li> + * <li>{@link #METADATA_KEY_TRACK_NUMBER}</li> + * <li>{@link #METADATA_KEY_NUM_TRACKS}</li> + * <li>{@link #METADATA_KEY_DISC_NUMBER}</li> + * </ul> + * + * @param key The key for referencing this value + * @param value The String value to store + * @return The Builder to allow chaining + */ + public Builder putLong(String key, long value) { + if (METADATA_KEYS_TYPE.containsKey(key)) { + if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_LONG) { + throw new IllegalArgumentException("The " + key + + " key cannot be used to put a long"); + } + } + mBundle.putLong(key, value); + return this; + } + + /** + * Put a {@link Rating} into the metadata. Custom keys may be used, but + * if the METADATA_KEYs defined in this class are used they may only be + * one of the following: + * <ul> + * <li>{@link #METADATA_KEY_RATING}</li> + * <li>{@link #METADATA_KEY_USER_RATING}</li> + * </ul> + * + * @param key The key for referencing this value + * @param value The String value to store + * @return The Builder to allow chaining + */ + public Builder putRating(String key, Rating value) { + if (METADATA_KEYS_TYPE.containsKey(key)) { + if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_RATING) { + throw new IllegalArgumentException("The " + key + + " key cannot be used to put a Rating"); + } + } + mBundle.putParcelable(key, value); + return this; + } + + /** + * Put a {@link Bitmap} into the metadata. Custom keys may be used, but + * if the METADATA_KEYs defined in this class are used they may only be + * one of the following: + * <ul> + * <li>{@link #METADATA_KEY_ART}</li> + * <li>{@link #METADATA_KEY_ALBUM_ART}</li> + * </ul> + * + * @param key The key for referencing this value + * @param value The Bitmap to store + * @return The Builder to allow chaining + */ + public Builder putBitmap(String key, Bitmap value) { + if (METADATA_KEYS_TYPE.containsKey(key)) { + if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_BITMAP) { + throw new IllegalArgumentException("The " + key + + " key cannot be used to put a Bitmap"); + } + } + mBundle.putParcelable(key, value); + return this; + } + + /** + * Creates a {@link MediaMetadata} instance with the specified fields. + * + * @return The new MediaMetadata instance + */ + public MediaMetadata build() { + return new MediaMetadata(mBundle); + } + } + +} diff --git a/media/java/android/media/session/MediaSession.java b/media/java/android/media/session/MediaSession.java index 1f1533b..23c3035 100644 --- a/media/java/android/media/session/MediaSession.java +++ b/media/java/android/media/session/MediaSession.java @@ -17,17 +17,21 @@ package android.media.session; import android.content.Intent; +import android.media.Rating; import android.media.session.IMediaController; import android.media.session.IMediaSession; import android.media.session.IMediaSessionCallback; -import android.media.RemoteControlClient; 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.ArrayMap; import android.util.Log; +import java.lang.ref.WeakReference; import java.util.ArrayList; /** @@ -58,12 +62,13 @@ import java.util.ArrayList; public final class MediaSession { private static final String TAG = "MediaSession"; - private static final int MESSAGE_MEDIA_BUTTON = 1; - private static final int MESSAGE_COMMAND = 2; - private static final int MESSAGE_ROUTE_CHANGE = 3; + private static final int MSG_MEDIA_BUTTON = 1; + private static final int MSG_COMMAND = 2; + private static final int MSG_ROUTE_CHANGE = 3; private static final String KEY_COMMAND = "command"; private static final String KEY_EXTRAS = "extras"; + private static final String KEY_CALLBACK = "callback"; private final Object mLock = new Object(); @@ -71,7 +76,14 @@ public final class MediaSession { private final IMediaSession mBinder; private final CallbackStub mCbStub; - private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>(); + private final ArrayList<MessageHandler> mCallbacks = new ArrayList<MessageHandler>(); + // TODO route interfaces + private final ArrayMap<String, RouteInterface.Stub> mInterfaces + = new ArrayMap<String, RouteInterface.Stub>(); + + private TransportPerformer mPerformer; + + private boolean mPublished = false;; /** * @hide @@ -81,7 +93,7 @@ public final class MediaSession { mCbStub = cbStub; IMediaController controllerBinder = null; try { - controllerBinder = mBinder.getMediaSessionToken(); + controllerBinder = mBinder.getMediaController(); } catch (RemoteException e) { throw new RuntimeException("Dead object in MediaSessionController constructor: ", e); } @@ -102,34 +114,117 @@ public final class MediaSession { throw new IllegalArgumentException("Callback cannot be null"); } synchronized (mLock) { - if (mCallbacks.contains(callback)) { + if (getHandlerForCallbackLocked(callback) != null) { Log.w(TAG, "Callback is already added, ignoring"); + return; } if (handler == null) { handler = new Handler(); } MessageHandler msgHandler = new MessageHandler(handler.getLooper(), callback); - callback.setHandler(msgHandler); - mCallbacks.add(callback); + mCallbacks.add(msgHandler); } } public void removeCallback(Callback callback) { - mCallbacks.remove(callback); + synchronized (mLock) { + removeCallbackLocked(callback); + } + } + + /** + * Start using a TransportPerformer with this media session. This must be + * called before calling publish and cannot be called more than once. + * Calling this will allow MediaControllers to retrieve a + * TransportController. + * + * @see TransportController + * @return The TransportPerformer created for this session + */ + public TransportPerformer setTransportPerformerEnabled() { + if (mPerformer != null) { + throw new IllegalStateException("setTransportPerformer can only be called once."); + } + if (mPublished) { + throw new IllegalStateException("setTransportPerformer cannot be called after publish"); + } + + mPerformer = new TransportPerformer(mBinder); + try { + mBinder.setTransportPerformerEnabled(); + } catch (RemoteException e) { + Log.wtf(TAG, "Failure in setTransportPerformerEnabled.", e); + } + return mPerformer; + } + + /** + * Retrieves the TransportPerformer used by this session. If called before + * {@link #setTransportPerformerEnabled} null will be returned. + * + * @return The TransportPerformer associated with this session or null + */ + public TransportPerformer getTransportPerformer() { + return mPerformer; + } + + /** + * Call after you have finished setting up the session. This will make it + * available to listeners and begin pushing updates to MediaControllers. + * This can only be called once. + */ + public void publish() { + if (mPublished) { + throw new RuntimeException("publish() may only be called once."); + } + try { + mBinder.publish(); + } catch (RemoteException e) { + Log.wtf(TAG, "Failure in publish.", e); + } + mPublished = true; + } + + /** + * Add an interface that can be used by MediaSessions. TODO make this a + * route provider api + * + * @see RouteInterface + * @param iface The interface to add + * @hide + */ + public void addInterface(RouteInterface.Stub iface) { + if (iface == null) { + throw new IllegalArgumentException("Stub cannot be null"); + } + String name = iface.getName(); + if (TextUtils.isEmpty(name)) { + throw new IllegalArgumentException("Stub must return a valid name"); + } + if (mInterfaces.containsKey(iface)) { + throw new IllegalArgumentException("Interface is already added"); + } + synchronized (mLock) { + mInterfaces.put(iface.getName(), iface); + } } /** - * Publish the current playback state to the system and any controllers. - * Valid values are defined in {@link RemoteControlClient}. TODO move play - * states somewhere else. + * 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 state + * @param event The name of the event to send + * @param extras Any extras included with the event */ - public void setPlaybackState(int state) { + public void sendEvent(String event, Bundle extras) { + if (TextUtils.isEmpty(event)) { + throw new IllegalArgumentException("event cannot be null or empty"); + } try { - mBinder.setPlaybackState(state); + mBinder.sendEvent(event, extras); } catch (RemoteException e) { - Log.e(TAG, "Dead object in setPlaybackState: ", e); + Log.wtf(TAG, "Error sending event", e); } } @@ -142,7 +237,7 @@ public final class MediaSession { try { mBinder.destroy(); } catch (RemoteException e) { - Log.e(TAG, "Dead object in onDestroy: ", e); + Log.wtf(TAG, "Error releasing session: ", e); } } @@ -158,15 +253,38 @@ public final class MediaSession { return mSessionToken; } - private void postCommand(String command, Bundle extras) { - Bundle commandBundle = new Bundle(); - commandBundle.putString(KEY_COMMAND, command); - commandBundle.putBundle(KEY_EXTRAS, extras); + 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 boolean removeCallbackLocked(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) { + mCallbacks.remove(i); + return true; + } + } + return false; + } + + private void postCommand(String command, Bundle extras, ResultReceiver resultCb) { + Command cmd = new Command(command, extras, resultCb); synchronized (mLock) { for (int i = mCallbacks.size() - 1; i >= 0; i--) { - Callback cb = mCallbacks.get(i); - Message msg = cb.mHandler.obtainMessage(MESSAGE_COMMAND, commandBundle); - cb.mHandler.sendMessage(msg); + mCallbacks.get(i).post(MSG_COMMAND, cmd); } } } @@ -174,9 +292,7 @@ public final class MediaSession { private void postMediaButton(Intent mediaButtonIntent) { synchronized (mLock) { for (int i = mCallbacks.size() - 1; i >= 0; i--) { - Callback cb = mCallbacks.get(i); - Message msg = cb.mHandler.obtainMessage(MESSAGE_MEDIA_BUTTON, mediaButtonIntent); - cb.mHandler.sendMessage(msg); + mCallbacks.get(i).post(MSG_MEDIA_BUTTON, mediaButtonIntent); } } } @@ -184,9 +300,7 @@ public final class MediaSession { private void postRequestRouteChange(Bundle mediaRouteDescriptor) { synchronized (mLock) { for (int i = mCallbacks.size() - 1; i >= 0; i--) { - Callback cb = mCallbacks.get(i); - Message msg = cb.mHandler.obtainMessage(MESSAGE_ROUTE_CHANGE, mediaRouteDescriptor); - cb.mHandler.sendMessage(msg); + mCallbacks.get(i).post(MSG_ROUTE_CHANGE, mediaRouteDescriptor); } } } @@ -197,7 +311,6 @@ public final class MediaSession { * MediaSession (TODO). */ public abstract static class Callback { - private MessageHandler mHandler; public Callback() { } @@ -225,7 +338,7 @@ public final class MediaSession { * @param command * @param extras optional */ - public void onCommand(String command, Bundle extras) { + public void onCommand(String command, Bundle extras, ResultReceiver cb) { } /** @@ -237,35 +350,140 @@ public final class MediaSession { */ public void onRequestRouteChange(Bundle descriptor) { } - - private void setHandler(MessageHandler handler) { - mHandler = handler; - } } /** * @hide */ public static class CallbackStub extends IMediaSessionCallback.Stub { - private MediaSession mMediaSession; + private WeakReference<MediaSession> mMediaSession; public void setMediaSession(MediaSession session) { - mMediaSession = session; + mMediaSession = new WeakReference<MediaSession>(session); } @Override - public void onCommand(String command, Bundle extras) throws RemoteException { - mMediaSession.postCommand(command, extras); + public void onCommand(String command, Bundle extras, ResultReceiver cb) + throws RemoteException { + MediaSession session = mMediaSession.get(); + if (session != null) { + session.postCommand(command, extras, cb); + } } @Override public void onMediaButton(Intent mediaButtonIntent) throws RemoteException { - mMediaSession.postMediaButton(mediaButtonIntent); + MediaSession session = mMediaSession.get(); + if (session != null) { + session.postMediaButton(mediaButtonIntent); + } } @Override public void onRequestRouteChange(Bundle mediaRouteDescriptor) throws RemoteException { - mMediaSession.postRequestRouteChange(mediaRouteDescriptor); + MediaSession session = mMediaSession.get(); + if (session != null) { + session.postRequestRouteChange(mediaRouteDescriptor); + } + } + + @Override + public void onPlay() throws RemoteException { + MediaSession session = mMediaSession.get(); + if (session != null) { + TransportPerformer tp = session.getTransportPerformer(); + if (tp != null) { + tp.onPlay(); + } + } + } + + @Override + public void onPause() throws RemoteException { + MediaSession session = mMediaSession.get(); + if (session != null) { + TransportPerformer tp = session.getTransportPerformer(); + if (tp != null) { + tp.onPause(); + } + } + } + + @Override + public void onStop() throws RemoteException { + MediaSession session = mMediaSession.get(); + if (session != null) { + TransportPerformer tp = session.getTransportPerformer(); + if (tp != null) { + tp.onStop(); + } + } + } + + @Override + public void onNext() throws RemoteException { + MediaSession session = mMediaSession.get(); + if (session != null) { + TransportPerformer tp = session.getTransportPerformer(); + if (tp != null) { + tp.onNext(); + } + } + } + + @Override + public void onPrevious() throws RemoteException { + MediaSession session = mMediaSession.get(); + if (session != null) { + TransportPerformer tp = session.getTransportPerformer(); + if (tp != null) { + tp.onPrevious(); + } + } + } + + @Override + public void onFastForward() throws RemoteException { + MediaSession session = mMediaSession.get(); + if (session != null) { + TransportPerformer tp = session.getTransportPerformer(); + if (tp != null) { + tp.onFastForward(); + } + } + } + + @Override + public void onRewind() throws RemoteException { + MediaSession session = mMediaSession.get(); + if (session != null) { + TransportPerformer tp = session.getTransportPerformer(); + if (tp != null) { + tp.onRewind(); + } + } + } + + @Override + public void onSeekTo(long pos) throws RemoteException { + MediaSession session = mMediaSession.get(); + if (session != null) { + TransportPerformer tp = session.getTransportPerformer(); + if (tp != null) { + tp.onSeekTo(pos); + } + } + } + + @Override + public void onRate(Rating rating) throws RemoteException { + MediaSession session = mMediaSession.get(); + if (session != null) { + TransportPerformer tp = session.getTransportPerformer(); + if (tp != null) { + tp.onRate(rating); + } + } } } @@ -274,7 +492,7 @@ public final class MediaSession { private MediaSession.Callback mCallback; public MessageHandler(Looper looper, MediaSession.Callback callback) { - super(looper); + super(looper, null, true); mCallback = callback; } @@ -285,21 +503,35 @@ public final class MediaSession { return; } switch (msg.what) { - case MESSAGE_MEDIA_BUTTON: + case MSG_MEDIA_BUTTON: mCallback.onMediaButton((Intent) msg.obj); break; - case MESSAGE_COMMAND: - Bundle commandBundle = (Bundle) msg.obj; - String command = commandBundle.getString(KEY_COMMAND); - Bundle extras = commandBundle.getBundle(KEY_EXTRAS); - mCallback.onCommand(command, extras); + case MSG_COMMAND: + Command cmd = (Command) msg.obj; + mCallback.onCommand(cmd.command, cmd.extras, cmd.stub); break; - case MESSAGE_ROUTE_CHANGE: + case MSG_ROUTE_CHANGE: mCallback.onRequestRouteChange((Bundle) msg.obj); break; } } msg.recycle(); } + + public void post(int what, Object obj) { + obtainMessage(what, obj).sendToTarget(); + } + } + + 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; + } } } diff --git a/media/java/android/media/session/PlaybackState.aidl b/media/java/android/media/session/PlaybackState.aidl new file mode 100644 index 0000000..0876ebd --- /dev/null +++ b/media/java/android/media/session/PlaybackState.aidl @@ -0,0 +1,18 @@ +/* Copyright 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; + +parcelable PlaybackState; diff --git a/media/java/android/media/session/PlaybackState.java b/media/java/android/media/session/PlaybackState.java new file mode 100644 index 0000000..b3506b3 --- /dev/null +++ b/media/java/android/media/session/PlaybackState.java @@ -0,0 +1,351 @@ +/* + * 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.media.RemoteControlClient; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Playback state for a {@link MediaSession}. This includes a state like + * {@link PlaybackState#PLAYSTATE_PLAYING}, the current playback position, + * and the current control capabilities. + */ +public final class PlaybackState implements Parcelable { + /** + * Indicates this performer supports the stop command. + * + * @see #setActions + */ + public static final long ACTION_STOP = 1 << 0; + + /** + * Indicates this performer supports the pause command. + * + * @see #setActions + */ + public static final long ACTION_PAUSE = 1 << 1; + + /** + * Indicates this performer supports the play command. + * + * @see #setActions + */ + public static final long ACTION_PLAY = 1 << 2; + + /** + * Indicates this performer supports the rewind command. + * + * @see #setActions + */ + public static final long ACTION_REWIND = 1 << 3; + + /** + * Indicates this performer supports the previous command. + * + * @see #setActions + */ + public static final long ACTION_PREVIOUS_ITEM = 1 << 4; + + /** + * Indicates this performer supports the next command. + * + * @see #setActions + */ + public static final long ACTION_NEXT_ITEM = 1 << 5; + + /** + * Indicates this performer supports the fast forward command. + * + * @see #setActions + */ + public static final long ACTION_FASTFORWARD = 1 << 6; + + /** + * Indicates this performer supports the set rating command. + * + * @see #setActions + */ + public static final long ACTION_RATING = 1 << 7; + + /** + * Indicates this performer supports the seek to command. + * + * @see #setActions + */ + public static final long ACTION_SEEK_TO = 1 << 8; + + /** + * This is the default playback state and indicates that no media has been + * added yet, or the performer has been reset and has no content to play. + * + * @see #setState + */ + public final static int PLAYSTATE_NONE = 0; + + /** + * State indicating this item is currently stopped. + * + * @see #setState + */ + public final static int PLAYSTATE_STOPPED = 1; + + /** + * State indicating this item is currently paused. + * + * @see #setState + */ + public final static int PLAYSTATE_PAUSED = 2; + + /** + * State indicating this item is currently playing. + * + * @see #setState + */ + public final static int PLAYSTATE_PLAYING = 3; + + /** + * State indicating this item is currently fast forwarding. + * + * @see #setState + */ + public final static int PLAYSTATE_FAST_FORWARDING = 4; + + /** + * State indicating this item is currently rewinding. + * + * @see #setState + */ + public final static int PLAYSTATE_REWINDING = 5; + + /** + * State indicating this item is currently buffering and will begin playing + * when enough data has buffered. + * + * @see #setState + */ + public final static int PLAYSTATE_BUFFERING = 6; + + /** + * State indicating this item is currently in an error state. The error + * message should also be set when entering this state. + * + * @see #setState + */ + public final static int PLAYSTATE_ERROR = 7; + + private int mState; + private long mPosition; + private long mBufferPosition; + private float mSpeed; + private long mCapabilities; + private String mErrorMessage; + + /** + * Create an empty PlaybackState. At minimum a state and actions should be + * set before publishing a PlaybackState. + */ + public PlaybackState() { + } + + /** + * Create a new PlaybackState from an existing PlaybackState. All fields + * will be copied to the new state. + * + * @param from The PlaybackState to duplicate + */ + public PlaybackState(PlaybackState from) { + this.setState(from.getState()); + this.setPosition(from.getPosition()); + this.setBufferPosition(from.getBufferPosition()); + this.setSpeed(from.getSpeed()); + this.setActions(from.getActions()); + this.setErrorMessage(from.getErrorMessage()); + } + + private PlaybackState(Parcel in) { + this.setState(in.readInt()); + this.setPosition(in.readLong()); + this.setBufferPosition(in.readLong()); + this.setSpeed(in.readFloat()); + this.setActions(in.readLong()); + this.setErrorMessage(in.readString()); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(getState()); + dest.writeLong(getPosition()); + dest.writeLong(getBufferPosition()); + dest.writeFloat(getSpeed()); + dest.writeLong(getActions()); + dest.writeString(getErrorMessage()); + } + + /** + * Get the current state of playback. One of the following: + * <ul> + * <li> {@link PlaybackState#PLAYSTATE_NONE}</li> + * <li> {@link PlaybackState#PLAYSTATE_STOPPED}</li> + * <li> {@link PlaybackState#PLAYSTATE_PLAYING}</li> + * <li> {@link PlaybackState#PLAYSTATE_PAUSED}</li> + * <li> {@link PlaybackState#PLAYSTATE_FAST_FORWARDING}</li> + * <li> {@link PlaybackState#PLAYSTATE_REWINDING}</li> + * <li> {@link PlaybackState#PLAYSTATE_BUFFERING}</li> + * <li> {@link PlaybackState#PLAYSTATE_ERROR}</li> + */ + public int getState() { + return mState; + } + + /** + * Set the current state of playback. One of the following: + * <ul> + * <li> {@link PlaybackState#PLAYSTATE_NONE}</li> + * <li> {@link PlaybackState#PLAYSTATE_STOPPED}</li> + * <li> {@link PlaybackState#PLAYSTATE_PLAYING}</li> + * <li> {@link PlaybackState#PLAYSTATE_PAUSED}</li> + * <li> {@link PlaybackState#PLAYSTATE_FAST_FORWARDING}</li> + * <li> {@link PlaybackState#PLAYSTATE_REWINDING}</li> + * <li> {@link PlaybackState#PLAYSTATE_BUFFERING}</li> + * <li> {@link PlaybackState#PLAYSTATE_ERROR}</li> + */ + public void setState(int mState) { + this.mState = mState; + } + + /** + * Get the current playback position in ms. + */ + public long getPosition() { + return mPosition; + } + + /** + * Set the current playback position in ms. + */ + public void setPosition(long position) { + mPosition = position; + } + + /** + * Get the current buffer position in ms. This is the farthest playback + * point that can be reached from the current position using only buffered + * content. + */ + public long getBufferPosition() { + return mBufferPosition; + } + + /** + * Set the current buffer position in ms. This is the farthest playback + * point that can be reached from the current position using only buffered + * content. + */ + public void setBufferPosition(long bufferPosition) { + mBufferPosition = bufferPosition; + } + + /** + * Get the current playback speed as a multiple of normal playback. This + * should be negative when rewinding. A value of 1 means normal playback and + * 0 means paused. + */ + public float getSpeed() { + return mSpeed; + } + + /** + * Set the current playback speed as a multiple of normal playback. This + * should be negative when rewinding. A value of 1 means normal playback and + * 0 means paused. + */ + public void setSpeed(float speed) { + mSpeed = speed; + } + + /** + * Get the current actions available on this session. This should use a + * bitmask of the available actions. + * <ul> + * <li> {@link PlaybackState#ACTION_PREVIOUS_ITEM}</li> + * <li> {@link PlaybackState#ACTION_REWIND}</li> + * <li> {@link PlaybackState#ACTION_PLAY}</li> + * <li> {@link PlaybackState#ACTION_PAUSE}</li> + * <li> {@link PlaybackState#ACTION_STOP}</li> + * <li> {@link PlaybackState#ACTION_FASTFORWARD}</li> + * <li> {@link PlaybackState#ACTION_NEXT_ITEM}</li> + * <li> {@link PlaybackState#ACTION_SEEK_TO}</li> + * <li> {@link PlaybackState#ACTION_RATING}</li> + * </ul> + */ + public long getActions() { + return mCapabilities; + } + + /** + * Set the current capabilities available on this session. This should use a + * bitmask of the available capabilities. + * <ul> + * <li> {@link PlaybackState#ACTION_PREVIOUS_ITEM}</li> + * <li> {@link PlaybackState#ACTION_REWIND}</li> + * <li> {@link PlaybackState#ACTION_PLAY}</li> + * <li> {@link PlaybackState#ACTION_PAUSE}</li> + * <li> {@link PlaybackState#ACTION_STOP}</li> + * <li> {@link PlaybackState#ACTION_FASTFORWARD}</li> + * <li> {@link PlaybackState#ACTION_NEXT_ITEM}</li> + * <li> {@link PlaybackState#ACTION_SEEK_TO}</li> + * <li> {@link PlaybackState#ACTION_RATING}</li> + * </ul> + */ + public void setActions(long capabilities) { + mCapabilities = capabilities; + } + + /** + * Get a user readable error message. This should be set when the state is + * {@link PlaybackState#PLAYSTATE_ERROR}. + */ + public String getErrorMessage() { + return mErrorMessage; + } + + /** + * Set a user readable error message. This should be set when the state is + * {@link PlaybackState#PLAYSTATE_ERROR}. + */ + public void setErrorMessage(String errorMessage) { + mErrorMessage = errorMessage; + } + + public static final Parcelable.Creator<PlaybackState> CREATOR + = new Parcelable.Creator<PlaybackState>() { + @Override + public PlaybackState createFromParcel(Parcel in) { + return new PlaybackState(in); + } + + @Override + public PlaybackState[] newArray(int size) { + return new PlaybackState[size]; + } + }; +} diff --git a/media/java/android/media/session/RouteInterface.java b/media/java/android/media/session/RouteInterface.java new file mode 100644 index 0000000..2391f27 --- /dev/null +++ b/media/java/android/media/session/RouteInterface.java @@ -0,0 +1,187 @@ +/* + * 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.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.Parcelable; +import android.os.ResultReceiver; + +/** + * Routes can support multiple interfaces for MediaSessions to interact with. To + * add a standard interface you should implement that interface's RouteInterface + * Stub and register it with the session. The set of supported commands is + * dependent on the specific interface's implementation. + * <p> + * A MediaInterface can be registered by calling TODO. Once added an interface + * will be used by Sessions to decide how they communicate with a session and + * cannot be removed, so all interfaces that you plan to support should be added + * when the route is created. + * + * @see RouteTransportControls + */ +public final class RouteInterface { + private static final String TAG = "MediaInterface"; + + private static final String KEY_RESULT = "result"; + + private final MediaController mController; + private final String mIface; + + /** + * @hide + */ + RouteInterface(MediaController controller, String iface) { + mController = controller; + mIface = iface; + } + + public void sendCommand(String command, Bundle params, ResultReceiver cb) { + // TODO + } + + public void addListener(EventListener listener) { + addListener(listener, null); + } + + public void addListener(EventListener listener, Handler handler) { + // TODO See MediaController for add/remove pattern + } + + public void removeListener(EventListener listener) { + // TODO + } + + // TODO decide on list of supported types + private static Bundle writeResultToBundle(Object v) { + Bundle b = new Bundle(); + if (v == null) { + // Don't send anything if null + } else if (v instanceof String) { + b.putString(KEY_RESULT, (String) v); + } else if (v instanceof Integer) { + b.putInt(KEY_RESULT, (Integer) v); + } else if (v instanceof Bundle) { + // Must be before Parcelable + b.putBundle(KEY_RESULT, (Bundle) v); + } else if (v instanceof Parcelable) { + b.putParcelable(KEY_RESULT, (Parcelable) v); + } else if (v instanceof Short) { + b.putShort(KEY_RESULT, (Short) v); + } else if (v instanceof Long) { + b.putLong(KEY_RESULT, (Long) v); + } else if (v instanceof Float) { + b.putFloat(KEY_RESULT, (Float) v); + } else if (v instanceof Double) { + b.putDouble(KEY_RESULT, (Double) v); + } else if (v instanceof Boolean) { + b.putBoolean(KEY_RESULT, (Boolean) v); + } else if (v instanceof CharSequence) { + // Must be after String + b.putCharSequence(KEY_RESULT, (CharSequence) v); + } else if (v instanceof boolean[]) { + b.putBooleanArray(KEY_RESULT, (boolean[]) v); + } else if (v instanceof byte[]) { + b.putByteArray(KEY_RESULT, (byte[]) v); + } else if (v instanceof String[]) { + b.putStringArray(KEY_RESULT, (String[]) v); + } else if (v instanceof CharSequence[]) { + // Must be after String[] and before Object[] + b.putCharSequenceArray(KEY_RESULT, (CharSequence[]) v); + } else if (v instanceof IBinder) { + b.putBinder(KEY_RESULT, (IBinder) v); + } else if (v instanceof Parcelable[]) { + b.putParcelableArray(KEY_RESULT, (Parcelable[]) v); + } else if (v instanceof int[]) { + b.putIntArray(KEY_RESULT, (int[]) v); + } else if (v instanceof long[]) { + b.putLongArray(KEY_RESULT, (long[]) v); + } else if (v instanceof Byte) { + b.putByte(KEY_RESULT, (Byte) v); + } + return b; + } + + public abstract static class Stub { + + /** + * The name of an interface should be a fully qualified name to prevent + * namespace collisions. Example: "com.myproject.MyPlaybackInterface" + * + * @return The name of this interface + */ + public abstract String getName(); + + /** + * This is called when a command is received that matches the interface + * you registered. Commands can come from any app with a MediaController + * reference to the session. + * + * @see MediaController + * @see MediaSession + * @param command The command or method to invoke. + * @param args Any args that were included with the command. May be + * null. + * @param cb The callback provided to send a response on. May be null. + */ + public abstract void onCommand(String command, Bundle args, ResultReceiver cb); + + public final void sendEvent(MediaSession session, String event, Bundle extras) { + // TODO + } + } + + /** + * An EventListener can be registered by an app with TODO to handle events + * sent by the session on a specific interface. + */ + public static abstract class EventListener { + /** + * This is called when an event is received from the interface. Events + * are sent by the session owner and will be delivered to all + * controllers that are listening to the interface. + * + * @param event The event that occurred. + * @param args Any extras that were included with the event. May be + * null. + */ + public abstract void onEvent(String event, Bundle args); + } + + private static final class EventHandler extends Handler { + + private final RouteInterface.EventListener mListener; + + public EventHandler(Looper looper, RouteInterface.EventListener cb) { + super(looper, null, true); + mListener = cb; + } + + @Override + public void handleMessage(Message msg) { + mListener.onEvent((String) msg.obj, msg.getData()); + } + + public void postEvent(String event, Bundle args) { + Message msg = obtainMessage(0, event); + msg.setData(args); + msg.sendToTarget(); + } + } +} diff --git a/media/java/android/media/session/RouteTransportControls.java b/media/java/android/media/session/RouteTransportControls.java new file mode 100644 index 0000000..665fd10 --- /dev/null +++ b/media/java/android/media/session/RouteTransportControls.java @@ -0,0 +1,230 @@ +/* + * 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.media.RemoteControlClient; +import android.os.Bundle; +import android.os.Handler; +import android.os.ResultReceiver; +import android.text.TextUtils; +import android.util.Log; + +/** + * A standard media control interface for Routes. Routes can support multiple + * interfaces for MediaSessions to interact with. TODO rewrite for routes + */ +public final class RouteTransportControls { + private static final String TAG = "RouteTransportControls"; + public static final String NAME = "android.media.session.RouteTransportControls"; + + private static final String KEY_VALUE1 = "value1"; + + private static final String METHOD_FAST_FORWARD = "fastForward"; + private static final String METHOD_GET_CURRENT_POSITION = "getCurrentPosition"; + private static final String METHOD_GET_CAPABILITIES = "getCapabilities"; + + private static final String EVENT_PLAYSTATE_CHANGE = "playstateChange"; + private static final String EVENT_METADATA_CHANGE = "metadataChange"; + + private final MediaController mController; + private final RouteInterface mIface; + + private RouteTransportControls(RouteInterface iface, MediaController controller) { + mIface = iface; + mController = controller; + } + + public static RouteTransportControls from(MediaController controller) { +// MediaInterface iface = controller.getInterface(NAME); +// if (iface != null) { +// return new RouteTransportControls(iface, controller); +// } + return null; + } + + /** + * Send a play command to the route. TODO rename resume() and use messaging + * protocol, not KeyEvent + */ + public void play() { + // TODO + } + + /** + * Send a pause command to the session. + */ + public void pause() { + // TODO + } + + /** + * Set the rate at which to fastforward. Valid values are in the range [0,1] + * with actual rates depending on the implementation. + * + * @param rate + */ + public void fastForward(float rate) { + if (rate < 0 || rate > 1) { + throw new IllegalArgumentException("Rate must be between 0 and 1 inclusive"); + } + Bundle b = new Bundle(); + b.putFloat(KEY_VALUE1, rate); + mIface.sendCommand(METHOD_FAST_FORWARD, b, null); + } + + public void getCurrentPosition(ResultReceiver cb) { + mIface.sendCommand(METHOD_GET_CURRENT_POSITION, null, cb); + } + + public void getCapabilities(ResultReceiver cb) { + mIface.sendCommand(METHOD_GET_CAPABILITIES, null, cb); + } + + public void addListener(Listener listener) { + mIface.addListener(listener.mListener); + } + + public void addListener(Listener listener, Handler handler) { + mIface.addListener(listener.mListener, handler); + } + + public void removeListener(Listener listener) { + mIface.removeListener(listener.mListener); + } + + public static abstract class Stub extends RouteInterface.Stub { + private final MediaSession mSession; + + public Stub(MediaSession session) { + mSession = session; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public void onCommand(String method, Bundle extras, ResultReceiver cb) { + if (TextUtils.isEmpty(method)) { + return; + } + Bundle result; + if (METHOD_FAST_FORWARD.equals(method)) { + fastForward(extras.getFloat(KEY_VALUE1, -1)); + } else if (METHOD_GET_CURRENT_POSITION.equals(method)) { + if (cb != null) { + result = new Bundle(); + result.putLong(KEY_VALUE1, getCurrentPosition()); + cb.send(0, result); + } + } else if (METHOD_GET_CAPABILITIES.equals(method)) { + if (cb != null) { + result = new Bundle(); + result.putLong(KEY_VALUE1, getCapabilities()); + cb.send(0, result); + } + } + } + + /** + * Override to handle fast forwarding. Valid values are [0,1] inclusive. + * The interpretation of the rate is up to the implementation. If no + * rate was included with the command a rate of -1 will be used by + * default. + * + * @param rate The rate at which to fast forward as a multiplier + */ + public void fastForward(float rate) { + Log.w(TAG, "fastForward is not supported."); + } + + /** + * Override to handle getting the current position of playback in + * millis. + * + * @return The current position in millis or -1 + */ + public long getCurrentPosition() { + Log.w(TAG, "getCurrentPosition is not supported"); + return -1; + } + + /** + * Override to handle getting the set of capabilities currently + * available. + * + * @return A bit mask of the supported capabilities + */ + public long getCapabilities() { + Log.w(TAG, "getCapabilities is not supported"); + return 0; + } + + /** + * Publish the current playback state to the system and any controllers. + * Valid values are defined in {@link RemoteControlClient}. TODO move + * play states somewhere else. + * + * @param state + */ + public final void updatePlaybackState(int state) { + Bundle extras = new Bundle(); + extras.putInt(KEY_VALUE1, state); + sendEvent(mSession, EVENT_PLAYSTATE_CHANGE, extras); + } + } + + /** + * Register this event listener using TODO to receive + * TransportControlInterface events from a session. + * + * @see RouteInterface.EventListener + */ + public static abstract class Listener { + + private RouteInterface.EventListener mListener = new RouteInterface.EventListener() { + @Override + public final void onEvent(String event, Bundle args) { + if (EVENT_PLAYSTATE_CHANGE.equals(event)) { + onPlaybackStateChange(args.getInt(KEY_VALUE1)); + } else if (EVENT_METADATA_CHANGE.equals(event)) { + onMetadataUpdate(args); + } + } + }; + + /** + * Override to handle updates to the playback state. Valid values are in + * {@link TransportPerformer}. TODO put playstate values somewhere more + * generic. + * + * @param state + */ + public void onPlaybackStateChange(int state) { + } + + /** + * Override to handle metadata changes for this session's media. The + * default supported fields are those in {@link MediaMetadata}. + * + * @param metadata + */ + public void onMetadataUpdate(Bundle metadata) { + } + } + +} diff --git a/media/java/android/media/session/TransportController.java b/media/java/android/media/session/TransportController.java new file mode 100644 index 0000000..15b11f3 --- /dev/null +++ b/media/java/android/media/session/TransportController.java @@ -0,0 +1,342 @@ +/* + * 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.media.Rating; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.util.Log; + +import java.util.ArrayList; + +/** + * Interface for controlling media playback on a session. This allows an app to + * request changes in playback, retrieve the current playback state and + * metadata, and listen for changes to the playback state and metadata. + */ +public final class TransportController { + private static final String TAG = "TransportController"; + + private final Object mLock = new Object(); + private final ArrayList<MessageHandler> mListeners = new ArrayList<MessageHandler>(); + private final IMediaController mBinder; + + /** + * @hide + */ + public TransportController(IMediaController binder) { + mBinder = binder; + } + + /** + * Start listening to changes in playback state. + */ + public void addStateListener(TransportStateListener listener) { + addStateListener(listener, null); + } + + public void addStateListener(TransportStateListener listener, Handler handler) { + if (listener == null) { + throw new IllegalArgumentException("Listener cannot be null"); + } + synchronized (mLock) { + if (getHandlerForListenerLocked(listener) != null) { + Log.w(TAG, "Listener is already added, ignoring"); + return; + } + if (handler == null) { + handler = new Handler(); + } + + MessageHandler msgHandler = new MessageHandler(handler.getLooper(), listener); + mListeners.add(msgHandler); + } + } + + /** + * Stop listening to changes in playback state. + */ + public void removeStateListener(TransportStateListener listener) { + if (listener == null) { + throw new IllegalArgumentException("Listener cannot be null"); + } + synchronized (mLock) { + removeStateListenerLocked(listener); + } + } + + /** + * Request that the player start its playback at its current position. + */ + public void play() { + try { + mBinder.play(); + } catch (RemoteException e) { + Log.wtf(TAG, "Error calling play.", e); + } + } + + /** + * Request that the player pause its playback and stay at its current + * position. + */ + public void pause() { + try { + mBinder.pause(); + } 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. + */ + public void stop() { + try { + mBinder.stop(); + } catch (RemoteException e) { + Log.wtf(TAG, "Error calling stop.", e); + } + } + + /** + * Move to a new location in the media stream. + * + * @param pos Position to move to, in milliseconds. + */ + public void seekTo(long pos) { + try { + mBinder.seekTo(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. + */ + public void fastForward() { + try { + mBinder.fastForward(); + } catch (RemoteException e) { + Log.wtf(TAG, "Error calling fastForward.", e); + } + } + + /** + * Skip to the next item. + */ + public void next() { + try { + mBinder.next(); + } catch (RemoteException e) { + Log.wtf(TAG, "Error calling next.", e); + } + } + + /** + * Start rewinding. If playback is already rewinding this may increase the + * rate. + */ + public void rewind() { + try { + mBinder.rewind(); + } catch (RemoteException e) { + Log.wtf(TAG, "Error calling rewind.", e); + } + } + + /** + * Skip to the previous item. + */ + public void previous() { + try { + mBinder.previous(); + } 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 + * {@link #getRatingType()}. + * + * @param rating The rating to set for the current content + */ + public void rate(Rating rating) { + try { + mBinder.rate(rating); + } catch (RemoteException e) { + Log.wtf(TAG, "Error calling rate.", e); + } + } + + /** + * 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 mBinder.getRatingType(); + } catch (RemoteException e) { + Log.wtf(TAG, "Error calling getRatingType.", e); + return Rating.RATING_NONE; + } + } + + /** + * Get the current playback state for this session. + * + * @return The current PlaybackState or null + */ + public PlaybackState getPlaybackState() { + try { + return mBinder.getPlaybackState(); + } catch (RemoteException e) { + Log.wtf(TAG, "Error calling getPlaybackState.", e); + return null; + } + } + + /** + * Get the current metadata for this session. + * + * @return The current MediaMetadata or null. + */ + public MediaMetadata getMetadata() { + try { + return mBinder.getMetadata(); + } catch (RemoteException e) { + Log.wtf(TAG, "Error calling getMetadata.", e); + return null; + } + } + + /** + * @hide + */ + public final void postPlaybackStateChanged(PlaybackState state) { + synchronized (mLock) { + for (int i = mListeners.size() - 1; i >= 0; i--) { + mListeners.get(i).post(MessageHandler.MSG_UPDATE_PLAYBACK_STATE, state); + } + } + } + + /** + * @hide + */ + public final void postMetadataChanged(MediaMetadata metadata) { + synchronized (mLock) { + for (int i = mListeners.size() - 1; i >= 0; i--) { + mListeners.get(i).post(MessageHandler.MSG_UPDATE_METADATA, + metadata); + } + } + } + + private MessageHandler getHandlerForListenerLocked(TransportStateListener listener) { + for (int i = mListeners.size() - 1; i >= 0; i--) { + MessageHandler handler = mListeners.get(i); + if (listener == handler.mListener) { + return handler; + } + } + return null; + } + + private boolean removeStateListenerLocked(TransportStateListener listener) { + for (int i = mListeners.size() - 1; i >= 0; i--) { + if (listener == mListeners.get(i).mListener) { + mListeners.remove(i); + return true; + } + } + return false; + } + + /** + * Register using {@link #addStateListener} to receive updates when there + * are playback changes on the session. + */ + public static abstract class TransportStateListener { + private MessageHandler mHandler; + /** + * Override to handle changes in playback state. + * + * @param state The new playback state of the session + */ + public void onPlaybackStateChanged(PlaybackState state) { + } + + /** + * Override to handle changes to the current metadata. + * + * @see MediaMetadata + * @param metadata The current metadata for the session or null + */ + public void onMetadataChanged(MediaMetadata metadata) { + } + + private void setHandler(Handler handler) { + mHandler = new MessageHandler(handler.getLooper(), this); + } + } + + private static class MessageHandler extends Handler { + private static final int MSG_UPDATE_PLAYBACK_STATE = 1; + private static final int MSG_UPDATE_METADATA = 2; + + private TransportStateListener mListener; + + public MessageHandler(Looper looper, TransportStateListener cb) { + super(looper, null, true); + mListener = cb; + } + + public void post(int msg, Object obj) { + obtainMessage(msg, obj).sendToTarget(); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_UPDATE_PLAYBACK_STATE: + mListener.onPlaybackStateChanged((PlaybackState) msg.obj); + break; + case MSG_UPDATE_METADATA: + mListener.onMetadataChanged((MediaMetadata) msg.obj); + break; + } + } + } + +} diff --git a/media/java/android/media/session/TransportPerformer.java b/media/java/android/media/session/TransportPerformer.java new file mode 100644 index 0000000..b96db20 --- /dev/null +++ b/media/java/android/media/session/TransportPerformer.java @@ -0,0 +1,357 @@ +/* + * 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.media.AudioManager; +import android.media.Rating; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.util.Log; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; + +/** + * Allows broadcasting of playback changes. + */ +public final class TransportPerformer { + private static final String TAG = "TransportPerformer"; + private final Object mLock = new Object(); + private final ArrayList<MessageHandler> mListeners = new ArrayList<MessageHandler>(); + + private IMediaSession mBinder; + + /** + * @hide + */ + public TransportPerformer(IMediaSession binder) { + mBinder = binder; + } + + /** + * Add a listener to receive updates on. + * + * @param listener The callback object + */ + public void addListener(Listener listener) { + addListener(listener, null); + } + + /** + * Add a listener to receive updates on. The updates will be posted to the + * specified handler. If no handler is provided they will be posted to the + * caller's thread. + * + * @param listener The listener to receive updates on + * @param handler The handler to post the updates on + */ + public void addListener(Listener listener, Handler handler) { + if (listener == null) { + throw new IllegalArgumentException("Listener cannot be null"); + } + synchronized (mLock) { + if (getHandlerForListenerLocked(listener) != null) { + Log.w(TAG, "Listener is already added, ignoring"); + } + if (handler == null) { + handler = new Handler(); + } + MessageHandler msgHandler = new MessageHandler(handler.getLooper(), listener); + mListeners.add(msgHandler); + } + } + + /** + * Stop receiving updates on the specified handler. If an update has already + * been posted you may still receive it after this call returns. + * + * @param listener The listener to stop receiving updates on + */ + public void removeListener(Listener listener) { + if (listener == null) { + throw new IllegalArgumentException("Listener cannot be null"); + } + synchronized (mLock) { + removeListenerLocked(listener); + } + } + + /** + * Update the current playback state. + * + * @param state The current state of playback + */ + public final void setPlaybackState(PlaybackState 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 MediaMetadata.Builder}. + * + * @param metadata The new metadata + */ + public final void setMetadata(MediaMetadata metadata) { + try { + mBinder.setMetadata(metadata); + } catch (RemoteException e) { + Log.wtf(TAG, "Dead object in setPlaybackState.", e); + } + } + + /** + * @hide + */ + public final void onPlay() { + post(MessageHandler.MESSAGE_PLAY); + } + + /** + * @hide + */ + public final void onPause() { + post(MessageHandler.MESSAGE_PAUSE); + } + + /** + * @hide + */ + public final void onStop() { + post(MessageHandler.MESSAGE_STOP); + } + + /** + * @hide + */ + public final void onNext() { + post(MessageHandler.MESSAGE_NEXT); + } + + /** + * @hide + */ + public final void onPrevious() { + post(MessageHandler.MESSAGE_PREVIOUS); + } + + /** + * @hide + */ + public final void onFastForward() { + post(MessageHandler.MESSAGE_FAST_FORWARD); + } + + /** + * @hide + */ + public final void onRewind() { + post(MessageHandler.MESSAGE_REWIND); + } + + /** + * @hide + */ + public final void onSeekTo(long pos) { + post(MessageHandler.MESSAGE_SEEK_TO, pos); + } + + /** + * @hide + */ + public final void onRate(Rating rating) { + post(MessageHandler.MESSAGE_RATE, rating); + } + + private MessageHandler getHandlerForListenerLocked(Listener listener) { + for (int i = mListeners.size() - 1; i >= 0; i--) { + MessageHandler handler = mListeners.get(i); + if (listener == handler.mListener) { + return handler; + } + } + return null; + } + + private boolean removeListenerLocked(Listener listener) { + for (int i = mListeners.size() - 1; i >= 0; i--) { + if (listener == mListeners.get(i).mListener) { + mListeners.remove(i); + return true; + } + } + return false; + } + + private void post(int what, Object obj) { + synchronized (mLock) { + for (int i = mListeners.size() - 1; i >= 0; i--) { + mListeners.get(i).post(what, obj); + } + } + } + + private void post(int what) { + post(what, null); + } + + /** + * Extend Listener to handle transport controls. Listeners can be registered + * using {@link #addListener}. + */ + public static abstract class Listener { + + /** + * 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 onNext() { + } + + /** + * Override to handle requests to skip to the previous media item. + */ + public void onPrevious() { + } + + /** + * 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 onRate(Rating rating) { + } + + /** + * Report that audio focus has changed on the app. This only happens if + * you have indicated you have started playing with + * {@link #setPlaybackState}. TODO figure out route focus apis/handling. + * + * @param focusChange The type of focus change, TBD. The default + * implementation will deliver a call to {@link #onPause} + * when focus is lost. + */ + public void onRouteFocusChange(int focusChange) { + switch (focusChange) { + case AudioManager.AUDIOFOCUS_LOSS: + onPause(); + break; + } + } + } + + private class MessageHandler extends Handler { + private static final int MESSAGE_PLAY = 1; + private static final int MESSAGE_PAUSE = 2; + private static final int MESSAGE_STOP = 3; + private static final int MESSAGE_NEXT = 4; + private static final int MESSAGE_PREVIOUS = 5; + private static final int MESSAGE_FAST_FORWARD = 6; + private static final int MESSAGE_REWIND = 7; + private static final int MESSAGE_SEEK_TO = 8; + private static final int MESSAGE_RATE = 9; + + private Listener mListener; + + public MessageHandler(Looper looper, Listener cb) { + super(looper); + mListener = cb; + } + + public void post(int what, Object obj) { + obtainMessage(what, obj).sendToTarget(); + } + + public void post(int what) { + post(what, null); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MESSAGE_PLAY: + mListener.onPlay(); + break; + case MESSAGE_PAUSE: + mListener.onPause(); + break; + case MESSAGE_STOP: + mListener.onStop(); + break; + case MESSAGE_NEXT: + mListener.onNext(); + break; + case MESSAGE_PREVIOUS: + mListener.onPrevious(); + break; + case MESSAGE_FAST_FORWARD: + mListener.onFastForward(); + break; + case MESSAGE_REWIND: + mListener.onRewind(); + break; + case MESSAGE_SEEK_TO: + mListener.onSeekTo((Long) msg.obj); + break; + case MESSAGE_RATE: + mListener.onRate((Rating) msg.obj); + break; + } + } + } +} diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java index 89acec9..1ff925c 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecord.java +++ b/services/core/java/com/android/server/media/MediaSessionRecord.java @@ -21,14 +21,22 @@ import android.media.session.IMediaController; import android.media.session.IMediaControllerCallback; import android.media.session.IMediaSession; import android.media.session.IMediaSessionCallback; -import android.media.RemoteControlClient; +import android.media.session.MediaMetadata; +import android.media.session.PlaybackState; +import android.media.Rating; import android.os.Bundle; +import android.os.Handler; import android.os.IBinder; +import android.os.Looper; +import android.os.Message; import android.os.RemoteException; +import android.os.ResultReceiver; import android.util.Log; +import android.util.Slog; import android.view.KeyEvent; import java.util.ArrayList; +import java.util.List; /** * This is the system implementation of a Session. Apps will interact with the @@ -37,6 +45,8 @@ import java.util.ArrayList; public class MediaSessionRecord implements IBinder.DeathRecipient { private static final String TAG = "MediaSessionImpl"; + private final MessageHandler mHandler; + private final int mPid; private final String mPackageName; private final String mTag; @@ -45,13 +55,25 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { private final SessionCb mSessionCb; private final MediaSessionService mService; - private final ArrayList<IMediaControllerCallback> mSessionCallbacks = + private final Object mControllerLock = new Object(); + private final ArrayList<IMediaControllerCallback> mControllerCallbacks = new ArrayList<IMediaControllerCallback>(); + private final ArrayList<String> mInterfaces = new ArrayList<String>(); + + private boolean mTransportPerformerEnabled = false; + private Bundle mRoute; + + // TransportPerformer fields - private int mPlaybackState = RemoteControlClient.PLAYSTATE_NONE; + private MediaMetadata mMetadata; + private PlaybackState mPlaybackState; + private int mRatingType; + // End TransportPerformer fields + + private boolean mIsPublished = false; public MediaSessionRecord(int pid, String packageName, IMediaSessionCallback cb, String tag, - MediaSessionService service) { + MediaSessionService service, Handler handler) { mPid = pid; mPackageName = packageName; mTag = tag; @@ -59,6 +81,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { mSession = new SessionStub(); mSessionCb = new SessionCb(cb); mService = service; + mHandler = new MessageHandler(handler.getLooper()); } public IMediaSession getSessionBinder() { @@ -69,61 +92,132 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { return mController; } - public void setPlaybackStateInternal(int state) { - mPlaybackState = state; - for (int i = mSessionCallbacks.size() - 1; i >= 0; i--) { - IMediaControllerCallback cb = mSessionCallbacks.get(i); - try { - cb.onPlaybackUpdate(state); - } catch (RemoteException e) { - Log.d(TAG, "SessionCallback object dead in setPlaybackState.", e); - mSessionCallbacks.remove(i); - } - } - } - @Override public void binderDied() { mService.sessionDied(this); } + public boolean isPublished() { + return mIsPublished; + } + private void onDestroy() { mService.destroySession(this); } - private final class SessionStub extends IMediaSession.Stub { + private void pushPlaybackStateUpdate() { + synchronized (mControllerLock) { + for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) { + IMediaControllerCallback cb = mControllerCallbacks.get(i); + try { + cb.onPlaybackStateChanged(mPlaybackState); + } catch (RemoteException e) { + Log.w(TAG, "Removing dead callback in pushPlaybackStateUpdate.", e); + mControllerCallbacks.remove(i); + } + } + } + } - @Override - public void setPlaybackState(int state) throws RemoteException { - setPlaybackStateInternal(state); + private void pushMetadataUpdate() { + synchronized (mControllerLock) { + for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) { + IMediaControllerCallback cb = mControllerCallbacks.get(i); + try { + cb.onMetadataChanged(mMetadata); + } catch (RemoteException e) { + Log.w(TAG, "Removing dead callback in pushMetadataUpdate.", e); + mControllerCallbacks.remove(i); + } + } } + } + + private void pushRouteUpdate() { + synchronized (mControllerLock) { + for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) { + IMediaControllerCallback cb = mControllerCallbacks.get(i); + try { + cb.onRouteChanged(mRoute); + } catch (RemoteException e) { + Log.w(TAG, "Removing dead callback in pushRouteUpdate.", e); + mControllerCallbacks.remove(i); + } + } + } + } + + private void pushEvent(String event, Bundle data) { + synchronized (mControllerLock) { + for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) { + IMediaControllerCallback cb = mControllerCallbacks.get(i); + try { + cb.onEvent(event, data); + } catch (RemoteException e) { + Log.w(TAG, "Removing dead callback in pushRouteUpdate.", e); + mControllerCallbacks.remove(i); + } + } + } + } + + private final class SessionStub extends IMediaSession.Stub { @Override - public void destroy() throws RemoteException { + public void destroy() { onDestroy(); } @Override - public void sendEvent(Bundle data) throws RemoteException { + public void sendEvent(String event, Bundle data) { + mHandler.post(MessageHandler.MSG_SEND_EVENT, event, data); } @Override - public IMediaController getMediaSessionToken() throws RemoteException { + public IMediaController getMediaController() { return mController; } @Override - public void setMetadata(Bundle metadata) throws RemoteException { + public void setRouteState(Bundle routeState) { + } + + @Override + public void setRoute(Bundle mediaRouteDescriptor) { + mRoute = mediaRouteDescriptor; + mHandler.post(MessageHandler.MSG_UPDATE_ROUTE); + } + + @Override + public void publish() { + mIsPublished = true; // TODO push update to service + } + @Override + public void setTransportPerformerEnabled() { + mTransportPerformerEnabled = true; } @Override - public void setRouteState(Bundle routeState) throws RemoteException { + public List<String> getSupportedInterfaces() { + return mInterfaces; } @Override - public void setRoute(Bundle medaiRouteDescriptor) throws RemoteException { + public void setMetadata(MediaMetadata metadata) { + mMetadata = metadata; + mHandler.post(MessageHandler.MSG_UPDATE_METADATA); } + @Override + public void setPlaybackState(PlaybackState state) { + mPlaybackState = state; + mHandler.post(MessageHandler.MSG_UPDATE_PLAYBACK_STATE); + } + + @Override + public void setRatingType(int type) { + mRatingType = type; + } } class SessionCb { @@ -139,32 +233,96 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { try { mCb.onMediaButton(mediaButtonIntent); } catch (RemoteException e) { - Log.d(TAG, "Controller object dead in sendMediaRequest.", e); - onDestroy(); + Slog.e(TAG, "Remote failure in sendMediaRequest.", e); + } + } + + public void sendCommand(String command, Bundle extras, ResultReceiver cb) { + try { + mCb.onCommand(command, extras, cb); + } catch (RemoteException e) { + Slog.e(TAG, "Remote failure in sendCommand.", e); + } + } + + public void play() { + try { + mCb.onPlay(); + } catch (RemoteException e) { + Slog.e(TAG, "Remote failure in play.", e); } } - public void sendCommand(String command, Bundle extras) { + public void pause() { try { - mCb.onCommand(command, extras); + mCb.onPause(); } catch (RemoteException e) { - Log.d(TAG, "Controller object dead in sendCommand.", e); - onDestroy(); + Slog.e(TAG, "Remote failure in pause.", e); } } - public void registerCallbackListener(IMediaSessionCallback cb) { + public void stop() { + try { + mCb.onStop(); + } catch (RemoteException e) { + Slog.e(TAG, "Remote failure in stop.", e); + } + } + public void next() { + try { + mCb.onNext(); + } catch (RemoteException e) { + Slog.e(TAG, "Remote failure in next.", e); + } } + public void previous() { + try { + mCb.onPrevious(); + } catch (RemoteException e) { + Slog.e(TAG, "Remote failure in previous.", e); + } + } + + public void fastForward() { + try { + mCb.onFastForward(); + } catch (RemoteException e) { + Slog.e(TAG, "Remote failure in fastForward.", e); + } + } + + public void rewind() { + try { + mCb.onRewind(); + } catch (RemoteException e) { + Slog.e(TAG, "Remote failure in rewind.", e); + } + } + + public void seekTo(long pos) { + try { + mCb.onSeekTo(pos); + } catch (RemoteException e) { + Slog.e(TAG, "Remote failure in seekTo.", e); + } + } + + public void rate(Rating rating) { + try { + mCb.onRate(rating); + } catch (RemoteException e) { + Slog.e(TAG, "Remote failure in rate.", e); + } + } } class ControllerStub extends IMediaController.Stub { - /* - */ @Override - public void sendCommand(String command, Bundle extras) throws RemoteException { - mSessionCb.sendCommand(command, extras); + public void sendCommand(String command, Bundle extras, ResultReceiver cb) + throws RemoteException { + mSessionCb.sendCommand(command, extras, cb); } @Override @@ -172,29 +330,130 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { mSessionCb.sendMediaButton(mediaButtonIntent); } - /* - */ @Override - public void registerCallbackListener(IMediaControllerCallback cb) throws RemoteException { - if (!mSessionCallbacks.contains(cb)) { - mSessionCallbacks.add(cb); + public void registerCallbackListener(IMediaControllerCallback cb) { + synchronized (mControllerLock) { + if (!mControllerCallbacks.contains(cb)) { + mControllerCallbacks.add(cb); + } } } - /* - */ @Override public void unregisterCallbackListener(IMediaControllerCallback cb) throws RemoteException { - mSessionCallbacks.remove(cb); + synchronized (mControllerLock) { + mControllerCallbacks.remove(cb); + } } - /* - */ @Override - public int getPlaybackState() throws RemoteException { + public void play() throws RemoteException { + mSessionCb.play(); + } + + @Override + public void pause() throws RemoteException { + mSessionCb.pause(); + } + + @Override + public void stop() throws RemoteException { + mSessionCb.stop(); + } + + @Override + public void next() throws RemoteException { + mSessionCb.next(); + } + + @Override + public void previous() throws RemoteException { + mSessionCb.previous(); + } + + @Override + public void fastForward() throws RemoteException { + mSessionCb.fastForward(); + } + + @Override + public void rewind() throws RemoteException { + mSessionCb.rewind(); + } + + @Override + public void seekTo(long pos) throws RemoteException { + mSessionCb.seekTo(pos); + } + + @Override + public void rate(Rating rating) throws RemoteException { + mSessionCb.rate(rating); + } + + + @Override + public MediaMetadata getMetadata() { + return mMetadata; + } + + @Override + public PlaybackState getPlaybackState() { return mPlaybackState; } + + @Override + public int getRatingType() { + return mRatingType; + } + + @Override + public boolean isTransportControlEnabled() throws RemoteException { + return mTransportPerformerEnabled; + } + } + + private class MessageHandler extends Handler { + private static final int MSG_UPDATE_METADATA = 1; + private static final int MSG_UPDATE_PLAYBACK_STATE = 2; + private static final int MSG_UPDATE_ROUTE = 3; + private static final int MSG_SEND_EVENT = 4; + + public MessageHandler(Looper looper) { + super(looper); + } + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_UPDATE_METADATA: + pushMetadataUpdate(); + break; + case MSG_UPDATE_PLAYBACK_STATE: + pushPlaybackStateUpdate(); + break; + case MSG_UPDATE_ROUTE: + pushRouteUpdate(); + break; + case MSG_SEND_EVENT: + pushEvent((String) msg.obj, msg.getData()); + break; + } + } + + public void post(int what) { + post(what, null); + } + + public void post(int what, Object obj) { + obtainMessage(what, obj).sendToTarget(); + } + + public void post(int what, Object obj, Bundle data) { + Message msg = obtainMessage(what, obj); + msg.setData(data); + msg.sendToTarget(); + } } } diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java index a7ff926..8fe6055 100644 --- a/services/core/java/com/android/server/media/MediaSessionService.java +++ b/services/core/java/com/android/server/media/MediaSessionService.java @@ -21,6 +21,7 @@ import android.media.session.IMediaSession; import android.media.session.IMediaSessionCallback; import android.media.session.IMediaSessionManager; import android.os.Binder; +import android.os.Handler; import android.os.RemoteException; import android.text.TextUtils; import android.util.Log; @@ -41,6 +42,8 @@ public class MediaSessionService extends SystemService { private final ArrayList<MediaSessionRecord> mSessions = new ArrayList<MediaSessionRecord>(); private final Object mLock = new Object(); + // TODO do we want a separate thread for handling mediasession messages? + private final Handler mHandler = new Handler(); public MediaSessionService(Context context) { super(context); @@ -91,7 +94,8 @@ public class MediaSessionService extends SystemService { private MediaSessionRecord createSessionLocked(int pid, String packageName, IMediaSessionCallback cb, String tag) { - final MediaSessionRecord session = new MediaSessionRecord(pid, packageName, cb, tag, this); + final MediaSessionRecord session = new MediaSessionRecord(pid, packageName, cb, tag, this, + mHandler); try { cb.asBinder().linkToDeath(session, 0); } catch (RemoteException e) { diff --git a/tests/OneMedia/src/com/android/onemedia/OnePlayerActivity.java b/tests/OneMedia/src/com/android/onemedia/OnePlayerActivity.java index 7ff81e4..3114ca9 100644 --- a/tests/OneMedia/src/com/android/onemedia/OnePlayerActivity.java +++ b/tests/OneMedia/src/com/android/onemedia/OnePlayerActivity.java @@ -1,7 +1,24 @@ +/* + * 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 com.android.onemedia; import android.app.Activity; +import android.media.session.MediaMetadata; +import android.media.session.PlaybackState; import android.os.Bundle; import android.util.Log; import android.view.Menu; @@ -79,10 +96,10 @@ public class OnePlayerActivity extends Activity { switch (v.getId()) { case R.id.play_button: Log.d(TAG, "Play button pressed, in state " + mPlaybackState); - if (mPlaybackState == Renderer.STATE_PAUSED - || mPlaybackState == Renderer.STATE_ENDED) { + if (mPlaybackState == PlaybackState.PLAYSTATE_PAUSED + || mPlaybackState == PlaybackState.PLAYSTATE_STOPPED) { mPlayer.play(); - } else if (mPlaybackState == Renderer.STATE_PLAYING) { + } else if (mPlaybackState == PlaybackState.PLAYSTATE_PLAYING) { mPlayer.pause(); } break; @@ -97,48 +114,55 @@ public class OnePlayerActivity extends Activity { private PlayerController.Listener mListener = new PlayerController.Listener() { @Override - public void onSessionStateChange(int state) { - mPlaybackState = state; + public void onPlaybackStateChange(PlaybackState state) { + mPlaybackState = state.getState(); boolean enablePlay = false; + StringBuilder statusBuilder = new StringBuilder(); switch (mPlaybackState) { - case Renderer.STATE_PLAYING: - mStatusView.setText("playing"); + case PlaybackState.PLAYSTATE_PLAYING: + statusBuilder.append("playing"); mPlayButton.setText("Pause"); enablePlay = true; break; - case Renderer.STATE_PAUSED: - mStatusView.setText("paused"); + case PlaybackState.PLAYSTATE_PAUSED: + statusBuilder.append("paused"); mPlayButton.setText("Play"); enablePlay = true; break; - case Renderer.STATE_ENDED: - mStatusView.setText("ended"); + case PlaybackState.PLAYSTATE_STOPPED: + statusBuilder.append("ended"); mPlayButton.setText("Play"); enablePlay = true; break; - case Renderer.STATE_ERROR: - mStatusView.setText("error"); + case PlaybackState.PLAYSTATE_ERROR: + statusBuilder.append("error: ").append(state.getErrorMessage()); break; - case Renderer.STATE_PREPARING: - mStatusView.setText("preparing"); + case PlaybackState.PLAYSTATE_BUFFERING: + statusBuilder.append("buffering"); break; - case Renderer.STATE_READY: - mStatusView.setText("ready"); - break; - case Renderer.STATE_STOPPED: - mStatusView.setText("stopped"); + case PlaybackState.PLAYSTATE_NONE: + statusBuilder.append("none"); break; + default: + statusBuilder.append(mPlaybackState); } + statusBuilder.append(" -- At position: ").append(state.getPosition()); + mStatusView.setText(statusBuilder.toString()); mPlayButton.setEnabled(enablePlay); } @Override - public void onPlayerStateChange(int state) { + public void onConnectionStateChange(int state) { if (state == PlayerController.STATE_DISCONNECTED) { setControlsEnabled(false); } else if (state == PlayerController.STATE_CONNECTED) { setControlsEnabled(true); } } + + @Override + public void onMetadataChange(MediaMetadata metadata) { + Log.d(TAG, "Metadata update! Title: " + metadata); + } }; } diff --git a/tests/OneMedia/src/com/android/onemedia/OnePlayerService.java b/tests/OneMedia/src/com/android/onemedia/OnePlayerService.java index 01610cd..573f7ff 100644 --- a/tests/OneMedia/src/com/android/onemedia/OnePlayerService.java +++ b/tests/OneMedia/src/com/android/onemedia/OnePlayerService.java @@ -1,3 +1,18 @@ +/* + * 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 com.android.onemedia; import android.content.Context; @@ -5,9 +20,6 @@ import android.content.Intent; import java.util.ArrayList; -/** - * TODO: Insert description here. (generated by epastern) - */ public class OnePlayerService extends PlayerService { private static final String TAG = "OnePlayerService"; diff --git a/tests/OneMedia/src/com/android/onemedia/PlayerController.java b/tests/OneMedia/src/com/android/onemedia/PlayerController.java index 3f15db5..e831ec6 100644 --- a/tests/OneMedia/src/com/android/onemedia/PlayerController.java +++ b/tests/OneMedia/src/com/android/onemedia/PlayerController.java @@ -1,8 +1,27 @@ +/* + * 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 com.android.onemedia; import android.media.session.MediaController; +import android.media.session.MediaMetadata; import android.media.session.MediaSessionManager; +import android.media.session.PlaybackState; +import android.media.session.TransportController; +import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; @@ -11,22 +30,23 @@ import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.util.Log; -import android.view.KeyEvent; import com.android.onemedia.playback.RequestUtils; public class PlayerController { - private static final String TAG = "PlayerSession"; + private static final String TAG = "PlayerController"; public static final int STATE_DISCONNECTED = 0; public static final int STATE_CONNECTED = 1; protected MediaController mController; protected IPlayerService mBinder; + protected TransportController mTransportControls; private final Intent mServiceIntent; private Context mContext; private Listener mListener; + private TransportListener mTransportListener = new TransportListener(); private SessionCallback mControllerCb; private MediaSessionManager mManager; private Handler mHandler = new Handler(); @@ -52,7 +72,7 @@ public class PlayerController { Log.d(TAG, "Listener set to " + listener + " session is " + mController); if (mListener != null) { mHandler = new Handler(); - mListener.onPlayerStateChange( + mListener.onConnectionStateChange( mController == null ? STATE_DISCONNECTED : STATE_CONNECTED); } } @@ -70,11 +90,15 @@ public class PlayerController { } public void play() { - mController.sendMediaButton(KeyEvent.KEYCODE_MEDIA_PLAY); + if (mTransportControls != null) { + mTransportControls.play(); + } } public void pause() { - mController.sendMediaButton(KeyEvent.KEYCODE_MEDIA_PAUSE); + if (mTransportControls != null) { + mTransportControls.pause(); + } } public void setContent(String source) { @@ -113,10 +137,11 @@ public class PlayerController { } mBinder = null; mController = null; + mTransportControls = null; Log.d(TAG, "Disconnected from PlayerService"); if (mListener != null) { - mListener.onPlayerStateChange(STATE_DISCONNECTED); + mListener.onConnectionStateChange(STATE_DISCONNECTED); } } @@ -125,33 +150,60 @@ public class PlayerController { mBinder = IPlayerService.Stub.asInterface(service); Log.d(TAG, "service is " + service + " binder is " + mBinder); try { - mController = new MediaController(mBinder.getSessionToken()); + mController = MediaController.fromToken(mBinder.getSessionToken()); } catch (RemoteException e) { Log.e(TAG, "Error getting session", e); return; } mController.addCallback(mControllerCb, mHandler); + mTransportControls = mController.getTransportController(); + if (mTransportControls != null) { + mTransportControls.addStateListener(mTransportListener); + } Log.d(TAG, "Ready to use PlayerService"); if (mListener != null) { - mListener.onPlayerStateChange(STATE_CONNECTED); + mListener.onConnectionStateChange(STATE_CONNECTED); + if (mTransportControls != null) { + mListener.onPlaybackStateChange(mTransportControls.getPlaybackState()); + } } } }; private class SessionCallback extends MediaController.Callback { @Override - public void onPlaybackStateChange(int state) { + public void onRouteChanged(Bundle route) { + // TODO + } + } + + private class TransportListener extends TransportController.TransportStateListener { + @Override + public void onPlaybackStateChanged(PlaybackState state) { + if (state == null) { + return; + } + Log.d(TAG, "Received playback state change to state " + state.getState()); if (mListener != null) { - mListener.onSessionStateChange(state); + mListener.onPlaybackStateChange(state); } } + + @Override + public void onMetadataChanged(MediaMetadata metadata) { + if (metadata == null) { + return; + } + Log.d(TAG, "Received metadata change, title is " + + metadata.getString(MediaMetadata.METADATA_KEY_TITLE)); + } } public interface Listener { - public void onSessionStateChange(int state); - - public void onPlayerStateChange(int state); + public void onPlaybackStateChange(PlaybackState state); + public void onMetadataChange(MediaMetadata metadata); + public void onConnectionStateChange(int state); } } diff --git a/tests/OneMedia/src/com/android/onemedia/PlayerService.java b/tests/OneMedia/src/com/android/onemedia/PlayerService.java index 0b2ba8f..0ad6dd1 100644 --- a/tests/OneMedia/src/com/android/onemedia/PlayerService.java +++ b/tests/OneMedia/src/com/android/onemedia/PlayerService.java @@ -1,11 +1,28 @@ +/* + * 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 com.android.onemedia; import android.app.Service; import android.content.Intent; import android.media.session.MediaSessionToken; +import android.media.session.PlaybackState; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; +import android.util.Log; import com.android.onemedia.playback.IRequestCallback; import com.android.onemedia.playback.RequestUtils; @@ -18,14 +35,19 @@ public class PlayerService extends Service { private PlayerBinder mBinder; private PlayerSession mSession; private Intent mIntent; + private boolean mStarted = false; private ArrayList<IPlayerCallback> mCbs = new ArrayList<IPlayerCallback>(); @Override public void onCreate() { + Log.d(TAG, "onCreate"); mIntent = onCreateServiceIntent(); - mSession = onCreatePlayerController(); - mSession.createSession(); + if (mSession == null) { + mSession = onCreatePlayerController(); + mSession.createSession(); + mSession.setListener(mPlayerListener); + } } @Override @@ -38,12 +60,31 @@ public class PlayerService extends Service { @Override public int onStartCommand(Intent intent, int flags, int startId) { + Log.d(TAG, "onStartCommand"); return START_STICKY; } @Override public void onDestroy() { + Log.d(TAG, "onDestroy"); mSession.onDestroy(); + mSession = null; + } + + public void onPlaybackStarted() { + if (!mStarted) { + Log.d(TAG, "Starting self"); + startService(onCreateServiceIntent()); + mStarted = true; + } + } + + public void onPlaybackEnded() { + if (mStarted) { + Log.d(TAG, "Stopping self"); + stopSelf(); + mStarted = false; + } } protected Intent onCreateServiceIntent() { @@ -58,6 +99,21 @@ public class PlayerService extends Service { return null; } + private final PlayerSession.Listener mPlayerListener = new PlayerSession.Listener() { + @Override + public void onPlayStateChanged(PlaybackState state) { + switch (state.getState()) { + case PlaybackState.PLAYSTATE_PLAYING: + onPlaybackStarted(); + break; + case PlaybackState.PLAYSTATE_STOPPED: + case PlaybackState.PLAYSTATE_ERROR: + onPlaybackEnded(); + break; + } + } + }; + public class PlayerBinder extends IPlayerService.Stub { @Override public void sendRequest(String action, Bundle params, IRequestCallback cb) { @@ -94,7 +150,6 @@ public class PlayerService extends Service { @Override public MediaSessionToken getSessionToken() throws RemoteException { - // TODO(epastern): Auto-generated method stub return mSession.getSessionToken(); } } diff --git a/tests/OneMedia/src/com/android/onemedia/PlayerSession.java b/tests/OneMedia/src/com/android/onemedia/PlayerSession.java index e5fb0d0..a2d7897 100644 --- a/tests/OneMedia/src/com/android/onemedia/PlayerSession.java +++ b/tests/OneMedia/src/com/android/onemedia/PlayerSession.java @@ -1,3 +1,18 @@ +/* + * 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 com.android.onemedia; import android.content.Context; @@ -5,6 +20,8 @@ import android.content.Intent; import android.media.session.MediaSession; import android.media.session.MediaSessionManager; import android.media.session.MediaSessionToken; +import android.media.session.PlaybackState; +import android.media.session.TransportPerformer; import android.os.Bundle; import android.util.Log; import android.view.KeyEvent; @@ -14,14 +31,18 @@ import com.android.onemedia.playback.Renderer; import com.android.onemedia.playback.RendererFactory; public class PlayerSession { - private static final String TAG = "PlayerController"; + private static final String TAG = "PlayerSession"; protected MediaSession mSession; protected Context mContext; protected RendererFactory mRendererFactory; protected LocalRenderer mRenderer; - protected ControllerCb mCallback; - protected RenderListener mRenderListener; + protected MediaSession.Callback mCallback; + protected Renderer.Listener mRenderListener; + protected TransportPerformer mPerformer; + + protected PlaybackState mPlaybackState; + protected Listener mListener; public PlayerSession(Context context) { mContext = context; @@ -29,6 +50,9 @@ public class PlayerSession { mRenderer = new LocalRenderer(context, null); mCallback = new ControllerCb(); mRenderListener = new RenderListener(); + mPlaybackState = new PlaybackState(); + mPlaybackState.setActions(PlaybackState.ACTION_PAUSE + | PlaybackState.ACTION_PLAY); mRenderer.registerListener(mRenderListener); } @@ -42,6 +66,10 @@ public class PlayerSession { Log.d(TAG, "Creating session for package " + mContext.getBasePackageName()); mSession = man.createSession("OneMedia"); mSession.addCallback(mCallback); + mPerformer = mSession.setTransportPerformerEnabled(); + mPerformer.addListener(new TransportListener()); + mPerformer.setPlaybackState(mPlaybackState); + mSession.publish(); } public void onDestroy() { @@ -54,6 +82,10 @@ public class PlayerSession { } } + public void setListener(Listener listener) { + mListener = listener; + } + public MediaSessionToken getSessionToken() { return mSession.getSessionToken(); } @@ -66,16 +98,58 @@ public class PlayerSession { mRenderer.setNextContent(request); } - protected class RenderListener implements Renderer.Listener { + public interface Listener { + public void onPlayStateChanged(PlaybackState state); + } + + private class RenderListener implements Renderer.Listener { @Override public void onError(int type, int extra, Bundle extras, Throwable error) { - mSession.setPlaybackState(Renderer.STATE_ERROR); + Log.d(TAG, "Sending onError with type " + type + " and extra " + extra); + mPlaybackState.setState(PlaybackState.PLAYSTATE_ERROR); + if (error != null) { + mPlaybackState.setErrorMessage(error.getLocalizedMessage()); + } + mPerformer.setPlaybackState(mPlaybackState); + if (mListener != null) { + mListener.onPlayStateChanged(mPlaybackState); + } } @Override public void onStateChanged(int newState) { - mSession.setPlaybackState(newState); + if (newState != Renderer.STATE_ERROR) { + mPlaybackState.setErrorMessage(null); + } + switch (newState) { + case Renderer.STATE_ENDED: + case Renderer.STATE_STOPPED: + mPlaybackState.setState(PlaybackState.PLAYSTATE_STOPPED); + break; + case Renderer.STATE_INIT: + case Renderer.STATE_PREPARING: + mPlaybackState.setState(PlaybackState.PLAYSTATE_BUFFERING); + break; + case Renderer.STATE_ERROR: + mPlaybackState.setState(PlaybackState.PLAYSTATE_ERROR); + break; + case Renderer.STATE_PAUSED: + mPlaybackState.setState(PlaybackState.PLAYSTATE_PAUSED); + break; + case Renderer.STATE_PLAYING: + mPlaybackState.setState(PlaybackState.PLAYSTATE_PLAYING); + break; + default: + mPlaybackState.setState(PlaybackState.PLAYSTATE_ERROR); + mPlaybackState.setErrorMessage("unkown state"); + break; + } + mPlaybackState.setPosition(mRenderer.getSeekPosition()); + mPerformer.setPlaybackState(mPlaybackState); + if (mListener != null) { + mListener.onPlayStateChanged(mPlaybackState); + } } @Override @@ -84,7 +158,13 @@ public class PlayerSession { @Override public void onFocusLost() { - mSession.setPlaybackState(Renderer.STATE_PAUSED); + Log.d(TAG, "Focus lost, changing state to " + Renderer.STATE_PAUSED); + mPlaybackState.setState(PlaybackState.PLAYSTATE_PAUSED); + mPlaybackState.setPosition(mRenderer.getSeekPosition()); + mPerformer.setPlaybackState(mPlaybackState); + if (mListener != null) { + mListener.onPlayStateChanged(mPlaybackState); + } } @Override @@ -93,7 +173,7 @@ public class PlayerSession { } - protected class ControllerCb extends MediaSession.Callback { + private class ControllerCb extends MediaSession.Callback { @Override public void onMediaButton(Intent mediaRequestIntent) { @@ -114,4 +194,16 @@ public class PlayerSession { } } + private class TransportListener extends TransportPerformer.Listener { + @Override + public void onPlay() { + mRenderer.onPlay(); + } + + @Override + public void onPause() { + mRenderer.onPause(); + } + } + } diff --git a/tests/OneMedia/src/com/android/onemedia/playback/LocalRenderer.java b/tests/OneMedia/src/com/android/onemedia/playback/LocalRenderer.java index 7493366..7f62f66 100644 --- a/tests/OneMedia/src/com/android/onemedia/playback/LocalRenderer.java +++ b/tests/OneMedia/src/com/android/onemedia/playback/LocalRenderer.java @@ -499,11 +499,12 @@ public class LocalRenderer extends Renderer implements OnPreparedListener, @Override public boolean onPause() { MediaPlayer player = mPlayer; + // If the user paused us make sure we won't start playing again until + // asked to + mPlayOnReady = false; if (player != null && (mState & CAN_PAUSE) != 0) { player.pause(); setState(STATE_PAUSED); - } else if ((mState & CAN_READY_PLAY) != 0) { - mPlayOnReady = false; } else if (!isPaused()) { return false; } |