summaryrefslogtreecommitdiffstats
path: root/media
diff options
context:
space:
mode:
authorRoboErik <epastern@google.com>2014-02-24 18:02:08 -0800
committerRoboErik <epastern@google.com>2014-03-12 15:09:42 -0700
commit8ae0f34db936a649ddaf9cdd086c224f6514efeb (patch)
tree6699d2481e68c205dc62a35c1c434f05d721c03f /media
parent1ebd4ad6cd645830d1eca12b2108c6ab4327c108 (diff)
downloadframeworks_base-8ae0f34db936a649ddaf9cdd086c224f6514efeb.zip
frameworks_base-8ae0f34db936a649ddaf9cdd086c224f6514efeb.tar.gz
frameworks_base-8ae0f34db936a649ddaf9cdd086c224f6514efeb.tar.bz2
Adds a TransportController and TransportPerformer to session
This makes transport controls a primitive interface on sessions with a way to create the performer, register callbacks, and send commands and updates between controllers and performers. This still needs some cleanup but has been tested with OneMedia. Change-Id: I373d35f7ccc383b8421bd14044457467d80425f3
Diffstat (limited to 'media')
-rw-r--r--media/java/android/media/Rating.java7
-rw-r--r--media/java/android/media/session/IMediaController.aidl23
-rw-r--r--media/java/android/media/session/IMediaControllerCallback.aidl8
-rw-r--r--media/java/android/media/session/IMediaSession.aidl16
-rw-r--r--media/java/android/media/session/IMediaSessionCallback.aidl16
-rw-r--r--media/java/android/media/session/MediaController.java278
-rw-r--r--media/java/android/media/session/MediaMetadata.aidl18
-rw-r--r--media/java/android/media/session/MediaMetadata.java404
-rw-r--r--media/java/android/media/session/MediaSession.java334
-rw-r--r--media/java/android/media/session/PlaybackState.aidl18
-rw-r--r--media/java/android/media/session/PlaybackState.java351
-rw-r--r--media/java/android/media/session/RouteInterface.java187
-rw-r--r--media/java/android/media/session/RouteTransportControls.java230
-rw-r--r--media/java/android/media/session/TransportController.java342
-rw-r--r--media/java/android/media/session/TransportPerformer.java357
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;
+ }
+ }
+ }
+}