summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Android.mk13
-rw-r--r--api/current.txt243
-rw-r--r--core/java/android/app/ContextImpl.java4
-rw-r--r--core/java/android/content/Context.java4
-rw-r--r--core/res/AndroidManifest.xml8
-rw-r--r--core/res/res/values/strings.xml6
-rw-r--r--media/java/android/media/routeprovider/IRouteConnection.aidl28
-rw-r--r--media/java/android/media/routeprovider/IRouteProvider.aidl36
-rw-r--r--media/java/android/media/routeprovider/IRouteProviderCallback.aidl32
-rw-r--r--media/java/android/media/routeprovider/RouteConnection.java164
-rw-r--r--media/java/android/media/routeprovider/RouteInterfaceHandler.java245
-rw-r--r--media/java/android/media/routeprovider/RoutePlaybackControlsHandler.java221
-rw-r--r--media/java/android/media/routeprovider/RouteProviderService.java227
-rw-r--r--media/java/android/media/routeprovider/RouteRequest.aidl18
-rw-r--r--media/java/android/media/routeprovider/RouteRequest.java96
-rw-r--r--media/java/android/media/session/ISession.aidl (renamed from media/java/android/media/session/IMediaSession.aidl)20
-rw-r--r--media/java/android/media/session/ISessionCallback.aidl (renamed from media/java/android/media/session/IMediaSessionCallback.aidl)12
-rw-r--r--media/java/android/media/session/ISessionController.aidl (renamed from media/java/android/media/session/IMediaController.aidl)9
-rw-r--r--media/java/android/media/session/ISessionControllerCallback.aidl (renamed from media/java/android/media/session/IMediaControllerCallback.aidl)5
-rw-r--r--media/java/android/media/session/ISessionManager.aidl (renamed from media/java/android/media/session/IMediaSessionManager.aidl)8
-rw-r--r--media/java/android/media/session/PlaybackState.java11
-rw-r--r--media/java/android/media/session/Route.java99
-rw-r--r--media/java/android/media/session/RouteCommand.aidl (renamed from media/java/android/media/session/MediaSessionToken.aidl)2
-rw-r--r--media/java/android/media/session/RouteCommand.java117
-rw-r--r--media/java/android/media/session/RouteEvent.aidl18
-rw-r--r--media/java/android/media/session/RouteEvent.java120
-rw-r--r--media/java/android/media/session/RouteInfo.aidl18
-rw-r--r--media/java/android/media/session/RouteInfo.java233
-rw-r--r--media/java/android/media/session/RouteInterface.java219
-rw-r--r--media/java/android/media/session/RouteOptions.aidl18
-rw-r--r--media/java/android/media/session/RouteOptions.java163
-rw-r--r--media/java/android/media/session/RoutePlaybackControls.java161
-rw-r--r--media/java/android/media/session/RouteTransportControls.java230
-rw-r--r--media/java/android/media/session/Session.java (renamed from media/java/android/media/session/MediaSession.java)265
-rw-r--r--media/java/android/media/session/SessionController.java (renamed from media/java/android/media/session/MediaController.java)64
-rw-r--r--media/java/android/media/session/SessionInfo.java82
-rw-r--r--media/java/android/media/session/SessionManager.java (renamed from media/java/android/media/session/MediaSessionManager.java)28
-rw-r--r--media/java/android/media/session/SessionToken.aidl18
-rw-r--r--media/java/android/media/session/SessionToken.java (renamed from media/java/android/media/session/MediaSessionToken.java)26
-rw-r--r--media/java/android/media/session/TransportController.java4
-rw-r--r--media/java/android/media/session/TransportPerformer.java4
-rw-r--r--services/core/java/com/android/server/media/MediaRouteProviderProxy.java379
-rw-r--r--services/core/java/com/android/server/media/MediaRouteProviderWatcher.java229
-rw-r--r--services/core/java/com/android/server/media/MediaSessionRecord.java319
-rw-r--r--services/core/java/com/android/server/media/MediaSessionService.java146
-rw-r--r--services/core/java/com/android/server/media/RouteConnectionRecord.java108
-rw-r--r--tests/OneMedia/AndroidManifest.xml9
-rw-r--r--tests/OneMedia/res/layout/activity_one_player.xml6
-rw-r--r--tests/OneMedia/res/values/strings.xml1
-rw-r--r--tests/OneMedia/src/com/android/onemedia/IPlayerCallback.aidl4
-rw-r--r--tests/OneMedia/src/com/android/onemedia/IPlayerService.aidl4
-rw-r--r--tests/OneMedia/src/com/android/onemedia/OnePlayerActivity.java12
-rw-r--r--tests/OneMedia/src/com/android/onemedia/PlayerController.java21
-rw-r--r--tests/OneMedia/src/com/android/onemedia/PlayerService.java4
-rw-r--r--tests/OneMedia/src/com/android/onemedia/PlayerSession.java100
-rw-r--r--tests/OneMedia/src/com/android/onemedia/playback/LocalRenderer.java17
-rw-r--r--tests/OneMedia/src/com/android/onemedia/playback/MediaItem.java15
-rw-r--r--tests/OneMedia/src/com/android/onemedia/playback/OneMRPRenderer.java44
-rw-r--r--tests/OneMedia/src/com/android/onemedia/playback/PlaybackError.java15
-rw-r--r--tests/OneMedia/src/com/android/onemedia/playback/Renderer.java48
-rw-r--r--tests/OneMedia/src/com/android/onemedia/playback/RendererFactory.java22
-rw-r--r--tests/OneMedia/src/com/android/onemedia/playback/RequestUtils.java15
-rw-r--r--tests/OneMedia/src/com/android/onemedia/provider/OneMediaRouteProvider.java204
63 files changed, 4352 insertions, 669 deletions
diff --git a/Android.mk b/Android.mk
index adc9ef1..c2910fd 100644
--- a/Android.mk
+++ b/Android.mk
@@ -288,11 +288,14 @@ LOCAL_SRC_FILES += \
media/java/android/media/IRemoteDisplayProvider.aidl \
media/java/android/media/IRemoteVolumeObserver.aidl \
media/java/android/media/IRingtonePlayer.aidl \
- media/java/android/media/session/IMediaController.aidl \
- media/java/android/media/session/IMediaControllerCallback.aidl \
- media/java/android/media/session/IMediaSession.aidl \
- media/java/android/media/session/IMediaSessionCallback.aidl \
- media/java/android/media/session/IMediaSessionManager.aidl \
+ media/java/android/media/routeprovider/IRouteConnection.aidl \
+ media/java/android/media/routeprovider/IRouteProvider.aidl \
+ media/java/android/media/routeprovider/IRouteProviderCallback.aidl \
+ media/java/android/media/session/ISessionController.aidl \
+ media/java/android/media/session/ISessionControllerCallback.aidl \
+ media/java/android/media/session/ISession.aidl \
+ media/java/android/media/session/ISessionCallback.aidl \
+ media/java/android/media/session/ISessionManager.aidl \
telephony/java/com/android/internal/telephony/IPhoneStateListener.aidl \
telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl \
telephony/java/com/android/internal/telephony/ITelephony.aidl \
diff --git a/api/current.txt b/api/current.txt
index 9edc6d3..fd111e1 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -27,6 +27,7 @@ package android {
field public static final java.lang.String BIND_NOTIFICATION_LISTENER_SERVICE = "android.permission.BIND_NOTIFICATION_LISTENER_SERVICE";
field public static final java.lang.String BIND_PRINT_SERVICE = "android.permission.BIND_PRINT_SERVICE";
field public static final java.lang.String BIND_REMOTEVIEWS = "android.permission.BIND_REMOTEVIEWS";
+ field public static final java.lang.String BIND_ROUTE_PROVIDER = "android.permission.BIND_ROUTE_PROVIDER";
field public static final java.lang.String BIND_TEXT_SERVICE = "android.permission.BIND_TEXT_SERVICE";
field public static final java.lang.String BIND_TRUST_AGENT_SERVICE = "android.permission.BIND_TRUST_AGENT_SERVICE";
field public static final java.lang.String BIND_TV_INPUT = "android.permission.BIND_TV_INPUT";
@@ -14924,24 +14925,68 @@ package android.media.effect {
}
-package android.media.session {
+package android.media.routeprovider {
- public final class MediaController {
- method public void addCallback(android.media.session.MediaController.Callback);
- method public void addCallback(android.media.session.MediaController.Callback, android.os.Handler);
- method public static android.media.session.MediaController fromToken(android.media.session.MediaSessionToken);
- method public android.media.session.TransportController getTransportController();
- method public void removeCallback(android.media.session.MediaController.Callback);
- method public void sendCommand(java.lang.String, android.os.Bundle, android.os.ResultReceiver);
- method public void sendMediaButton(int);
+ public final class RouteConnection {
+ ctor public RouteConnection(android.media.routeprovider.RouteProviderService, android.media.session.RouteInfo);
+ method public android.media.routeprovider.RouteInterfaceHandler addRouteInterface(java.lang.String);
+ method public android.media.routeprovider.RouteInterfaceHandler getRouteInterface(java.lang.String);
+ method public void shutDown();
}
- public static abstract class MediaController.Callback {
- ctor public MediaController.Callback();
- method public void onEvent(java.lang.String, android.os.Bundle);
- method public void onRouteChanged(android.os.Bundle);
+ public final class RouteInterfaceHandler {
+ method public void addListener(android.media.routeprovider.RouteInterfaceHandler.CommandListener, android.os.Handler);
+ method public java.lang.String getName();
+ method public void removeListener(android.media.routeprovider.RouteInterfaceHandler.CommandListener);
+ method public void sendEvent(java.lang.String, android.os.Bundle);
+ method public static void sendResult(android.os.ResultReceiver, int, android.os.Bundle);
+ }
+
+ public static abstract class RouteInterfaceHandler.CommandListener {
+ ctor public RouteInterfaceHandler.CommandListener();
+ method public abstract boolean onCommand(android.media.routeprovider.RouteInterfaceHandler, java.lang.String, android.os.Bundle, android.os.ResultReceiver);
+ }
+
+ public final class RoutePlaybackControlsHandler {
+ method public void addListener(android.media.routeprovider.RoutePlaybackControlsHandler.Listener);
+ method public void addListener(android.media.routeprovider.RoutePlaybackControlsHandler.Listener, android.os.Handler);
+ method public static android.media.routeprovider.RoutePlaybackControlsHandler addTo(android.media.routeprovider.RouteConnection);
+ method public void removeListener(android.media.routeprovider.RoutePlaybackControlsHandler.Listener);
+ method public void sendPlaybackChangeEvent(int);
+ }
+
+ public static abstract class RoutePlaybackControlsHandler.Listener extends android.media.routeprovider.RouteInterfaceHandler.CommandListener {
+ ctor public RoutePlaybackControlsHandler.Listener();
+ method public boolean fastForward();
+ method public long getCapabilities();
+ method public long getCurrentPosition();
+ method public final boolean onCommand(android.media.routeprovider.RouteInterfaceHandler, java.lang.String, android.os.Bundle, android.os.ResultReceiver);
+ method public boolean pause();
+ method public void playNow(java.lang.String, android.os.ResultReceiver);
+ method public boolean resume();
}
+ public abstract class RouteProviderService extends android.app.Service {
+ ctor public RouteProviderService();
+ method public abstract android.media.routeprovider.RouteConnection connect(android.media.session.RouteInfo, android.media.routeprovider.RouteRequest);
+ method public abstract java.util.List<android.media.session.RouteInfo> getMatchingRoutes(java.util.List<android.media.routeprovider.RouteRequest>);
+ method public android.os.IBinder onBind(android.content.Intent);
+ method public void updateDiscoveryRequests(java.util.List<android.media.routeprovider.RouteRequest>);
+ field public static final java.lang.String SERVICE_INTERFACE = "com.android.media.session.MediaRouteProvider";
+ }
+
+ public final class RouteRequest implements android.os.Parcelable {
+ method public int describeContents();
+ method public android.media.session.RouteOptions getConnectionOptions();
+ method public android.media.session.SessionInfo getSessionInfo();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator CREATOR;
+ }
+
+}
+
+package android.media.session {
+
public final class MediaMetadata implements android.os.Parcelable {
method public int describeContents();
method public android.graphics.Bitmap getBitmap(java.lang.String);
@@ -14982,36 +15027,6 @@ package android.media.session {
method public android.media.session.MediaMetadata.Builder putString(java.lang.String, java.lang.String);
}
- public final class MediaSession {
- method public void addCallback(android.media.session.MediaSession.Callback);
- method public void addCallback(android.media.session.MediaSession.Callback, android.os.Handler);
- method public android.media.session.MediaSessionToken getSessionToken();
- method public android.media.session.TransportPerformer getTransportPerformer();
- method public void publish();
- method public void release();
- method public void removeCallback(android.media.session.MediaSession.Callback);
- method public void sendEvent(java.lang.String, android.os.Bundle);
- method public android.media.session.TransportPerformer setTransportPerformerEnabled();
- }
-
- public static abstract class MediaSession.Callback {
- ctor public MediaSession.Callback();
- method public void onCommand(java.lang.String, android.os.Bundle, android.os.ResultReceiver);
- method public void onMediaButton(android.content.Intent);
- method public void onRequestRouteChange(android.os.Bundle);
- }
-
- public final class MediaSessionManager {
- method public android.media.session.MediaSession createSession(java.lang.String);
- method public java.util.List<android.media.session.MediaController> getActiveSessions();
- }
-
- public class MediaSessionToken implements android.os.Parcelable {
- method public int describeContents();
- method public void writeToParcel(android.os.Parcel, int);
- field public static final android.os.Parcelable.Creator CREATOR;
- }
-
public final class PlaybackState implements android.os.Parcelable {
ctor public PlaybackState();
ctor public PlaybackState(android.media.session.PlaybackState);
@@ -15040,6 +15055,7 @@ package android.media.session {
field public static final long ACTION_STOP = 1L; // 0x1L
field public static final android.os.Parcelable.Creator CREATOR;
field public static final int PLAYSTATE_BUFFERING = 6; // 0x6
+ field public static final int PLAYSTATE_CONNECTING = 8; // 0x8
field public static final int PLAYSTATE_ERROR = 7; // 0x7
field public static final int PLAYSTATE_FAST_FORWARDING = 4; // 0x4
field public static final int PLAYSTATE_NONE = 0; // 0x0
@@ -15049,11 +15065,44 @@ package android.media.session {
field public static final int PLAYSTATE_STOPPED = 1; // 0x1
}
+ public final class Route {
+ method public android.media.session.RouteInterface getInterface(java.lang.String);
+ method public android.media.session.RouteOptions getOptions();
+ method public android.media.session.RouteInfo getRouteInfo();
+ }
+
+ public final class RouteInfo implements android.os.Parcelable {
+ method public int describeContents();
+ method public java.util.List<android.media.session.RouteOptions> getConnectionMethods();
+ method public java.lang.String getId();
+ method public java.lang.String getName();
+ method public java.lang.String getProvider();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator CREATOR;
+ }
+
+ public static final class RouteInfo.Builder {
+ ctor public RouteInfo.Builder(android.media.session.RouteInfo);
+ ctor public RouteInfo.Builder();
+ method public android.media.session.RouteInfo.Builder addRouteOptions(android.media.session.RouteOptions);
+ method public android.media.session.RouteInfo build();
+ method public android.media.session.RouteInfo.Builder clearRouteOptions();
+ method public int getOptionsSize();
+ method public android.media.session.RouteInfo.Builder setId(java.lang.String);
+ method public android.media.session.RouteInfo.Builder setName(java.lang.String);
+ }
+
public final class RouteInterface {
method public void addListener(android.media.session.RouteInterface.EventListener);
method public void addListener(android.media.session.RouteInterface.EventListener, android.os.Handler);
method public void removeListener(android.media.session.RouteInterface.EventListener);
- method public void sendCommand(java.lang.String, android.os.Bundle, android.os.ResultReceiver);
+ method public boolean sendCommand(java.lang.String, android.os.Bundle, android.os.ResultReceiver);
+ field public static final int RESULT_COMMAND_NOT_SUPPORTED = -3; // 0xfffffffd
+ field public static final int RESULT_ERROR = -1; // 0xffffffff
+ field public static final int RESULT_INTERFACE_NOT_SUPPORTED = -2; // 0xfffffffe
+ field public static final int RESULT_NOT_CONNECTED = -5; // 0xfffffffb
+ field public static final int RESULT_ROUTE_IS_STALE = -4; // 0xfffffffc
+ field public static final int RESULT_SUCCESS = 1; // 0x1
}
public static abstract class RouteInterface.EventListener {
@@ -15061,40 +15110,100 @@ package android.media.session {
method public abstract void onEvent(java.lang.String, android.os.Bundle);
}
- public static abstract class RouteInterface.Stub {
- ctor public RouteInterface.Stub();
- method public abstract java.lang.String getName();
- method public abstract void onCommand(java.lang.String, android.os.Bundle, android.os.ResultReceiver);
- method public final void sendEvent(android.media.session.MediaSession, java.lang.String, android.os.Bundle);
+ public final class RouteOptions implements android.os.Parcelable {
+ method public int describeContents();
+ method public android.os.Bundle getConnectionParams();
+ method public java.util.List<java.lang.String> getInterfaceNames();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator CREATOR;
}
- public final class RouteTransportControls {
- method public void addListener(android.media.session.RouteTransportControls.Listener);
- method public void addListener(android.media.session.RouteTransportControls.Listener, android.os.Handler);
- method public void fastForward(float);
- method public static android.media.session.RouteTransportControls from(android.media.session.MediaController);
+ public static final class RouteOptions.Builder {
+ ctor public RouteOptions.Builder();
+ method public android.media.session.RouteOptions.Builder addInterface(java.lang.String);
+ method public android.media.session.RouteOptions build();
+ method public android.media.session.RouteOptions.Builder setParameters(android.os.Bundle);
+ }
+
+ public final class RoutePlaybackControls {
+ method public void addListener(android.media.session.RoutePlaybackControls.Listener);
+ method public void addListener(android.media.session.RoutePlaybackControls.Listener, android.os.Handler);
+ method public void fastForward();
+ method public static android.media.session.RoutePlaybackControls from(android.media.session.Route);
method public void getCapabilities(android.os.ResultReceiver);
method public void getCurrentPosition(android.os.ResultReceiver);
method public void pause();
- method public void play();
- method public void removeListener(android.media.session.RouteTransportControls.Listener);
- field public static final java.lang.String NAME = "android.media.session.RouteTransportControls";
+ method public void playNow(java.lang.String);
+ method public void removeListener(android.media.session.RoutePlaybackControls.Listener);
+ method public void resume();
+ field public static final java.lang.String NAME = "android.media.session.RoutePlaybackControls";
}
- public static abstract class RouteTransportControls.Listener {
- ctor public RouteTransportControls.Listener();
- method public void onMetadataUpdate(android.os.Bundle);
+ public static abstract class RoutePlaybackControls.Listener extends android.media.session.RouteInterface.EventListener {
+ ctor public RoutePlaybackControls.Listener();
+ method public final void onEvent(java.lang.String, android.os.Bundle);
+ method public void onMetadataUpdate(android.media.session.MediaMetadata);
method public void onPlaybackStateChange(int);
}
- public static abstract class RouteTransportControls.Stub extends android.media.session.RouteInterface.Stub {
- ctor public RouteTransportControls.Stub(android.media.session.MediaSession);
- method public void fastForward(float);
- method public long getCapabilities();
- method public long getCurrentPosition();
- method public java.lang.String getName();
+ public final class Session {
+ method public void addCallback(android.media.session.Session.Callback);
+ method public void addCallback(android.media.session.Session.Callback, android.os.Handler);
+ method public void connect(android.media.session.RouteInfo, android.media.session.RouteOptions);
+ method public void disconnect(android.media.session.RouteInfo);
+ method public android.media.session.SessionToken getSessionToken();
+ method public android.media.session.TransportPerformer getTransportPerformer();
+ method public void publish();
+ method public void release();
+ method public void removeCallback(android.media.session.Session.Callback);
+ method public void sendEvent(java.lang.String, android.os.Bundle);
+ method public void setRouteOptions(java.util.List<android.media.session.RouteOptions>);
+ method public android.media.session.TransportPerformer setTransportPerformerEnabled();
+ }
+
+ public static abstract class Session.Callback {
+ ctor public Session.Callback();
method public void onCommand(java.lang.String, android.os.Bundle, android.os.ResultReceiver);
- method public final void updatePlaybackState(int);
+ method public void onMediaButton(android.content.Intent);
+ method public void onRequestRouteChange(android.media.session.RouteInfo);
+ method public void onRouteConnected(android.media.session.Route);
+ method public void onRouteDisconnected(android.media.session.Route, int);
+ }
+
+ public final class SessionController {
+ method public void addCallback(android.media.session.SessionController.Callback);
+ method public void addCallback(android.media.session.SessionController.Callback, android.os.Handler);
+ method public static android.media.session.SessionController fromToken(android.media.session.SessionToken);
+ method public android.media.session.TransportController getTransportController();
+ method public void removeCallback(android.media.session.SessionController.Callback);
+ method public void sendCommand(java.lang.String, android.os.Bundle, android.os.ResultReceiver);
+ method public void sendMediaButton(int);
+ method public void showRoutePicker();
+ }
+
+ public static abstract class SessionController.Callback {
+ ctor public SessionController.Callback();
+ method public void onEvent(java.lang.String, android.os.Bundle);
+ method public void onRouteChanged(android.media.session.RouteInfo);
+ }
+
+ public final class SessionInfo implements android.os.Parcelable {
+ method public int describeContents();
+ method public java.lang.String getId();
+ method public java.lang.String getPackageName();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator CREATOR;
+ }
+
+ public final class SessionManager {
+ method public android.media.session.Session createSession(java.lang.String);
+ method public java.util.List<android.media.session.SessionController> getActiveSessions();
+ }
+
+ public class SessionToken implements android.os.Parcelable {
+ method public int describeContents();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator CREATOR;
}
public final class TransportController {
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index 77b5485..f1ce54a 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -67,7 +67,7 @@ import android.location.ILocationManager;
import android.location.LocationManager;
import android.media.AudioManager;
import android.media.MediaRouter;
-import android.media.session.MediaSessionManager;
+import android.media.session.SessionManager;
import android.net.ConnectivityManager;
import android.net.IConnectivityManager;
import android.net.INetworkPolicyManager;
@@ -639,7 +639,7 @@ class ContextImpl extends Context {
registerService(MEDIA_SESSION_SERVICE, new ServiceFetcher() {
public Object createService(ContextImpl ctx) {
- return new MediaSessionManager(ctx);
+ return new SessionManager(ctx);
}
});
registerService(TRUST_SERVICE, new ServiceFetcher() {
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index ff92d82..906484a 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -2387,10 +2387,10 @@ public abstract class Context {
/**
* Use with {@link #getSystemService} to retrieve a
- * {@link android.media.session.MediaSessionManager} for managing media Sessions.
+ * {@link android.media.session.SessionManager} for managing media Sessions.
*
* @see #getSystemService
- * @see android.media.session.MediaSessionManager
+ * @see android.media.session.SessionManager
*/
public static final String MEDIA_SESSION_SERVICE = "media_session";
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 606a4b1..66f947b 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2070,6 +2070,14 @@
android:description="@string/permdesc_bindTvInput"
android:protectionLevel="signature|system" />
+ <!-- Must be required by a {@link android.media.routeprovider.RouteProviderService}
+ to ensure that only the system can interact with it.
+ -->
+ <permission android:name="android.permission.BIND_ROUTE_PROVIDER"
+ android:label="@string/permlab_bindRouteProvider"
+ android:description="@string/permdesc_bindRouteProvider"
+ android:protectionLevel="signature" />
+
<!-- Must be required by device administration receiver, to ensure that only the
system can interact with it. -->
<permission android:name="android.permission.BIND_DEVICE_ADMIN"
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index b0e1150..cacb41f 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -1077,6 +1077,12 @@
interface of a widget service. Should never be needed for normal apps.</string>
<!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+ <string name="permlab_bindRouteProvider">bind to a route provider service</string>
+ <!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
+ <string name="permdesc_bindRouteProvider">Allows the holder to bind to any registered
+ route providers. Should never be needed for normal apps.</string>
+
+ <!-- Title of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
<string name="permlab_bindDeviceAdmin">interact with a device admin</string>
<!-- Description of an application permission, listed so the user can choose whether they want to allow the application to do this. -->
<string name="permdesc_bindDeviceAdmin">Allows the holder to send intents to
diff --git a/media/java/android/media/routeprovider/IRouteConnection.aidl b/media/java/android/media/routeprovider/IRouteConnection.aidl
new file mode 100644
index 0000000..15c8039
--- /dev/null
+++ b/media/java/android/media/routeprovider/IRouteConnection.aidl
@@ -0,0 +1,28 @@
+/* Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media.routeprovider;
+
+import android.media.session.RouteCommand;
+import android.os.ResultReceiver;
+
+/**
+ * Interface for a specific connected route.
+ * @hide
+ */
+oneway interface IRouteConnection {
+ void onCommand(in RouteCommand command, in ResultReceiver cb);
+ void disconnect();
+} \ No newline at end of file
diff --git a/media/java/android/media/routeprovider/IRouteProvider.aidl b/media/java/android/media/routeprovider/IRouteProvider.aidl
new file mode 100644
index 0000000..c36f6a7
--- /dev/null
+++ b/media/java/android/media/routeprovider/IRouteProvider.aidl
@@ -0,0 +1,36 @@
+/* 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.routeprovider;
+
+import android.media.routeprovider.IRouteConnection;
+import android.media.routeprovider.IRouteProviderCallback;
+import android.media.routeprovider.RouteRequest;
+import android.media.session.RouteInfo;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+
+/**
+ * Interface to an app's RouteProviderService.
+ * @hide
+ */
+oneway interface IRouteProvider {
+ void registerCallback(in IRouteProviderCallback cb);
+ void unregisterCallback(in IRouteProviderCallback cb);
+ void updateDiscoveryRequests(in List<RouteRequest> requests);
+
+ void getAvailableRoutes(in List<RouteRequest> requests, in ResultReceiver cb);
+ void connect(in RouteInfo route, in RouteRequest request, in ResultReceiver cb);
+} \ No newline at end of file
diff --git a/media/java/android/media/routeprovider/IRouteProviderCallback.aidl b/media/java/android/media/routeprovider/IRouteProviderCallback.aidl
new file mode 100644
index 0000000..9185347
--- /dev/null
+++ b/media/java/android/media/routeprovider/IRouteProviderCallback.aidl
@@ -0,0 +1,32 @@
+/* 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.routeprovider;
+
+import android.media.routeprovider.IRouteConnection;
+import android.media.session.RouteEvent;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+
+/**
+ * System's provider callback interface.
+ * @hide
+ */
+oneway interface IRouteProviderCallback {
+ void onRoutesChanged();
+ void onConnectionStateChanged(in IRouteConnection connection, int state);
+ void onConnectionTerminated(in IRouteConnection connection);
+ void onRouteEvent(in RouteEvent event);
+} \ No newline at end of file
diff --git a/media/java/android/media/routeprovider/RouteConnection.java b/media/java/android/media/routeprovider/RouteConnection.java
new file mode 100644
index 0000000..9214ff8
--- /dev/null
+++ b/media/java/android/media/routeprovider/RouteConnection.java
@@ -0,0 +1,164 @@
+/*
+ * 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.routeprovider;
+
+import android.media.routeprovider.IRouteConnection;
+import android.media.session.RouteCommand;
+import android.media.session.RouteEvent;
+import android.media.session.RouteInfo;
+import android.media.session.RouteInterface;
+import android.os.Bundle;
+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;
+import java.util.List;
+
+/**
+ * Represents an ongoing connection between an application and a media route
+ * offered by a media route provider.
+ * <p>
+ * The media route provider should add interfaces to the connection before
+ * returning it to the system in order to receive commands from clients on those
+ * interfaces. Use {@link #addRouteInterface(String)} to add an interface and
+ * {@link #getRouteInterface(String)} to retrieve the interface's handle anytime
+ * after it has been added.
+ */
+public final class RouteConnection {
+ private static final String TAG = "RouteConnection";
+ private final ConnectionStub mBinder;
+ private final ArrayList<String> mIfaceNames = new ArrayList<String>();
+ private final ArrayMap<String, RouteInterfaceHandler> mIfaces
+ = new ArrayMap<String, RouteInterfaceHandler>();
+ private final RouteProviderService mProvider;
+ private final RouteInfo mRoute;
+
+ private boolean mPublished;
+
+ /**
+ * Create a new connection for the given Provider and Route.
+ *
+ * @param provider The provider this route is associated with.
+ * @param route The route this is a connection to.
+ */
+ public RouteConnection(RouteProviderService provider, RouteInfo route) {
+ if (provider == null) {
+ throw new IllegalArgumentException("provider may not be null.");
+ }
+ if (route == null) {
+ throw new IllegalArgumentException("route may not be null.");
+ }
+ mBinder = new ConnectionStub(this);
+ mProvider = provider;
+ mRoute = route;
+ }
+
+ /**
+ * Add an interface to this route connection. All interfaces must be added
+ * to the connection before the connection is returned to the system.
+ *
+ * @param ifaceName The name of the interface to add
+ * @return The route interface that was registered
+ */
+ public RouteInterfaceHandler addRouteInterface(String ifaceName) {
+ if (TextUtils.isEmpty(ifaceName)) {
+ throw new IllegalArgumentException("The interface's name may not be empty");
+ }
+ if (mPublished) {
+ throw new IllegalStateException(
+ "Connection has already been published to the system.");
+ }
+ RouteInterfaceHandler iface = mIfaces.get(ifaceName);
+ if (iface == null) {
+ iface = new RouteInterfaceHandler(this, ifaceName);
+ mIfaceNames.add(ifaceName);
+ mIfaces.put(ifaceName, iface);
+ } else {
+ Log.w(TAG, "Attempted to add an interface that already exists");
+ }
+ return iface;
+ }
+
+ /**
+ * Get the interface instance for the specified interface name. If the
+ * interface was not added to this connection null will be returned.
+ *
+ * @param ifaceName The name of the interface to get.
+ * @return The route interface with that name or null.
+ */
+ public RouteInterfaceHandler getRouteInterface(String ifaceName) {
+ return mIfaces.get(ifaceName);
+ }
+
+ /**
+ * Close the connection and inform the system that it may no longer be used.
+ */
+ public void shutDown() {
+ mProvider.disconnect(this);
+ }
+
+ /**
+ * @hide
+ */
+ public void sendEvent(String iface, String event, Bundle extras) {
+ RouteEvent e = new RouteEvent(mBinder, iface, event, extras);
+ mProvider.sendRouteEvent(e);
+ }
+
+ /**
+ * @hide
+ */
+ IRouteConnection.Stub getBinder() {
+ return mBinder;
+ }
+
+ /**
+ * @hide
+ */
+ void publish() {
+ mPublished = true;
+ }
+
+ private static class ConnectionStub extends IRouteConnection.Stub {
+ private final WeakReference<RouteConnection> mConnection;
+
+ public ConnectionStub(RouteConnection connection) {
+ mConnection = new WeakReference<RouteConnection>(connection);
+ }
+
+ @Override
+ public void onCommand(RouteCommand command, ResultReceiver cb) {
+ RouteConnection connection = mConnection.get();
+ if (connection != null) {
+ RouteInterfaceHandler iface = connection.mIfaces.get(command.getIface());
+ if (iface != null) {
+ iface.onCommand(command.getEvent(), command.getExtras(), cb);
+ } else if (cb != null) {
+ cb.send(RouteInterface.RESULT_INTERFACE_NOT_SUPPORTED, null);
+ }
+ }
+ }
+
+ @Override
+ public void disconnect() {
+ // TODO
+ }
+ }
+}
diff --git a/media/java/android/media/routeprovider/RouteInterfaceHandler.java b/media/java/android/media/routeprovider/RouteInterfaceHandler.java
new file mode 100644
index 0000000..9693dc6
--- /dev/null
+++ b/media/java/android/media/routeprovider/RouteInterfaceHandler.java
@@ -0,0 +1,245 @@
+/*
+ * 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.routeprovider;
+
+import android.media.session.Route;
+import android.media.session.Session;
+import android.media.session.RouteInterface;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.ResultReceiver;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+/**
+ * Represents an interface that an application may use to send requests to a
+ * connected media route.
+ * <p>
+ * A {@link RouteProviderService} may expose multiple interfaces on a
+ * {@link RouteConnection} for a {@link Session} to interact with. A
+ * provider creates an interface with
+ * {@link RouteConnection#addRouteInterface(String)} to allow messages to be
+ * routed appropriately. Events are then sent through a specific interface and
+ * all commands being sent on the interface will be sent to any registered
+ * {@link CommandListener}s.
+ * <p>
+ * An interface instance can only be registered on one {@link RouteConnection}.
+ * To use the same interface on multiple connections a new instance must be
+ * created for each connection.
+ * <p>
+ * It is recommended you wrap this interface with a standard implementation to
+ * avoid errors, but for simple interfaces this class may be used directly. TODO
+ * add link to sample code.
+ */
+public final class RouteInterfaceHandler {
+ private static final String TAG = "RouteInterfaceHandler";
+
+ private final Object mLock = new Object();
+ private final RouteConnection mConnection;
+ private final String mName;
+
+ private ArrayList<MessageHandler> mListeners = new ArrayList<MessageHandler>();
+
+ /**
+ * Create a new RouteInterface for a given connection. This can be used to
+ * send events on the given interface and register listeners for commands
+ * from the connected session.
+ *
+ * @param connection The connection this interface sends events on
+ * @param ifaceName The name of this interface
+ * @hide
+ */
+ public RouteInterfaceHandler(RouteConnection connection, String ifaceName) {
+ if (connection == null) {
+ throw new IllegalArgumentException("connection may not be null");
+ }
+ if (TextUtils.isEmpty(ifaceName)) {
+ throw new IllegalArgumentException("ifaceName can not be empty");
+ }
+ mConnection = connection;
+ mName = ifaceName;
+ }
+
+ /**
+ * Send an event on this interface to the connected session.
+ *
+ * @param event The event to send
+ * @param extras Any extras for the event
+ */
+ public void sendEvent(String event, Bundle extras) {
+ mConnection.sendEvent(mName, event, extras);
+ }
+
+ /**
+ * Send a result from a command to the specified callback. The result codes
+ * in {@link RouteInterface} must be used. More information
+ * about the result, whether successful or an error, should be included in
+ * the extras.
+ *
+ * @param cb The callback to send the result to
+ * @param resultCode The result code for the call
+ * @param extras Any extras to include
+ */
+ public static void sendResult(ResultReceiver cb, int resultCode, Bundle extras) {
+ if (cb != null) {
+ cb.send(resultCode, extras);
+ }
+ }
+
+ /**
+ * Add a listener for this interface. If a handler is specified callbacks
+ * will be performed on the handler's thread, otherwise the callers thread
+ * will be used.
+ *
+ * @param listener The listener to receive calls on.
+ * @param handler The handler whose thread to post calls on or null.
+ */
+ public void addListener(CommandListener listener, Handler handler) {
+ if (listener == null) {
+ throw new IllegalArgumentException("listener may not be null");
+ }
+ Looper looper = handler != null ? handler.getLooper() : Looper.myLooper();
+ synchronized (mLock) {
+ if (findIndexOfListenerLocked(listener) != -1) {
+ Log.d(TAG, "Listener is already added, ignoring");
+ return;
+ }
+ mListeners.add(new MessageHandler(looper, listener));
+ }
+ }
+
+ /**
+ * Remove a listener from this interface.
+ *
+ * @param listener The listener to stop receiving commands on.
+ */
+ public void removeListener(CommandListener listener) {
+ if (listener == null) {
+ throw new IllegalArgumentException("listener may not be null");
+ }
+ synchronized (mLock) {
+ int index = findIndexOfListenerLocked(listener);
+ if (index != -1) {
+ mListeners.remove(index);
+ }
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public void onCommand(String command, Bundle args, ResultReceiver cb) {
+ synchronized (mLock) {
+ Command cmd = new Command(command, args, cb);
+ for (int i = mListeners.size() - 1; i >= 0; i--) {
+ mListeners.get(i).post(MessageHandler.MSG_COMMAND, cmd);
+ }
+ }
+ }
+
+ /**
+ * Get the interface name.
+ *
+ * @return The name of this interface
+ */
+ public String getName() {
+ return mName;
+ }
+
+ private int findIndexOfListenerLocked(CommandListener listener) {
+ if (listener == null) {
+ throw new IllegalArgumentException("Callback cannot be null");
+ }
+ for (int i = mListeners.size() - 1; i >= 0; i--) {
+ MessageHandler handler = mListeners.get(i);
+ if (listener == handler.mListener) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Handles commands sent to the interface.
+ * <p>
+ * Register an InterfaceListener using {@link #addListener}.
+ */
+ public abstract static class CommandListener {
+ /**
+ * This is called when a command is received that matches this
+ * interface. Commands are sent by a {@link Session} that is
+ * connected to the route this interface is registered with.
+ *
+ * @param iface The interface the command was received on.
+ * @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.
+ * @return true if the command was handled, false otherwise. If the
+ * command was not handled an error will be sent automatically.
+ * true may be returned if the command will be handled
+ * asynchronously.
+ * @see Route
+ * @see Session
+ */
+ public abstract boolean onCommand(RouteInterfaceHandler iface, String command, Bundle args,
+ ResultReceiver cb);
+ }
+
+ private class MessageHandler extends Handler {
+ private static final int MSG_COMMAND = 1;
+
+ private final CommandListener mListener;
+
+ public MessageHandler(Looper looper, CommandListener listener) {
+ super(looper, null, true /* async */);
+ mListener = listener;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_COMMAND:
+ Command cmd = (Command) msg.obj;
+ if (!mListener.onCommand(RouteInterfaceHandler.this, cmd.command, cmd.args, cmd.cb)) {
+ sendResult(cmd.cb, RouteInterface.RESULT_COMMAND_NOT_SUPPORTED,
+ null);
+ }
+ break;
+ }
+ }
+
+ public void post(int what, Object obj) {
+ obtainMessage(what, obj).sendToTarget();
+ }
+ }
+
+ private final static class Command {
+ public final String command;
+ public final Bundle args;
+ public final ResultReceiver cb;
+
+ public Command(String command, Bundle args, ResultReceiver cb) {
+ this.command = command;
+ this.args = args;
+ this.cb = cb;
+ }
+ }
+}
diff --git a/media/java/android/media/routeprovider/RoutePlaybackControlsHandler.java b/media/java/android/media/routeprovider/RoutePlaybackControlsHandler.java
new file mode 100644
index 0000000..dcef79a
--- /dev/null
+++ b/media/java/android/media/routeprovider/RoutePlaybackControlsHandler.java
@@ -0,0 +1,221 @@
+/*
+ * 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.routeprovider;
+
+import android.media.session.RoutePlaybackControls;
+import android.media.session.RouteInterface;
+import android.media.session.PlaybackState;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ResultReceiver;
+import android.text.TextUtils;
+import android.util.Log;
+
+/**
+ * Standard wrapper for using playback controls over a {@link RouteInterfaceHandler}.
+ * This is the provider half of the interface. Sessions should use
+ * {@link RoutePlaybackControls} to interact with this interface.
+ */
+public final class RoutePlaybackControlsHandler {
+ private static final String TAG = "RoutePlaybackControls";
+
+ private final RouteInterfaceHandler mIface;
+
+ private RoutePlaybackControlsHandler(RouteInterfaceHandler iface) {
+ mIface = iface;
+ }
+
+ /**
+ * Add this interface to the specified route and return a handle for
+ * communicating on the interface.
+ *
+ * @param connection The connection to register this interface on.
+ * @return A handle for communicating on this interface.
+ */
+ public static RoutePlaybackControlsHandler addTo(RouteConnection connection) {
+ if (connection == null) {
+ throw new IllegalArgumentException("connection may not be null");
+ }
+ RouteInterfaceHandler iface = connection
+ .addRouteInterface(RoutePlaybackControls.NAME);
+
+ return new RoutePlaybackControlsHandler(iface);
+ }
+
+ /**
+ * Add a {@link Listener} to this interface. The listener will receive
+ * commands on the caller's thread.
+ *
+ * @param listener The listener to send commands to.
+ */
+ public void addListener(Listener listener) {
+ addListener(listener, null);
+ }
+
+ /**
+ * Add a {@link Listener} to this interface. The listener will receive
+ * updates on the handler's thread. If no handler is specified the caller's
+ * thread will be used instead.
+ *
+ * @param listener The listener to send commands to.
+ * @param handler The handler whose thread calls should be posted on. May be
+ * null.
+ */
+ public void addListener(Listener listener, Handler handler) {
+ mIface.addListener(listener, handler);
+ }
+
+ /**
+ * Remove a {@link Listener} from this interface.
+ *
+ * @param listener The Listener to remove.
+ */
+ public void removeListener(Listener listener) {
+ mIface.removeListener(listener);
+ }
+
+ /**
+ * Publish the current playback state to the system and any controllers.
+ * Valid values are defined in {@link PlaybackState}. TODO create
+ * RoutePlaybackState.
+ *
+ * @param state
+ */
+ public void sendPlaybackChangeEvent(int state) {
+ Bundle extras = new Bundle();
+ extras.putInt(RoutePlaybackControls.KEY_VALUE1, state);
+ mIface.sendEvent(RoutePlaybackControls.EVENT_PLAYSTATE_CHANGE, extras);
+ }
+
+ /**
+ * Command handler for the RoutePlaybackControls interface. You can add a
+ * Listener to the interface using {@link #addListener}.
+ */
+ public static abstract class Listener extends RouteInterfaceHandler.CommandListener {
+
+ @Override
+ public final boolean onCommand(RouteInterfaceHandler iface, String method, Bundle extras,
+ ResultReceiver cb) {
+ if (RoutePlaybackControls.CMD_FAST_FORWARD.equals(method)) {
+ boolean success = fastForward();
+ // TODO specify type of error
+ RouteInterfaceHandler.sendResult(cb, success
+ ? RouteInterface.RESULT_SUCCESS
+ : RouteInterface.RESULT_ERROR, null);
+ return true;
+ } else if (RoutePlaybackControls.CMD_GET_CURRENT_POSITION.equals(method)) {
+ Bundle result = new Bundle();
+ result.putLong(RoutePlaybackControls.KEY_VALUE1, getCurrentPosition());
+ RouteInterfaceHandler.sendResult(cb, RouteInterface.RESULT_SUCCESS,
+ result);
+ return true;
+ } else if (RoutePlaybackControls.CMD_GET_CAPABILITIES.equals(method)) {
+ Bundle result = new Bundle();
+ result.putLong(RoutePlaybackControls.KEY_VALUE1, getCapabilities());
+ RouteInterfaceHandler.sendResult(cb, RouteInterface.RESULT_SUCCESS,
+ result);
+ return true;
+ } else if (RoutePlaybackControls.CMD_PLAY_NOW.equals(method)) {
+ playNow(extras.getString(RoutePlaybackControls.KEY_VALUE1, null), cb);
+ return true;
+ } else if (RoutePlaybackControls.CMD_RESUME.equals(method)) {
+ boolean success = resume();
+ RouteInterfaceHandler.sendResult(cb, success
+ ? RouteInterface.RESULT_SUCCESS
+ : RouteInterface.RESULT_ERROR, null);
+ return true;
+ } else if (RoutePlaybackControls.CMD_PAUSE.equals(method)) {
+ boolean success = pause();
+ RouteInterfaceHandler.sendResult(cb, success
+ ? RouteInterface.RESULT_SUCCESS
+ : RouteInterface.RESULT_ERROR, null);
+ return true;
+ } else {
+ // The command wasn't recognized
+ }
+ return false;
+ }
+
+ /**
+ * Override to handle fast forwarding.
+ *
+ * @return true if the request succeeded, false otherwise
+ */
+ public boolean fastForward() {
+ Log.w(TAG, "fastForward is not supported.");
+ return false;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Override to handle play now requests.
+ *
+ * @param content The uri of the item to play.
+ * @param cb The callback to send the result to.
+ */
+ public void playNow(String content, ResultReceiver cb) {
+ Log.w(TAG, "playNow is not supported");
+ if (cb != null) {
+ // We do this directly since we don't have a reference to the
+ // iface
+ cb.send(RouteInterface.RESULT_COMMAND_NOT_SUPPORTED, null);
+ }
+ }
+
+ /**
+ * Override to handle resume requests. Return true if the call was
+ * handled, even if it was a no-op.
+ *
+ * @return true if the call was handled.
+ */
+ public boolean resume() {
+ Log.w(TAG, "resume is not supported");
+ return false;
+ }
+
+ /**
+ * Override to handle pause requests. Return true if the call was
+ * handled, even if it was a no-op.
+ *
+ * @return true if the call was handled.
+ */
+ public boolean pause() {
+ Log.w(TAG, "pause is not supported");
+ return false;
+ }
+ }
+}
diff --git a/media/java/android/media/routeprovider/RouteProviderService.java b/media/java/android/media/routeprovider/RouteProviderService.java
new file mode 100644
index 0000000..6ebfb5b
--- /dev/null
+++ b/media/java/android/media/routeprovider/RouteProviderService.java
@@ -0,0 +1,227 @@
+/*
+ * 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.routeprovider;
+
+import android.app.Service;
+import android.content.Intent;
+import android.media.routeprovider.IRouteProvider;
+import android.media.routeprovider.IRouteProviderCallback;
+import android.media.session.RouteEvent;
+import android.media.session.RouteInfo;
+import android.media.session.RouteOptions;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Base class for defining a route provider service.
+ * <p>
+ * A route provider offers media routes which represent destinations to which
+ * applications may connect, control, and send content. This provides a means
+ * for Android applications to interact with a variety of media streaming
+ * devices such as speakers or television sets.
+ * <p>
+ * The system will bind to your provider when an active app is interested in
+ * routes that may be discovered through your provider. After binding, the
+ * system will send updates on which routes to discover through
+ * {@link #updateDiscoveryRequests(List)}. The system will call
+ * {@link #getMatchingRoutes(List)} with a subset of filters when a route is
+ * needed for a specific app.
+ * <p>
+ * TODO add documentation for how the sytem knows an app is interested. Maybe
+ * interface declarations in the manifest.
+ * <p>
+ * The system will only start a provider when an app may discover routes through
+ * it. If your service needs to run at other times you are responsible for
+ * managing its lifecycle.
+ * <p>
+ * Declare your route provider service in your application manifest like this:
+ * <p>
+ *
+ * <pre>
+ * &lt;service android:name=".MyRouteProviderService"
+ * android:label="@string/my_route_provider_service">
+ * &lt;intent-filter>
+ * &lt;action android:name="com.android.media.session.MediaRouteProvider" />
+ * &lt;/intent-filter>
+ * &lt;/service>
+ * </pre>
+ */
+public abstract class RouteProviderService extends Service {
+ private static final String TAG = "RouteProvider";
+ /**
+ * A service that implements a RouteProvider must declare that it handles
+ * this action in its AndroidManifest.
+ */
+ public static final String SERVICE_INTERFACE =
+ "com.android.media.session.MediaRouteProvider";
+
+ /**
+ * @hide
+ */
+ public static final String KEY_ROUTES = "routes";
+ /**
+ * @hide
+ */
+ public static final String KEY_CONNECTION = "connection";
+ /**
+ * @hide
+ */
+ public static final int RESULT_FAILURE = -1;
+ /**
+ * @hide
+ */
+ public static final int RESULT_SUCCESS = 0;
+
+ // The system's callback once it has bound to the service
+ private IRouteProviderCallback mCb;
+
+ /**
+ * If your service overrides onBind it must return super.onBind() in
+ * response to the {@link #SERVICE_INTERFACE} action.
+ */
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (intent != null && RouteProviderService.SERVICE_INTERFACE.equals(intent.getAction())) {
+ return mBinder;
+ }
+ return null;
+ }
+
+ /**
+ * Disconnect the specified RouteConnection. The system will stop sending
+ * commands to this connection.
+ *
+ * @param connection The connection to disconnect.
+ * @hide
+ */
+ public final void disconnect(RouteConnection connection) {
+ if (mCb != null) {
+ try {
+ mCb.onConnectionTerminated(connection.getBinder());
+ } catch (RemoteException e) {
+ Log.wtf(TAG, "Error in disconnect.", e);
+ }
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public final void sendRouteEvent(RouteEvent event) {
+ if (mCb != null) {
+ try {
+ mCb.onRouteEvent(event);
+ } catch (RemoteException e) {
+ Log.wtf(TAG, "Unable to send MediaRouteEvent to system", e);
+ }
+ }
+ }
+
+ /**
+ * Override to handle updates to the routes that are of interest. Each
+ * {@link RouteRequest} will specify if it is an active or passive request.
+ * Route discovery may perform more aggressive discovery on behalf of active
+ * requests but should use low power discovery methods otherwise.
+ * <p>
+ * A single app may have more than one request. Your provider is responsible
+ * for deciding the set of features that are important for discovery given
+ * the set of requests. If your provider only has one method of discovery it
+ * may simply verify that one or more requests are valid before starting
+ * discovery.
+ *
+ * @param requests The route requests that are currently relevant.
+ */
+ public void updateDiscoveryRequests(List<RouteRequest> requests) {
+ }
+
+ /**
+ * Return a list of matching routes for the given set of requests. Returning
+ * null or an empty list indicates there are no matches. A route is
+ * considered matching if it supports one or more of the
+ * {@link RouteOptions} specified. Each returned {@link RouteInfo}
+ * should include all the requested connections that it supports.
+ *
+ * @param options The set of requests for routes
+ * @return The routes that this caller may connect to using one or more of
+ * the route options.
+ */
+ public abstract List<RouteInfo> getMatchingRoutes(List<RouteRequest> options);
+
+ /**
+ * Handle a request to connect to a specific route with a specific request.
+ * The {@link RouteConnection} must be fully defined before being returned,
+ * though the actual connection to the route may be performed in the
+ * background.
+ *
+ * @param route The route to connect to
+ * @param request The connection request parameters
+ * @return A MediaRouteConnection representing the connection to the route
+ */
+ public abstract RouteConnection connect(RouteInfo route, RouteRequest request);
+
+ private IRouteProvider.Stub mBinder = new IRouteProvider.Stub() {
+
+ @Override
+ public void registerCallback(IRouteProviderCallback cb) throws RemoteException {
+ mCb = cb;
+ }
+
+ @Override
+ public void unregisterCallback(IRouteProviderCallback cb) throws RemoteException {
+ mCb = null;
+ }
+
+ @Override
+ public void updateDiscoveryRequests(List<RouteRequest> requests)
+ throws RemoteException {
+ RouteProviderService.this.updateDiscoveryRequests(requests);
+ }
+
+ @Override
+ public void getAvailableRoutes(List<RouteRequest> requests, ResultReceiver cb)
+ throws RemoteException {
+ List<RouteInfo> routes = RouteProviderService.this.getMatchingRoutes(requests);
+ ArrayList<RouteInfo> routesArray;
+ if (routes instanceof ArrayList) {
+ routesArray = (ArrayList<RouteInfo>) routes;
+ } else {
+ routesArray = new ArrayList<RouteInfo>(routes);
+ }
+ Bundle resultData = new Bundle();
+ resultData.putParcelableArrayList(KEY_ROUTES, routesArray);
+ cb.send(routes == null ? RESULT_FAILURE : RESULT_SUCCESS, resultData);
+ }
+
+ @Override
+ public void connect(RouteInfo route, RouteRequest request, ResultReceiver cb)
+ throws RemoteException {
+ RouteConnection connection = RouteProviderService.this.connect(route, request);
+ Bundle resultData = new Bundle();
+ if (connection != null) {
+ connection.publish();
+ resultData.putBinder(KEY_CONNECTION, connection.getBinder());
+ }
+
+ cb.send(connection == null ? RESULT_FAILURE : RESULT_SUCCESS, resultData);
+ }
+ };
+}
diff --git a/media/java/android/media/routeprovider/RouteRequest.aidl b/media/java/android/media/routeprovider/RouteRequest.aidl
new file mode 100644
index 0000000..7bc5722
--- /dev/null
+++ b/media/java/android/media/routeprovider/RouteRequest.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.routeprovider;
+
+parcelable RouteRequest;
diff --git a/media/java/android/media/routeprovider/RouteRequest.java b/media/java/android/media/routeprovider/RouteRequest.java
new file mode 100644
index 0000000..9913566
--- /dev/null
+++ b/media/java/android/media/routeprovider/RouteRequest.java
@@ -0,0 +1,96 @@
+/*
+ * 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.routeprovider;
+
+import android.media.session.RouteOptions;
+import android.media.session.SessionInfo;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A request to connect or discover routes with certain capabilities.
+ * <p>
+ * Passed to a {@link RouteProviderService} when a request for discovery or to
+ * connect to a route is made. This identifies the app making the request and
+ * provides the full set of connection parameters they would like to use for a
+ * connection. An app that can connect in multiple ways will be represented by
+ * multiple requests.
+ */
+public final class RouteRequest implements Parcelable {
+ private final SessionInfo mSessionInfo;
+ private final RouteOptions mOptions;
+ private final boolean mActive;
+
+ /**
+ * @hide
+ */
+ public RouteRequest(SessionInfo info, RouteOptions connRequest,
+ boolean active) {
+ mSessionInfo = info;
+ mOptions = connRequest;
+ mActive = active;
+ }
+
+ private RouteRequest(Parcel in) {
+ mSessionInfo = SessionInfo.CREATOR.createFromParcel(in);
+ mOptions = RouteOptions.CREATOR.createFromParcel(in);
+ mActive = in.readInt() != 0;
+ }
+
+ /**
+ * Get information about the session making the request.
+ *
+ * @return Info on the session making the request
+ */
+ public SessionInfo getSessionInfo() {
+ return mSessionInfo;
+ }
+
+ /**
+ * Get the connection options, which includes the interfaces and other
+ * connection params the session wants to use with a route.
+ *
+ * @return The connection options
+ */
+ public RouteOptions getConnectionOptions() {
+ return mOptions;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ mSessionInfo.writeToParcel(dest, flags);
+ mOptions.writeToParcel(dest, flags);
+ dest.writeInt(mActive ? 1 : 0);
+ }
+
+ public static final Parcelable.Creator<RouteRequest> CREATOR
+ = new Parcelable.Creator<RouteRequest>() {
+ @Override
+ public RouteRequest createFromParcel(Parcel in) {
+ return new RouteRequest(in);
+ }
+
+ @Override
+ public RouteRequest[] newArray(int size) {
+ return new RouteRequest[size];
+ }
+ };
+}
diff --git a/media/java/android/media/session/IMediaSession.aidl b/media/java/android/media/session/ISession.aidl
index aed7641..ca77f04 100644
--- a/media/java/android/media/session/IMediaSession.aidl
+++ b/media/java/android/media/session/ISession.aidl
@@ -15,25 +15,33 @@
package android.media.session;
-import android.media.session.IMediaController;
+import android.media.session.ISessionController;
import android.media.session.MediaMetadata;
+import android.media.session.RouteOptions;
+import android.media.session.RouteCommand;
+import android.media.session.RouteInfo;
import android.media.session.PlaybackState;
import android.os.Bundle;
+import android.os.ResultReceiver;
/**
* Interface to a MediaSession in the system.
* @hide
*/
-interface IMediaSession {
+interface ISession {
void sendEvent(String event, in Bundle data);
- IMediaController getMediaController();
+ ISessionController getController();
void setTransportPerformerEnabled();
- void setRouteState(in Bundle routeState);
- void setRoute(in Bundle mediaRouteDescriptor);
- List<String> getSupportedInterfaces();
void publish();
void destroy();
+ // These commands are for setting up and communicating with routes
+ // Returns true if the route was set for this session
+ boolean setRoute(in RouteInfo route);
+ void setRouteOptions(in List<RouteOptions> options);
+ void connectToRoute(in RouteInfo route, in RouteOptions options);
+ void sendRouteCommand(in RouteCommand event, in ResultReceiver cb);
+
// These commands are for the TransportPerformer
void setMetadata(in MediaMetadata metadata);
void setPlaybackState(in PlaybackState state);
diff --git a/media/java/android/media/session/IMediaSessionCallback.aidl b/media/java/android/media/session/ISessionCallback.aidl
index 7c183e0..f04cbcc 100644
--- a/media/java/android/media/session/IMediaSessionCallback.aidl
+++ b/media/java/android/media/session/ISessionCallback.aidl
@@ -16,6 +16,9 @@
package android.media.session;
import android.media.Rating;
+import android.media.session.RouteEvent;
+import android.media.session.RouteInfo;
+import android.media.session.RouteOptions;
import android.content.Intent;
import android.os.Bundle;
import android.os.ResultReceiver;
@@ -23,10 +26,13 @@ import android.os.ResultReceiver;
/**
* @hide
*/
-oneway interface IMediaSessionCallback {
+oneway interface ISessionCallback {
void onCommand(String command, in Bundle extras, in ResultReceiver cb);
- void onMediaButton(in Intent mediaRequestIntent);
- void onRequestRouteChange(in Bundle route);
+ void onMediaButton(in Intent mediaButtonIntent);
+ void onRequestRouteChange(in RouteInfo route);
+ void onRouteConnected(in RouteInfo route, in RouteOptions options);
+ void onRouteStateChange(int state);
+ void onRouteEvent(in RouteEvent event);
// These callbacks are for the TransportPerformer
void onPlay();
diff --git a/media/java/android/media/session/IMediaController.aidl b/media/java/android/media/session/ISessionController.aidl
index d34e973..e2e046f 100644
--- a/media/java/android/media/session/IMediaController.aidl
+++ b/media/java/android/media/session/ISessionController.aidl
@@ -17,7 +17,7 @@ package android.media.session;
import android.content.Intent;
import android.media.Rating;
-import android.media.session.IMediaControllerCallback;
+import android.media.session.ISessionControllerCallback;
import android.media.session.MediaMetadata;
import android.media.session.PlaybackState;
import android.os.Bundle;
@@ -28,12 +28,13 @@ import android.view.KeyEvent;
* Interface to a MediaSession in the system.
* @hide
*/
-interface IMediaController {
+interface ISessionController {
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);
+ void registerCallbackListener(in ISessionControllerCallback cb);
+ void unregisterCallbackListener(in ISessionControllerCallback cb);
boolean isTransportControlEnabled();
+ void showRoutePicker();
// These commands are for the TransportController
void play();
diff --git a/media/java/android/media/session/IMediaControllerCallback.aidl b/media/java/android/media/session/ISessionControllerCallback.aidl
index 3651f1b..bc1ae05 100644
--- a/media/java/android/media/session/IMediaControllerCallback.aidl
+++ b/media/java/android/media/session/ISessionControllerCallback.aidl
@@ -16,15 +16,16 @@
package android.media.session;
import android.media.session.MediaMetadata;
+import android.media.session.RouteInfo;
import android.media.session.PlaybackState;
import android.os.Bundle;
/**
* @hide
*/
-oneway interface IMediaControllerCallback {
+oneway interface ISessionControllerCallback {
void onEvent(String event, in Bundle extras);
- void onRouteChanged(in Bundle route);
+ void onRouteChanged(in RouteInfo route);
// These callbacks are for the TransportController
void onPlaybackStateChanged(in PlaybackState state);
diff --git a/media/java/android/media/session/IMediaSessionManager.aidl b/media/java/android/media/session/ISessionManager.aidl
index 0b4328e..84b9a0f 100644
--- a/media/java/android/media/session/IMediaSessionManager.aidl
+++ b/media/java/android/media/session/ISessionManager.aidl
@@ -15,14 +15,14 @@
package android.media.session;
-import android.media.session.IMediaSession;
-import android.media.session.IMediaSessionCallback;
+import android.media.session.ISession;
+import android.media.session.ISessionCallback;
import android.os.Bundle;
/**
* Interface to the MediaSessionManagerService
* @hide
*/
-interface IMediaSessionManager {
- IMediaSession createSession(String packageName, in IMediaSessionCallback cb, String tag);
+interface ISessionManager {
+ ISession createSession(String packageName, in ISessionCallback cb, String tag);
} \ No newline at end of file
diff --git a/media/java/android/media/session/PlaybackState.java b/media/java/android/media/session/PlaybackState.java
index b3506b3..14d9fb1 100644
--- a/media/java/android/media/session/PlaybackState.java
+++ b/media/java/android/media/session/PlaybackState.java
@@ -15,12 +15,11 @@
*/
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
+ * Playback state for a {@link Session}. This includes a state like
* {@link PlaybackState#PLAYSTATE_PLAYING}, the current playback position,
* and the current control capabilities.
*/
@@ -147,6 +146,14 @@ public final class PlaybackState implements Parcelable {
*/
public final static int PLAYSTATE_ERROR = 7;
+ /**
+ * State indicating the class doing playback is currently connecting to a
+ * route. Depending on the implementation you may return to the previous
+ * state when the connection finishes or enter {@link #PLAYSTATE_NONE}. If
+ * the connection failed {@link #PLAYSTATE_ERROR} should be used.
+ */
+ public final static int PLAYSTATE_CONNECTING = 8;
+
private int mState;
private long mPosition;
private long mBufferPosition;
diff --git a/media/java/android/media/session/Route.java b/media/java/android/media/session/Route.java
new file mode 100644
index 0000000..c9530a6
--- /dev/null
+++ b/media/java/android/media/session/Route.java
@@ -0,0 +1,99 @@
+/*
+ * 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.text.TextUtils;
+import android.util.Log;
+
+import java.util.List;
+
+/**
+ * Represents a destination which an application has connected to and may send
+ * media content.
+ * <p>
+ * This allows a session owner to interact with a route it has been connected
+ * to. The MediaRoute must be used to get {@link RouteInterface}
+ * instances which can be used to communicate over a specific interface on the
+ * route.
+ */
+public final class Route {
+ private static final String TAG = "Route";
+ private final RouteInfo mInfo;
+ private final Session mSession;
+ private final RouteOptions mOptions;
+
+ /**
+ * @hide
+ */
+ public Route(RouteInfo info, RouteOptions options, Session session) {
+ if (info == null || options == null) {
+ throw new IllegalStateException("Route info was not valid!");
+ }
+ mInfo = info;
+ mOptions = options;
+ mSession = session;
+ }
+
+ /**
+ * Get the {@link RouteInfo} for this route.
+ *
+ * @return The info for this route.
+ */
+ public RouteInfo getRouteInfo() {
+ return mInfo;
+ }
+
+ /**
+ * Get the {@link RouteOptions} that were used to connect this route.
+ *
+ * @return The options used to connect to this route.
+ */
+ public RouteOptions getOptions() {
+ return mOptions;
+ }
+
+ /**
+ * Gets an interface provided by this route. If the interface is not
+ * supported by the route, returns null.
+ *
+ * @see RouteInterface
+ * @param iface The name of the interface to create
+ * @return A {@link RouteInterface} or null if the interface is
+ * not supported.
+ */
+ public RouteInterface getInterface(String iface) {
+ if (TextUtils.isEmpty(iface)) {
+ throw new IllegalArgumentException("iface may not be empty.");
+ }
+ List<String> ifaces = mOptions.getInterfaceNames();
+ if (ifaces != null) {
+ for (int i = ifaces.size() - 1; i >= 0; i--) {
+ if (iface.equals(ifaces.get(i))) {
+ return new RouteInterface(this, iface, mSession);
+ }
+ }
+ }
+ Log.e(TAG, "Interface not supported by route");
+ return null;
+ }
+
+ /**
+ * @hide
+ */
+ Session getSession() {
+ return mSession;
+ }
+}
diff --git a/media/java/android/media/session/MediaSessionToken.aidl b/media/java/android/media/session/RouteCommand.aidl
index 5812682..725b308 100644
--- a/media/java/android/media/session/MediaSessionToken.aidl
+++ b/media/java/android/media/session/RouteCommand.aidl
@@ -15,4 +15,4 @@
package android.media.session;
-parcelable MediaSessionToken;
+parcelable RouteCommand;
diff --git a/media/java/android/media/session/RouteCommand.java b/media/java/android/media/session/RouteCommand.java
new file mode 100644
index 0000000..358bc0a
--- /dev/null
+++ b/media/java/android/media/session/RouteCommand.java
@@ -0,0 +1,117 @@
+/*
+ * 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.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Represents a command that an application may send to a route.
+ * <p>
+ * Commands are associated with a specific route and interface supported by that
+ * route and sent through the session. This class isn't used directly by apps.
+ *
+ * @hide
+ */
+public final class RouteCommand implements Parcelable {
+ private final String mRoute;
+ private final String mIface;
+ private final String mEvent;
+ private final Bundle mExtras;
+
+ /**
+ * @param route The id of the route this event is being sent on
+ * @param iface The interface the sender used
+ * @param event The event or command
+ * @param extras Any extras included with the event
+ */
+ public RouteCommand(String route, String iface, String event, Bundle extras) {
+ mRoute = route;
+ mIface = iface;
+ mEvent = event;
+ mExtras = extras;
+ }
+
+ private RouteCommand(Parcel in) {
+ mRoute = in.readString();
+ mIface = in.readString();
+ mEvent = in.readString();
+ mExtras = in.readBundle();
+ }
+
+ /**
+ * Get the id for the route this event was sent on.
+ *
+ * @return The route id this event is using
+ */
+ public String getRouteInfo() {
+ return mRoute;
+ }
+
+ /**
+ * Get the interface this event was sent from
+ *
+ * @return The interface for this event
+ */
+ public String getIface() {
+ return mIface;
+ }
+
+ /**
+ * Get the action/name of the event.
+ *
+ * @return The name of event/command.
+ */
+ public String getEvent() {
+ return mEvent;
+ }
+
+ /**
+ * Get any extras included with the event.
+ *
+ * @return The bundle included with the event or null
+ */
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mRoute);
+ dest.writeString(mIface);
+ dest.writeString(mEvent);
+ dest.writeBundle(mExtras);
+ }
+
+ public static final Parcelable.Creator<RouteCommand> CREATOR
+ = new Parcelable.Creator<RouteCommand>() {
+ @Override
+ public RouteCommand createFromParcel(Parcel in) {
+ return new RouteCommand(in);
+ }
+
+ @Override
+ public RouteCommand[] newArray(int size) {
+ return new RouteCommand[size];
+ }
+ };
+}
diff --git a/media/java/android/media/session/RouteEvent.aidl b/media/java/android/media/session/RouteEvent.aidl
new file mode 100644
index 0000000..6966207
--- /dev/null
+++ b/media/java/android/media/session/RouteEvent.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 RouteEvent;
diff --git a/media/java/android/media/session/RouteEvent.java b/media/java/android/media/session/RouteEvent.java
new file mode 100644
index 0000000..918e410
--- /dev/null
+++ b/media/java/android/media/session/RouteEvent.java
@@ -0,0 +1,120 @@
+/*
+ * 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.routeprovider.RouteConnection;
+import android.media.routeprovider.RouteProviderService;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Represents an event that a route provider is sending to a particular
+ * {@link RouteConnection}. Events are associated with a specific interface
+ * supported by the connection and sent through the {@link RouteProviderService}.
+ * This class isn't used directly by apps.
+ *
+ * @hide
+ */
+public class RouteEvent implements Parcelable {
+ private final IBinder mConnection;
+ private final String mIface;
+ private final String mEvent;
+ private final Bundle mExtras;
+
+ /**
+ * @param connection The connection that this event is for
+ * @param iface The interface the sender used
+ * @param event The event or command
+ * @param extras Any extras included with the event
+ */
+ public RouteEvent(IBinder connection, String iface, String event, Bundle extras) {
+ mConnection = connection;
+ mIface = iface;
+ mEvent = event;
+ mExtras = extras;
+ }
+
+ private RouteEvent(Parcel in) {
+ mConnection = in.readStrongBinder();
+ mIface = in.readString();
+ mEvent = in.readString();
+ mExtras = in.readBundle();
+ }
+
+ /**
+ * Get the connection this event was sent on.
+ *
+ * @return The connection this event is using
+ */
+ public IBinder getConnection() {
+ return mConnection;
+ }
+
+ /**
+ * Get the interface this event was sent from
+ *
+ * @return The interface for this event
+ */
+ public String getIface() {
+ return mIface;
+ }
+
+ /**
+ * Get the action/name of the event.
+ *
+ * @return The name of event/command.
+ */
+ public String getEvent() {
+ return mEvent;
+ }
+
+ /**
+ * Get any extras included with the event.
+ *
+ * @return The bundle included with the event or null
+ */
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeStrongBinder(mConnection);
+ dest.writeString(mIface);
+ dest.writeString(mEvent);
+ dest.writeBundle(mExtras);
+ }
+
+ public static final Parcelable.Creator<RouteEvent> CREATOR
+ = new Parcelable.Creator<RouteEvent>() {
+ @Override
+ public RouteEvent createFromParcel(Parcel in) {
+ return new RouteEvent(in);
+ }
+
+ @Override
+ public RouteEvent[] newArray(int size) {
+ return new RouteEvent[size];
+ }
+ };
+}
diff --git a/media/java/android/media/session/RouteInfo.aidl b/media/java/android/media/session/RouteInfo.aidl
new file mode 100644
index 0000000..c5f50c8
--- /dev/null
+++ b/media/java/android/media/session/RouteInfo.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 RouteInfo;
diff --git a/media/java/android/media/session/RouteInfo.java b/media/java/android/media/session/RouteInfo.java
new file mode 100644
index 0000000..17df969
--- /dev/null
+++ b/media/java/android/media/session/RouteInfo.java
@@ -0,0 +1,233 @@
+/*
+ * 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.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Information about a route, including its display name, a way to identify it,
+ * and the ways it can be connected to.
+ */
+public final class RouteInfo implements Parcelable {
+ private final String mName;
+ private final String mId;
+ private final String mProviderId;
+ private final List<RouteOptions> mOptions;
+
+ private RouteInfo(String id, String name, String providerId,
+ List<RouteOptions> connRequests) {
+ mId = id;
+ mName = name;
+ mProviderId = providerId;
+ mOptions = connRequests;
+ }
+
+ private RouteInfo(Parcel in) {
+ mId = in.readString();
+ mName = in.readString();
+ mProviderId = in.readString();
+ mOptions = new ArrayList<RouteOptions>();
+ in.readTypedList(mOptions, RouteOptions.CREATOR);
+ }
+
+ /**
+ * Get the displayable name of this route.
+ *
+ * @return A short, user readable name for this route
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Get the unique id for this route.
+ *
+ * @return A unique route id.
+ */
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * Get the package name of this route's provider.
+ *
+ * @return The package name of this route's provider.
+ */
+ public String getProvider() {
+ return mProviderId;
+ }
+
+ /**
+ * Get the set of connections that may be used with this route.
+ *
+ * @return An array of connection requests that may be used to connect
+ */
+ public List<RouteOptions> getConnectionMethods() {
+ return mOptions;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mId);
+ dest.writeString(mName);
+ dest.writeString(mProviderId);
+ dest.writeTypedList(mOptions);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder bob = new StringBuilder();
+ bob.append("RouteInfo: id=").append(mId).append(", name=").append(mName)
+ .append(", provider=").append(mProviderId).append(", options={");
+ for (int i = 0; i < mOptions.size(); i++) {
+ if (i != 0) {
+ bob.append(", ");
+ }
+ bob.append(mOptions.get(i).toString());
+ }
+ bob.append("}");
+ return bob.toString();
+ }
+
+ public static final Parcelable.Creator<RouteInfo> CREATOR
+ = new Parcelable.Creator<RouteInfo>() {
+ @Override
+ public RouteInfo createFromParcel(Parcel in) {
+ return new RouteInfo(in);
+ }
+
+ @Override
+ public RouteInfo[] newArray(int size) {
+ return new RouteInfo[size];
+ }
+ };
+
+ /**
+ * Helper for creating MediaRouteInfos. A route must have a name and an id.
+ * While options are not strictly required the route cannot be connected to
+ * without at least one set of options.
+ */
+ public static final class Builder {
+ private String mName;
+ private String mId;
+ private String mProviderPackage;
+ private ArrayList<RouteOptions> mOptions;
+
+ /**
+ * Copies an existing route info object. TODO Remove once we have
+ * helpers for creating route infos.
+ *
+ * @param from The existing info to copy.
+ */
+ public Builder(RouteInfo from) {
+ mOptions = new ArrayList<RouteOptions>(from.getConnectionMethods());
+ mName = from.mName;
+ mId = from.mId;
+ mProviderPackage = from.mProviderId;
+ }
+
+ public Builder() {
+ mOptions = new ArrayList<RouteOptions>();
+ }
+
+ /**
+ * Set the user visible name for this route.
+ *
+ * @param name The name of the route
+ * @return The builder for easy chaining.
+ */
+ public Builder setName(String name) {
+ mName = name;
+ return this;
+ }
+
+ /**
+ * Set the id of the route. This should be unique to the provider.
+ *
+ * @param id The unique id of the route.
+ * @return The builder for easy chaining.
+ */
+ public Builder setId(String id) {
+ mId = id;
+ return this;
+ }
+
+ /**
+ * @hide
+ */
+ public Builder setProviderId(String packageName) {
+ mProviderPackage = packageName;
+ return this;
+ }
+
+ /**
+ * Add a set of {@link RouteOptions} to the route. Multiple options
+ * may be added to the same route.
+ *
+ * @param options The options to add to this route.
+ * @return The builder for easy chaining.
+ */
+ public Builder addRouteOptions(RouteOptions options) {
+ mOptions.add(options);
+ return this;
+ }
+
+ /**
+ * Clear the set of {@link RouteOptions} on the route.
+ *
+ * @return The builder for easy chaining
+ */
+ public Builder clearRouteOptions() {
+ mOptions.clear();
+ return this;
+ }
+
+ /**
+ * Build a new MediaRouteInfo.
+ *
+ * @return A new MediaRouteInfo with the values that were set.
+ */
+ public RouteInfo build() {
+ if (TextUtils.isEmpty(mName)) {
+ throw new IllegalArgumentException("Must set a name before building");
+ }
+ if (TextUtils.isEmpty(mId)) {
+ throw new IllegalArgumentException("Must set an id before building");
+ }
+ return new RouteInfo(mId, mName, mProviderPackage, mOptions);
+ }
+
+ /**
+ * Get the current number of options that have been added to this
+ * builder.
+ *
+ * @return The number of options that have been added.
+ */
+ public int getOptionsSize() {
+ return mOptions.size();
+ }
+ }
+}
diff --git a/media/java/android/media/session/RouteInterface.java b/media/java/android/media/session/RouteInterface.java
index 2391f27..e9c9fd3 100644
--- a/media/java/android/media/session/RouteInterface.java
+++ b/media/java/android/media/session/RouteInterface.java
@@ -17,135 +17,160 @@ 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;
+import android.util.Log;
+
+import java.util.ArrayList;
/**
- * 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.
+ * A route can support multiple interfaces for a {@link Session} to
+ * interact with. To use a specific interface with a route a
+ * MediaSessionRouteInterface needs to be retrieved from the route. An
+ * implementation of the specific interface, like
+ * {@link RoutePlaybackControls}, should be used to simplify communication
+ * and reduce errors on that interface.
*
- * @see RouteTransportControls
+ * @see RoutePlaybackControls for an example
*/
public final class RouteInterface {
- private static final String TAG = "MediaInterface";
+ private static final String TAG = "RouteInterface";
- private static final String KEY_RESULT = "result";
+ /**
+ * Error indicating the route is currently not connected.
+ */
+ public static final int RESULT_NOT_CONNECTED = -5;
+ /**
+ * Error indicating the session is no longer using the route this command
+ * was sent to.
+ */
+ public static final int RESULT_ROUTE_IS_STALE = -4;
+ /**
+ * Error indicating that the interface does not support the command.
+ */
+ public static final int RESULT_COMMAND_NOT_SUPPORTED = -3;
+ /**
+ * Error indicating that the route does not support the interface.
+ */
+ public static final int RESULT_INTERFACE_NOT_SUPPORTED = -2;
+ /**
+ * Generic error. Extra information about the error may be included in the
+ * result bundle.
+ */
+ public static final int RESULT_ERROR = -1;
+ /**
+ * The command was successful. Extra information may be included in the
+ * result bundle.
+ */
+ public static final int RESULT_SUCCESS = 1;
- private final MediaController mController;
+ private final Route mRoute;
private final String mIface;
+ private final Session mSession;
+
+ private final Object mLock = new Object();
+ private final ArrayList<EventHandler> mListeners = new ArrayList<EventHandler>();
/**
* @hide
*/
- RouteInterface(MediaController controller, String iface) {
- mController = controller;
+ RouteInterface(Route route, String iface, Session session) {
+ mRoute = route;
mIface = iface;
+ mSession = session;
+ mSession.addInterfaceListener(iface, mEventListener);
}
- public void sendCommand(String command, Bundle params, ResultReceiver cb) {
- // TODO
+ /**
+ * Send a command using this interface.
+ *
+ * @param command The command to send.
+ * @param extras Any extras to include with the command.
+ * @param cb The callback to receive the result on.
+ * @return true if the command was sent, false otherwise.
+ */
+ public boolean sendCommand(String command, Bundle extras, ResultReceiver cb) {
+ RouteCommand cmd = new RouteCommand(mRoute.getRouteInfo().getId(), mIface,
+ command, extras);
+ return mSession.sendRouteCommand(cmd, cb);
}
+ /**
+ * Add a listener to this interface. Events will be sent on the caller's
+ * thread.
+ *
+ * @param listener The listener to receive events on.
+ */
public void addListener(EventListener listener) {
addListener(listener, null);
}
+ /**
+ * Add a listener for this interface. If a handler is specified events will
+ * be performed on the handler's thread, otherwise the caller's thread will
+ * be used.
+ *
+ * @param listener The listener to receive events on
+ * @param handler The handler whose thread to post calls on
+ */
public void addListener(EventListener listener, Handler handler) {
- // TODO See MediaController for add/remove pattern
+ if (listener == null) {
+ throw new IllegalArgumentException("listener may not be null");
+ }
+ if (handler == null) {
+ handler = new Handler();
+ }
+ synchronized (mLock) {
+ if (findIndexOfListenerLocked(listener) != -1) {
+ Log.d(TAG, "Listener is already added, ignoring");
+ return;
+ }
+ mListeners.add(new EventHandler(handler.getLooper(), listener));
+ }
}
+ /**
+ * Remove a listener from this interface.
+ *
+ * @param listener The listener to stop receiving events on.
+ */
public void removeListener(EventListener listener) {
- // TODO
+ if (listener == null) {
+ throw new IllegalArgumentException("listener may not be null");
+ }
+ synchronized (mLock) {
+ int index = findIndexOfListenerLocked(listener);
+ if (index != -1) {
+ mListeners.remove(index);
+ }
+ }
}
- // 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);
+ private int findIndexOfListenerLocked(EventListener listener) {
+ if (listener == null) {
+ throw new IllegalArgumentException("Callback cannot be null");
+ }
+ for (int i = mListeners.size() - 1; i >= 0; i--) {
+ EventHandler handler = mListeners.get(i);
+ if (listener == handler.mListener) {
+ return i;
+ }
}
- return b;
+ return -1;
}
- 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
+ private EventListener mEventListener = new EventListener() {
+ @Override
+ public void onEvent(String event, Bundle args) {
+ synchronized (mLock) {
+ for (int i = mListeners.size() - 1; i >= 0; i--) {
+ mListeners.get(i).postEvent(event, args);
+ }
+ }
}
- }
+
+ };
/**
* An EventListener can be registered by an app with TODO to handle events
@@ -166,9 +191,9 @@ public final class RouteInterface {
private static final class EventHandler extends Handler {
- private final RouteInterface.EventListener mListener;
+ private final EventListener mListener;
- public EventHandler(Looper looper, RouteInterface.EventListener cb) {
+ public EventHandler(Looper looper, EventListener cb) {
super(looper, null, true);
mListener = cb;
}
diff --git a/media/java/android/media/session/RouteOptions.aidl b/media/java/android/media/session/RouteOptions.aidl
new file mode 100644
index 0000000..feaf517
--- /dev/null
+++ b/media/java/android/media/session/RouteOptions.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 RouteOptions;
diff --git a/media/java/android/media/session/RouteOptions.java b/media/java/android/media/session/RouteOptions.java
new file mode 100644
index 0000000..5105867
--- /dev/null
+++ b/media/java/android/media/session/RouteOptions.java
@@ -0,0 +1,163 @@
+/*
+ * 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.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Specifies options that an application might use when connecting to a route.
+ * This includes things like interfaces, connection parameters, and required
+ * features.
+ * <p>
+ * An application may create several different route options that describe
+ * alternative sets of capabilities that it can use and choose the most
+ * appropriate route options when it is ready to connect to the route. Each
+ * route options instance must specify a complete set of capabilities to request
+ * when the connection is established.
+ */
+public final class RouteOptions implements Parcelable {
+ private static final String TAG = "RouteOptions";
+
+ private final ArrayList<String> mIfaces;
+ private final Bundle mConnectionParams;
+
+ private RouteOptions(List<String> ifaces, Bundle params) {
+ mIfaces = new ArrayList<String>(ifaces);
+ mConnectionParams = params;
+ }
+
+ private RouteOptions(Parcel in) {
+ mIfaces = new ArrayList<String>();
+ in.readStringList(mIfaces);
+ mConnectionParams = in.readBundle();
+ }
+
+ /**
+ * Get the interfaces this connection wants to use.
+ *
+ * @return The interfaces for this connection
+ */
+ public List<String> getInterfaceNames() {
+ return mIfaces;
+ }
+
+ /**
+ * Get the parameters that will be used for connecting.
+ *
+ * @return The set of connection parameters this connections uses
+ */
+ public Bundle getConnectionParams() {
+ return mConnectionParams;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeStringList(mIfaces);
+ dest.writeBundle(mConnectionParams);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder bob = new StringBuilder();
+ bob.append("Options: interfaces={");
+ for (int i = 0; i < mIfaces.size(); i++) {
+ if (i != 0) {
+ bob.append(", ");
+ }
+ bob.append(mIfaces.get(i));
+ }
+ bob.append("}");
+ bob.append(", parameters=");
+ bob.append(mConnectionParams == null ? "null" : mConnectionParams.toString());
+ return bob.toString();
+ }
+
+ public static final Parcelable.Creator<RouteOptions> CREATOR
+ = new Parcelable.Creator<RouteOptions>() {
+ @Override
+ public RouteOptions createFromParcel(Parcel in) {
+ return new RouteOptions(in);
+ }
+
+ @Override
+ public RouteOptions[] newArray(int size) {
+ return new RouteOptions[size];
+ }
+ };
+
+ /**
+ * Builder for creating {@link RouteOptions}.
+ */
+ public final static class Builder {
+ private ArrayList<String> mIfaces = new ArrayList<String>();
+ private Bundle mConnectionParams;
+
+ public Builder() {
+ }
+
+ /**
+ * Add a required interface to the options.
+ *
+ * @param interfaceName The name of the interface to add.
+ * @return The builder to allow chaining commands.
+ */
+ public Builder addInterface(String interfaceName) {
+ if (TextUtils.isEmpty(interfaceName)) {
+ throw new IllegalArgumentException("interfaceName cannot be empty");
+ }
+ if (!mIfaces.contains(interfaceName)) {
+ mIfaces.add(interfaceName);
+ } else {
+ Log.w(TAG, "Attempted to add interface that is already added");
+ }
+ return this;
+ }
+
+ /**
+ * Set the connection parameters to use with the options. TODO replace
+ * with more specific calls once we decide on the standard way to
+ * express parameters.
+ *
+ * @param parameters The parameters to use.
+ * @return The builder to allow chaining commands.
+ */
+ public Builder setParameters(Bundle parameters) {
+ mConnectionParams = parameters;
+ return this;
+ }
+
+ /**
+ * Generate a set of options.
+ *
+ * @return The options with the specified components.
+ */
+ public RouteOptions build() {
+ return new RouteOptions(mIfaces, mConnectionParams);
+ }
+ }
+}
diff --git a/media/java/android/media/session/RoutePlaybackControls.java b/media/java/android/media/session/RoutePlaybackControls.java
new file mode 100644
index 0000000..a3ffb58
--- /dev/null
+++ b/media/java/android/media/session/RoutePlaybackControls.java
@@ -0,0 +1,161 @@
+/*
+ * 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.ResultReceiver;
+
+/**
+ * A standard media control interface for Routes that support queueing and
+ * transport controls. Routes may support multiple interfaces for MediaSessions
+ * to interact with.
+ */
+public final class RoutePlaybackControls {
+ private static final String TAG = "RoutePlaybackControls";
+ public static final String NAME = "android.media.session.RoutePlaybackControls";
+
+ /** @hide */
+ public static final String KEY_VALUE1 = "value1";
+
+ /** @hide */
+ public static final String CMD_FAST_FORWARD = "fastForward";
+ /** @hide */
+ public static final String CMD_GET_CURRENT_POSITION = "getCurrentPosition";
+ /** @hide */
+ public static final String CMD_GET_CAPABILITIES = "getCapabilities";
+ /** @hide */
+ public static final String CMD_PLAY_NOW = "playNow";
+ /** @hide */
+ public static final String CMD_RESUME = "resume";
+ /** @hide */
+ public static final String CMD_PAUSE = "pause";
+
+ /** @hide */
+ public static final String EVENT_PLAYSTATE_CHANGE = "playstateChange";
+ /** @hide */
+ public static final String EVENT_METADATA_CHANGE = "metadataChange";
+
+ private final RouteInterface mIface;
+
+ private RoutePlaybackControls(RouteInterface iface) {
+ mIface = iface;
+ }
+
+ /**
+ * Get a new MediaRoutePlaybackControls instance for sending commands using
+ * this interface. If the provided route doesn't support this interface null
+ * will be returned.
+ *
+ * @param route The route to send commands to.
+ * @return A MediaRoutePlaybackControls instance or null if not supported.
+ */
+ public static RoutePlaybackControls from(Route route) {
+ RouteInterface iface = route.getInterface(NAME);
+ if (iface != null) {
+ return new RoutePlaybackControls(iface);
+ }
+ return null;
+ }
+
+ /**
+ * Send a resume command to the route.
+ */
+ public void resume() {
+ mIface.sendCommand(CMD_RESUME, null, null);
+ }
+
+ /**
+ * Send a pause command to the route.
+ */
+ public void pause() {
+ mIface.sendCommand(CMD_PAUSE, null, null);
+ }
+
+ /**
+ * Send a fast forward command.
+ */
+ public void fastForward() {
+ Bundle b = new Bundle();
+ mIface.sendCommand(CMD_FAST_FORWARD, b, null);
+ }
+
+ /**
+ * Retrieves the current playback position.
+ *
+ * @param cb The callback to receive the result on.
+ */
+ public void getCurrentPosition(ResultReceiver cb) {
+ mIface.sendCommand(CMD_GET_CURRENT_POSITION, null, cb);
+ }
+
+ public void getCapabilities(ResultReceiver cb) {
+ mIface.sendCommand(CMD_GET_CAPABILITIES, null, cb);
+ }
+
+ public void addListener(Listener listener) {
+ mIface.addListener(listener);
+ }
+
+ public void addListener(Listener listener, Handler handler) {
+ mIface.addListener(listener, handler);
+ }
+
+ public void removeListener(Listener listener) {
+ mIface.removeListener(listener);
+ }
+
+ public void playNow(String content) {
+ Bundle bundle = new Bundle();
+ bundle.putString(KEY_VALUE1, content);
+ mIface.sendCommand(CMD_PLAY_NOW, bundle, null);
+ }
+
+ /**
+ * Register this event listener using {@link #addListener} to receive
+ * RoutePlaybackControl events from a session.
+ */
+ public static abstract class Listener extends RouteInterface.EventListener {
+ @Override
+ public final void onEvent(String event, Bundle args) {
+ if (EVENT_PLAYSTATE_CHANGE.equals(event)) {
+ onPlaybackStateChange(args.getInt(KEY_VALUE1, 0));
+ } else if (EVENT_METADATA_CHANGE.equals(event)) {
+ onMetadataUpdate((MediaMetadata) args.getParcelable(KEY_VALUE1));
+ }
+ }
+
+ /**
+ * 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(MediaMetadata metadata) {
+ }
+ }
+
+}
diff --git a/media/java/android/media/session/RouteTransportControls.java b/media/java/android/media/session/RouteTransportControls.java
deleted file mode 100644
index 665fd10..0000000
--- a/media/java/android/media/session/RouteTransportControls.java
+++ /dev/null
@@ -1,230 +0,0 @@
-/*
- * 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/MediaSession.java b/media/java/android/media/session/Session.java
index 23c3035..8ccd788 100644
--- a/media/java/android/media/session/MediaSession.java
+++ b/media/java/android/media/session/Session.java
@@ -18,9 +18,9 @@ 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.session.ISessionController;
+import android.media.session.ISession;
+import android.media.session.ISessionCallback;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
@@ -33,6 +33,7 @@ import android.util.Log;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
+import java.util.List;
/**
* Allows interaction with media controllers, media routes, volume keys, media
@@ -44,11 +45,11 @@ import java.util.ArrayList;
* media to multiple routes or to provide finer grain controls of media.
* <p>
* A MediaSession is created by calling
- * {@link MediaSessionManager#createSession(String)}. Once a session is created
+ * {@link SessionManager#createSession(String)}. Once a session is created
* apps that have the MEDIA_CONTENT_CONTROL permission can interact with the
- * session through {@link MediaSessionManager#getActiveSessions()}. The owner of
+ * session through {@link SessionManager#getActiveSessions()}. The owner of
* the session may also use {@link #getSessionToken()} to allow apps without
- * this permission to create a {@link MediaController} to interact with this
+ * this permission to create a {@link SessionController} to interact with this
* session.
* <p>
* To receive commands, media keys, and other events a Callback must be set with
@@ -59,12 +60,13 @@ import java.util.ArrayList;
* <p>
* MediaSession objects are thread safe
*/
-public final class MediaSession {
- private static final String TAG = "MediaSession";
+public final class Session {
+ private static final String TAG = "Session";
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 int MSG_ROUTE_CONNECTED = 4;
private static final String KEY_COMMAND = "command";
private static final String KEY_EXTRAS = "extras";
@@ -72,32 +74,33 @@ public final class MediaSession {
private final Object mLock = new Object();
- private final MediaSessionToken mSessionToken;
- private final IMediaSession mBinder;
+ private final SessionToken mSessionToken;
+ private final ISession mBinder;
private final CallbackStub mCbStub;
private final ArrayList<MessageHandler> mCallbacks = new ArrayList<MessageHandler>();
// TODO route interfaces
- private final ArrayMap<String, RouteInterface.Stub> mInterfaces
- = new ArrayMap<String, RouteInterface.Stub>();
+ private final ArrayMap<String, RouteInterface.EventListener> mInterfaceListeners
+ = new ArrayMap<String, RouteInterface.EventListener>();
private TransportPerformer mPerformer;
+ private Route mRoute;
private boolean mPublished = false;;
/**
* @hide
*/
- public MediaSession(IMediaSession binder, CallbackStub cbStub) {
+ public Session(ISession binder, CallbackStub cbStub) {
mBinder = binder;
mCbStub = cbStub;
- IMediaController controllerBinder = null;
+ ISessionController controllerBinder = null;
try {
- controllerBinder = mBinder.getMediaController();
+ controllerBinder = mBinder.getController();
} catch (RemoteException e) {
throw new RuntimeException("Dead object in MediaSessionController constructor: ", e);
}
- mSessionToken = new MediaSessionToken(controllerBinder);
+ mSessionToken = new SessionToken(controllerBinder);
}
/**
@@ -109,6 +112,13 @@ public final class MediaSession {
addCallback(callback, null);
}
+ /**
+ * Add a callback to receive updates for the MediaSession. This includes
+ * events like route updates, media buttons, and focus changes.
+ *
+ * @param callback The callback to receive updates on.
+ * @param handler The handler that events should be posted on.
+ */
public void addCallback(Callback callback, Handler handler) {
if (callback == null) {
throw new IllegalArgumentException("Callback cannot be null");
@@ -126,6 +136,11 @@ public final class MediaSession {
}
}
+ /**
+ * Remove a callback. It will no longer receive updates.
+ *
+ * @param callback The callback to remove.
+ */
public void removeCallback(Callback callback) {
synchronized (mLock) {
removeCallbackLocked(callback);
@@ -186,30 +201,6 @@ public final class MediaSession {
}
/**
- * 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);
- }
- }
-
- /**
* 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.
@@ -243,16 +234,92 @@ public final class MediaSession {
/**
* Retrieve a token object that can be used by apps to create a
- * {@link MediaController} for interacting with this session. The owner of
+ * {@link SessionController} for interacting with this session. The owner of
* the session is responsible for deciding how to distribute these tokens.
*
* @return A token that can be used to create a MediaController for this
* session
*/
- public MediaSessionToken getSessionToken() {
+ public SessionToken getSessionToken() {
return mSessionToken;
}
+ /**
+ * Connect to the current route using the specified request.
+ * <p>
+ * Connection updates will be sent to the callback's
+ * {@link Callback#onRouteConnected(Route)} and
+ * {@link Callback#onRouteDisconnected(Route, int)} methods. If the
+ * connection fails {@link Callback#onRouteDisconnected(Route, int)}
+ * will be called.
+ * <p>
+ * If you already have a connection to this route it will be disconnected
+ * before the new connection is established. TODO add an easy way to compare
+ * MediaRouteOptions.
+ *
+ * @param route The route the app is trying to connect to.
+ * @param request The connection request to use.
+ */
+ public void connect(RouteInfo route, RouteOptions request) {
+ if (route == null) {
+ throw new IllegalArgumentException("Must specify the route");
+ }
+ if (request == null) {
+ throw new IllegalArgumentException("Must specify the connection request");
+ }
+ try {
+ mBinder.connectToRoute(route, request);
+ } catch (RemoteException e) {
+ Log.wtf(TAG, "Error starting connection to route", e);
+ }
+ }
+
+ /**
+ * Disconnect from the current route. After calling you will be switched
+ * back to the default route.
+ *
+ * @param route The route to disconnect from.
+ */
+ public void disconnect(RouteInfo route) {
+ // TODO
+ }
+
+ /**
+ * Set the list of route options your app is interested in connecting to. It
+ * will be used for picking valid routes.
+ *
+ * @param options The set of route options your app may use to connect.
+ */
+ public void setRouteOptions(List<RouteOptions> options) {
+ try {
+ mBinder.setRouteOptions(options);
+ } catch (RemoteException e) {
+ Log.wtf(TAG, "Error setting route options.", e);
+ }
+ }
+
+ /**
+ * @hide
+ * TODO allow multiple listeners for the same interface, allow removal
+ */
+ public void addInterfaceListener(String iface,
+ RouteInterface.EventListener listener) {
+ mInterfaceListeners.put(iface, listener);
+ }
+
+ /**
+ * @hide
+ */
+ public boolean sendRouteCommand(RouteCommand command, ResultReceiver cb) {
+ try {
+ mBinder.sendRouteCommand(command, cb);
+ } catch (RemoteException e) {
+ Log.wtf(TAG, "Error sending command to route.", e);
+ return false;
+ }
+ return true;
+ }
+
private MessageHandler getHandlerForCallbackLocked(Callback cb) {
if (cb == null) {
throw new IllegalArgumentException("Callback cannot be null");
@@ -297,10 +364,19 @@ public final class MediaSession {
}
}
- private void postRequestRouteChange(Bundle mediaRouteDescriptor) {
+ private void postRequestRouteChange(RouteInfo route) {
+ synchronized (mLock) {
+ for (int i = mCallbacks.size() - 1; i >= 0; i--) {
+ mCallbacks.get(i).post(MSG_ROUTE_CHANGE, route);
+ }
+ }
+ }
+
+ private void postRouteConnected(RouteInfo route, RouteOptions options) {
synchronized (mLock) {
+ mRoute = new Route(route, options, this);
for (int i = mCallbacks.size() - 1; i >= 0; i--) {
- mCallbacks.get(i).post(MSG_ROUTE_CHANGE, mediaRouteDescriptor);
+ mCallbacks.get(i).post(MSG_ROUTE_CONNECTED, mRoute);
}
}
}
@@ -346,26 +422,49 @@ public final class MediaSession {
* The app is responsible for connecting to the new route and migrating
* ongoing playback if necessary.
*
- * @param descriptor
+ * @param route
*/
- public void onRequestRouteChange(Bundle descriptor) {
+ public void onRequestRouteChange(RouteInfo route) {
+ }
+
+ /**
+ * Called when a route has successfully connected. Calls to the route
+ * are now valid.
+ *
+ * @param route The route that was connected
+ */
+ public void onRouteConnected(Route route) {
+ }
+
+ /**
+ * Called when a route was disconnected. Further calls to the route will
+ * fail. If available a reason for being disconnected will be provided.
+ * <p>
+ * Valid reasons are:
+ * <ul>
+ * </ul>
+ *
+ * @param route The route that disconnected
+ * @param reason The reason for the disconnect
+ */
+ public void onRouteDisconnected(Route route, int reason) {
}
}
/**
* @hide
*/
- public static class CallbackStub extends IMediaSessionCallback.Stub {
- private WeakReference<MediaSession> mMediaSession;
+ public static class CallbackStub extends ISessionCallback.Stub {
+ private WeakReference<Session> mMediaSession;
- public void setMediaSession(MediaSession session) {
- mMediaSession = new WeakReference<MediaSession>(session);
+ public void setMediaSession(Session session) {
+ mMediaSession = new WeakReference<Session>(session);
}
@Override
public void onCommand(String command, Bundle extras, ResultReceiver cb)
throws RemoteException {
- MediaSession session = mMediaSession.get();
+ Session session = mMediaSession.get();
if (session != null) {
session.postCommand(command, extras, cb);
}
@@ -373,23 +472,31 @@ public final class MediaSession {
@Override
public void onMediaButton(Intent mediaButtonIntent) throws RemoteException {
- MediaSession session = mMediaSession.get();
+ Session session = mMediaSession.get();
if (session != null) {
session.postMediaButton(mediaButtonIntent);
}
}
@Override
- public void onRequestRouteChange(Bundle mediaRouteDescriptor) throws RemoteException {
- MediaSession session = mMediaSession.get();
+ public void onRequestRouteChange(RouteInfo route) throws RemoteException {
+ Session session = mMediaSession.get();
if (session != null) {
- session.postRequestRouteChange(mediaRouteDescriptor);
+ session.postRequestRouteChange(route);
+ }
+ }
+
+ @Override
+ public void onRouteConnected(RouteInfo route, RouteOptions options) {
+ Session session = mMediaSession.get();
+ if (session != null) {
+ session.postRouteConnected(route, options);
}
}
@Override
public void onPlay() throws RemoteException {
- MediaSession session = mMediaSession.get();
+ Session session = mMediaSession.get();
if (session != null) {
TransportPerformer tp = session.getTransportPerformer();
if (tp != null) {
@@ -400,7 +507,7 @@ public final class MediaSession {
@Override
public void onPause() throws RemoteException {
- MediaSession session = mMediaSession.get();
+ Session session = mMediaSession.get();
if (session != null) {
TransportPerformer tp = session.getTransportPerformer();
if (tp != null) {
@@ -411,7 +518,7 @@ public final class MediaSession {
@Override
public void onStop() throws RemoteException {
- MediaSession session = mMediaSession.get();
+ Session session = mMediaSession.get();
if (session != null) {
TransportPerformer tp = session.getTransportPerformer();
if (tp != null) {
@@ -422,7 +529,7 @@ public final class MediaSession {
@Override
public void onNext() throws RemoteException {
- MediaSession session = mMediaSession.get();
+ Session session = mMediaSession.get();
if (session != null) {
TransportPerformer tp = session.getTransportPerformer();
if (tp != null) {
@@ -433,7 +540,7 @@ public final class MediaSession {
@Override
public void onPrevious() throws RemoteException {
- MediaSession session = mMediaSession.get();
+ Session session = mMediaSession.get();
if (session != null) {
TransportPerformer tp = session.getTransportPerformer();
if (tp != null) {
@@ -444,7 +551,7 @@ public final class MediaSession {
@Override
public void onFastForward() throws RemoteException {
- MediaSession session = mMediaSession.get();
+ Session session = mMediaSession.get();
if (session != null) {
TransportPerformer tp = session.getTransportPerformer();
if (tp != null) {
@@ -455,7 +562,7 @@ public final class MediaSession {
@Override
public void onRewind() throws RemoteException {
- MediaSession session = mMediaSession.get();
+ Session session = mMediaSession.get();
if (session != null) {
TransportPerformer tp = session.getTransportPerformer();
if (tp != null) {
@@ -466,7 +573,7 @@ public final class MediaSession {
@Override
public void onSeekTo(long pos) throws RemoteException {
- MediaSession session = mMediaSession.get();
+ Session session = mMediaSession.get();
if (session != null) {
TransportPerformer tp = session.getTransportPerformer();
if (tp != null) {
@@ -477,7 +584,7 @@ public final class MediaSession {
@Override
public void onRate(Rating rating) throws RemoteException {
- MediaSession session = mMediaSession.get();
+ Session session = mMediaSession.get();
if (session != null) {
TransportPerformer tp = session.getTransportPerformer();
if (tp != null) {
@@ -486,12 +593,32 @@ public final class MediaSession {
}
}
+ @Override
+ public void onRouteEvent(RouteEvent event) throws RemoteException {
+ Session session = mMediaSession.get();
+ if (session != null) {
+ RouteInterface.EventListener iface
+ = session.mInterfaceListeners.get(event.getIface());
+ Log.d(TAG, "Received route event on iface " + event.getIface() + ". Listener is "
+ + iface);
+ if (iface != null) {
+ iface.onEvent(event.getEvent(), event.getExtras());
+ }
+ }
+ }
+
+ @Override
+ public void onRouteStateChange(int state) throws RemoteException {
+ // TODO
+
+ }
+
}
private class MessageHandler extends Handler {
- private MediaSession.Callback mCallback;
+ private Session.Callback mCallback;
- public MessageHandler(Looper looper, MediaSession.Callback callback) {
+ public MessageHandler(Looper looper, Session.Callback callback) {
super(looper, null, true);
mCallback = callback;
}
@@ -511,11 +638,13 @@ public final class MediaSession {
mCallback.onCommand(cmd.command, cmd.extras, cmd.stub);
break;
case MSG_ROUTE_CHANGE:
- mCallback.onRequestRouteChange((Bundle) msg.obj);
+ mCallback.onRequestRouteChange((RouteInfo) msg.obj);
+ break;
+ case MSG_ROUTE_CONNECTED:
+ mCallback.onRouteConnected((Route) msg.obj);
break;
}
}
- msg.recycle();
}
public void post(int what, Object obj) {
diff --git a/media/java/android/media/session/MediaController.java b/media/java/android/media/session/SessionController.java
index afd8b11..dc4f7d9 100644
--- a/media/java/android/media/session/MediaController.java
+++ b/media/java/android/media/session/SessionController.java
@@ -34,21 +34,21 @@ import java.util.ArrayList;
* other commands can be sent to the session. A callback may be registered to
* receive updates from the session, such as metadata and play state changes.
* <p>
- * A MediaController can be created through {@link MediaSessionManager} if you
+ * A MediaController can be created through {@link SessionManager} if you
* hold the "android.permission.MEDIA_CONTENT_CONTROL" permission or directly if
- * you have a {@link MediaSessionToken} from the session owner.
+ * you have a {@link SessionToken} from the session owner.
* <p>
* MediaController objects are thread-safe.
*/
-public final class MediaController {
- private static final String TAG = "MediaController";
+public final class SessionController {
+ private static final String TAG = "SessionController";
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 MSG_ROUTE = 4;
- private final IMediaController mSessionBinder;
+ private final ISessionController mSessionBinder;
private final CallbackStub mCbStub = new CallbackStub(this);
private final ArrayList<MessageHandler> mCallbacks = new ArrayList<MessageHandler>();
@@ -58,15 +58,15 @@ public final class MediaController {
private TransportController mTransportController;
- private MediaController(IMediaController sessionBinder) {
+ private SessionController(ISessionController sessionBinder) {
mSessionBinder = sessionBinder;
}
/**
* @hide
*/
- public static MediaController fromBinder(IMediaController sessionBinder) {
- MediaController controller = new MediaController(sessionBinder);
+ public static SessionController fromBinder(ISessionController sessionBinder) {
+ SessionController controller = new SessionController(sessionBinder);
try {
controller.mSessionBinder.registerCallbackListener(controller.mCbStub);
if (controller.mSessionBinder.isTransportControlEnabled()) {
@@ -87,7 +87,7 @@ public final class MediaController {
* @param token The session token to use
* @return A controller for the session or null
*/
- public static MediaController fromToken(MediaSessionToken token) {
+ public static SessionController fromToken(SessionToken token) {
return fromBinder(token.getBinder());
}
@@ -181,10 +181,22 @@ public final class MediaController {
}
}
+ /**
+ * Request that the route picker be shown for this session. This should
+ * generally be called in response to a user action.
+ */
+ public void showRoutePicker() {
+ try {
+ mSessionBinder.showRoutePicker();
+ } catch (RemoteException e) {
+ Log.d(TAG, "Dead object in showRoutePicker", e);
+ }
+ }
+
/*
* @hide
*/
- IMediaController getSessionBinder() {
+ ISessionController getSessionBinder() {
return mSessionBinder;
}
@@ -247,10 +259,10 @@ public final class MediaController {
}
}
- private void postRouteChanged(Bundle routeDescriptor) {
+ private void postRouteChanged(RouteInfo route) {
synchronized (mLock) {
for (int i = mCallbacks.size() - 1; i >= 0; i--) {
- mCallbacks.get(i).post(MSG_ROUTE, null, routeDescriptor);
+ mCallbacks.get(i).post(MSG_ROUTE, route, null);
}
}
}
@@ -275,36 +287,36 @@ public final class MediaController {
*
* @param route
*/
- public void onRouteChanged(Bundle route) {
+ public void onRouteChanged(RouteInfo route) {
}
}
- private final static class CallbackStub extends IMediaControllerCallback.Stub {
- private final WeakReference<MediaController> mController;
+ private final static class CallbackStub extends ISessionControllerCallback.Stub {
+ private final WeakReference<SessionController> mController;
- public CallbackStub(MediaController controller) {
- mController = new WeakReference<MediaController>(controller);
+ public CallbackStub(SessionController controller) {
+ mController = new WeakReference<SessionController>(controller);
}
@Override
public void onEvent(String event, Bundle extras) {
- MediaController controller = mController.get();
+ SessionController controller = mController.get();
if (controller != null) {
controller.postEvent(event, extras);
}
}
@Override
- public void onRouteChanged(Bundle mediaRouteDescriptor) {
- MediaController controller = mController.get();
+ public void onRouteChanged(RouteInfo route) {
+ SessionController controller = mController.get();
if (controller != null) {
- controller.postRouteChanged(mediaRouteDescriptor);
+ controller.postRouteChanged(route);
}
}
@Override
public void onPlaybackStateChanged(PlaybackState state) {
- MediaController controller = mController.get();
+ SessionController controller = mController.get();
if (controller != null) {
TransportController tc = controller.getTransportController();
if (tc != null) {
@@ -315,7 +327,7 @@ public final class MediaController {
@Override
public void onMetadataChanged(MediaMetadata metadata) {
- MediaController controller = mController.get();
+ SessionController controller = mController.get();
if (controller != null) {
TransportController tc = controller.getTransportController();
if (tc != null) {
@@ -327,9 +339,9 @@ public final class MediaController {
}
private final static class MessageHandler extends Handler {
- private final MediaController.Callback mCallback;
+ private final SessionController.Callback mCallback;
- public MessageHandler(Looper looper, MediaController.Callback cb) {
+ public MessageHandler(Looper looper, SessionController.Callback cb) {
super(looper, null, true);
mCallback = cb;
}
@@ -341,7 +353,7 @@ public final class MediaController {
mCallback.onEvent((String) msg.obj, msg.getData());
break;
case MSG_ROUTE:
- mCallback.onRouteChanged(msg.getData());
+ mCallback.onRouteChanged((RouteInfo) msg.obj);
}
}
diff --git a/media/java/android/media/session/SessionInfo.java b/media/java/android/media/session/SessionInfo.java
new file mode 100644
index 0000000..22d8ab1
--- /dev/null
+++ b/media/java/android/media/session/SessionInfo.java
@@ -0,0 +1,82 @@
+/*
+ * 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.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Information about a media session, including the owner's package name.
+ */
+public final class SessionInfo implements Parcelable {
+ private final String mId;
+ private final String mPackageName;
+
+ /**
+ * @hide
+ */
+ public SessionInfo(String id, String packageName) {
+ mId = id;
+ mPackageName = packageName;
+ }
+
+ private SessionInfo(Parcel in) {
+ mId = in.readString();
+ mPackageName = in.readString();
+ }
+
+ /**
+ * Get the package name of the owner of this session.
+ *
+ * @return The owner's package name
+ */
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * Get the unique id for this session.
+ *
+ * @return The id for the session.
+ */
+ public String getId() {
+ return mId;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mId);
+ dest.writeString(mPackageName);
+ }
+
+ public static final Parcelable.Creator<SessionInfo> CREATOR
+ = new Parcelable.Creator<SessionInfo>() {
+ @Override
+ public SessionInfo createFromParcel(Parcel in) {
+ return new SessionInfo(in);
+ }
+
+ @Override
+ public SessionInfo[] newArray(int size) {
+ return new SessionInfo[size];
+ }
+ };
+}
diff --git a/media/java/android/media/session/MediaSessionManager.java b/media/java/android/media/session/SessionManager.java
index e3f2d9c..15bf0e3 100644
--- a/media/java/android/media/session/MediaSessionManager.java
+++ b/media/java/android/media/session/SessionManager.java
@@ -17,7 +17,7 @@
package android.media.session;
import android.content.Context;
-import android.media.session.IMediaSessionManager;
+import android.media.session.ISessionManager;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -35,37 +35,37 @@ import java.util.List;
* get an instance of this class.
* <p>
*
- * @see MediaSession
- * @see MediaController
+ * @see Session
+ * @see SessionController
*/
-public final class MediaSessionManager {
- private static final String TAG = "MediaSessionManager";
+public final class SessionManager {
+ private static final String TAG = "SessionManager";
- private final IMediaSessionManager mService;
+ private final ISessionManager mService;
private Context mContext;
/**
* @hide
*/
- public MediaSessionManager(Context context) {
+ public SessionManager(Context context) {
// Consider rewriting like DisplayManagerGlobal
// Decide if we need context
mContext = context;
IBinder b = ServiceManager.getService(Context.MEDIA_SESSION_SERVICE);
- mService = IMediaSessionManager.Stub.asInterface(b);
+ mService = ISessionManager.Stub.asInterface(b);
}
/**
* Creates a new session.
*
* @param tag A short name for debugging purposes
- * @return a {@link MediaSession} for the new session
+ * @return a {@link Session} for the new session
*/
- public MediaSession createSession(String tag) {
+ public Session createSession(String tag) {
try {
- MediaSession.CallbackStub cbStub = new MediaSession.CallbackStub();
- MediaSession session = new MediaSession(mService
+ Session.CallbackStub cbStub = new Session.CallbackStub();
+ Session session = new Session(mService
.createSession(mContext.getPackageName(), cbStub, tag), cbStub);
cbStub.setMediaSession(session);
@@ -83,8 +83,8 @@ public final class MediaSessionManager {
*
* @return a list of controllers for ongoing sessions
*/
- public List<MediaController> getActiveSessions() {
+ public List<SessionController> getActiveSessions() {
// TODO
- return new ArrayList<MediaController>();
+ return new ArrayList<SessionController>();
}
}
diff --git a/media/java/android/media/session/SessionToken.aidl b/media/java/android/media/session/SessionToken.aidl
new file mode 100644
index 0000000..db35f85
--- /dev/null
+++ b/media/java/android/media/session/SessionToken.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 SessionToken;
diff --git a/media/java/android/media/session/MediaSessionToken.java b/media/java/android/media/session/SessionToken.java
index dbb4964..59486f6 100644
--- a/media/java/android/media/session/MediaSessionToken.java
+++ b/media/java/android/media/session/SessionToken.java
@@ -16,28 +16,28 @@
package android.media.session;
-import android.media.session.IMediaController;
+import android.media.session.ISessionController;
import android.os.Parcel;
import android.os.Parcelable;
-public class MediaSessionToken implements Parcelable {
- private IMediaController mBinder;
+public class SessionToken implements Parcelable {
+ private ISessionController mBinder;
/**
* @hide
*/
- MediaSessionToken(IMediaController binder) {
+ SessionToken(ISessionController binder) {
mBinder = binder;
}
- private MediaSessionToken(Parcel in) {
- mBinder = IMediaController.Stub.asInterface(in.readStrongBinder());
+ private SessionToken(Parcel in) {
+ mBinder = ISessionController.Stub.asInterface(in.readStrongBinder());
}
/**
* @hide
*/
- IMediaController getBinder() {
+ ISessionController getBinder() {
return mBinder;
}
@@ -51,16 +51,16 @@ public class MediaSessionToken implements Parcelable {
dest.writeStrongBinder(mBinder.asBinder());
}
- public static final Parcelable.Creator<MediaSessionToken> CREATOR
- = new Parcelable.Creator<MediaSessionToken>() {
+ public static final Parcelable.Creator<SessionToken> CREATOR
+ = new Parcelable.Creator<SessionToken>() {
@Override
- public MediaSessionToken createFromParcel(Parcel in) {
- return new MediaSessionToken(in);
+ public SessionToken createFromParcel(Parcel in) {
+ return new SessionToken(in);
}
@Override
- public MediaSessionToken[] newArray(int size) {
- return new MediaSessionToken[size];
+ public SessionToken[] newArray(int size) {
+ return new SessionToken[size];
}
};
}
diff --git a/media/java/android/media/session/TransportController.java b/media/java/android/media/session/TransportController.java
index 15b11f3..9574df6 100644
--- a/media/java/android/media/session/TransportController.java
+++ b/media/java/android/media/session/TransportController.java
@@ -34,12 +34,12 @@ public final class TransportController {
private final Object mLock = new Object();
private final ArrayList<MessageHandler> mListeners = new ArrayList<MessageHandler>();
- private final IMediaController mBinder;
+ private final ISessionController mBinder;
/**
* @hide
*/
- public TransportController(IMediaController binder) {
+ public TransportController(ISessionController binder) {
mBinder = binder;
}
diff --git a/media/java/android/media/session/TransportPerformer.java b/media/java/android/media/session/TransportPerformer.java
index b96db20..eddffd1 100644
--- a/media/java/android/media/session/TransportPerformer.java
+++ b/media/java/android/media/session/TransportPerformer.java
@@ -34,12 +34,12 @@ public final class TransportPerformer {
private final Object mLock = new Object();
private final ArrayList<MessageHandler> mListeners = new ArrayList<MessageHandler>();
- private IMediaSession mBinder;
+ private ISession mBinder;
/**
* @hide
*/
- public TransportPerformer(IMediaSession binder) {
+ public TransportPerformer(ISession binder) {
mBinder = binder;
}
diff --git a/services/core/java/com/android/server/media/MediaRouteProviderProxy.java b/services/core/java/com/android/server/media/MediaRouteProviderProxy.java
new file mode 100644
index 0000000..d314ea7
--- /dev/null
+++ b/services/core/java/com/android/server/media/MediaRouteProviderProxy.java
@@ -0,0 +1,379 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.media;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.media.routeprovider.IRouteConnection;
+import android.media.routeprovider.IRouteProvider;
+import android.media.routeprovider.IRouteProviderCallback;
+import android.media.routeprovider.RouteProviderService;
+import android.media.routeprovider.RouteRequest;
+import android.media.session.RouteEvent;
+import android.media.session.RouteInfo;
+import android.media.session.Session;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.UserHandle;
+import android.util.Log;
+import android.util.Slog;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * System representation and interface to a MediaRouteProvider. This class is
+ * not thread safe so all calls should be made on the main thread.
+ */
+public class MediaRouteProviderProxy {
+ private static final String TAG = "MRPProxy";
+ private static final boolean DEBUG = true;
+
+ private static final int MAX_RETRIES = 3;
+
+ private final Object mLock = new Object();
+ private final Context mContext;
+ private final String mId;
+ private final ComponentName mComponentName;
+ private final int mUserId;
+
+ private Intent mBindIntent;
+ // Interfaces declared in the manifest
+ private ArrayList<String> mInterfaces;
+ private ArrayList<RouteConnectionRecord> mConnections = new ArrayList<RouteConnectionRecord>();
+ private Handler mHandler = new Handler();
+
+ private IRouteProvider mBinder;
+ private boolean mRunning;
+ private boolean mInterested;
+ private boolean mBound;
+ private int mRetryCount;
+
+ private RoutesListener mRouteListener;
+
+ public MediaRouteProviderProxy(Context context, String id, ComponentName component, int uid,
+ ArrayList<String> interfaces) {
+ mContext = context;
+ mId = id;
+ mComponentName = component;
+ mUserId = uid;
+ mInterfaces = interfaces;
+ mBindIntent = new Intent(RouteProviderService.SERVICE_INTERFACE);
+ mBindIntent.setComponent(mComponentName);
+ }
+
+ /**
+ * Send any cleanup messages and unbind from the media route provider
+ */
+ public void stop() {
+ if (mRunning) {
+ mRunning = false;
+ mRetryCount = 0;
+ updateBinding();
+ }
+ }
+
+ /**
+ * Bind to the media route provider and perform any setup needed
+ */
+ public void start() {
+ if (!mRunning) {
+ mRunning = true;
+ updateBinding();
+ }
+ }
+
+ /**
+ * Set whether or not this provider is currently interesting to the system.
+ * In the future this may take a list of interfaces instead.
+ *
+ * @param interested True if we want to connect to this provider
+ */
+ public void setInterested(boolean interested) {
+ mInterested = interested;
+ updateBinding();
+ }
+
+ /**
+ * Set a listener to get route updates on.
+ *
+ * @param listener The listener to receive updates on.
+ */
+ public void setRoutesListener(RoutesListener listener) {
+ mRouteListener = listener;
+ }
+
+ /**
+ * Send a request to the Provider to get all the routes that the session can
+ * use.
+ *
+ * @param record The session to get routes for.
+ * @param requestId An id to identify this request.
+ */
+ public void getRoutes(MediaSessionRecord record, final int requestId) {
+ // TODO change routes to have a system global id and maintain a mapping
+ // to the original route
+ if (mBinder == null) {
+ Log.wtf(TAG, "Attempted to call getRoutes without a binder connection");
+ return;
+ }
+ List<RouteRequest> requests = record.getRouteRequests();
+ final String sessionId = record.getSessionInfo().getId();
+ try {
+ mBinder.getAvailableRoutes(requests, new ResultReceiver(mHandler) {
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (resultCode != RouteProviderService.RESULT_SUCCESS) {
+ // ignore failures, just means no routes were generated
+ return;
+ }
+ ArrayList<RouteInfo> routes
+ = resultData.getParcelableArrayList(RouteProviderService.KEY_ROUTES);
+ ArrayList<RouteInfo> sysRoutes = new ArrayList<RouteInfo>();
+ for (int i = 0; i < routes.size(); i++) {
+ RouteInfo route = routes.get(i);
+ RouteInfo.Builder bob = new RouteInfo.Builder(route);
+ bob.setProviderId(mId);
+ sysRoutes.add(bob.build());
+ }
+ if (mRouteListener != null) {
+ mRouteListener.onRoutesUpdated(sessionId, sysRoutes, requestId);
+ }
+ }
+ });
+ } catch (RemoteException e) {
+ Log.d(TAG, "Error in getRoutes", e);
+ }
+ }
+
+ /**
+ * Try connecting again if we've been disconnected.
+ */
+ public void rebindIfDisconnected() {
+ if (mBinder == null && shouldBind()) {
+ unbind();
+ bind();
+ }
+ }
+
+ /**
+ * Send a request to connect to a route.
+ *
+ * @param session The session that is trying to connect.
+ * @param route The route it is connecting to.
+ * @param request The request with the connection parameters.
+ * @return true if the request was sent, false otherwise.
+ */
+ public boolean connectToRoute(MediaSessionRecord session, final RouteInfo route,
+ final RouteRequest request) {
+ final String sessionId = session.getSessionInfo().getId();
+ try {
+ mBinder.connect(route, request, new ResultReceiver(mHandler) {
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (resultCode != RouteProviderService.RESULT_SUCCESS) {
+ // TODO handle connection failure
+ return;
+ }
+ IBinder binder = resultData.getBinder(RouteProviderService.KEY_CONNECTION);
+ IRouteConnection connection = null;
+ if (binder != null) {
+ connection = IRouteConnection.Stub.asInterface(binder);
+ }
+
+ if (connection != null) {
+ RouteConnectionRecord record = new RouteConnectionRecord(
+ connection);
+ mConnections.add(record);
+ if (mRouteListener != null) {
+ mRouteListener.onRouteConnected(sessionId, route, request, record);
+ }
+ }
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error connecting to route.", e);
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Check if this is the provider you're looking for.
+ */
+ public boolean hasComponentName(String packageName, String className) {
+ return mComponentName.getPackageName().equals(packageName)
+ && mComponentName.getClassName().equals(className);
+ }
+
+ /**
+ * Get the unique id for this provider.
+ *
+ * @return The provider's id.
+ */
+ public String getId() {
+ return mId;
+ }
+
+ private void updateBinding() {
+ if (shouldBind()) {
+ bind();
+ } else {
+ unbind();
+ }
+ }
+
+ private boolean shouldBind() {
+ return mRunning && mInterested;
+ }
+
+ private void bind() {
+ if (!mBound) {
+ if (DEBUG) {
+ Slog.d(TAG, this + ": Binding");
+ }
+
+ try {
+ mBound = mContext.bindServiceAsUser(mBindIntent, mServiceConn,
+ Context.BIND_AUTO_CREATE, new UserHandle(mUserId));
+ if (!mBound && DEBUG) {
+ Slog.d(TAG, this + ": Bind failed");
+ }
+ } catch (SecurityException ex) {
+ if (DEBUG) {
+ Slog.d(TAG, this + ": Bind failed", ex);
+ }
+ }
+ }
+ }
+
+ private void unbind() {
+ if (mBound) {
+ if (DEBUG) {
+ Slog.d(TAG, this + ": Unbinding");
+ }
+
+ mBound = false;
+ mContext.unbindService(mServiceConn);
+ }
+ }
+
+ private RouteConnectionRecord getConnectionLocked(IBinder binder) {
+ for (int i = mConnections.size() - 1; i >= 0; i--) {
+ RouteConnectionRecord record = mConnections.get(i);
+ if (record.isConnection(binder)) {
+ return record;
+ }
+ }
+ return null;
+ }
+
+ private ServiceConnection mServiceConn = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ mBinder = IRouteProvider.Stub.asInterface(service);
+ if (DEBUG) {
+ Slog.d(TAG, "Connected to route provider");
+ }
+ try {
+ mBinder.registerCallback(mCbStub);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Error registering callback on route provider. Retry count: "
+ + mRetryCount, e);
+ if (mRetryCount < MAX_RETRIES) {
+ mRetryCount++;
+ rebindIfDisconnected();
+ }
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ mBinder = null;
+ if (DEBUG) {
+ Slog.d(TAG, "Disconnected from route provider");
+ }
+ }
+
+ };
+
+ private IRouteProviderCallback.Stub mCbStub = new IRouteProviderCallback.Stub() {
+ @Override
+ public void onConnectionStateChanged(IRouteConnection connection, int state)
+ throws RemoteException {
+ // TODO
+ }
+
+ @Override
+ public void onRouteEvent(RouteEvent event) throws RemoteException {
+ synchronized (mLock) {
+ RouteConnectionRecord record = getConnectionLocked(event.getConnection());
+ Log.d(TAG, "Received route event for record " + record);
+ if (record != null) {
+ record.sendEvent(event);
+ }
+ }
+ }
+
+ @Override
+ public void onConnectionTerminated(IRouteConnection connection) throws RemoteException {
+ synchronized (mLock) {
+ RouteConnectionRecord record = getConnectionLocked(connection.asBinder());
+ if (record != null) {
+ record.disconnect();
+ mConnections.remove(record);
+ }
+ }
+ }
+
+ @Override
+ public void onRoutesChanged() throws RemoteException {
+ // TODO
+ }
+ };
+
+ /**
+ * Listener for receiving responses to route requests on the provider.
+ */
+ public interface RoutesListener {
+ /**
+ * Called when routes have been returned from a request to getRoutes.
+ *
+ * @param record The session that the routes were requested for.
+ * @param routes The matching routes returned by the provider.
+ * @param reqId The request id this is responding to.
+ */
+ public void onRoutesUpdated(String sessionId, ArrayList<RouteInfo> routes,
+ int reqId);
+
+ /**
+ * Called when a route has successfully connected.
+ *
+ * @param session The session that was connected.
+ * @param route The route it connected to.
+ * @param options The options that were used for the connection.
+ * @param connection The connection instance that was created.
+ */
+ public void onRouteConnected(String sessionId, RouteInfo route,
+ RouteRequest options, RouteConnectionRecord connection);
+ }
+}
diff --git a/services/core/java/com/android/server/media/MediaRouteProviderWatcher.java b/services/core/java/com/android/server/media/MediaRouteProviderWatcher.java
new file mode 100644
index 0000000..cf1d95a
--- /dev/null
+++ b/services/core/java/com/android/server/media/MediaRouteProviderWatcher.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.media;
+
+import android.Manifest;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.media.routeprovider.RouteProviderService;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.Slog;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.UUID;
+
+/**
+ * Watches for media route provider services to be installed. Adds a provider to
+ * the media session service for each registered service. For now just run all
+ * providers. In the future define a policy for when to run providers.
+ */
+public class MediaRouteProviderWatcher {
+ private static final String TAG = "MRPWatcher";
+ private static final boolean DEBUG = true; // Log.isLoggable(TAG,
+ // Log.DEBUG);
+
+ private final Context mContext;
+ private final Callback mCallback;
+ private final Handler mHandler;
+ private final int mUserId;
+ private final PackageManager mPackageManager;
+
+ private final ArrayList<MediaRouteProviderProxy> mProviders =
+ new ArrayList<MediaRouteProviderProxy>();
+ private boolean mRunning;
+
+ public MediaRouteProviderWatcher(Context context, Callback callback, Handler handler,
+ int userId) {
+ mContext = context;
+ mCallback = callback;
+ mHandler = handler;
+ mUserId = userId;
+ mPackageManager = context.getPackageManager();
+ }
+
+ public void dump(PrintWriter pw, String prefix) {
+ pw.println(prefix + " mUserId=" + mUserId);
+ pw.println(prefix + " mRunning=" + mRunning);
+ pw.println(prefix + " mProviders.size()=" + mProviders.size());
+ }
+
+ public void start() {
+ if (!mRunning) {
+ mRunning = true;
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_PACKAGE_ADDED);
+ filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+ filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+ filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
+ filter.addAction(Intent.ACTION_PACKAGE_RESTARTED);
+ filter.addDataScheme("package");
+ mContext.registerReceiverAsUser(mScanPackagesReceiver,
+ new UserHandle(mUserId), filter, null, mHandler);
+
+ // Scan packages.
+ // Also has the side-effect of restarting providers if needed.
+ mHandler.post(mScanPackagesRunnable);
+ }
+ }
+
+ public void stop() {
+ if (mRunning) {
+ mRunning = false;
+
+ mContext.unregisterReceiver(mScanPackagesReceiver);
+ mHandler.removeCallbacks(mScanPackagesRunnable);
+
+ // Stop all providers.
+ for (int i = mProviders.size() - 1; i >= 0; i--) {
+ mProviders.get(i).stop();
+ }
+ }
+ }
+
+ public ArrayList<MediaRouteProviderProxy> getProviders() {
+ return mProviders;
+ }
+
+ public MediaRouteProviderProxy getProvider(String id) {
+ int providerIndex = findProvider(id);
+ if (providerIndex != -1) {
+ return mProviders.get(providerIndex);
+ }
+ return null;
+ }
+
+ private void scanPackages() {
+ if (!mRunning) {
+ return;
+ }
+
+ // Add providers for all new services.
+ // Reorder the list so that providers left at the end will be the ones
+ // to remove.
+ int targetIndex = 0;
+ Intent intent = new Intent(RouteProviderService.SERVICE_INTERFACE);
+ for (ResolveInfo resolveInfo : mPackageManager.queryIntentServicesAsUser(
+ intent, 0, mUserId)) {
+ ServiceInfo serviceInfo = resolveInfo.serviceInfo;
+ if (DEBUG) {
+ Slog.d(TAG, "Checking service " + (serviceInfo == null ? null : serviceInfo.name));
+ }
+ if (serviceInfo != null && verifyServiceTrusted(serviceInfo)) {
+ int sourceIndex = findProvider(serviceInfo.packageName, serviceInfo.name);
+ if (sourceIndex < 0) {
+ // TODO get declared interfaces from manifest
+ if (DEBUG) {
+ Slog.d(TAG, "Creating new provider proxy for service");
+ }
+ MediaRouteProviderProxy provider =
+ new MediaRouteProviderProxy(mContext, UUID.randomUUID().toString(),
+ new ComponentName(serviceInfo.packageName, serviceInfo.name),
+ mUserId, null);
+ provider.start();
+ mProviders.add(targetIndex++, provider);
+ mCallback.addProvider(provider);
+ } else if (sourceIndex >= targetIndex) {
+ MediaRouteProviderProxy provider = mProviders.get(sourceIndex);
+ provider.start(); // restart the provider if needed
+ provider.rebindIfDisconnected();
+ Collections.swap(mProviders, sourceIndex, targetIndex++);
+ }
+ }
+ }
+
+ // Remove providers for missing services.
+ if (targetIndex < mProviders.size()) {
+ for (int i = mProviders.size() - 1; i >= targetIndex; i--) {
+ MediaRouteProviderProxy provider = mProviders.get(i);
+ mCallback.removeProvider(provider);
+ mProviders.remove(provider);
+ provider.stop();
+ }
+ }
+ }
+
+ private boolean verifyServiceTrusted(ServiceInfo serviceInfo) {
+ if (serviceInfo.permission == null || !serviceInfo.permission.equals(
+ Manifest.permission.BIND_ROUTE_PROVIDER)) {
+ // If the service does not require this permission then any app
+ // could potentially bind to it and mess with their routes. So we
+ // only want to trust providers that require the
+ // correct permissions.
+ Slog.w(TAG, "Ignoring route provider service because it did not "
+ + "require the BIND_ROUTE_PROVIDER permission in its manifest: "
+ + serviceInfo.packageName + "/" + serviceInfo.name);
+ return false;
+ }
+ // Looks good.
+ return true;
+ }
+
+ private int findProvider(String id) {
+ int count = mProviders.size();
+ for (int i = 0; i < count; i++) {
+ MediaRouteProviderProxy provider = mProviders.get(i);
+ if (TextUtils.equals(id, provider.getId())) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private int findProvider(String packageName, String className) {
+ int count = mProviders.size();
+ for (int i = 0; i < count; i++) {
+ MediaRouteProviderProxy provider = mProviders.get(i);
+ if (provider.hasComponentName(packageName, className)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private final BroadcastReceiver mScanPackagesReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (DEBUG) {
+ Slog.d(TAG, "Received package manager broadcast: " + intent);
+ }
+ scanPackages();
+ }
+ };
+
+ private final Runnable mScanPackagesRunnable = new Runnable() {
+ @Override
+ public void run() {
+ scanPackages();
+ }
+ };
+
+ public interface Callback {
+ void addProvider(MediaRouteProviderProxy provider);
+
+ void removeProvider(MediaRouteProviderProxy provider);
+ }
+}
diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java
index 1ff925c..ac7f4f3 100644
--- a/services/core/java/com/android/server/media/MediaSessionRecord.java
+++ b/services/core/java/com/android/server/media/MediaSessionRecord.java
@@ -17,11 +17,20 @@
package com.android.server.media;
import android.content.Intent;
-import android.media.session.IMediaController;
-import android.media.session.IMediaControllerCallback;
-import android.media.session.IMediaSession;
-import android.media.session.IMediaSessionCallback;
+import android.media.routeprovider.RouteRequest;
+import android.media.session.ISessionController;
+import android.media.session.ISessionControllerCallback;
+import android.media.session.ISession;
+import android.media.session.ISessionCallback;
+import android.media.session.SessionController;
import android.media.session.MediaMetadata;
+import android.media.session.RouteCommand;
+import android.media.session.RouteInfo;
+import android.media.session.RouteOptions;
+import android.media.session.RouteEvent;
+import android.media.session.Session;
+import android.media.session.SessionInfo;
+import android.media.session.RouteInterface;
import android.media.session.PlaybackState;
import android.media.Rating;
import android.os.Bundle;
@@ -31,37 +40,44 @@ 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.util.Pair;
import android.util.Slog;
import android.view.KeyEvent;
import java.util.ArrayList;
import java.util.List;
+import java.util.UUID;
/**
* This is the system implementation of a Session. Apps will interact with the
* MediaSession wrapper class instead.
*/
public class MediaSessionRecord implements IBinder.DeathRecipient {
- private static final String TAG = "MediaSessionImpl";
+ private static final String TAG = "MediaSessionRecord";
private final MessageHandler mHandler;
private final int mPid;
- private final String mPackageName;
+ private final SessionInfo mSessionInfo;
private final String mTag;
private final ControllerStub mController;
private final SessionStub mSession;
private final SessionCb mSessionCb;
private final MediaSessionService mService;
- private final Object mControllerLock = new Object();
- private final ArrayList<IMediaControllerCallback> mControllerCallbacks =
- new ArrayList<IMediaControllerCallback>();
- private final ArrayList<String> mInterfaces = new ArrayList<String>();
+ private final Object mLock = new Object();
+ private final ArrayList<ISessionControllerCallback> mControllerCallbacks =
+ new ArrayList<ISessionControllerCallback>();
+ private final ArrayList<RouteRequest> mRequests = new ArrayList<RouteRequest>();
private boolean mTransportPerformerEnabled = false;
- private Bundle mRoute;
+ private RouteInfo mRoute;
+ private RouteOptions mRequest;
+ private RouteConnectionRecord mConnection;
+ // TODO define a RouteState class with relevant info
+ private int mRouteState;
// TransportPerformer fields
@@ -72,10 +88,10 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
private boolean mIsPublished = false;
- public MediaSessionRecord(int pid, String packageName, IMediaSessionCallback cb, String tag,
+ public MediaSessionRecord(int pid, String packageName, ISessionCallback cb, String tag,
MediaSessionService service, Handler handler) {
mPid = pid;
- mPackageName = packageName;
+ mSessionInfo = new SessionInfo(UUID.randomUUID().toString(), packageName);
mTag = tag;
mController = new ControllerStub();
mSession = new SessionStub();
@@ -84,31 +100,140 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
mHandler = new MessageHandler(handler.getLooper());
}
- public IMediaSession getSessionBinder() {
+ /**
+ * Get the binder for the {@link Session}.
+ *
+ * @return The session binder apps talk to.
+ */
+ public ISession getSessionBinder() {
return mSession;
}
- public IMediaController getControllerBinder() {
+ /**
+ * Get the binder for the {@link SessionController}.
+ *
+ * @return The controller binder apps talk to.
+ */
+ public ISessionController getControllerBinder() {
return mController;
}
- @Override
- public void binderDied() {
- mService.sessionDied(this);
+ /**
+ * Get the set of route requests this session is interested in.
+ *
+ * @return The list of RouteRequests
+ */
+ public List<RouteRequest> getRouteRequests() {
+ return mRequests;
+ }
+
+ /**
+ * Get the route this session is currently on.
+ *
+ * @return The route the session is on.
+ */
+ public RouteInfo getRoute() {
+ return mRoute;
+ }
+
+ /**
+ * Get the info for this session.
+ *
+ * @return Info that identifies this session.
+ */
+ public SessionInfo getSessionInfo() {
+ return mSessionInfo;
+ }
+
+ /**
+ * Set the selected route. This does not connect to the route, just notifies
+ * the app that a new route has been selected.
+ *
+ * @param route The route that was selected.
+ */
+ public void selectRoute(RouteInfo route) {
+ synchronized (mLock) {
+ if (route != mRoute) {
+ if (mConnection != null) {
+ mConnection.disconnect();
+ mConnection = null;
+ }
+ }
+ mRoute = route;
+ }
+ mSessionCb.sendRouteChange(route);
+ }
+
+ /**
+ * Update the state of the route this session is using and notify the
+ * session.
+ *
+ * @param state The new state of the route.
+ */
+ public void setRouteState(int state) {
+ mSessionCb.sendRouteStateChange(state);
}
+ /**
+ * Send an event to this session from the route it is using.
+ *
+ * @param event The event to send.
+ */
+ public void sendRouteEvent(RouteEvent event) {
+ mSessionCb.sendRouteEvent(event);
+ }
+
+ /**
+ * Set the connection to use for the selected route and notify the app it is
+ * now connected.
+ *
+ * @param route The route the connection is to.
+ * @param request The request that was used to connect.
+ * @param connection The connection to the route.
+ * @return True if this connection is still valid, false if it is stale.
+ */
+ public boolean setRouteConnected(RouteInfo route, RouteOptions request,
+ RouteConnectionRecord connection) {
+ synchronized (mLock) {
+ if (mRoute == null || !TextUtils.equals(route.getId(), mRoute.getId())) {
+ Log.w(TAG, "setRouteConnected: connected route is stale");
+ // TODO figure out disconnection path
+ return false;
+ }
+ if (request != mRequest) {
+ Log.w(TAG, "setRouteConnected: connection request is stale");
+ // TODO figure out disconnection path
+ return false;
+ }
+ mConnection = connection;
+ mConnection.setListener(mConnectionListener);
+ mSessionCb.sendRouteConnected();
+ }
+ return true;
+ }
+
+ /**
+ * Check if this session has been published by the app yet.
+ *
+ * @return True if it has been published, false otherwise.
+ */
public boolean isPublished() {
return mIsPublished;
}
+ @Override
+ public void binderDied() {
+ mService.sessionDied(this);
+ }
+
private void onDestroy() {
mService.destroySession(this);
}
private void pushPlaybackStateUpdate() {
- synchronized (mControllerLock) {
+ synchronized (mLock) {
for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) {
- IMediaControllerCallback cb = mControllerCallbacks.get(i);
+ ISessionControllerCallback cb = mControllerCallbacks.get(i);
try {
cb.onPlaybackStateChanged(mPlaybackState);
} catch (RemoteException e) {
@@ -120,9 +245,9 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
}
private void pushMetadataUpdate() {
- synchronized (mControllerLock) {
+ synchronized (mLock) {
for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) {
- IMediaControllerCallback cb = mControllerCallbacks.get(i);
+ ISessionControllerCallback cb = mControllerCallbacks.get(i);
try {
cb.onMetadataChanged(mMetadata);
} catch (RemoteException e) {
@@ -134,9 +259,9 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
}
private void pushRouteUpdate() {
- synchronized (mControllerLock) {
+ synchronized (mLock) {
for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) {
- IMediaControllerCallback cb = mControllerCallbacks.get(i);
+ ISessionControllerCallback cb = mControllerCallbacks.get(i);
try {
cb.onRouteChanged(mRoute);
} catch (RemoteException e) {
@@ -148,44 +273,63 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
}
private void pushEvent(String event, Bundle data) {
- synchronized (mControllerLock) {
+ synchronized (mLock) {
for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) {
- IMediaControllerCallback cb = mControllerCallbacks.get(i);
+ ISessionControllerCallback cb = mControllerCallbacks.get(i);
try {
cb.onEvent(event, data);
} catch (RemoteException e) {
- Log.w(TAG, "Removing dead callback in pushRouteUpdate.", e);
- mControllerCallbacks.remove(i);
+ Log.w(TAG, "Error with callback in pushEvent.", e);
}
}
}
}
- private final class SessionStub extends IMediaSession.Stub {
+ private void pushRouteCommand(RouteCommand command, ResultReceiver cb) {
+ synchronized (mLock) {
+ if (mRoute == null || !TextUtils.equals(command.getRouteInfo(), mRoute.getId())) {
+ if (cb != null) {
+ cb.send(RouteInterface.RESULT_ROUTE_IS_STALE, null);
+ return;
+ }
+ }
+ if (mConnection != null) {
+ mConnection.sendCommand(command, cb);
+ } else if (cb != null) {
+ cb.send(RouteInterface.RESULT_NOT_CONNECTED, null);
+ }
+ }
+ }
+ private final RouteConnectionRecord.Listener mConnectionListener
+ = new RouteConnectionRecord.Listener() {
@Override
- public void destroy() {
- onDestroy();
+ public void onEvent(RouteEvent event) {
+ RouteEvent eventForSession = new RouteEvent(null, event.getIface(),
+ event.getEvent(), event.getExtras());
+ mSessionCb.sendRouteEvent(eventForSession);
}
@Override
- public void sendEvent(String event, Bundle data) {
- mHandler.post(MessageHandler.MSG_SEND_EVENT, event, data);
+ public void disconnect() {
+ // TODO
}
+ };
+ private final class SessionStub extends ISession.Stub {
@Override
- public IMediaController getMediaController() {
- return mController;
+ public void destroy() {
+ onDestroy();
}
@Override
- public void setRouteState(Bundle routeState) {
+ public void sendEvent(String event, Bundle data) {
+ mHandler.post(MessageHandler.MSG_SEND_EVENT, event, data);
}
@Override
- public void setRoute(Bundle mediaRouteDescriptor) {
- mRoute = mediaRouteDescriptor;
- mHandler.post(MessageHandler.MSG_UPDATE_ROUTE);
+ public ISessionController getController() {
+ return mController;
}
@Override
@@ -198,11 +342,6 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
}
@Override
- public List<String> getSupportedInterfaces() {
- return mInterfaces;
- }
-
- @Override
public void setMetadata(MediaMetadata metadata) {
mMetadata = metadata;
mHandler.post(MessageHandler.MSG_UPDATE_METADATA);
@@ -218,12 +357,44 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
public void setRatingType(int type) {
mRatingType = type;
}
+
+ @Override
+ public void sendRouteCommand(RouteCommand command, ResultReceiver cb) {
+ mHandler.post(MessageHandler.MSG_SEND_COMMAND,
+ new Pair<RouteCommand, ResultReceiver>(command, cb));
+ }
+
+ @Override
+ public boolean setRoute(RouteInfo route) throws RemoteException {
+ // TODO decide if allowed to set route and if the route exists
+ return false;
+ }
+
+ @Override
+ public void connectToRoute(RouteInfo route, RouteOptions request)
+ throws RemoteException {
+ if (mRoute == null || !TextUtils.equals(route.getId(), mRoute.getId())) {
+ throw new RemoteException("RouteInfo does not match current route");
+ }
+ mService.connectToRoute(MediaSessionRecord.this, route, request);
+ mRequest = request;
+ }
+
+ @Override
+ public void setRouteOptions(List<RouteOptions> options) throws RemoteException {
+ mRequests.clear();
+ for (int i = options.size() - 1; i >= 0; i--) {
+ RouteRequest request = new RouteRequest(mSessionInfo, options.get(i),
+ false);
+ mRequests.add(request);
+ }
+ }
}
class SessionCb {
- private final IMediaSessionCallback mCb;
+ private final ISessionCallback mCb;
- public SessionCb(IMediaSessionCallback cb) {
+ public SessionCb(ISessionCallback cb) {
mCb = cb;
}
@@ -245,6 +416,38 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
}
}
+ public void sendRouteChange(RouteInfo route) {
+ try {
+ mCb.onRequestRouteChange(route);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Remote failure in sendRouteChange.", e);
+ }
+ }
+
+ public void sendRouteStateChange(int state) {
+ try {
+ mCb.onRouteStateChange(state);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Remote failure in sendRouteStateChange.", e);
+ }
+ }
+
+ public void sendRouteEvent(RouteEvent event) {
+ try {
+ mCb.onRouteEvent(event);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Remote failure in sendRouteEvent.", e);
+ }
+ }
+
+ public void sendRouteConnected() {
+ try {
+ mCb.onRouteConnected(mRoute, mRequest);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Remote failure in sendRouteStateChange.", e);
+ }
+ }
+
public void play() {
try {
mCb.onPlay();
@@ -318,7 +521,7 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
}
}
- class ControllerStub extends IMediaController.Stub {
+ class ControllerStub extends ISessionController.Stub {
@Override
public void sendCommand(String command, Bundle extras, ResultReceiver cb)
throws RemoteException {
@@ -331,8 +534,8 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
}
@Override
- public void registerCallbackListener(IMediaControllerCallback cb) {
- synchronized (mControllerLock) {
+ public void registerCallbackListener(ISessionControllerCallback cb) {
+ synchronized (mLock) {
if (!mControllerCallbacks.contains(cb)) {
mControllerCallbacks.add(cb);
}
@@ -340,9 +543,9 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
}
@Override
- public void unregisterCallbackListener(IMediaControllerCallback cb)
+ public void unregisterCallbackListener(ISessionControllerCallback cb)
throws RemoteException {
- synchronized (mControllerLock) {
+ synchronized (mLock) {
mControllerCallbacks.remove(cb);
}
}
@@ -409,9 +612,14 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
}
@Override
- public boolean isTransportControlEnabled() throws RemoteException {
+ public boolean isTransportControlEnabled() {
return mTransportPerformerEnabled;
}
+
+ @Override
+ public void showRoutePicker() {
+ mService.showRoutePickerForSession(MediaSessionRecord.this);
+ }
}
private class MessageHandler extends Handler {
@@ -419,6 +627,8 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
private static final int MSG_UPDATE_PLAYBACK_STATE = 2;
private static final int MSG_UPDATE_ROUTE = 3;
private static final int MSG_SEND_EVENT = 4;
+ private static final int MSG_UPDATE_ROUTE_FILTERS = 5;
+ private static final int MSG_SEND_COMMAND = 6;
public MessageHandler(Looper looper) {
super(looper);
@@ -438,6 +648,11 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
case MSG_SEND_EVENT:
pushEvent((String) msg.obj, msg.getData());
break;
+ case MSG_SEND_COMMAND:
+ Pair<RouteCommand, ResultReceiver> cmd =
+ (Pair<RouteCommand, ResultReceiver>) msg.obj;
+ pushRouteCommand(cmd.first, cmd.second);
+ break;
}
}
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index 8fe6055..bc91370 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -17,9 +17,12 @@
package com.android.server.media;
import android.content.Context;
-import android.media.session.IMediaSession;
-import android.media.session.IMediaSessionCallback;
-import android.media.session.IMediaSessionManager;
+import android.media.routeprovider.RouteRequest;
+import android.media.session.ISession;
+import android.media.session.ISessionCallback;
+import android.media.session.ISessionManager;
+import android.media.session.RouteInfo;
+import android.media.session.RouteOptions;
import android.os.Binder;
import android.os.Handler;
import android.os.RemoteException;
@@ -38,21 +41,77 @@ public class MediaSessionService extends SystemService {
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private final SessionManagerImpl mSessionManagerImpl;
+ private final MediaRouteProviderWatcher mRouteProviderWatcher;
private final ArrayList<MediaSessionRecord> mSessions
= new ArrayList<MediaSessionRecord>();
+ private final ArrayList<MediaRouteProviderProxy> mProviders
+ = new ArrayList<MediaRouteProviderProxy>();
private final Object mLock = new Object();
// TODO do we want a separate thread for handling mediasession messages?
private final Handler mHandler = new Handler();
+ // Used to keep track of the current request to show routes for a specific
+ // session so we drop late callbacks properly.
+ private int mShowRoutesRequestId = 0;
+
+ // TODO refactor to have per user state. See MediaRouterService for an
+ // example
+
public MediaSessionService(Context context) {
super(context);
mSessionManagerImpl = new SessionManagerImpl();
+ mRouteProviderWatcher = new MediaRouteProviderWatcher(context, mProviderWatcherCallback,
+ mHandler, context.getUserId());
}
@Override
public void onStart() {
publishBinderService(Context.MEDIA_SESSION_SERVICE, mSessionManagerImpl);
+ mRouteProviderWatcher.start();
+ }
+
+ /**
+ * Should trigger showing the Media route picker dialog. Right now it just
+ * kicks off a query to all the providers to get routes.
+ *
+ * @param record The session to show the picker for.
+ */
+ public void showRoutePickerForSession(MediaSessionRecord record) {
+ // TODO for now just toggle the route to test (we will only have one
+ // match for now)
+ if (record.getRoute() != null) {
+ // For now send null to mean the local route
+ record.selectRoute(null);
+ return;
+ }
+ mShowRoutesRequestId++;
+ ArrayList<MediaRouteProviderProxy> providers = mRouteProviderWatcher.getProviders();
+ for (int i = providers.size() - 1; i >= 0; i--) {
+ MediaRouteProviderProxy provider = providers.get(i);
+ provider.getRoutes(record, mShowRoutesRequestId);
+ }
+ }
+
+ /**
+ * Connect a session to the given route.
+ *
+ * @param session The session to connect.
+ * @param route The route to connect to.
+ * @param options The options to use for the connection.
+ */
+ public void connectToRoute(MediaSessionRecord session, RouteInfo route,
+ RouteOptions options) {
+ synchronized (mLock) {
+ MediaRouteProviderProxy proxy = getProviderLocked(route.getProvider());
+ if (proxy == null) {
+ Log.w(TAG, "Provider for route " + route.getName() + " does not exist.");
+ return;
+ }
+ RouteRequest request = new RouteRequest(session.getSessionInfo(), options, true);
+ // TODO make connect an async call to a ThreadPoolExecutor
+ proxy.connectToRoute(session, route, request);
+ }
}
void sessionDied(MediaSessionRecord session) {
@@ -86,14 +145,14 @@ public class MediaSessionService extends SystemService {
}
private MediaSessionRecord createSessionInternal(int pid, String packageName,
- IMediaSessionCallback cb, String tag) {
+ ISessionCallback cb, String tag) {
synchronized (mLock) {
return createSessionLocked(pid, packageName, cb, tag);
}
}
private MediaSessionRecord createSessionLocked(int pid, String packageName,
- IMediaSessionCallback cb, String tag) {
+ ISessionCallback cb, String tag) {
final MediaSessionRecord session = new MediaSessionRecord(pid, packageName, cb, tag, this,
mHandler);
try {
@@ -110,9 +169,82 @@ public class MediaSessionService extends SystemService {
return session;
}
- class SessionManagerImpl extends IMediaSessionManager.Stub {
+ private MediaRouteProviderProxy getProviderLocked(String providerId) {
+ for (int i = mProviders.size() - 1; i >= 0; i--) {
+ MediaRouteProviderProxy provider = mProviders.get(i);
+ if (TextUtils.equals(providerId, provider.getId())) {
+ return provider;
+ }
+ }
+ return null;
+ }
+
+ private int findIndexOfSessionForIdLocked(String sessionId) {
+ for (int i = mSessions.size() - 1; i >= 0; i--) {
+ MediaSessionRecord session = mSessions.get(i);
+ if (TextUtils.equals(session.getSessionInfo().getId(), sessionId)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private MediaRouteProviderWatcher.Callback mProviderWatcherCallback
+ = new MediaRouteProviderWatcher.Callback() {
+ @Override
+ public void removeProvider(MediaRouteProviderProxy provider) {
+ synchronized (mLock) {
+ mProviders.remove(provider);
+ provider.setRoutesListener(null);
+ provider.setInterested(false);
+ }
+ }
+
+ @Override
+ public void addProvider(MediaRouteProviderProxy provider) {
+ synchronized (mLock) {
+ mProviders.add(provider);
+ provider.setRoutesListener(mRoutesCallback);
+ provider.setInterested(true);
+ }
+ }
+ };
+
+ private MediaRouteProviderProxy.RoutesListener mRoutesCallback
+ = new MediaRouteProviderProxy.RoutesListener() {
+ @Override
+ public void onRoutesUpdated(String sessionId, ArrayList<RouteInfo> routes,
+ int reqId) {
+ // TODO for now select the first route to test, eventually add the
+ // new routes to the dialog if it is still open
+ synchronized (mLock) {
+ int index = findIndexOfSessionForIdLocked(sessionId);
+ if (index != -1 && routes != null && routes.size() > 0) {
+ MediaSessionRecord record = mSessions.get(index);
+ record.selectRoute(routes.get(0));
+ }
+ }
+ }
+
+ @Override
+ public void onRouteConnected(String sessionId, RouteInfo route,
+ RouteRequest options, RouteConnectionRecord connection) {
+ synchronized (mLock) {
+ int index = findIndexOfSessionForIdLocked(sessionId);
+ if (index != -1) {
+ MediaSessionRecord session = mSessions.get(index);
+ session.setRouteConnected(route, options.getConnectionOptions(), connection);
+ }
+ }
+ }
+ };
+
+ class SessionManagerImpl extends ISessionManager.Stub {
+ // TODO add createSessionAsUser, pass user-id to
+ // ActivityManagerNative.handleIncomingUser and stash result for use
+ // when starting services on that session's behalf.
@Override
- public IMediaSession createSession(String packageName, IMediaSessionCallback cb, String tag)
+ public ISession createSession(String packageName, ISessionCallback cb, String tag)
throws RemoteException {
final int pid = Binder.getCallingPid();
final int uid = Binder.getCallingUid();
diff --git a/services/core/java/com/android/server/media/RouteConnectionRecord.java b/services/core/java/com/android/server/media/RouteConnectionRecord.java
new file mode 100644
index 0000000..8da0f95
--- /dev/null
+++ b/services/core/java/com/android/server/media/RouteConnectionRecord.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.media;
+
+import android.media.routeprovider.IRouteConnection;
+import android.media.session.RouteCommand;
+import android.media.session.RouteEvent;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.util.Log;
+
+/**
+ * A connection between a Session and a Route.
+ */
+public class RouteConnectionRecord {
+ private static final String TAG = "RouteConnRecord";
+ private final IRouteConnection mBinder;
+ private Listener mListener;
+
+ public RouteConnectionRecord(IRouteConnection binder) {
+ mBinder = binder;
+ }
+
+ /**
+ * Add a listener to get route events on.
+ *
+ * @param listener The listener to get events on.
+ */
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * Check if this connection matches the token given.
+ *
+ * @param binder The token to check
+ * @return True if this is the connection you're looking for, false
+ * otherwise.
+ */
+ public boolean isConnection(IBinder binder) {
+ return binder != null && binder.equals(mBinder.asBinder());
+ }
+
+ /**
+ * Send an event from this connection.
+ *
+ * @param event The event to send.
+ */
+ public void sendEvent(RouteEvent event) {
+ if (mListener != null) {
+ mListener.onEvent(event);
+ }
+ }
+
+ /**
+ * Send a command to this connection.
+ *
+ * @param command The command to send.
+ * @param cb The receiver to get a result on.
+ */
+ public void sendCommand(RouteCommand command, ResultReceiver cb) {
+ try {
+ mBinder.onCommand(command, cb);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error in sendCommand", e);
+ }
+ }
+
+ /**
+ * Tell the session that the provider has disconnected it.
+ */
+ public void disconnect() {
+ if (mListener != null) {
+ mListener.disconnect();
+ }
+ }
+
+ /**
+ * Listener to receive updates from the provider for this connection.
+ */
+ public static interface Listener {
+ /**
+ * Called when an event is sent on this connection.
+ *
+ * @param event The event that was sent.
+ */
+ public void onEvent(RouteEvent event);
+
+ /**
+ * Called when the provider has disconnected the route.
+ */
+ public void disconnect();
+ }
+} \ No newline at end of file
diff --git a/tests/OneMedia/AndroidManifest.xml b/tests/OneMedia/AndroidManifest.xml
index 7d6ba1d..504d471 100644
--- a/tests/OneMedia/AndroidManifest.xml
+++ b/tests/OneMedia/AndroidManifest.xml
@@ -25,6 +25,15 @@
android:name="com.android.onemedia.OnePlayerService"
android:exported="false"
android:process="com.android.onemedia.service" />
+ <service
+ android:name=".provider.OneMediaRouteProvider"
+ android:permission="android.permission.BIND_ROUTE_PROVIDER"
+ android:exported="true"
+ android:process="com.android.onemedia.provider">
+ <intent-filter>
+ <action android:name="com.android.media.session.MediaRouteProvider" />
+ </intent-filter>
+ </service>
</application>
</manifest>
diff --git a/tests/OneMedia/res/layout/activity_one_player.xml b/tests/OneMedia/res/layout/activity_one_player.xml
index 4208355..516562f 100644
--- a/tests/OneMedia/res/layout/activity_one_player.xml
+++ b/tests/OneMedia/res/layout/activity_one_player.xml
@@ -53,6 +53,12 @@
android:layout_weight="1"
android:text="@string/play_button" />
</LinearLayout>
+ <Button
+ android:id="@+id/route_button"
+ style="@style/BottomBarButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/route_button" />
<TextView
android:id="@+id/status"
android:layout_width="match_parent"
diff --git a/tests/OneMedia/res/values/strings.xml b/tests/OneMedia/res/values/strings.xml
index 1b0cebb..3735c8d 100644
--- a/tests/OneMedia/res/values/strings.xml
+++ b/tests/OneMedia/res/values/strings.xml
@@ -7,6 +7,7 @@
<string name="start_button">Start</string>
<string name="play_button">Play</string>
+ <string name="route_button">Change route</string>
<string name="media_content_hint">Content</string>
<string name="media_next_hint">Next content</string>
<string name="has_video">Is video</string>
diff --git a/tests/OneMedia/src/com/android/onemedia/IPlayerCallback.aidl b/tests/OneMedia/src/com/android/onemedia/IPlayerCallback.aidl
index 2b14384..189fa6a 100644
--- a/tests/OneMedia/src/com/android/onemedia/IPlayerCallback.aidl
+++ b/tests/OneMedia/src/com/android/onemedia/IPlayerCallback.aidl
@@ -15,8 +15,8 @@
package com.android.onemedia;
-import android.media.session.MediaSessionToken;
+import android.media.session.SessionToken;
interface IPlayerCallback {
- void onSessionChanged(in MediaSessionToken session);
+ void onSessionChanged(in SessionToken session);
} \ No newline at end of file
diff --git a/tests/OneMedia/src/com/android/onemedia/IPlayerService.aidl b/tests/OneMedia/src/com/android/onemedia/IPlayerService.aidl
index efdbe9a..15ea25f 100644
--- a/tests/OneMedia/src/com/android/onemedia/IPlayerService.aidl
+++ b/tests/OneMedia/src/com/android/onemedia/IPlayerService.aidl
@@ -15,14 +15,14 @@
package com.android.onemedia;
-import android.media.session.MediaSessionToken;
+import android.media.session.SessionToken;
import android.os.Bundle;
import com.android.onemedia.IPlayerCallback;
import com.android.onemedia.playback.IRequestCallback;
interface IPlayerService {
- MediaSessionToken getSessionToken();
+ SessionToken getSessionToken();
void registerCallback(in IPlayerCallback cb);
void unregisterCallback(in IPlayerCallback cb);
void sendRequest(String action, in Bundle params, in IRequestCallback cb);
diff --git a/tests/OneMedia/src/com/android/onemedia/OnePlayerActivity.java b/tests/OneMedia/src/com/android/onemedia/OnePlayerActivity.java
index 3114ca9..b9a6470 100644
--- a/tests/OneMedia/src/com/android/onemedia/OnePlayerActivity.java
+++ b/tests/OneMedia/src/com/android/onemedia/OnePlayerActivity.java
@@ -37,6 +37,7 @@ public class OnePlayerActivity extends Activity {
private Button mStartButton;
private Button mPlayButton;
+ private Button mRouteButton;
private TextView mStatusView;
private EditText mContentText;
@@ -54,6 +55,7 @@ public class OnePlayerActivity extends Activity {
mStartButton = (Button) findViewById(R.id.start_button);
mPlayButton = (Button) findViewById(R.id.play_button);
+ mRouteButton = (Button) findViewById(R.id.route_button);
mStatusView = (TextView) findViewById(R.id.status);
mContentText = (EditText) findViewById(R.id.content);
mNextContentText = (EditText) findViewById(R.id.next_content);
@@ -61,6 +63,7 @@ public class OnePlayerActivity extends Activity {
mStartButton.setOnClickListener(mButtonListener);
mPlayButton.setOnClickListener(mButtonListener);
+ mRouteButton.setOnClickListener(mButtonListener);
}
@@ -107,6 +110,9 @@ public class OnePlayerActivity extends Activity {
Log.d(TAG, "Start button pressed, in state " + mPlaybackState);
mPlayer.setContent(mContentText.getText().toString());
break;
+ case R.id.route_button:
+ mPlayer.showRoutePicker();
+ break;
}
}
@@ -117,6 +123,7 @@ public class OnePlayerActivity extends Activity {
public void onPlaybackStateChange(PlaybackState state) {
mPlaybackState = state.getState();
boolean enablePlay = false;
+ boolean enableControls = true;
StringBuilder statusBuilder = new StringBuilder();
switch (mPlaybackState) {
case PlaybackState.PLAYSTATE_PLAYING:
@@ -143,12 +150,17 @@ public class OnePlayerActivity extends Activity {
case PlaybackState.PLAYSTATE_NONE:
statusBuilder.append("none");
break;
+ case PlaybackState.PLAYSTATE_CONNECTING:
+ statusBuilder.append("connecting");
+ enableControls = false;
+ break;
default:
statusBuilder.append(mPlaybackState);
}
statusBuilder.append(" -- At position: ").append(state.getPosition());
mStatusView.setText(statusBuilder.toString());
mPlayButton.setEnabled(enablePlay);
+ setControlsEnabled(enableControls);
}
@Override
diff --git a/tests/OneMedia/src/com/android/onemedia/PlayerController.java b/tests/OneMedia/src/com/android/onemedia/PlayerController.java
index e831ec6..e3f5c0c 100644
--- a/tests/OneMedia/src/com/android/onemedia/PlayerController.java
+++ b/tests/OneMedia/src/com/android/onemedia/PlayerController.java
@@ -16,9 +16,10 @@
*/
package com.android.onemedia;
-import android.media.session.MediaController;
+import android.media.session.SessionController;
import android.media.session.MediaMetadata;
-import android.media.session.MediaSessionManager;
+import android.media.session.RouteInfo;
+import android.media.session.SessionManager;
import android.media.session.PlaybackState;
import android.media.session.TransportController;
import android.os.Bundle;
@@ -39,7 +40,7 @@ public class PlayerController {
public static final int STATE_DISCONNECTED = 0;
public static final int STATE_CONNECTED = 1;
- protected MediaController mController;
+ protected SessionController mController;
protected IPlayerService mBinder;
protected TransportController mTransportControls;
@@ -48,7 +49,7 @@ public class PlayerController {
private Listener mListener;
private TransportListener mTransportListener = new TransportListener();
private SessionCallback mControllerCb;
- private MediaSessionManager mManager;
+ private SessionManager mManager;
private Handler mHandler = new Handler();
private boolean mResumed;
@@ -61,7 +62,7 @@ public class PlayerController {
mServiceIntent = serviceIntent;
}
mControllerCb = new SessionCallback();
- mManager = (MediaSessionManager) context
+ mManager = (SessionManager) context
.getSystemService(Context.MEDIA_SESSION_SERVICE);
mResumed = false;
@@ -121,6 +122,10 @@ public class PlayerController {
}
}
+ public void showRoutePicker() {
+ mController.showRoutePicker();
+ }
+
private void unbindFromService() {
mContext.unbindService(mServiceConnection);
}
@@ -150,7 +155,7 @@ public class PlayerController {
mBinder = IPlayerService.Stub.asInterface(service);
Log.d(TAG, "service is " + service + " binder is " + mBinder);
try {
- mController = MediaController.fromToken(mBinder.getSessionToken());
+ mController = SessionController.fromToken(mBinder.getSessionToken());
} catch (RemoteException e) {
Log.e(TAG, "Error getting session", e);
return;
@@ -171,9 +176,9 @@ public class PlayerController {
}
};
- private class SessionCallback extends MediaController.Callback {
+ private class SessionCallback extends SessionController.Callback {
@Override
- public void onRouteChanged(Bundle route) {
+ public void onRouteChanged(RouteInfo route) {
// TODO
}
}
diff --git a/tests/OneMedia/src/com/android/onemedia/PlayerService.java b/tests/OneMedia/src/com/android/onemedia/PlayerService.java
index 0ad6dd1..8b53ddf 100644
--- a/tests/OneMedia/src/com/android/onemedia/PlayerService.java
+++ b/tests/OneMedia/src/com/android/onemedia/PlayerService.java
@@ -17,7 +17,7 @@ package com.android.onemedia;
import android.app.Service;
import android.content.Intent;
-import android.media.session.MediaSessionToken;
+import android.media.session.SessionToken;
import android.media.session.PlaybackState;
import android.os.Bundle;
import android.os.IBinder;
@@ -149,7 +149,7 @@ public class PlayerService extends Service {
}
@Override
- public MediaSessionToken getSessionToken() throws RemoteException {
+ public SessionToken getSessionToken() throws RemoteException {
return mSession.getSessionToken();
}
}
diff --git a/tests/OneMedia/src/com/android/onemedia/PlayerSession.java b/tests/OneMedia/src/com/android/onemedia/PlayerSession.java
index a2d7897..5dc3904 100644
--- a/tests/OneMedia/src/com/android/onemedia/PlayerSession.java
+++ b/tests/OneMedia/src/com/android/onemedia/PlayerSession.java
@@ -17,9 +17,13 @@ package com.android.onemedia;
import android.content.Context;
import android.content.Intent;
-import android.media.session.MediaSession;
-import android.media.session.MediaSessionManager;
-import android.media.session.MediaSessionToken;
+import android.media.session.Route;
+import android.media.session.RouteInfo;
+import android.media.session.RouteOptions;
+import android.media.session.RoutePlaybackControls;
+import android.media.session.Session;
+import android.media.session.SessionManager;
+import android.media.session.SessionToken;
import android.media.session.PlaybackState;
import android.media.session.TransportPerformer;
import android.os.Bundle;
@@ -27,41 +31,55 @@ import android.util.Log;
import android.view.KeyEvent;
import com.android.onemedia.playback.LocalRenderer;
+import com.android.onemedia.playback.OneMRPRenderer;
import com.android.onemedia.playback.Renderer;
-import com.android.onemedia.playback.RendererFactory;
+import com.android.onemedia.playback.RequestUtils;
+
+import java.util.ArrayList;
public class PlayerSession {
private static final String TAG = "PlayerSession";
- protected MediaSession mSession;
+ protected Session mSession;
protected Context mContext;
- protected RendererFactory mRendererFactory;
- protected LocalRenderer mRenderer;
- protected MediaSession.Callback mCallback;
+ protected Renderer mRenderer;
+ protected Session.Callback mCallback;
protected Renderer.Listener mRenderListener;
protected TransportPerformer mPerformer;
protected PlaybackState mPlaybackState;
protected Listener mListener;
+ protected ArrayList<RouteOptions> mRouteOptions;
+ protected Route mRoute;
+ protected RoutePlaybackControls mRouteControls;
+ protected RouteListener mRouteListener;
+
+ private String mContent;
public PlayerSession(Context context) {
mContext = context;
- mRendererFactory = new RendererFactory();
mRenderer = new LocalRenderer(context, null);
- mCallback = new ControllerCb();
+ mCallback = new SessionCb();
mRenderListener = new RenderListener();
mPlaybackState = new PlaybackState();
mPlaybackState.setActions(PlaybackState.ACTION_PAUSE
| PlaybackState.ACTION_PLAY);
mRenderer.registerListener(mRenderListener);
+
+ // TODO need an easier way to build route options
+ mRouteOptions = new ArrayList<RouteOptions>();
+ RouteOptions.Builder bob = new RouteOptions.Builder();
+ bob.addInterface(RoutePlaybackControls.NAME);
+ mRouteOptions.add(bob.build());
+ mRouteListener = new RouteListener();
}
public void createSession() {
if (mSession != null) {
mSession.release();
}
- MediaSessionManager man = (MediaSessionManager) mContext
+ SessionManager man = (SessionManager) mContext
.getSystemService(Context.MEDIA_SESSION_SERVICE);
Log.d(TAG, "Creating session for package " + mContext.getBasePackageName());
mSession = man.createSession("OneMedia");
@@ -69,6 +87,7 @@ public class PlayerSession {
mPerformer = mSession.setTransportPerformerEnabled();
mPerformer.addListener(new TransportListener());
mPerformer.setPlaybackState(mPlaybackState);
+ mSession.setRouteOptions(mRouteOptions);
mSession.publish();
}
@@ -86,18 +105,24 @@ public class PlayerSession {
mListener = listener;
}
- public MediaSessionToken getSessionToken() {
+ public SessionToken getSessionToken() {
return mSession.getSessionToken();
}
public void setContent(Bundle request) {
mRenderer.setContent(request);
+ mContent = request.getString(RequestUtils.EXTRA_KEY_SOURCE);
}
public void setNextContent(Bundle request) {
mRenderer.setNextContent(request);
}
+ private void updateState(int newState) {
+ mPlaybackState.setState(newState);
+ mPerformer.setPlaybackState(mPlaybackState);
+ }
+
public interface Listener {
public void onPlayStateChanged(PlaybackState state);
}
@@ -145,7 +170,11 @@ public class PlayerSession {
mPlaybackState.setErrorMessage("unkown state");
break;
}
- mPlaybackState.setPosition(mRenderer.getSeekPosition());
+ if (mRenderer != null) {
+ mPlaybackState.setPosition(mRenderer.getSeekPosition());
+ } else {
+ mPlaybackState.setPosition(-1);
+ }
mPerformer.setPlaybackState(mPlaybackState);
if (mListener != null) {
mListener.onPlayStateChanged(mPlaybackState);
@@ -173,8 +202,7 @@ public class PlayerSession {
}
- private class ControllerCb extends MediaSession.Callback {
-
+ private class SessionCb extends Session.Callback {
@Override
public void onMediaButton(Intent mediaRequestIntent) {
if (Intent.ACTION_MEDIA_BUTTON.equals(mediaRequestIntent.getAction())) {
@@ -192,6 +220,40 @@ public class PlayerSession {
}
}
}
+
+ @Override
+ public void onRequestRouteChange(RouteInfo route) {
+ if (mRenderer != null) {
+ mRenderer.onStop();
+ }
+ if (route == null) {
+ // Use local route
+ mRoute = null;
+ mRenderer = new LocalRenderer(mContext, null);
+ mRenderer.registerListener(mRenderListener);
+ updateState(PlaybackState.PLAYSTATE_NONE);
+ } else {
+ // Use remote route
+ mSession.connect(route, mRouteOptions.get(0));
+ mRenderer = null;
+ updateState(PlaybackState.PLAYSTATE_CONNECTING);
+ }
+ }
+
+ @Override
+ public void onRouteConnected(Route route) {
+ mRoute = route;
+ mRouteControls = RoutePlaybackControls.from(route);
+ mRouteControls.addListener(mRouteListener);
+ Log.d(TAG, "Connected to route, registering listener");
+ mRenderer = new OneMRPRenderer(mRouteControls);
+ updateState(PlaybackState.PLAYSTATE_NONE);
+ }
+
+ @Override
+ public void onRouteDisconnected(Route route, int reason) {
+
+ }
}
private class TransportListener extends TransportPerformer.Listener {
@@ -206,4 +268,12 @@ public class PlayerSession {
}
}
+ private class RouteListener extends RoutePlaybackControls.Listener {
+ @Override
+ public void onPlaybackStateChange(int state) {
+ Log.d(TAG, "Updating state to " + state);
+ updateState(state);
+ }
+ }
+
}
diff --git a/tests/OneMedia/src/com/android/onemedia/playback/LocalRenderer.java b/tests/OneMedia/src/com/android/onemedia/playback/LocalRenderer.java
index 7f62f66..c8a8d6c 100644
--- a/tests/OneMedia/src/com/android/onemedia/playback/LocalRenderer.java
+++ b/tests/OneMedia/src/com/android/onemedia/playback/LocalRenderer.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
package com.android.onemedia.playback;
import org.apache.http.Header;
@@ -370,6 +385,8 @@ public class LocalRenderer extends Renderer implements OnPreparedListener,
* Prepares the player for the given playback request. If the holder is null
* it is assumed this is an audio only source. If playOnReady is set to true
* the media will begin playing as soon as it can.
+ *
+ * @see RequestUtils for the set of valid keys.
*/
public void setContent(Bundle request, SurfaceHolder holder) {
String source = request.getString(RequestUtils.EXTRA_KEY_SOURCE);
diff --git a/tests/OneMedia/src/com/android/onemedia/playback/MediaItem.java b/tests/OneMedia/src/com/android/onemedia/playback/MediaItem.java
index f9e6794..05516d2 100644
--- a/tests/OneMedia/src/com/android/onemedia/playback/MediaItem.java
+++ b/tests/OneMedia/src/com/android/onemedia/playback/MediaItem.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
package com.android.onemedia.playback;
import android.os.Bundle;
diff --git a/tests/OneMedia/src/com/android/onemedia/playback/OneMRPRenderer.java b/tests/OneMedia/src/com/android/onemedia/playback/OneMRPRenderer.java
new file mode 100644
index 0000000..9b0a2b2
--- /dev/null
+++ b/tests/OneMedia/src/com/android/onemedia/playback/OneMRPRenderer.java
@@ -0,0 +1,44 @@
+package com.android.onemedia.playback;
+
+import android.media.session.RoutePlaybackControls;
+import android.os.Bundle;
+
+/**
+ * Renderer for communicating with the OneMRP route
+ */
+public class OneMRPRenderer extends Renderer {
+ private final RoutePlaybackControls mControls;
+
+ public OneMRPRenderer(RoutePlaybackControls controls) {
+ super(null, null);
+ mControls = controls;
+ }
+
+ @Override
+ public void setContent(Bundle request) {
+ mControls.playNow(request.getString(RequestUtils.EXTRA_KEY_SOURCE));
+ }
+
+ @Override
+ public boolean onStop() {
+ mControls.pause();
+ return true;
+ }
+
+ @Override
+ public boolean onPlay() {
+ mControls.resume();
+ return true;
+ }
+
+ @Override
+ public boolean onPause() {
+ mControls.pause();
+ return true;
+ }
+
+ @Override
+ public long getSeekPosition() {
+ return -1;
+ }
+}
diff --git a/tests/OneMedia/src/com/android/onemedia/playback/PlaybackError.java b/tests/OneMedia/src/com/android/onemedia/playback/PlaybackError.java
index 72d936c..ac9da23 100644
--- a/tests/OneMedia/src/com/android/onemedia/playback/PlaybackError.java
+++ b/tests/OneMedia/src/com/android/onemedia/playback/PlaybackError.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
package com.android.onemedia.playback;
import android.os.Bundle;
diff --git a/tests/OneMedia/src/com/android/onemedia/playback/Renderer.java b/tests/OneMedia/src/com/android/onemedia/playback/Renderer.java
index 2451bdf..09debcf 100644
--- a/tests/OneMedia/src/com/android/onemedia/playback/Renderer.java
+++ b/tests/OneMedia/src/com/android/onemedia/playback/Renderer.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
package com.android.onemedia.playback;
import android.content.Context;
@@ -77,39 +92,54 @@ public abstract class Renderer {
}
public boolean onPlay() {
- throw new UnsupportedOperationException("play is not supported.");
+ // TODO consider making these log warnings instead of crashes (or
+ // Log.wtf)
+ // throw new UnsupportedOperationException("play is not supported.");
+ return false;
}
public boolean onPause() {
- throw new UnsupportedOperationException("pause is not supported.");
+ // throw new UnsupportedOperationException("pause is not supported.");
+ return false;
}
public boolean onNext() {
- throw new UnsupportedOperationException("next is not supported.");
+ // throw new UnsupportedOperationException("next is not supported.");
+ return false;
}
public boolean onPrevious() {
- throw new UnsupportedOperationException("previous is not supported.");
+ // throw new
+ // UnsupportedOperationException("previous is not supported.");
+ return false;
}
public boolean onStop() {
- throw new UnsupportedOperationException("stop is not supported.");
+ // throw new UnsupportedOperationException("stop is not supported.");
+ return false;
}
public boolean onSeekTo(int time) {
- throw new UnsupportedOperationException("seekTo is not supported.");
+ // throw new UnsupportedOperationException("seekTo is not supported.");
+ return false;
}
public long getSeekPosition() {
- throw new UnsupportedOperationException("getSeekPosition is not supported.");
+ // throw new
+ // UnsupportedOperationException("getSeekPosition is not supported.");
+ return -1;
}
public long getDuration() {
- throw new UnsupportedOperationException("getDuration is not supported.");
+ // throw new
+ // UnsupportedOperationException("getDuration is not supported.");
+ return -1;
}
public int getPlayState() {
- throw new UnsupportedOperationException("getPlayState is not supported.");
+ // throw new
+ // UnsupportedOperationException("getPlayState is not supported.");
+ return 0;
}
public void onDestroy() {
diff --git a/tests/OneMedia/src/com/android/onemedia/playback/RendererFactory.java b/tests/OneMedia/src/com/android/onemedia/playback/RendererFactory.java
deleted file mode 100644
index f333fce..0000000
--- a/tests/OneMedia/src/com/android/onemedia/playback/RendererFactory.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.android.onemedia.playback;
-
-import android.content.Context;
-import android.media.MediaRouter;
-import android.os.Bundle;
-import android.util.Log;
-
-/**
- * TODO: Insert description here.
- */
-public class RendererFactory {
- private static final String TAG = "RendererFactory";
-
- public Renderer createRenderer(MediaRouter.RouteInfo route, Context context, Bundle params) {
- if (route.getPlaybackType() == MediaRouter.RouteInfo.PLAYBACK_TYPE_LOCAL) {
- return new LocalRenderer(context, params);
- }
- Log.e(TAG, "Unable to create renderer for route of playback type "
- + route.getPlaybackType());
- return null;
- }
-}
diff --git a/tests/OneMedia/src/com/android/onemedia/playback/RequestUtils.java b/tests/OneMedia/src/com/android/onemedia/playback/RequestUtils.java
index 9b50dad..dd0d982 100644
--- a/tests/OneMedia/src/com/android/onemedia/playback/RequestUtils.java
+++ b/tests/OneMedia/src/com/android/onemedia/playback/RequestUtils.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
package com.android.onemedia.playback;
import android.os.Bundle;
diff --git a/tests/OneMedia/src/com/android/onemedia/provider/OneMediaRouteProvider.java b/tests/OneMedia/src/com/android/onemedia/provider/OneMediaRouteProvider.java
new file mode 100644
index 0000000..6edcd7d
--- /dev/null
+++ b/tests/OneMedia/src/com/android/onemedia/provider/OneMediaRouteProvider.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.onemedia.provider;
+
+import android.media.routeprovider.RouteConnection;
+import android.media.routeprovider.RouteInterfaceHandler;
+import android.media.routeprovider.RoutePlaybackControlsHandler;
+import android.media.routeprovider.RouteProviderService;
+import android.media.routeprovider.RouteRequest;
+import android.media.session.RouteInfo;
+import android.media.session.RoutePlaybackControls;
+import android.media.session.RouteInterface;
+import android.media.session.PlaybackState;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.ResultReceiver;
+import android.util.Log;
+
+import com.android.onemedia.playback.LocalRenderer;
+import com.android.onemedia.playback.Renderer;
+import com.android.onemedia.playback.RequestUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Test of MediaRouteProvider. Show a dummy provider with a simple interface for
+ * playing music.
+ */
+public class OneMediaRouteProvider extends RouteProviderService {
+ private static final String TAG = "OneMRP";
+ private static final boolean DEBUG = true;
+
+ private Renderer mRenderer;
+ private RenderListener mRenderListener;
+ private PlaybackState mPlaybackState;
+ private RouteConnection mConnection;
+ private RoutePlaybackControlsHandler mControls;
+ private String mRouteId;
+ private Handler mHandler;
+
+ @Override
+ public void onCreate() {
+ mHandler = new Handler();
+ mRouteId = UUID.randomUUID().toString();
+ mRenderer = new LocalRenderer(this, null);
+ mRenderListener = new RenderListener();
+ mPlaybackState = new PlaybackState();
+ mPlaybackState.setActions(PlaybackState.ACTION_PAUSE
+ | PlaybackState.ACTION_PLAY);
+
+ mRenderer.registerListener(mRenderListener);
+
+ if (DEBUG) {
+ Log.d(TAG, "onCreate, routeId is " + mRouteId);
+ }
+ }
+
+ @Override
+ public List<RouteInfo> getMatchingRoutes(List<RouteRequest> requests) {
+ RouteInfo.Builder bob = new RouteInfo.Builder();
+ bob.setName("OneMedia").setId(mRouteId);
+ // TODO add a helper library for generating route info with the correct
+ // options
+ Log.d(TAG, "Requests:");
+ for (RouteRequest request : requests) {
+ List<String> ifaces = request.getConnectionOptions().getInterfaceNames();
+ Log.d(TAG, " request ifaces:" + ifaces.toString());
+ if (ifaces != null && ifaces.size() == 1
+ && RoutePlaybackControls.NAME.equals(ifaces.get(0))) {
+ bob.addRouteOptions(request.getConnectionOptions());
+ }
+ }
+ ArrayList<RouteInfo> result = new ArrayList<RouteInfo>();
+ if (bob.getOptionsSize() > 0) {
+ RouteInfo info = bob.build();
+ result.add(info);
+ }
+ if (DEBUG) {
+ Log.d(TAG, "getRoutes returning " + result.toString());
+ }
+ return result;
+ }
+
+ @Override
+ public RouteConnection connect(RouteInfo route, RouteRequest request) {
+ if (mConnection != null) {
+ disconnect(mConnection);
+ }
+ RouteConnection connection = new RouteConnection(this, route);
+ mControls = RoutePlaybackControlsHandler.addTo(connection);
+ mControls.addListener(new PlayHandler(mRouteId), mHandler);
+ if (DEBUG) {
+ Log.d(TAG, "Connected to route");
+ }
+ return connection;
+ }
+
+ private class PlayHandler extends RoutePlaybackControlsHandler.Listener {
+ private final String mRouteId;
+
+ public PlayHandler(String routeId) {
+ mRouteId = routeId;
+ }
+
+ @Override
+ public void playNow(String content, ResultReceiver cb) {
+ if (DEBUG) {
+ Log.d(TAG, "Attempting to play " + content);
+ }
+ // look up the route and send a play command to it
+ Bundle bundle = new Bundle();
+ bundle.putString(RequestUtils.EXTRA_KEY_SOURCE, content);
+ mRenderer.setContent(bundle);
+ RouteInterfaceHandler.sendResult(cb, RouteInterface.RESULT_SUCCESS, null);
+ }
+
+ @Override
+ public boolean resume() {
+ mRenderer.onPlay();
+ return true;
+ }
+
+ @Override
+ public boolean pause() {
+ mRenderer.onPause();
+ return true;
+ }
+ }
+
+ private class RenderListener implements Renderer.Listener {
+
+ @Override
+ public void onError(int type, int extra, Bundle extras, Throwable error) {
+ Log.d(TAG, "Sending onError with type " + type + " and extra " + extra);
+ if (mControls != null) {
+ mControls.sendPlaybackChangeEvent(PlaybackState.PLAYSTATE_ERROR);
+ }
+ }
+
+ @Override
+ public void onStateChanged(int newState) {
+ if (newState != Renderer.STATE_ERROR) {
+ mPlaybackState.setErrorMessage(null);
+ }
+ switch (newState) {
+ case Renderer.STATE_ENDED:
+ case Renderer.STATE_STOPPED:
+ mPlaybackState.setState(PlaybackState.PLAYSTATE_STOPPED);
+ break;
+ case Renderer.STATE_INIT:
+ case Renderer.STATE_PREPARING:
+ mPlaybackState.setState(PlaybackState.PLAYSTATE_BUFFERING);
+ break;
+ case Renderer.STATE_ERROR:
+ mPlaybackState.setState(PlaybackState.PLAYSTATE_ERROR);
+ break;
+ case Renderer.STATE_PAUSED:
+ mPlaybackState.setState(PlaybackState.PLAYSTATE_PAUSED);
+ break;
+ case Renderer.STATE_PLAYING:
+ mPlaybackState.setState(PlaybackState.PLAYSTATE_PLAYING);
+ break;
+ default:
+ mPlaybackState.setState(PlaybackState.PLAYSTATE_ERROR);
+ mPlaybackState.setErrorMessage("unkown state");
+ break;
+ }
+ mPlaybackState.setPosition(mRenderer.getSeekPosition());
+
+ mControls.sendPlaybackChangeEvent(mPlaybackState.getState());
+ }
+
+ @Override
+ public void onBufferingUpdate(int percent) {
+ }
+
+ @Override
+ public void onFocusLost() {
+ Log.d(TAG, "Focus lost, changing state to " + Renderer.STATE_PAUSED);
+ mPlaybackState.setState(PlaybackState.PLAYSTATE_PAUSED);
+ mPlaybackState.setPosition(mRenderer.getSeekPosition());
+ }
+
+ @Override
+ public void onNextStarted() {
+ }
+ }
+}