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