diff options
Diffstat (limited to 'media')
-rw-r--r-- | media/java/android/media/Rating.java | 7 | ||||
-rw-r--r-- | media/java/android/media/session/IMediaController.aidl | 23 | ||||
-rw-r--r-- | media/java/android/media/session/IMediaControllerCallback.aidl | 8 | ||||
-rw-r--r-- | media/java/android/media/session/IMediaSession.aidl | 16 | ||||
-rw-r--r-- | media/java/android/media/session/IMediaSessionCallback.aidl | 16 | ||||
-rw-r--r-- | media/java/android/media/session/MediaController.java | 278 | ||||
-rw-r--r-- | media/java/android/media/session/MediaMetadata.aidl | 18 | ||||
-rw-r--r-- | media/java/android/media/session/MediaMetadata.java | 404 | ||||
-rw-r--r-- | media/java/android/media/session/MediaSession.java | 334 | ||||
-rw-r--r-- | media/java/android/media/session/PlaybackState.aidl | 18 | ||||
-rw-r--r-- | media/java/android/media/session/PlaybackState.java | 351 | ||||
-rw-r--r-- | media/java/android/media/session/RouteInterface.java | 187 | ||||
-rw-r--r-- | media/java/android/media/session/RouteTransportControls.java | 230 | ||||
-rw-r--r-- | media/java/android/media/session/TransportController.java | 342 | ||||
-rw-r--r-- | media/java/android/media/session/TransportPerformer.java | 357 |
15 files changed, 2380 insertions, 209 deletions
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; + } + } + } +} |