Add RouteProviders to the new Media APIs

Compiles and works with OneMedia. This currently is a rough test of
the system for finding, connecting to, and sending messages to routes.
This will just connect to the first route it finds when a request to
open the route picker is made (and disconnect when another request is
made).

Change-Id: I5de5521a079471b9e02664be4654c0591dfd9a6d
This commit is contained in:
RoboErik
2014-03-20 13:33:52 -07:00
parent 46175e1528
commit 07c7077c54
63 changed files with 4360 additions and 677 deletions

View File

@@ -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 \

View File

@@ -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.routeprovider {
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 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 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 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 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 {

View File

@@ -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() {

View File

@@ -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";

View File

@@ -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"

View File

@@ -1076,6 +1076,12 @@
<string name="permdesc_bindRemoteViews">Allows the holder to bind to the top-level
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. -->

View File

@@ -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();
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
};
}

View File

@@ -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;

View File

@@ -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];
}
};
}

View File

@@ -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);

View File

@@ -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();

View File

@@ -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();

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -15,4 +15,4 @@
package android.media.session;
parcelable MediaSessionToken;
parcelable RouteCommand;

View File

@@ -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];
}
};
}

View File

@@ -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;

View File

@@ -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];
}
};
}

View File

@@ -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;

View File

@@ -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();
}
}
}

View File

@@ -17,136 +17,161 @@ 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
}
// 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);
if (listener == null) {
throw new IllegalArgumentException("listener may not be null");
}
return b;
}
public abstract static class Stub {
/**
* The name of an interface should be a fully qualified name to prevent
* namespace collisions. Example: "com.myproject.MyPlaybackInterface"
*
* @return The name of this interface
*/
public abstract String getName();
/**
* This is called when a command is received that matches the interface
* you registered. Commands can come from any app with a MediaController
* reference to the session.
*
* @see MediaController
* @see MediaSession
* @param command The command or method to invoke.
* @param args Any args that were included with the command. May be
* null.
* @param cb The callback provided to send a response on. May be null.
*/
public abstract void onCommand(String command, Bundle args, ResultReceiver cb);
public final void sendEvent(MediaSession session, String event, Bundle extras) {
// TODO
synchronized (mLock) {
int index = findIndexOfListenerLocked(listener);
if (index != -1) {
mListeners.remove(index);
}
}
}
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 -1;
}
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
* sent by the session on a specific interface.
@@ -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;
}

View File

@@ -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;

View File

@@ -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);
}
}
}

View File

@@ -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) {
}
}
}

View File

@@ -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) {
}
}
}

View File

@@ -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);
@@ -185,30 +200,6 @@ public final class MediaSession {
mPublished = true;
}
/**
* Add an interface that can be used by MediaSessions. TODO make this a
* route provider api
*
* @see RouteInterface
* @param iface The interface to add
* @hide
*/
public void addInterface(RouteInterface.Stub iface) {
if (iface == null) {
throw new IllegalArgumentException("Stub cannot be null");
}
String name = iface.getName();
if (TextUtils.isEmpty(name)) {
throw new IllegalArgumentException("Stub must return a valid name");
}
if (mInterfaces.containsKey(iface)) {
throw new IllegalArgumentException("Interface is already added");
}
synchronized (mLock) {
mInterfaces.put(iface.getName(), iface);
}
}
/**
* Send a proprietary event to all MediaControllers listening to this
* Session. It's up to the Controller/Session owner to determine the meaning
@@ -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, mediaRouteDescriptor);
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_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) {

View File

@@ -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);
}
}

View File

@@ -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];
}
};
}

View File

@@ -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>();
}
}

View File

@@ -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;

View File

@@ -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];
}
};
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
/**
* 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);
}
public boolean isPublished() {
return mIsPublished;
}
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,21 +273,50 @@ 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 onEvent(RouteEvent event) {
RouteEvent eventForSession = new RouteEvent(null, event.getIface(),
event.getEvent(), event.getExtras());
mSessionCb.sendRouteEvent(eventForSession);
}
@Override
public void disconnect() {
// TODO
}
};
private final class SessionStub extends ISession.Stub {
@Override
public void destroy() {
onDestroy();
@@ -174,20 +328,10 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
}
@Override
public IMediaController getMediaController() {
public ISessionController getController() {
return mController;
}
@Override
public void setRouteState(Bundle routeState) {
}
@Override
public void setRoute(Bundle mediaRouteDescriptor) {
mRoute = mediaRouteDescriptor;
mHandler.post(MessageHandler.MSG_UPDATE_ROUTE);
}
@Override
public void publish() {
mIsPublished = true; // TODO push update to service
@@ -197,11 +341,6 @@ public class MediaSessionRecord implements IBinder.DeathRecipient {
mTransportPerformerEnabled = true;
}
@Override
public List<String> getSupportedInterfaces() {
return mInterfaces;
}
@Override
public void setMetadata(MediaMetadata metadata) {
mMetadata = 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;
}
}

View File

@@ -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 IMediaSession createSession(String packageName, IMediaSessionCallback cb, String tag)
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 ISession createSession(String packageName, ISessionCallback cb, String tag)
throws RemoteException {
final int pid = Binder.getCallingPid();
final int uid = Binder.getCallingUid();

View File

@@ -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();
}
}

View File

@@ -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>

View File

@@ -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"

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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() {

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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() {
}
}
}