diff options
59 files changed, 4214 insertions, 3933 deletions
@@ -320,9 +320,12 @@ LOCAL_SRC_FILES += \ media/java/android/media/IRemoteVolumeObserver.aidl \ media/java/android/media/IRingtonePlayer.aidl \ media/java/android/media/IVolumeController.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/routing/IMediaRouteService.aidl \ + media/java/android/media/routing/IMediaRouteClientCallback.aidl \ + media/java/android/media/routing/IMediaRouter.aidl \ + media/java/android/media/routing/IMediaRouterDelegate.aidl \ + media/java/android/media/routing/IMediaRouterRoutingCallback.aidl \ + media/java/android/media/routing/IMediaRouterStateCallback.aidl \ media/java/android/media/session/IActiveSessionsListener.aidl \ media/java/android/media/session/ISessionController.aidl \ media/java/android/media/session/ISessionControllerCallback.aidl \ @@ -527,6 +530,7 @@ aidl_files := \ frameworks/base/location/java/com/android/internal/location/ProviderRequest.aidl \ frameworks/base/media/java/android/media/MediaMetadata.aidl \ frameworks/base/media/java/android/media/Rating.aidl \ + frameworks/base/media/java/android/media/routing/MediaRouteSelector.aidl \ frameworks/base/media/java/android/media/session/MediaSession.aidl \ frameworks/base/media/java/android/media/session/PlaybackState.aidl \ frameworks/base/telephony/java/android/telephony/ServiceState.aidl \ diff --git a/api/current.txt b/api/current.txt index d283360..623c281 100644 --- a/api/current.txt +++ b/api/current.txt @@ -23,6 +23,7 @@ package android { field public static final java.lang.String BIND_DEVICE_ADMIN = "android.permission.BIND_DEVICE_ADMIN"; field public static final java.lang.String BIND_DREAM_SERVICE = "android.permission.BIND_DREAM_SERVICE"; field public static final java.lang.String BIND_INPUT_METHOD = "android.permission.BIND_INPUT_METHOD"; + field public static final java.lang.String BIND_MEDIA_ROUTE_SERVICE = "android.permission.BIND_MEDIA_ROUTE_SERVICE"; field public static final java.lang.String BIND_NFC_SERVICE = "android.permission.BIND_NFC_SERVICE"; 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"; @@ -15871,12 +15872,249 @@ package android.media.effect { } +package android.media.routing { + + public final class MediaRouteSelector implements android.os.Parcelable { + method public boolean containsProtocol(java.lang.Class<?>); + method public boolean containsProtocol(java.lang.String); + method public int describeContents(); + method public android.os.Bundle getExtras(); + method public int getOptionalFeatures(); + method public java.util.List<java.lang.String> getOptionalProtocols(); + method public int getRequiredFeatures(); + method public java.util.List<java.lang.String> getRequiredProtocols(); + method public java.lang.String getServicePackageName(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator CREATOR; + } + + public static final class MediaRouteSelector.Builder { + ctor public MediaRouteSelector.Builder(); + method public android.media.routing.MediaRouteSelector.Builder addOptionalProtocol(java.lang.Class<?>); + method public android.media.routing.MediaRouteSelector.Builder addOptionalProtocol(java.lang.String); + method public android.media.routing.MediaRouteSelector.Builder addRequiredProtocol(java.lang.Class<?>); + method public android.media.routing.MediaRouteSelector.Builder addRequiredProtocol(java.lang.String); + method public android.media.routing.MediaRouteSelector build(); + method public android.media.routing.MediaRouteSelector.Builder setExtras(android.os.Bundle); + method public android.media.routing.MediaRouteSelector.Builder setOptionalFeatures(int); + method public android.media.routing.MediaRouteSelector.Builder setRequiredFeatures(int); + method public android.media.routing.MediaRouteSelector.Builder setServicePackageName(java.lang.String); + } + + public abstract class MediaRouteService extends android.app.Service { + ctor public MediaRouteService(); + method public android.media.routing.MediaRouter.ServiceMetadata getServiceMetadata(); + method public android.os.IBinder onBind(android.content.Intent); + method public abstract android.media.routing.MediaRouteService.ClientSession onCreateClientSession(android.media.routing.MediaRouteService.ClientInfo); + field public static final java.lang.String SERVICE_INTERFACE = "android.media.routing.MediaRouteService"; + } + + public static final class MediaRouteService.ClientInfo { + method public java.lang.String getPackageName(); + method public int getUid(); + } + + public static abstract class MediaRouteService.ClientSession { + ctor public MediaRouteService.ClientSession(); + method public abstract boolean onConnect(android.media.routing.MediaRouter.ConnectionRequest, android.media.routing.MediaRouteService.ConnectionCallback); + method public abstract void onDisconnect(); + method public void onPauseStream(); + method public void onRelease(); + method public void onResumeStream(); + method public abstract boolean onStartDiscovery(android.media.routing.MediaRouter.DiscoveryRequest, android.media.routing.MediaRouteService.DiscoveryCallback); + method public abstract void onStopDiscovery(); + } + + public final class MediaRouteService.ConnectionCallback { + method public void onConnected(android.media.routing.MediaRouter.ConnectionInfo); + method public void onConnectionFailed(int, java.lang.CharSequence, android.os.Bundle); + method public void onDisconnected(); + } + + public final class MediaRouteService.DiscoveryCallback { + method public void onDestinationFound(android.media.routing.MediaRouter.DestinationInfo, java.util.List<android.media.routing.MediaRouter.RouteInfo>); + method public void onDestinationLost(android.media.routing.MediaRouter.DestinationInfo); + method public void onDiscoveryFailed(int, java.lang.CharSequence, android.os.Bundle); + } + + public final class MediaRouter { + ctor public MediaRouter(android.content.Context); + method public void addSelector(android.media.routing.MediaRouteSelector); + method public void clearSelectors(); + method public android.media.routing.MediaRouter.Delegate createDelegate(); + method public android.media.routing.MediaRouter.ConnectionInfo getConnection(); + method public int getConnectionState(); + method public java.util.List<android.media.routing.MediaRouter.DestinationInfo> getDiscoveredDestinations(); + method public java.util.List<android.media.routing.MediaRouter.RouteInfo> getDiscoveredRoutes(android.media.routing.MediaRouter.DestinationInfo); + method public int getDiscoveryState(); + method public android.media.AudioAttributes getPreferredAudioAttributes(); + method public android.view.Display getPreferredPresentationDisplay(); + method public android.media.VolumeProvider getPreferredVolumeProvider(); + method public android.media.routing.MediaRouter.DestinationInfo getSelectedDestination(); + method public android.media.routing.MediaRouter.RouteInfo getSelectedRoute(); + method public java.util.List<android.media.routing.MediaRouteSelector> getSelectors(); + method public boolean isReleased(); + method public void pauseStream(); + method public void release(); + method public void removeSelector(android.media.routing.MediaRouteSelector); + method public void resumeStream(); + method public void setRoutingCallback(android.media.routing.MediaRouter.RoutingCallback, android.os.Handler); + field public static final int CONNECTION_ERROR_ABORTED = 1; // 0x1 + field public static final int CONNECTION_ERROR_BARGED = 7; // 0x7 + field public static final int CONNECTION_ERROR_BROKEN = 6; // 0x6 + field public static final int CONNECTION_ERROR_BUSY = 4; // 0x4 + field public static final int CONNECTION_ERROR_TIMEOUT = 5; // 0x5 + field public static final int CONNECTION_ERROR_UNAUTHORIZED = 2; // 0x2 + field public static final int CONNECTION_ERROR_UNKNOWN = 0; // 0x0 + field public static final int CONNECTION_ERROR_UNREACHABLE = 3; // 0x3 + field public static final int CONNECTION_FLAG_BARGE = 1; // 0x1 + field public static final int CONNECTION_STATE_CONNECTED = 2; // 0x2 + field public static final int CONNECTION_STATE_CONNECTING = 1; // 0x1 + field public static final int CONNECTION_STATE_DISCONNECTED = 0; // 0x0 + field public static final int DISCONNECTION_REASON_APPLICATION_REQUEST = 0; // 0x0 + field public static final int DISCONNECTION_REASON_ERROR = 2; // 0x2 + field public static final int DISCONNECTION_REASON_USER_REQUEST = 1; // 0x1 + field public static final int DISCOVERY_ERROR_ABORTED = 1; // 0x1 + field public static final int DISCOVERY_ERROR_NO_CONNECTIVITY = 2; // 0x2 + field public static final int DISCOVERY_ERROR_UNKNOWN = 0; // 0x0 + field public static final int DISCOVERY_FLAG_BACKGROUND = 1; // 0x1 + field public static final int DISCOVERY_STATE_STARTED = 1; // 0x1 + field public static final int DISCOVERY_STATE_STOPPED = 0; // 0x0 + field public static final int ROUTE_FEATURE_LIVE_AUDIO = 1; // 0x1 + field public static final int ROUTE_FEATURE_LIVE_VIDEO = 2; // 0x2 + } + + public static final class MediaRouter.ConnectionInfo { + method public android.media.AudioAttributes getAudioAttributes(); + method public android.os.Bundle getExtras(); + method public int getFeatures(); + method public android.view.Display getPresentationDisplay(); + method public android.os.IBinder getProtocolBinder(java.lang.String); + method public android.os.IBinder getProtocolBinder(int); + method public T getProtocolObject(java.lang.Class<T>); + method public java.util.List<java.lang.String> getProtocols(); + method public android.media.routing.MediaRouter.RouteInfo getRoute(); + method public android.media.VolumeProvider getVolumeProvider(); + } + + public static final class MediaRouter.ConnectionInfo.Builder { + ctor public MediaRouter.ConnectionInfo.Builder(android.media.routing.MediaRouter.RouteInfo); + method public android.media.routing.MediaRouter.ConnectionInfo build(); + method public android.media.routing.MediaRouter.ConnectionInfo.Builder setAudioAttributes(android.media.AudioAttributes); + method public android.media.routing.MediaRouter.ConnectionInfo.Builder setExtras(android.os.Bundle); + method public android.media.routing.MediaRouter.ConnectionInfo.Builder setPresentationDisplay(android.view.Display); + method public android.media.routing.MediaRouter.ConnectionInfo.Builder setProtocolBinder(java.lang.String, android.os.IBinder); + method public android.media.routing.MediaRouter.ConnectionInfo.Builder setProtocolStub(java.lang.Class<?>, android.os.IInterface); + method public android.media.routing.MediaRouter.ConnectionInfo.Builder setVolumeProvider(android.media.VolumeProvider); + } + + public static final class MediaRouter.ConnectionRequest { + method public android.os.Bundle getExtras(); + method public int getFlags(); + method public android.media.routing.MediaRouter.RouteInfo getRoute(); + method public void setExtras(android.os.Bundle); + method public void setFlags(int); + method public void setRoute(android.media.routing.MediaRouter.RouteInfo); + } + + public static final class MediaRouter.Delegate { + ctor public MediaRouter.Delegate(); + method public void addStateCallback(android.media.routing.MediaRouter.StateCallback, android.os.Handler); + method public void connect(android.media.routing.MediaRouter.DestinationInfo, int); + method public void disconnect(int); + method public int getConnectionState(); + method public java.util.List<android.media.routing.MediaRouter.DestinationInfo> getDiscoveredDestinations(); + method public int getDiscoveryState(); + method public android.media.routing.MediaRouter.DestinationInfo getSelectedDestination(); + method public boolean isReleased(); + method public void removeStateCallback(android.media.routing.MediaRouter.StateCallback); + method public void startDiscovery(int); + method public void stopDiscovery(); + } + + public static final class MediaRouter.DestinationInfo { + method public java.lang.CharSequence getDescription(); + method public android.os.Bundle getExtras(); + method public int getIconResourceId(); + method public java.lang.String getId(); + method public java.lang.CharSequence getName(); + method public android.media.routing.MediaRouter.ServiceMetadata getServiceMetadata(); + method public android.graphics.drawable.Drawable loadIcon(android.content.pm.PackageManager); + } + + public static final class MediaRouter.DestinationInfo.Builder { + ctor public MediaRouter.DestinationInfo.Builder(java.lang.String, android.media.routing.MediaRouter.ServiceMetadata, java.lang.CharSequence); + method public android.media.routing.MediaRouter.DestinationInfo build(); + method public android.media.routing.MediaRouter.DestinationInfo.Builder setDescription(java.lang.CharSequence); + method public android.media.routing.MediaRouter.DestinationInfo.Builder setExtras(android.os.Bundle); + method public android.media.routing.MediaRouter.DestinationInfo.Builder setIconResourceId(int); + } + + public static final class MediaRouter.DiscoveryRequest { + method public int getFlags(); + method public java.util.List<android.media.routing.MediaRouteSelector> getSelectors(); + method public void setFlags(int); + method public void setSelectors(java.util.List<android.media.routing.MediaRouteSelector>); + } + + public static final class MediaRouter.RouteInfo { + method public android.media.routing.MediaRouter.DestinationInfo getDestination(); + method public android.os.Bundle getExtras(); + method public int getFeatures(); + method public java.lang.String getId(); + method public java.util.List<java.lang.String> getProtocols(); + method public android.media.routing.MediaRouteSelector getSelector(); + } + + public static final class MediaRouter.RouteInfo.Builder { + ctor public MediaRouter.RouteInfo.Builder(java.lang.String, android.media.routing.MediaRouter.DestinationInfo, android.media.routing.MediaRouteSelector); + method public android.media.routing.MediaRouter.RouteInfo.Builder addProtocol(java.lang.Class<T>); + method public android.media.routing.MediaRouter.RouteInfo.Builder addProtocol(java.lang.String); + method public android.media.routing.MediaRouter.RouteInfo build(); + method public android.media.routing.MediaRouter.RouteInfo.Builder setExtras(android.os.Bundle); + method public android.media.routing.MediaRouter.RouteInfo.Builder setFeatures(int); + } + + public static abstract class MediaRouter.RoutingCallback extends android.media.routing.MediaRouter.StateCallback { + ctor public MediaRouter.RoutingCallback(); + method public boolean onPrepareConnectionRequest(android.media.routing.MediaRouter.ConnectionRequest, android.media.routing.MediaRouter.DestinationInfo, java.util.List<android.media.routing.MediaRouter.RouteInfo>); + method public boolean onPrepareDiscoveryRequest(android.media.routing.MediaRouter.DiscoveryRequest, java.util.List<android.media.routing.MediaRouteSelector>); + } + + public static final class MediaRouter.ServiceMetadata { + method public android.content.ComponentName getComponentName(); + method public android.graphics.drawable.Drawable getIcon(android.content.pm.PackageManager); + method public java.lang.CharSequence getLabel(android.content.pm.PackageManager); + method public java.lang.String getPackageName(); + method public android.content.pm.ServiceInfo getService(); + } + + public static abstract class MediaRouter.StateCallback { + ctor public MediaRouter.StateCallback(); + method public void onConnected(); + method public void onConnecting(); + method public void onConnectionFailed(int, java.lang.CharSequence, android.os.Bundle); + method public void onConnectionStateChanged(int); + method public void onDestinationFound(android.media.routing.MediaRouter.DestinationInfo); + method public void onDestinationLost(android.media.routing.MediaRouter.DestinationInfo); + method public void onDisconnected(); + method public void onDiscoveryFailed(int, java.lang.CharSequence, android.os.Bundle); + method public void onDiscoveryStarted(); + method public void onDiscoveryStateChanged(int); + method public void onDiscoveryStopped(); + method public void onReleased(); + method public void onSelectedDestinationChanged(android.media.routing.MediaRouter.DestinationInfo); + } + +} + package android.media.session { public final class MediaController { method public void addCallback(android.media.session.MediaController.Callback); method public void addCallback(android.media.session.MediaController.Callback, android.os.Handler); method public void adjustVolumeBy(int, int); + method public android.media.routing.MediaRouter.Delegate createMediaRouterDelegate(); method public boolean dispatchMediaButtonEvent(android.view.KeyEvent); method public static android.media.session.MediaController fromToken(android.media.session.MediaSession.Token); method public android.media.MediaMetadata getMetadata(); @@ -15931,6 +16169,7 @@ package android.media.session { method public void setActive(boolean); method public void setFlags(int); method public void setLaunchPendingIntent(android.app.PendingIntent); + method public void setMediaRouter(android.media.routing.MediaRouter); method public void setMetadata(android.media.MediaMetadata); method public void setPlaybackState(android.media.session.PlaybackState); method public void setPlaybackToLocal(int); @@ -16005,6 +16244,7 @@ package android.media.session { field public static final android.os.Parcelable.Creator CREATOR; field public static final long PLAYBACK_POSITION_UNKNOWN = -1L; // 0xffffffffffffffffL field public static final int STATE_BUFFERING = 6; // 0x6 + field public static final int STATE_CONNECTING = 8; // 0x8 field public static final int STATE_ERROR = 7; // 0x7 field public static final int STATE_FAST_FORWARDING = 4; // 0x4 field public static final int STATE_NONE = 0; // 0x0 @@ -16268,8 +16508,8 @@ package android.media.tv { method public void onChannelRetuned(java.lang.String, android.net.Uri); method public void onError(java.lang.String, int); method public void onTrackInfoChanged(java.lang.String, java.util.List<android.media.tv.TvTrackInfo>); - method public void onVideoSizeChanged(java.lang.String, int, int); method public void onVideoAvailable(java.lang.String); + method public void onVideoSizeChanged(java.lang.String, int, int); method public void onVideoUnavailable(java.lang.String, int); } @@ -24659,6 +24899,7 @@ package android.provider { field public static final java.lang.String ACTION_APPLICATION_SETTINGS = "android.settings.APPLICATION_SETTINGS"; field public static final java.lang.String ACTION_BLUETOOTH_SETTINGS = "android.settings.BLUETOOTH_SETTINGS"; field public static final java.lang.String ACTION_CAPTIONING_SETTINGS = "android.settings.CAPTIONING_SETTINGS"; + field public static final java.lang.String ACTION_CAST_SETTINGS = "android.settings.CAST_SETTINGS"; field public static final java.lang.String ACTION_DATA_ROAMING_SETTINGS = "android.settings.DATA_ROAMING_SETTINGS"; field public static final java.lang.String ACTION_DATE_SETTINGS = "android.settings.DATE_SETTINGS"; field public static final java.lang.String ACTION_DEVICE_INFO_SETTINGS = "android.settings.DEVICE_INFO_SETTINGS"; diff --git a/cmds/media/src/com/android/commands/media/Media.java b/cmds/media/src/com/android/commands/media/Media.java index 4e91361..800b925 100644 --- a/cmds/media/src/com/android/commands/media/Media.java +++ b/cmds/media/src/com/android/commands/media/Media.java @@ -25,7 +25,6 @@ import android.media.session.ISessionManager; import android.media.session.MediaController; import android.media.session.MediaSessionInfo; import android.media.session.PlaybackState; -import android.media.session.RouteInfo; import android.os.Bundle; import android.os.HandlerThread; import android.os.IBinder; @@ -181,11 +180,6 @@ public class Media extends BaseCommand { } @Override - public void onRouteChanged(RouteInfo route) { - System.out.println("onRouteChanged " + route); - } - - @Override public void onPlaybackStateChanged(PlaybackState state) { System.out.println("onPlaybackStateChanged " + state); } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 07397973..2687bcc 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -266,6 +266,21 @@ public final class Settings { "android.settings.WIFI_DISPLAY_SETTINGS"; /** + * Activity Action: Show settings to allow configuration of + * {@link android.media.routing.MediaRouteService media route providers}. + * <p> + * In some cases, a matching Activity may not exist, so ensure you + * safeguard against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_CAST_SETTINGS = + "android.settings.CAST_SETTINGS"; + + /** * Activity Action: Show settings to allow configuration of date and time. * <p> * In some cases, a matching Activity may not exist, so ensure you diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 4725cfb..350660b 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -2149,7 +2149,7 @@ android:description="@string/permdesc_bindTvInput" android:protectionLevel="signature|system" /> - <!-- Must be required by a {@link android.media.routeprovider.RouteProviderService} + <!-- Must be required by a {@link android.media.routing.MediaRouteService} to ensure that only the system can interact with it. @hide --> <permission android:name="android.permission.BIND_ROUTE_PROVIDER" @@ -2720,6 +2720,13 @@ android:description="@string/permdesc_bindConditionProviderService" android:protectionLevel="signature" /> + <!-- Must be required by a {@link android.media.routing.MediaRouteService}, + to ensure that only the system can bind to it. --> + <permission android:name="android.permission.BIND_MEDIA_ROUTE_SERVICE" + android:label="@string/permlab_bindMediaRouteService" + android:description="@string/permdesc_bindMediaRouteService" + android:protectionLevel="signature" /> + <!-- Must be required by an {@link android.service.dreams.DreamService}, to ensure that only the system can bind to it. --> <permission android:name="android.permission.BIND_DREAM_SERVICE" diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 5d57262..193d3c0 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -2124,6 +2124,11 @@ <string name="permdesc_bindConditionProviderService">Allows the holder to bind to the top-level interface of a condition provider 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_bindMediaRouteService">bind to a media route 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_bindMediaRouteService">Allows the holder to bind to the top-level interface of a media route 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_bindDreamService">bind to a dream 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_bindDreamService">Allows the holder to bind to the top-level interface of a dream service. Should never be needed for normal apps.</string> diff --git a/media/java/android/media/routeprovider/IRouteProvider.aidl b/media/java/android/media/routeprovider/IRouteProvider.aidl deleted file mode 100644 index c36f6a7..0000000 --- a/media/java/android/media/routeprovider/IRouteProvider.aidl +++ /dev/null @@ -1,36 +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.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/RouteConnection.java b/media/java/android/media/routeprovider/RouteConnection.java deleted file mode 100644 index 43692c1..0000000 --- a/media/java/android/media/routeprovider/RouteConnection.java +++ /dev/null @@ -1,165 +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.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. - * @hide - */ -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 deleted file mode 100644 index e7f8bbf..0000000 --- a/media/java/android/media/routeprovider/RouteInterfaceHandler.java +++ /dev/null @@ -1,246 +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.routeprovider; - -import android.media.session.Route; -import android.media.session.MediaSession; -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 MediaSession} 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. - * @hide - */ -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 MediaSession} 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 MediaSession - */ - 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 deleted file mode 100644 index f2c40d2..0000000 --- a/media/java/android/media/routeprovider/RoutePlaybackControlsHandler.java +++ /dev/null @@ -1,222 +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.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. - * @hide - */ -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 deleted file mode 100644 index a6ef0bb..0000000 --- a/media/java/android/media/routeprovider/RouteProviderService.java +++ /dev/null @@ -1,228 +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.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> - * @hide - */ -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.java b/media/java/android/media/routeprovider/RouteRequest.java deleted file mode 100644 index 2ba75de..0000000 --- a/media/java/android/media/routeprovider/RouteRequest.java +++ /dev/null @@ -1,110 +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.routeprovider; - -import android.media.session.RouteOptions; -import android.media.session.MediaSessionInfo; -import android.os.Parcel; -import android.os.Parcelable; - -import java.io.PrintWriter; - -/** - * 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. - * @hide - */ -public final class RouteRequest implements Parcelable { - private final MediaSessionInfo mSessionInfo; - private final RouteOptions mOptions; - private final boolean mActive; - - /** - * @hide - */ - public RouteRequest(MediaSessionInfo info, RouteOptions connRequest, - boolean active) { - mSessionInfo = info; - mOptions = connRequest; - mActive = active; - } - - private RouteRequest(Parcel in) { - mSessionInfo = MediaSessionInfo.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 MediaSessionInfo 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 String toString() { - StringBuilder bob = new StringBuilder(); - bob.append("RouteRequest {"); - bob.append("active=").append(mActive); - bob.append(", info=").append(mSessionInfo.toString()); - bob.append(", options=").append(mOptions.toString()); - bob.append("}"); - return bob.toString(); - } - - @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/routing/IMediaRouteClientCallback.aidl b/media/java/android/media/routing/IMediaRouteClientCallback.aidl new file mode 100644 index 0000000..d90ea3b --- /dev/null +++ b/media/java/android/media/routing/IMediaRouteClientCallback.aidl @@ -0,0 +1,41 @@ +/* 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.routing; + +import android.media.routing.MediaRouteSelector; +import android.media.routing.ParcelableConnectionInfo; +import android.media.routing.ParcelableDestinationInfo; +import android.media.routing.ParcelableRouteInfo; +import android.os.IBinder; +import android.os.Bundle; + +/** + * @hide + */ +oneway interface IMediaRouteClientCallback { + void onDestinationFound(int seq, in ParcelableDestinationInfo destination, + in ParcelableRouteInfo[] routes); + + void onDestinationLost(int seq, String id); + + void onDiscoveryFailed(int seq, int error, in CharSequence message, in Bundle extras); + + void onConnected(int seq, in ParcelableConnectionInfo connection); + + void onDisconnected(int seq); + + void onConnectionFailed(int seq, int error, in CharSequence message, in Bundle extras); +} diff --git a/media/java/android/media/routing/IMediaRouteService.aidl b/media/java/android/media/routing/IMediaRouteService.aidl new file mode 100644 index 0000000..493ab6d --- /dev/null +++ b/media/java/android/media/routing/IMediaRouteService.aidl @@ -0,0 +1,46 @@ +/* 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.routing; + +import android.media.routing.IMediaRouteClientCallback; +import android.media.routing.MediaRouteSelector; +import android.os.Bundle; + +/** + * Interface to an app's MediaRouteService. + * @hide + */ +oneway interface IMediaRouteService { + void registerClient(int clientUid, String clientPackageName, + in IMediaRouteClientCallback callback); + + void unregisterClient(in IMediaRouteClientCallback callback); + + void startDiscovery(in IMediaRouteClientCallback callback, int seq, + in List<MediaRouteSelector> selectors, int flags); + + void stopDiscovery(in IMediaRouteClientCallback callback); + + void connect(in IMediaRouteClientCallback callback, int seq, + String destinationId, String routeId, int flags, in Bundle extras); + + void disconnect(in IMediaRouteClientCallback callback); + + void pauseStream(in IMediaRouteClientCallback callback); + + void resumeStream(in IMediaRouteClientCallback callback); +} + diff --git a/media/java/android/media/routeprovider/IRouteConnection.aidl b/media/java/android/media/routing/IMediaRouter.aidl index 15c8039..0abb258 100644 --- a/media/java/android/media/routeprovider/IRouteConnection.aidl +++ b/media/java/android/media/routing/IMediaRouter.aidl @@ -13,16 +13,10 @@ * limitations under the License. */ -package android.media.routeprovider; +package android.media.routing; -import android.media.session.RouteCommand; -import android.os.ResultReceiver; +/** @hide */ +interface IMediaRouter { + +} -/** - * 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/IRouteProviderCallback.aidl b/media/java/android/media/routing/IMediaRouterDelegate.aidl index 9185347..35f84c8 100644 --- a/media/java/android/media/routeprovider/IRouteProviderCallback.aidl +++ b/media/java/android/media/routing/IMediaRouterDelegate.aidl @@ -13,20 +13,10 @@ * limitations under the License. */ -package android.media.routeprovider; +package android.media.routing; -import android.media.routeprovider.IRouteConnection; -import android.media.session.RouteEvent; -import android.os.Bundle; -import android.os.ResultReceiver; +/** @hide */ +interface IMediaRouterDelegate { + +} -/** - * 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/routing/IMediaRouterRoutingCallback.aidl b/media/java/android/media/routing/IMediaRouterRoutingCallback.aidl new file mode 100644 index 0000000..173ae55 --- /dev/null +++ b/media/java/android/media/routing/IMediaRouterRoutingCallback.aidl @@ -0,0 +1,22 @@ +/* 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.routing; + +/** @hide */ +interface IMediaRouterRoutingCallback { + +} + diff --git a/media/java/android/media/routing/IMediaRouterStateCallback.aidl b/media/java/android/media/routing/IMediaRouterStateCallback.aidl new file mode 100644 index 0000000..0299904 --- /dev/null +++ b/media/java/android/media/routing/IMediaRouterStateCallback.aidl @@ -0,0 +1,22 @@ +/* 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.routing; + +/** @hide */ +interface IMediaRouterStateCallback { + +} + diff --git a/media/java/android/media/session/RouteInfo.aidl b/media/java/android/media/routing/MediaRouteSelector.aidl index c5f50c8..37bfa4a 100644 --- a/media/java/android/media/session/RouteInfo.aidl +++ b/media/java/android/media/routing/MediaRouteSelector.aidl @@ -13,6 +13,6 @@ ** limitations under the License. */ -package android.media.session; +package android.media.routing; -parcelable RouteInfo; +parcelable MediaRouteSelector; diff --git a/media/java/android/media/routing/MediaRouteSelector.java b/media/java/android/media/routing/MediaRouteSelector.java new file mode 100644 index 0000000..26a9b1c --- /dev/null +++ b/media/java/android/media/routing/MediaRouteSelector.java @@ -0,0 +1,357 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.media.routing; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.media.routing.MediaRouter.RouteFeatures; +import android.os.Bundle; +import android.os.IInterface; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * A media route selector consists of a set of constraints that are used to select + * the routes to which an application would like to connect. The constraints consist + * of a set of required or optional features and protocols. The constraints may also + * require the use of a specific media route service package or additional characteristics + * that are described by a bundle of extra parameters. + * <p> + * The application will typically create several different selectors that express + * various combinations of characteristics that it would like to use together when + * it connects to a destination media device. For each destination that is discovered, + * media route services will publish some number of routes and include information + * about which selector each route matches. The application will then choose among + * these routes to determine which best satisfies its desired purpose and connect to it. + * </p> + */ +public final class MediaRouteSelector implements Parcelable { + private final int mRequiredFeatures; + private final int mOptionalFeatures; + private final List<String> mRequiredProtocols; + private final List<String> mOptionalProtocols; + private final String mServicePackageName; + private final Bundle mExtras; + + MediaRouteSelector(int requiredFeatures, int optionalFeatures, + List<String> requiredProtocols, List<String> optionalProtocols, + String servicePackageName, Bundle extras) { + mRequiredFeatures = requiredFeatures; + mOptionalFeatures = optionalFeatures; + mRequiredProtocols = requiredProtocols; + mOptionalProtocols = optionalProtocols; + mServicePackageName = servicePackageName; + mExtras = extras; + } + + /** + * Gets the set of required route features. + * + * @return A set of required route feature flags. + */ + public @RouteFeatures int getRequiredFeatures() { + return mRequiredFeatures; + } + + /** + * Gets the set of optional route features. + * + * @return A set of optional route feature flags. + */ + public @RouteFeatures int getOptionalFeatures() { + return mOptionalFeatures; + } + + /** + * Gets the list of route protocols that a route must support in order to be selected. + * <p> + * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code> + * for more information. + * </p> + * + * @return The list of fully qualified route protocol names. + */ + public @NonNull List<String> getRequiredProtocols() { + return mRequiredProtocols; + } + + /** + * Gets the list of optional route protocols that a client may use if they are available. + * <p> + * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code> + * for more information. + * </p> + * + * @return The list of optional fully qualified route protocol names. + */ + public @NonNull List<String> getOptionalProtocols() { + return mOptionalProtocols; + } + + /** + * Returns true if the selector includes a required or optional request for + * the specified protocol using its fully qualified class name. + * <p> + * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code> + * for more information. + * </p> + * + * @param clazz The protocol class. + * @return True if the protocol was requested. + */ + public boolean containsProtocol(@NonNull Class<?> clazz) { + return containsProtocol(clazz.getName()); + } + + /** + * Returns true if the selector includes a required or optional request for + * the specified protocol. + * <p> + * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code> + * for more information. + * </p> + * + * @param name The name of the protocol. + * @return True if the protocol was requested. + */ + public boolean containsProtocol(@NonNull String name) { + return mRequiredProtocols.contains(name) + || mOptionalProtocols.contains(name); + } + + /** + * Gets the package name of a specific media route service that this route selector + * requires. + * + * @return The required media route service package name, or null if none. + */ + public @Nullable String getServicePackageName() { + return mServicePackageName; + } + + /** + * Gets optional extras that may be used to select or configure routes for a + * particular purpose. Some extras may be used by media route services to apply + * additional constraints or parameters for the routes to be discovered. + * + * @return The optional extras, or null if none. + */ + public @Nullable Bundle getExtras() { + return mExtras; + } + + @Override + public String toString() { + return "MediaRouteSelector{ " + + ", requiredFeatures=0x" + Integer.toHexString(mRequiredFeatures) + + ", optionalFeatures=0x" + Integer.toHexString(mOptionalFeatures) + + ", requiredProtocols=" + mRequiredProtocols + + ", optionalProtocols=" + mOptionalProtocols + + ", servicePackageName=" + mServicePackageName + + ", extras=" + mExtras + " }"; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mRequiredFeatures); + dest.writeInt(mOptionalFeatures); + dest.writeStringList(mRequiredProtocols); + dest.writeStringList(mOptionalProtocols); + dest.writeString(mServicePackageName); + dest.writeBundle(mExtras); + } + + public static final Parcelable.Creator<MediaRouteSelector> CREATOR = + new Parcelable.Creator<MediaRouteSelector>() { + @Override + public MediaRouteSelector createFromParcel(Parcel source) { + int requiredFeatures = source.readInt(); + int optionalFeatures = source.readInt(); + ArrayList<String> requiredProtocols = new ArrayList<String>(); + ArrayList<String> optionalProtocols = new ArrayList<String>(); + source.readStringList(requiredProtocols); + source.readStringList(optionalProtocols); + return new MediaRouteSelector(requiredFeatures, optionalFeatures, + requiredProtocols, optionalProtocols, + source.readString(), source.readBundle()); + } + + @Override + public MediaRouteSelector[] newArray(int size) { + return new MediaRouteSelector[size]; + } + }; + + /** + * Builder for {@link MediaRouteSelector} objects. + */ + public static final class Builder { + private int mRequiredFeatures; + private int mOptionalFeatures; + private final ArrayList<String> mRequiredProtocols = new ArrayList<String>(); + private final ArrayList<String> mOptionalProtocols = new ArrayList<String>(); + private String mServicePackageName; + private Bundle mExtras; + + /** + * Creates an initially empty selector builder. + */ + public Builder() { + } + + /** + * Sets the set of required route features. + * + * @param features A set of required route feature flags. + */ + public @NonNull Builder setRequiredFeatures(@RouteFeatures int features) { + mRequiredFeatures = features; + return this; + } + + /** + * Sets the set of optional route features. + * + * @param features A set of optional route feature flags. + */ + public @NonNull Builder setOptionalFeatures(@RouteFeatures int features) { + mOptionalFeatures = features; + return this; + } + + /** + * Adds a route protocol that a route must support in order to be selected + * using its fully qualified class name. + * <p> + * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code> + * for more information. + * </p> + * + * @param clazz The protocol class. + * @return this + */ + public @NonNull Builder addRequiredProtocol(@NonNull Class<?> clazz) { + if (clazz == null) { + throw new IllegalArgumentException("clazz must not be null"); + } + return addRequiredProtocol(clazz.getName()); + } + + /** + * Adds a route protocol that a route must support in order to be selected. + * <p> + * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code> + * for more information. + * </p> + * + * @param name The fully qualified name of the required protocol. + * @return this + */ + public @NonNull Builder addRequiredProtocol(@NonNull String name) { + if (TextUtils.isEmpty(name)) { + throw new IllegalArgumentException("name must not be null or empty"); + } + mRequiredProtocols.add(name); + return this; + } + + /** + * Adds an optional route protocol that a client may use if available + * using its fully qualified class name. + * <p> + * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code> + * for more information. + * </p> + * + * @param clazz The protocol class. + * @return this + */ + public @NonNull Builder addOptionalProtocol(@NonNull Class<?> clazz) { + if (clazz == null) { + throw new IllegalArgumentException("clazz must not be null"); + } + return addOptionalProtocol(clazz.getName()); + } + + /** + * Adds an optional route protocol that a client may use if available. + * <p> + * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code> + * for more information. + * </p> + * + * @param name The fully qualified name of the optional protocol. + * @return this + */ + public @NonNull Builder addOptionalProtocol(@NonNull String name) { + if (TextUtils.isEmpty(name)) { + throw new IllegalArgumentException("name must not be null or empty"); + } + mOptionalProtocols.add(name); + return this; + } + + /** + * Sets the package name of the media route service to which this selector + * appertains. + * <p> + * If a package name is specified here then this selector will only be + * passed to media route services from that package. This has the effect + * of restricting the set of matching routes to just those that are offered + * by that package. + * </p> + * + * @param packageName The required service package name, or null if none. + * @return this + */ + public @NonNull Builder setServicePackageName(@Nullable String packageName) { + mServicePackageName = packageName; + return this; + } + + /** + * Sets optional extras that may be used to select or configure routes for a + * particular purpose. Some extras may be used by route services to specify + * additional constraints or parameters for the routes to be discovered. + * + * @param extras The optional extras, or null if none. + * @return this + */ + public @NonNull Builder setExtras(@Nullable Bundle extras) { + mExtras = extras; + return this; + } + + /** + * Builds the {@link MediaRouteSelector} object. + * + * @return The new media route selector instance. + */ + public @NonNull MediaRouteSelector build() { + return new MediaRouteSelector(mRequiredFeatures, mOptionalFeatures, + mRequiredProtocols, mOptionalProtocols, mServicePackageName, mExtras); + } + } +} diff --git a/media/java/android/media/routing/MediaRouteService.java b/media/java/android/media/routing/MediaRouteService.java new file mode 100644 index 0000000..4d5a8a9 --- /dev/null +++ b/media/java/android/media/routing/MediaRouteService.java @@ -0,0 +1,1023 @@ +/* + * 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.routing; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SdkConstant; +import android.app.Service; +import android.content.Intent; +import android.content.pm.PackageManager.NameNotFoundException; +import android.media.routing.MediaRouter.ConnectionError; +import android.media.routing.MediaRouter.ConnectionInfo; +import android.media.routing.MediaRouter.ConnectionRequest; +import android.media.routing.MediaRouter.DestinationInfo; +import android.media.routing.MediaRouter.DiscoveryError; +import android.media.routing.MediaRouter.DiscoveryRequest; +import android.media.routing.MediaRouter.RouteInfo; +import android.media.routing.MediaRouter.ServiceMetadata; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.ArrayMap; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +/** + * Media route services implement strategies for discovering + * and establishing connections to media devices and their routes. These services + * are also known as media route providers. + * <p> + * Each media route service subclass is responsible for enabling applications + * and the system to interact with media devices of some kind. + * For example, one particular media route service implementation might + * offer support for discovering nearby wireless display devices and streaming + * video contents to them; another media route service implementation might + * offer support for discovering nearby speakers and streaming media appliances + * and sending commands to play content on request. + * </p><p> + * Subclasses must override the {@link #onCreateClientSession} method to return + * a {@link ClientSession} object that implements the {@link ClientSession#onStartDiscovery}, + * {@link ClientSession#onStopDiscovery}, and {@link ClientSession#onConnect} methods + * to allow clients to discover and connect to media devices. + * </p><p> + * This object is not thread-safe. All callbacks are invoked on the main looper. + * </p> + * + * <h3>Clients</h3> + * <p> + * The clients of this API are media applications that would like to discover + * and connect to media devices. The client may also be the system, such as + * when the user initiates display mirroring via the Cast Screen function. + * </p><p> + * There may be multiple client sessions active at the same time. Each client + * session can request discovery and connect to routes independently of any + * other client. It is the responsibility of the media route service to maintain + * separate state for each client session and to ensure that clients cannot interfere + * with one another in harmful ways. + * </p><p> + * Notwithstanding the requirement to support any number of concurrent client + * sessions, the media route service may impose constraints on how many clients + * can connect to the same media device in a particular mode at the same time. + * In some cases, media devices may support connections from an arbitrary number + * of clients simultaneously but often it may be necessary to ensure that only + * one client is in control. When this happens, the media route service should + * report a connection error unless the connection request specifies that the + * client should take control of the media device (and forcibly disconnect other + * clients that may be using it). + * </p> + * + * <h3>Destinations</h3> + * <p> + * The media devices to which an application may send media content are referred + * to in the API as destinations. Each destination therefore represents a single + * independent device such as a speaker or TV set. Destinations are given meaningful + * names and descriptions to help the user associate them with devices in their + * environment. + * </p><p> + * Destinations may be local or remote and may be accessed through various means, + * often wirelessly. The user may install media route services to enable + * media applications to connect to a variety of destinations with different + * capabilities. + * </p> + * + * <h3>Routes</h3> + * <p> + * Routes represent possible usages or means of reaching and interacting with + * a destination. Since destinations may support many different features, they may + * each offer multiple routes for applications to choose from based on their needs. + * For example, one route might express the ability to stream locally rendered audio + * and video to the device; another route might express the ability to send a URL for + * the destination to download from the network and play all by itself. + * </p><p> + * Routes are discovered according to the set of capabilities that + * an application or the system is seeking to use at a particular time. For example, + * if an application wants to stream music to a destination then it will ask the + * {@link MediaRouter} to find routes to destinations can stream music and ignore + * all other destinations that cannot. + * </p><p> + * In general, the application will inspect the set of routes that have been + * offered then connect to the most appropriate route for its desired purpose. + * </p> + * + * <h3>Discovery</h3> + * <p> + * Discovery is the process of finding destinations based on a description of the + * kinds of routes that an application or the system would like to use. + * </p><p> + * Discovery begins when {@link ClientSession#onStartDiscovery} is called and ends when + * {@link ClientSession#onStopDiscovery} is called. There may be multiple simultaneous + * discovery requests in progress at the same time from different clients. It is up to + * the media route service to perform these requests in parallel or multiplex them + * as required. + * </p><p> + * Media route services are <em>strongly encouraged</em> to use the information + * in the discovery request to optimize discovery and avoid redundant work. + * In the case where no media device supported by the media route service + * could possibly offer the requested capabilities, the + * {@link ClientSession#onStartDiscovery} method should return <code>false</code> to + * let the system know that it can unbind from the media route service and + * release its resources. + * </p> + * + * <h3>Settings</h3> + * <p> + * Many kinds of devices can be discovered on demand simply by scanning the local network + * or using wireless protocols such as Bluetooth to find them. However, in some cases + * it may be necessary for the user to manually configure destinations before they + * can be used (or to adjust settings later). Actual user configuration of destinations + * is beyond the scope of this API but media route services may specify an activity + * in their manifest that the user can launch to perform these tasks. + * </p><p> + * Note that media route services that are installed from the store must be enabled + * by the user before they become available for applications to use. + * The {@link android.provider.Settings#ACTION_CAST_SETTINGS Settings.ACTION_CAST_SETTINGS} + * settings activity provides the ability for the user to configure media route services. + * </p> + * + * <h3>Manifest Declaration</h3> + * <p> + * Media route services must be declared in the manifest along with meta-data + * about the kinds of routes that they are capable of discovering. The system + * uses this information to optimize the set of services to which it binds in + * order to satisfy a particular discovery request. + * </p><p> + * To extend this class, you must declare the service in your manifest file with + * the {@link android.Manifest.permission#BIND_MEDIA_ROUTE_SERVICE} permission + * and include an intent filter with the {@link #SERVICE_INTERFACE} action. You must + * also add meta-data to describe the kinds of routes that your service is capable + * of discovering. + * </p><p> + * For example: + * </p><pre> + * <service android:name=".MediaRouteProvider" + * android:label="@string/service_name" + * android:permission="android.permission.BIND_MEDIA_ROUTE_SERVICE"> + * <intent-filter> + * <action android:name="android.media.routing.MediaRouteService" /> + * </intent-filter> + * + * TODO: INSERT METADATA DECLARATIONS HERE + * + * </service> + * </pre> + */ +public abstract class MediaRouteService extends Service { + private static final String TAG = "MediaRouteService"; + + private static final boolean DEBUG = true; + + private final Handler mHandler; + private final BinderService mService; + private final ArrayMap<IBinder, ClientRecord> mClientRecords = + new ArrayMap<IBinder, ClientRecord>(); + + private ServiceMetadata mMetadata; + + /** + * The {@link Intent} that must be declared as handled by the service. + */ + @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) + public static final String SERVICE_INTERFACE = + "android.media.routing.MediaRouteService"; + + /** + * Creates a media route service. + */ + public MediaRouteService() { + mHandler = new Handler(true); + mService = new BinderService(); + } + + @Override + public @Nullable IBinder onBind(Intent intent) { + if (SERVICE_INTERFACE.equals(intent.getAction())) { + return mService; + } + return null; + } + + /** + * Creates a new client session on behalf of a client. + * <p> + * The implementation should return a {@link ClientSession} for the client + * to use. The media route service must take care to manage the state of + * each client session independently from any others that might also be + * in use at the same time. + * </p> + * + * @param client Information about the client. + * @return The client session object, or null if the client is not allowed + * to interact with this media route service. + */ + public abstract @Nullable ClientSession onCreateClientSession(@NonNull ClientInfo client); + + /** + * Gets metadata about this service. + * <p> + * Use this method to obtain a {@link ServiceMetadata} object to provide when creating + * a {@link android.media.routing.MediaRouter.DestinationInfo.Builder}. + * </p> + * + * @return Metadata about this service. + */ + public @NonNull ServiceMetadata getServiceMetadata() { + if (mMetadata == null) { + try { + mMetadata = new ServiceMetadata(this); + } catch (NameNotFoundException ex) { + Log.wtf(TAG, "Could not retrieve own service metadata!"); + } + } + return mMetadata; + } + + /** + * Enables a single client to access the functionality of the media route service. + */ + public static abstract class ClientSession { + /** + * Starts discovery. + * <p> + * If the media route service is capable of discovering routes that satisfy + * the request then this method should start discovery and return true. + * Otherwise, this method should return false. If false is returned, + * then the framework will not call {@link #onStopDiscovery} since discovery + * was never actually started. + * </p><p> + * There may already be other discovery requests in progress at the same time + * for other clients; the media route service must keep track of them all. + * </p> + * + * @param req The discovery request to start. + * @param callback A callback to receive discovery events related to this + * particular request. The events that the service sends to this callback + * will be sent to the client that initiated the discovery request. + * @return True if discovery has started. False if the media route service + * is unable to discover routes that satisfy the request. + */ + public abstract boolean onStartDiscovery(@NonNull DiscoveryRequest req, + @NonNull DiscoveryCallback callback); + + /** + * Stops discovery. + * <p> + * If {@link #onStartDiscovery} returned true, then this method will eventually + * be called when the framework no longer requires this discovery request + * to be performed. + * </p><p> + * There may still be other discovery requests in progress for other clients; + * they must keep working until they have each been stopped by their client. + * </p> + */ + public abstract void onStopDiscovery(); + + /** + * Starts connecting to a route. + * + * @param req The connection request. + * @param callback A callback to receive events connection events related + * to this particular request. The events that the service sends to this callback + * will be sent to the client that initiated the discovery request. + * @return True if the connection is in progress, or false if the client + * unable to connect to the requested route. + */ + public abstract boolean onConnect(@NonNull ConnectionRequest req, + @NonNull ConnectionCallback callback); + + /** + * Called when the client requests to disconnect from the route + * or abort a connection attempt in progress. + */ + public abstract void onDisconnect(); + + /** + * Called when the client requests to pause streaming of content to + * live audio/video routes such as when it goes into the background. + * <p> + * The default implementation does nothing. + * </p> + */ + public void onPauseStream() { } + + /** + * Called when the application requests to resume streaming of content to + * live audio/video routes such as when it returns to the foreground. + * <p> + * The default implementation does nothing. + * </p> + */ + public void onResumeStream() { } + + /** + * Called when the client is releasing the session. + * <p> + * The framework automatically takes care of stopping discovery and + * terminating the connection politely before calling this method to release + * the session. + * </p><p> + * The default implementation does nothing. + * </p> + */ + public void onRelease() { } + } + + /** + * Provides events in response to a discovery request. + */ + public final class DiscoveryCallback { + private final ClientRecord mRecord; + + DiscoveryCallback(ClientRecord record) { + mRecord = record; + } + + /** + * Called by the service when a destination is found that + * offers one or more routes that satisfy the discovery request. + * <p> + * This method should be called whenever the list of available routes + * at a destination changes or whenever the properties of the destination + * itself change. + * </p> + * + * @param destination The destination that was found. + * @param routes The list of that destination's routes that satisfy the + * discovery request. + */ + public void onDestinationFound(final @NonNull DestinationInfo destination, + final @NonNull List<RouteInfo> routes) { + if (destination == null) { + throw new IllegalArgumentException("destination must not be null"); + } + if (routes == null) { + throw new IllegalArgumentException("routes must not be null"); + } + for (int i = 0; i < routes.size(); i++) { + if (routes.get(i).getDestination() != destination) { + throw new IllegalArgumentException("routes must refer to the " + + "destination"); + } + } + + mHandler.post(new Runnable() { + @Override + public void run() { + mRecord.dispatchDestinationFound(DiscoveryCallback.this, + destination, routes); + } + }); + } + + /** + * Called by the service when a destination is no longer + * reachable or is no longer offering any routes that satisfy + * the discovery request. + * + * @param destination The destination that went away. + */ + public void onDestinationLost(final @NonNull DestinationInfo destination) { + if (destination == null) { + throw new IllegalArgumentException("destination must not be null"); + } + + mHandler.post(new Runnable() { + @Override + public void run() { + mRecord.dispatchDestinationLost(DiscoveryCallback.this, destination); + } + }); + } + + /** + * Called by the service when a discovery has failed in a non-recoverable manner. + * + * @param error The error code: one of + * {@link MediaRouter#DISCOVERY_ERROR_UNKNOWN}, + * {@link MediaRouter#DISCOVERY_ERROR_ABORTED}, + * or {@link MediaRouter#DISCOVERY_ERROR_NO_CONNECTIVITY}. + * @param message The localized error message, or null if none. This message + * may be shown to the user. + * @param extras Additional information about the error which a client + * may use, or null if none. + */ + public void onDiscoveryFailed(final @DiscoveryError int error, + final @Nullable CharSequence message, final @Nullable Bundle extras) { + mHandler.post(new Runnable() { + @Override + public void run() { + mRecord.dispatchDiscoveryFailed(DiscoveryCallback.this, + error, message, extras); + } + }); + } + } + + /** + * Provides events in response to a connection request. + */ + public final class ConnectionCallback { + private final ClientRecord mRecord; + + ConnectionCallback(ClientRecord record) { + mRecord = record; + } + + /** + * Called by the service when the connection succeeds. + * + * @param connection Immutable information about the connection. + */ + public void onConnected(final @NonNull ConnectionInfo connection) { + if (connection == null) { + throw new IllegalArgumentException("connection must not be null"); + } + + mHandler.post(new Runnable() { + @Override + public void run() { + mRecord.dispatchConnected(ConnectionCallback.this, connection); + } + }); + } + + /** + * Called by the service when the connection is terminated normally. + * <p> + * Abnormal termination is reported via {@link #onConnectionFailed}. + * </p> + */ + public void onDisconnected() { + mHandler.post(new Runnable() { + @Override + public void run() { + mRecord.dispatchDisconnected(ConnectionCallback.this); + } + }); + } + + /** + * Called by the service when a connection attempt or connection in + * progress has failed in a non-recoverable manner. + * + * @param error The error code: one of + * {@link MediaRouter#CONNECTION_ERROR_ABORTED}, + * {@link MediaRouter#CONNECTION_ERROR_UNAUTHORIZED}, + * {@link MediaRouter#CONNECTION_ERROR_UNREACHABLE}, + * {@link MediaRouter#CONNECTION_ERROR_BUSY}, + * {@link MediaRouter#CONNECTION_ERROR_TIMEOUT}, + * {@link MediaRouter#CONNECTION_ERROR_BROKEN}, + * or {@link MediaRouter#CONNECTION_ERROR_BARGED}. + * @param message The localized error message, or null if none. This message + * may be shown to the user. + * @param extras Additional information about the error which a client + * may use, or null if none. + */ + public void onConnectionFailed(final @ConnectionError int error, + final @Nullable CharSequence message, final @Nullable Bundle extras) { + mHandler.post(new Runnable() { + @Override + public void run() { + mRecord.dispatchConnectionFailed(ConnectionCallback.this, + error, message, extras); + } + }); + } + } + + /** + * Identifies a client of the media route service. + */ + public static final class ClientInfo { + private final int mUid; + private final String mPackageName; + + ClientInfo(int uid, String packageName) { + mUid = uid; + mPackageName = packageName; + } + + /** + * Gets the UID of the client application. + */ + public int getUid() { + return mUid; + } + + /** + * Gets the package name of the client application. + */ + public @NonNull String getPackageName() { + return mPackageName; + } + + @Override + public @NonNull String toString() { + return "ClientInfo{ uid=" + mUid + ", package=" + mPackageName + " }"; + } + } + + private final class BinderService extends IMediaRouteService.Stub { + @Override + public void registerClient(final int clientUid, final String clientPackageName, + final IMediaRouteClientCallback callback) { + mHandler.post(new Runnable() { + @Override + public void run() { + ClientInfo client = new ClientInfo(clientUid, clientPackageName); + if (DEBUG) { + Log.d(TAG, "registerClient: client=" + client); + } + + ClientSession session = onCreateClientSession(client); + if (session == null) { + // request refused by service + Log.w(TAG, "Media route service refused to create session for client: " + + "client=" + client); + return; + } + + ClientRecord record = new ClientRecord(callback, client, session); + try { + callback.asBinder().linkToDeath(record, 0); + } catch (RemoteException ex) { + // client died prematurely + Log.w(TAG, "Client died prematurely while creating session: " + + "client=" + client); + record.release(); + return; + } + + mClientRecords.put(callback.asBinder(), record); + } + }); + } + + @Override + public void unregisterClient(IMediaRouteClientCallback callback) { + unregisterClient(callback, false); + } + + void unregisterClient(final IMediaRouteClientCallback callback, + final boolean died) { + mHandler.post(new Runnable() { + @Override + public void run() { + ClientRecord record = mClientRecords.remove(callback.asBinder()); + if (record == null) { + return; // spurious + } + + if (DEBUG) { + Log.d(TAG, "unregisterClient: client=" + record.getClientInfo() + + ", died=" + died); + } + + record.release(); + callback.asBinder().unlinkToDeath(record, 0); + } + }); + } + + @Override + public void startDiscovery(final IMediaRouteClientCallback callback, + final int seq, final List<MediaRouteSelector> selectors, + final int flags) { + mHandler.post(new Runnable() { + @Override + public void run() { + ClientRecord record = mClientRecords.get(callback.asBinder()); + if (record == null) { + return; // spurious + } + + if (DEBUG) { + Log.d(TAG, "startDiscovery: client=" + record.getClientInfo() + + ", seq=" + seq + ", selectors=" + selectors + + ", flags=0x" + Integer.toHexString(flags)); + } + record.startDiscovery(seq, selectors, flags); + } + }); + } + + @Override + public void stopDiscovery(final IMediaRouteClientCallback callback) { + mHandler.post(new Runnable() { + @Override + public void run() { + ClientRecord record = mClientRecords.get(callback.asBinder()); + if (record == null) { + return; // spurious + } + + if (DEBUG) { + Log.d(TAG, "stopDiscovery: client=" + record.getClientInfo()); + } + record.stopDiscovery(); + } + }); + } + + @Override + public void connect(final IMediaRouteClientCallback callback, + final int seq, final String destinationId, final String routeId, + final int flags, final Bundle extras) { + mHandler.post(new Runnable() { + @Override + public void run() { + ClientRecord record = mClientRecords.get(callback.asBinder()); + if (record == null) { + return; // spurious + } + + if (DEBUG) { + Log.d(TAG, "connect: client=" + record.getClientInfo() + + ", seq=" + seq + ", destinationId=" + destinationId + + ", routeId=" + routeId + + ", flags=0x" + Integer.toHexString(flags) + + ", extras=" + extras); + } + record.connect(seq, destinationId, routeId, flags, extras); + } + }); + } + + @Override + public void disconnect(final IMediaRouteClientCallback callback) { + mHandler.post(new Runnable() { + @Override + public void run() { + ClientRecord record = mClientRecords.get(callback.asBinder()); + if (record == null) { + return; // spurious + } + + if (DEBUG) { + Log.d(TAG, "disconnect: client=" + record.getClientInfo()); + } + record.disconnect(); + } + }); + } + + @Override + public void pauseStream(final IMediaRouteClientCallback callback) { + mHandler.post(new Runnable() { + @Override + public void run() { + ClientRecord record = mClientRecords.get(callback.asBinder()); + if (record == null) { + return; // spurious + } + + if (DEBUG) { + Log.d(TAG, "pauseStream: client=" + record.getClientInfo()); + } + record.pauseStream(); + } + }); + } + + @Override + public void resumeStream(final IMediaRouteClientCallback callback) { + mHandler.post(new Runnable() { + @Override + public void run() { + ClientRecord record = mClientRecords.get(callback.asBinder()); + if (record == null) { + return; // spurious + } + + if (DEBUG) { + Log.d(TAG, "resumeStream: client=" + record.getClientInfo()); + } + record.resumeStream(); + } + }); + } + } + + // Must be accessed on handler + private final class ClientRecord implements IBinder.DeathRecipient { + private final IMediaRouteClientCallback mClientCallback; + private final ClientInfo mClient; + private final ClientSession mSession; + + private int mDiscoverySeq; + private DiscoveryRequest mDiscoveryRequest; + private DiscoveryCallback mDiscoveryCallback; + private final ArrayMap<String, DestinationRecord> mDestinations = + new ArrayMap<String, DestinationRecord>(); + + private int mConnectionSeq; + private ConnectionRequest mConnectionRequest; + private ConnectionCallback mConnectionCallback; + private ConnectionInfo mConnection; + private boolean mConnectionPaused; + + public ClientRecord(IMediaRouteClientCallback callback, + ClientInfo client, ClientSession session) { + mClientCallback = callback; + mClient = client; + mSession = session; + } + + // Invoked on binder thread unlike all other methods in this class. + @Override + public void binderDied() { + mService.unregisterClient(mClientCallback, true); + } + + public ClientInfo getClientInfo() { + return mClient; + } + + public void release() { + stopDiscovery(); + disconnect(); + } + + public void startDiscovery(int seq, List<MediaRouteSelector> selectors, + int flags) { + stopDiscovery(); + + mDiscoverySeq = seq; + mDiscoveryRequest = new DiscoveryRequest(selectors); + mDiscoveryRequest.setFlags(flags); + mDiscoveryCallback = new DiscoveryCallback(this); + boolean started = mSession.onStartDiscovery(mDiscoveryRequest, mDiscoveryCallback); + if (!started) { + dispatchDiscoveryFailed(mDiscoveryCallback, + MediaRouter.DISCOVERY_ERROR_ABORTED, null, null); + clearDiscovery(); + } + } + + public void stopDiscovery() { + if (mDiscoveryRequest != null) { + mSession.onStopDiscovery(); + clearDiscovery(); + } + } + + private void clearDiscovery() { + mDestinations.clear(); + mDiscoveryRequest = null; + mDiscoveryCallback = null; + } + + public void connect(int seq, String destinationId, String routeId, + int flags, Bundle extras) { + disconnect(); + + mConnectionSeq = seq; + mConnectionCallback = new ConnectionCallback(this); + + DestinationRecord destinationRecord = mDestinations.get(destinationId); + if (destinationRecord == null) { + Log.w(TAG, "Aborting connection to route since no matching destination " + + "was found in the list of known destinations: " + + "destinationId=" + destinationId); + dispatchConnectionFailed(mConnectionCallback, + MediaRouter.CONNECTION_ERROR_ABORTED, null, null); + clearConnection(); + return; + } + + RouteInfo route = destinationRecord.getRoute(routeId); + if (route == null) { + Log.w(TAG, "Aborting connection to route since no matching route " + + "was found in the list of known routes: " + + "destination=" + destinationRecord.destination + + ", routeId=" + routeId); + dispatchConnectionFailed(mConnectionCallback, + MediaRouter.CONNECTION_ERROR_ABORTED, null, null); + clearConnection(); + return; + } + + mConnectionRequest = new ConnectionRequest(route); + mConnectionRequest.setFlags(flags); + mConnectionRequest.setExtras(extras); + boolean started = mSession.onConnect(mConnectionRequest, mConnectionCallback); + if (!started) { + dispatchConnectionFailed(mConnectionCallback, + MediaRouter.CONNECTION_ERROR_ABORTED, null, null); + clearConnection(); + } + } + + public void disconnect() { + if (mConnectionRequest != null) { + mSession.onDisconnect(); + clearConnection(); + } + } + + private void clearConnection() { + mConnectionRequest = null; + mConnectionCallback = null; + if (mConnection != null) { + mConnection.close(); + mConnection = null; + } + mConnectionPaused = false; + } + + public void pauseStream() { + if (mConnectionRequest != null && !mConnectionPaused) { + mConnectionPaused = true; + mSession.onPauseStream(); + } + } + + public void resumeStream() { + if (mConnectionRequest != null && mConnectionPaused) { + mConnectionPaused = false; + mSession.onResumeStream(); + } + } + + public void dispatchDestinationFound(DiscoveryCallback callback, + DestinationInfo destination, List<RouteInfo> routes) { + if (callback == mDiscoveryCallback) { + if (DEBUG) { + Log.d(TAG, "destinationFound: destination=" + destination + + ", routes=" + routes); + } + mDestinations.put(destination.getId(), + new DestinationRecord(destination, routes)); + + ParcelableDestinationInfo pdi = new ParcelableDestinationInfo(); + pdi.id = destination.getId(); + pdi.name = destination.getName(); + pdi.description = destination.getDescription(); + pdi.iconResourceId = destination.getIconResourceId(); + pdi.extras = destination.getExtras(); + ArrayList<ParcelableRouteInfo> pris = new ArrayList<ParcelableRouteInfo>(); + for (RouteInfo route : routes) { + int selectorIndex = mDiscoveryRequest.getSelectors().indexOf( + route.getSelector()); + if (selectorIndex < 0) { + Log.w(TAG, "Ignoring route because the selector does not match " + + "any of those that were originally supplied by the " + + "client's discovery request: destination=" + destination + + ", route=" + route); + continue; + } + + ParcelableRouteInfo pri = new ParcelableRouteInfo(); + pri.id = route.getId(); + pri.selectorIndex = selectorIndex; + pri.features = route.getFeatures(); + pri.protocols = route.getProtocols().toArray( + new String[route.getProtocols().size()]); + pri.extras = route.getExtras(); + pris.add(pri); + } + try { + mClientCallback.onDestinationFound(mDiscoverySeq, pdi, + pris.toArray(new ParcelableRouteInfo[pris.size()])); + } catch (RemoteException ex) { + // binder death handled elsewhere + } + } + } + + public void dispatchDestinationLost(DiscoveryCallback callback, + DestinationInfo destination) { + if (callback == mDiscoveryCallback) { + if (DEBUG) { + Log.d(TAG, "destinationLost: destination=" + destination); + } + + if (mDestinations.get(destination.getId()).destination == destination) { + mDestinations.remove(destination.getId()); + try { + mClientCallback.onDestinationLost(mDiscoverySeq, destination.getId()); + } catch (RemoteException ex) { + // binder death handled elsewhere + } + } + } + } + + public void dispatchDiscoveryFailed(DiscoveryCallback callback, + int error, CharSequence message, Bundle extras) { + if (callback == mDiscoveryCallback) { + if (DEBUG) { + Log.d(TAG, "discoveryFailed: error=" + error + ", message=" + message + + ", extras=" + extras); + } + + try { + mClientCallback.onDiscoveryFailed(mDiscoverySeq, error, message, extras); + } catch (RemoteException ex) { + // binder death handled elsewhere + } + } + } + + public void dispatchConnected(ConnectionCallback callback, ConnectionInfo connection) { + if (callback == mConnectionCallback) { + if (DEBUG) { + Log.d(TAG, "connected: connection=" + connection); + } + if (mConnection == null) { + mConnection = connection; + + ParcelableConnectionInfo pci = new ParcelableConnectionInfo(); + pci.audioAttributes = connection.getAudioAttributes(); + pci.presentationDisplayId = connection.getPresentationDisplay() != null ? + connection.getPresentationDisplay().getDisplayId() : -1; + pci.protocolBinders = new IBinder[connection.getProtocols().size()]; + for (int i = 0; i < pci.protocolBinders.length; i++) { + pci.protocolBinders[i] = connection.getProtocolBinder(i); + } + pci.extras = connection.getExtras(); + try { + mClientCallback.onConnected(mConnectionSeq, pci); + } catch (RemoteException ex) { + // binder death handled elsewhere + } + } else { + Log.w(TAG, "Media route service called onConnected() while already " + + "connected."); + } + } + } + + public void dispatchDisconnected(ConnectionCallback callback) { + if (callback == mConnectionCallback) { + if (DEBUG) { + Log.d(TAG, "disconnected"); + } + + if (mConnection != null) { + mConnection.close(); + mConnection = null; + + try { + mClientCallback.onDisconnected(mConnectionSeq); + } catch (RemoteException ex) { + // binder death handled elsewhere + } + } + } + } + + public void dispatchConnectionFailed(ConnectionCallback callback, + int error, CharSequence message, Bundle extras) { + if (callback == mConnectionCallback) { + if (DEBUG) { + Log.d(TAG, "connectionFailed: error=" + error + ", message=" + message + + ", extras=" + extras); + } + + try { + mClientCallback.onConnectionFailed(mConnectionSeq, error, message, extras); + } catch (RemoteException ex) { + // binder death handled elsewhere + } + } + } + } + + private static final class DestinationRecord { + public final DestinationInfo destination; + public final List<RouteInfo> routes; + + public DestinationRecord(DestinationInfo destination, List<RouteInfo> routes) { + this.destination = destination; + this.routes = routes; + } + + public RouteInfo getRoute(String routeId) { + final int count = routes.size(); + for (int i = 0; i < count; i++) { + RouteInfo route = routes.get(i); + if (route.getId().equals(routeId)) { + return route; + } + } + return null; + } + } +} diff --git a/media/java/android/media/routing/MediaRouter.java b/media/java/android/media/routing/MediaRouter.java new file mode 100644 index 0000000..4f6d324 --- /dev/null +++ b/media/java/android/media/routing/MediaRouter.java @@ -0,0 +1,1886 @@ +/* + * 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.routing; + +import android.annotation.DrawableRes; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.Presentation; +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ServiceInfo; +import android.graphics.drawable.Drawable; +import android.hardware.display.DisplayManager; +import android.media.AudioAttributes; +import android.media.AudioManager; +import android.media.AudioTrack; +import android.media.VolumeProvider; +import android.media.session.MediaController; +import android.media.session.MediaSession; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.IInterface; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.view.Display; + +import java.io.Closeable; +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; + +/** + * Media router allows applications to discover, connect to, control, + * and send content to nearby media devices known as destinations. + * <p> + * There are generally two participants involved in media routing: an + * application that wants to send media content to a destination and a + * {@link MediaRouteService media route service} that provides the + * service of transporting that content where it needs to go on behalf of the + * application. + * </p><p> + * To send media content to a destination, the application must ask the system + * to discover available routes to destinations that provide certain capabilities, + * establish a connection to a route, then send messages through the connection to + * control the routing of audio and video streams, launch remote applications, + * and invoke other functions of the destination. + * </p><p> + * Media router objects are thread-safe. + * </p> + * + * <h3>Destinations</h3> + * <p> + * The media devices to which an application may send media content are referred + * to in the API as destinations. Each destination therefore represents a single + * independent device such as a speaker or TV set. Destinations are given meaningful + * names and descriptions to help the user associate them with devices in their + * environment. + * </p><p> + * Destinations may be local or remote and may be accessed through various means, + * often wirelessly. The user may install media route services to enable + * media applications to connect to a variety of destinations with different + * capabilities. + * </p> + * + * <h3>Routes</h3> + * <p> + * Routes represent possible usages or means of reaching and interacting with + * a destination. Since destinations may support many different features, they may + * each offer multiple routes for applications to choose from based on their needs. + * For example, one route might express the ability to stream locally rendered audio + * and video to the device; another route might express the ability to send a URL for + * the destination to download from the network and play all by itself. + * </p><p> + * Routes are discovered according to the set of capabilities that + * an application or the system is seeking to use at a particular time. For example, + * if an application wants to stream music to a destination then it will ask the + * {@link MediaRouter} to find routes to destinations can stream music and ignore + * all other destinations that cannot. + * </p><p> + * In general, the application will inspect the set of routes that have been + * offered then connect to the most appropriate route for its desired purpose. + * </p> + * + * <h3>Route Selection</h3> + * <p> + * When the user open the media route chooser activity, the system will display + * a list of nearby media destinations which have been discovered. After the + * choice is made the application may connect to one of the routes offered by + * this destination and begin communicating with the destination. + * </p><p> + * Destinations are located through a process called discovery. During discovery, + * the system will start installed {@link MediaRouteService media route services} + * to scan the network for nearby devices that offer the kinds of capabilities that the + * application is seeking to use. The application specifies the capabilities it requires by + * adding {@link MediaRouteSelector media route selectors} to the media router + * using the {@link #addSelector} method. Only destinations that provide routes + * which satisfy at least one of these media route selectors will be discovered. + * </p><p> + * Once the user has selected a destination, the application will be given a chance + * to choose one of the routes to which it would like to connect. The application + * may switch to a different route from the same destination at a later time but + * in order to connect to a new destination, the application must once again launch + * the media route chooser activity to ask the user to choose a destination. + * </p> + * + * <h3>Route Protocols</h3> + * <p> + * Route protocols express capabilities offered by routes. Each media route selector + * must specify at least one required protocol by which the routes will be selected. + * </p><p> + * The framework provides several predefined <code>MediaRouteProtocols</code> which are + * defined in the <code>android-support-media-protocols.jar</code> support library. + * Applications must statically link this library to make use of these protocols. + * </p><p> + * The static library approach is used to enable ongoing extension and refinement + * of protocols in the SDK and interoperability with the media router implementation + * for older platform versions which is offered by the framework support library. + * </p><p> + * Media route services may also define custom media route protocols of their own + * to enable applications to access specialized capabilities of certain destinations + * assuming they have linked in the required protocol code. + * </p><p> + * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code> for more information. + * </p> + * + * <h3>Connections</h3> + * <p> + * After connecting to a media route, the application can send commands to + * the route using any of the protocols that it requested. If the route supports live + * audio or video streaming then the application can create an {@link AudioTrack} or + * {@link Presentation} to route locally generated content to the destination. + * </p> + * + * <h3>Delegation</h3> + * <p> + * The creator of the media router is responsible for establishing the policy for + * discovering and connecting to destinations. UI components may observe the state + * of the media router by {@link #createDelegate creating} a {@link Delegate}. + * </p><p> + * The media router should also be attached to the {@link MediaSession media session} + * that is handling media playback lifecycle. This will allow + * authorized {@link MediaController media controllers}, possibly running in other + * processes, to provide UI to examine and change the media destination by + * {@link MediaController#createMediaRouterDelegate creating} a {@link Delegate} + * for the media router associated with the session. + * </p> + */ +public final class MediaRouter { + private final DisplayManager mDisplayManager; + + private final Object mLock = new Object(); + + private RoutingCallback mRoutingCallback; + private Handler mRoutingCallbackHandler; + + private boolean mReleased; + private int mDiscoveryState; + private int mConnectionState; + private final ArrayList<MediaRouteSelector> mSelectors = + new ArrayList<MediaRouteSelector>(); + private final ArrayMap<DestinationInfo, List<RouteInfo>> mDiscoveredDestinations = + new ArrayMap<DestinationInfo, List<RouteInfo>>(); + private RouteInfo mSelectedRoute; + private ConnectionInfo mConnection; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { DISCOVERY_STATE_STOPPED, DISCOVERY_STATE_STARTED }) + public @interface DiscoveryState { } + + /** + * Discovery state: Discovery is not currently in progress. + */ + public static final int DISCOVERY_STATE_STOPPED = 0; + + /** + * Discovery state: Discovery is being performed. + */ + public static final int DISCOVERY_STATE_STARTED = 1; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, value = { DISCOVERY_FLAG_BACKGROUND }) + public @interface DiscoveryFlags { } + + /** + * Discovery flag: Indicates that the client has requested passive discovery in + * the background. The media route service should try to use less power and rely + * more on its internal caches to minimize its impact. + */ + public static final int DISCOVERY_FLAG_BACKGROUND = 1 << 0; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { DISCOVERY_ERROR_UNKNOWN, DISCOVERY_ERROR_ABORTED, + DISCOVERY_ERROR_NO_CONNECTIVITY }) + public @interface DiscoveryError { } + + /** + * Discovery error: Unknown error; refer to the error message for details. + */ + public static final int DISCOVERY_ERROR_UNKNOWN = 0; + + /** + * Discovery error: The media router or media route service has decided not to + * handle the discovery request for some reason. + */ + public static final int DISCOVERY_ERROR_ABORTED = 1; + + /** + * Discovery error: The media route service is unable to perform discovery + * due to a lack of connectivity such as because the radio is disabled. + */ + public static final int DISCOVERY_ERROR_NO_CONNECTIVITY = 2; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { CONNECTION_STATE_DISCONNECTED, CONNECTION_STATE_CONNECTING, + CONNECTION_STATE_CONNECTED }) + public @interface ConnectionState { } + + /** + * Connection state: No destination has been selected. Media content should + * be sent to the default output. + */ + public static final int CONNECTION_STATE_DISCONNECTED = 0; + + /** + * Connection state: The application is in the process of connecting to + * a route offered by the selected destination. + */ + public static final int CONNECTION_STATE_CONNECTING = 1; + + /** + * Connection state: The application has connected to a route offered by + * the selected destination. + */ + public static final int CONNECTION_STATE_CONNECTED = 2; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, value = { CONNECTION_FLAG_BARGE }) + public @interface ConnectionFlags { } + + /** + * Connection flag: Indicates that the client has requested to barge in and evict + * other clients that might have already connected to the destination and that + * would otherwise prevent this client from connecting. When this flag is not + * set, the media route service should be polite and report + * {@link MediaRouter#CONNECTION_ERROR_BUSY} in case the destination is + * already occupied and cannot accept additional connections. + */ + public static final int CONNECTION_FLAG_BARGE = 1 << 0; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { CONNECTION_ERROR_UNKNOWN, CONNECTION_ERROR_ABORTED, + CONNECTION_ERROR_UNAUTHORIZED, CONNECTION_ERROR_UNAUTHORIZED, + CONNECTION_ERROR_BUSY, CONNECTION_ERROR_TIMEOUT, CONNECTION_ERROR_BROKEN }) + public @interface ConnectionError { } + + /** + * Connection error: Unknown error; refer to the error message for details. + */ + public static final int CONNECTION_ERROR_UNKNOWN = 0; + + /** + * Connection error: The media router or media route service has decided not to + * handle the connection request for some reason. + */ + public static final int CONNECTION_ERROR_ABORTED = 1; + + /** + * Connection error: The device has refused the connection from this client. + * This error should be avoided because the media route service should attempt + * to filter out devices that the client cannot access as it performs discovery + * on behalf of that client. + */ + public static final int CONNECTION_ERROR_UNAUTHORIZED = 2; + + /** + * Connection error: The device is unreachable over the network. + */ + public static final int CONNECTION_ERROR_UNREACHABLE = 3; + + /** + * Connection error: The device is already busy serving another client and + * the connection request did not ask to barge in. + */ + public static final int CONNECTION_ERROR_BUSY = 4; + + /** + * Connection error: A timeout occurred during connection. + */ + public static final int CONNECTION_ERROR_TIMEOUT = 5; + + /** + * Connection error: The connection to the device was severed unexpectedly. + */ + public static final int CONNECTION_ERROR_BROKEN = 6; + + /** + * Connection error: The connection was terminated because a different client barged + * in and took control of the destination. + */ + public static final int CONNECTION_ERROR_BARGED = 7; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { DISCONNECTION_REASON_APPLICATION_REQUEST, + DISCONNECTION_REASON_USER_REQUEST, DISCONNECTION_REASON_ERROR }) + public @interface DisconnectionReason { } + + /** + * Disconnection reason: The application requested disconnection itself. + */ + public static final int DISCONNECTION_REASON_APPLICATION_REQUEST = 0; + + /** + * Disconnection reason: The user requested disconnection. + */ + public static final int DISCONNECTION_REASON_USER_REQUEST = 1; + + /** + * Disconnection reason: An error occurred. + */ + public static final int DISCONNECTION_REASON_ERROR = 2; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, value = { ROUTE_FEATURE_LIVE_AUDIO, ROUTE_FEATURE_LIVE_VIDEO }) + public @interface RouteFeatures { } + + /** + * Route feature: Live audio. + * <p> + * A route that supports live audio streams audio rendered by the application + * to the destination. + * </p><p> + * To take advantage of live audio routing, the application must render its + * media using the audio attributes specified by {@link #getPreferredAudioAttributes}. + * </p> + * + * @see #getPreferredAudioAttributes + * @see android.media.AudioAttributes + */ + public static final int ROUTE_FEATURE_LIVE_AUDIO = 1 << 0; + + /** + * Route feature: Live video. + * <p> + * A route that supports live video streams video rendered by the application + * to the destination. + * </p><p> + * To take advantage of live video routing, the application must render its + * media to a {@link android.app.Presentation presentation window} on the + * display specified by {@link #getPreferredPresentationDisplay}. + * </p> + * + * @see #getPreferredPresentationDisplay + * @see android.app.Presentation + */ + public static final int ROUTE_FEATURE_LIVE_VIDEO = 1 << 1; + + /** + * Creates a media router. + * + * @param context The context with which the router is associated. + */ + public MediaRouter(@NonNull Context context) { + if (context == null) { + throw new IllegalArgumentException("context must not be null"); + } + + mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE); + } + + /** @hide */ + public IMediaRouter getBinder() { + // todo + return null; + } + + /** + * Disconnects from the selected destination and releases the media router. + * <p> + * This method should be called by the application when it no longer requires + * the media router to ensure that all bound resources may be cleaned up. + * </p> + */ + public void release() { + synchronized (mLock) { + mReleased = true; + // todo + } + } + + /** + * Returns true if the media router has been released. + */ + public boolean isReleased() { + synchronized (mLock) { + return mReleased; + } + } + + /** + * Gets the current route discovery state. + * + * @return The current discovery state: one of {@link #DISCOVERY_STATE_STOPPED}, + * {@link #DISCOVERY_STATE_STARTED}. + */ + public @DiscoveryState int getDiscoveryState() { + synchronized (mLock) { + return mDiscoveryState; + } + } + + /** + * Gets the current route connection state. + * + * @return The current state: one of {@link #CONNECTION_STATE_DISCONNECTED}, + * {@link #CONNECTION_STATE_CONNECTING} or {@link #CONNECTION_STATE_CONNECTED}. + */ + public @ConnectionState int getConnectionState() { + synchronized (mLock) { + return mConnectionState; + } + } + + /** + * Creates a media router delegate through which the destination of the media + * router may be controlled. + * <p> + * This is the point of entry for UI code that initiates discovery and + * connection to routes. + * </p> + */ + public @NonNull Delegate createDelegate() { + return null; // todo + } + + /** + * Sets a callback to participate in route discovery, filtering, and connection + * establishment. + * + * @param callback The callback to set, or null if none. + * @param handler The handler to receive callbacks, or null to use the current thread. + */ + public void setRoutingCallback(@Nullable RoutingCallback callback, + @Nullable Handler handler) { + synchronized (mLock) { + if (callback == null) { + mRoutingCallback = null; + mRoutingCallbackHandler = null; + } else { + mRoutingCallback = callback; + mRoutingCallbackHandler = handler != null ? handler : new Handler(); + } + } + } + + /** + * Adds a media route selector to use to find destinations that have + * routes with the specified capabilities during route discovery. + */ + public void addSelector(@NonNull MediaRouteSelector selector) { + if (selector == null) { + throw new IllegalArgumentException("selector must not be null"); + } + + synchronized (mLock) { + if (!mSelectors.contains(selector)) { + mSelectors.add(selector); + // todo + } + } + } + + /** + * Removes a media route selector. + */ + public void removeSelector(@NonNull MediaRouteSelector selector) { + if (selector == null) { + throw new IllegalArgumentException("selector must not be null"); + } + + synchronized (mLock) { + if (mSelectors.remove(selector)) { + // todo + } + } + } + + /** + * Removes all media route selectors. + * <p> + * Note that at least one selector must be added in order to perform discovery. + * </p> + */ + public void clearSelectors() { + synchronized (mLock) { + if (!mSelectors.isEmpty()) { + mSelectors.clear(); + // todo + } + } + } + + /** + * Gets a list of all media route selectors to consider during discovery. + */ + public @NonNull List<MediaRouteSelector> getSelectors() { + synchronized (mLock) { + return new ArrayList<MediaRouteSelector>(mSelectors); + } + } + + /** + * Gets the connection to the currently selected route. + * + * @return The connection to the currently selected route, or null if not connected. + */ + public @NonNull ConnectionInfo getConnection() { + synchronized (mLock) { + return mConnection; + } + } + + /** + * Gets the list of discovered destinations. + * <p> + * This list is only valid while discovery is running and is null otherwise. + * </p> + * + * @return The list of discovered destinations, or null if discovery is not running. + */ + public @NonNull List<DestinationInfo> getDiscoveredDestinations() { + synchronized (mLock) { + if (mDiscoveryState == DISCOVERY_STATE_STARTED) { + return new ArrayList<DestinationInfo>(mDiscoveredDestinations.keySet()); + } + return null; + } + } + + /** + * Gets the list of discovered routes for a particular destination. + * <p> + * This list is only valid while discovery is running and is null otherwise. + * </p> + * + * @param destination The destination for which to get the list of discovered routes. + * @return The list of discovered routes for the destination, or null if discovery + * is not running. + */ + public @NonNull List<RouteInfo> getDiscoveredRoutes(@NonNull DestinationInfo destination) { + if (destination == null) { + throw new IllegalArgumentException("destination must not be null"); + } + synchronized (mLock) { + if (mDiscoveryState == DISCOVERY_STATE_STARTED) { + List<RouteInfo> routes = mDiscoveredDestinations.get(destination); + if (routes != null) { + return new ArrayList<RouteInfo>(routes); + } + } + return null; + } + } + + /** + * Gets the destination that has been selected. + * + * @return The selected destination, or null if disconnected. + */ + public @Nullable DestinationInfo getSelectedDestination() { + synchronized (mLock) { + return mSelectedRoute != null ? mSelectedRoute.getDestination() : null; + } + } + + /** + * Gets the route that has been selected. + * + * @return The selected destination, or null if disconnected. + */ + public @Nullable RouteInfo getSelectedRoute() { + synchronized (mLock) { + return mSelectedRoute; + } + } + + /** + * Gets the preferred audio attributes that should be used to stream live audio content + * based on the connected route. + * <p> + * Use an {@link AudioTrack} to send audio content to the destination with these + * audio attributes. + * </p><p> + * The preferred audio attributes may change when a connection is established but it + * will remain constant until disconnected. + * </p> + * + * @return The preferred audio attributes to use. When connected, returns the + * route's audio attributes or null if it does not support live audio streaming. + * Otherwise returns audio attributes associated with {@link AudioAttributes#USAGE_MEDIA}. + */ + public @Nullable AudioAttributes getPreferredAudioAttributes() { + synchronized (mLock) { + if (mConnection != null) { + return mConnection.getAudioAttributes(); + } + return new AudioAttributes.Builder() + .setLegacyStreamType(AudioManager.STREAM_MUSIC) + .build(); + } + } + + /** + * Gets the preferred presentation display that should be used to stream live video content + * based on the connected route. + * <p> + * Use a {@link Presentation} to send video content to the destination with this display. + * </p><p> + * The preferred presentation display may change when a connection is established but it + * will remain constant until disconnected. + * </p> + * + * @return The preferred presentation display to use. When connected, returns + * the route's presentation display or null if it does not support live video + * streaming. Otherwise returns the first available + * {@link DisplayManager#DISPLAY_CATEGORY_PRESENTATION presentation display}, + * such as a mirrored wireless or HDMI display or null if none. + */ + public @Nullable Display getPreferredPresentationDisplay() { + synchronized (mLock) { + if (mConnection != null) { + return mConnection.getPresentationDisplay(); + } + Display[] displays = mDisplayManager.getDisplays( + DisplayManager.DISPLAY_CATEGORY_PRESENTATION); + return displays.length != 0 ? displays[0] : null; + } + } + + /** + * Gets the preferred volume provider that should be used to control the volume + * of content rendered on the currently selected route. + * <p> + * The preferred volume provider may change when a connection is established but it + * will remain the same until disconnected. + * </p> + * + * @return The preferred volume provider to use, or null if the currently + * selected route does not support remote volume adjustment or if the connection + * is not yet established. If no route is selected, returns null to indicate + * that system volume control should be used. + */ + public @Nullable VolumeProvider getPreferredVolumeProvider() { + synchronized (mLock) { + if (mConnection != null) { + return mConnection.getVolumeProvider(); + } + return null; + } + } + + /** + * Requests to pause streaming of live audio or video routes. + * Should be called when the application is going into the background and is + * no longer rendering content locally. + * <p> + * This method does nothing unless a connection has been established. + * </p> + */ + public void pauseStream() { + // todo + } + + /** + * Requests to resume streaming of live audio or video routes. + * May be called when the application is returning to the foreground and is + * about to resume rendering content locally. + * <p> + * This method does nothing unless a connection has been established. + * </p> + */ + public void resumeStream() { + // todo + } + + /** + * This class is used by UI components to let the user discover and + * select a destination to which the media router should connect. + * <p> + * This API has somewhat more limited functionality than the {@link MediaRouter} + * itself because it is designed to allow applications to control + * the destination of media router instances that belong to other processes. + * </p><p> + * To control the destination of your own media router, call + * {@link #createDelegate} to obtain a local delegate object. + * </p><p> + * To control the destination of a media router that belongs to another process, + * first obtain a {@link MediaController} that is associated with the media playback + * that is occurring in that process, then call + * {@link MediaController#createMediaRouterDelegate} to obtain an instance of + * its destination controls. Note that special permissions may be required to + * obtain the {@link MediaController} instance in the first place. + * </p> + */ + public static final class Delegate { + /** + * Returns true if the media router has been released. + */ + public boolean isReleased() { + // todo + return false; + } + + /** + * Gets the current route discovery state. + * + * @return The current discovery state: one of {@link #DISCOVERY_STATE_STOPPED}, + * {@link #DISCOVERY_STATE_STARTED}. + */ + public @DiscoveryState int getDiscoveryState() { + // todo + return -1; + } + + /** + * Gets the current route connection state. + * + * @return The current state: one of {@link #CONNECTION_STATE_DISCONNECTED}, + * {@link #CONNECTION_STATE_CONNECTING} or {@link #CONNECTION_STATE_CONNECTED}. + */ + public @ConnectionState int getConnectionState() { + // todo + return -1; + } + + /** + * Gets the currently selected destination. + * + * @return The destination information, or null if none. + */ + public @Nullable DestinationInfo getSelectedDestination() { + return null; + } + + /** + * Gets the list of discovered destinations. + * <p> + * This list is only valid while discovery is running and is null otherwise. + * </p> + * + * @return The list of discovered destinations, or null if discovery is not running. + */ + public @NonNull List<DestinationInfo> getDiscoveredDestinations() { + return null; + } + + /** + * Adds a callback to receive state changes. + * + * @param callback The callback to set, or null if none. + * @param handler The handler to receive callbacks, or null to use the current thread. + */ + public void addStateCallback(@Nullable StateCallback callback, + @Nullable Handler handler) { + if (callback == null) { + throw new IllegalArgumentException("callback must not be null"); + } + if (handler == null) { + handler = new Handler(); + } + // todo + } + + /** + * Removes a callback for state changes. + * + * @param callback The callback to set, or null if none. + */ + public void removeStateCallback(@Nullable StateCallback callback) { + // todo + } + + /** + * Starts performing discovery. + * <p> + * Performing discovery is expensive. Make sure to call {@link #stopDiscovery} + * as soon as possible once a new destination has been selected to allow the system + * to stop services associated with discovery. + * </p> + * + * @param flags The discovery flags, such as {@link MediaRouter#DISCOVERY_FLAG_BACKGROUND}. + */ + public void startDiscovery(@DiscoveryFlags int flags) { + // todo + } + + /** + * Stops performing discovery. + */ + public void stopDiscovery() { + // todo + } + + /** + * Connects to a destination during route discovery. + * <p> + * This method may only be called while route discovery is active and the + * destination appears in the + * {@link #getDiscoveredDestinations list of discovered destinations}. + * If the media router is already connected to a route then it will first disconnect + * from the current route then connect to the new route. + * </p> + * + * @param destination The destination to which the media router should connect. + * @param flags The connection flags, such as {@link MediaRouter#CONNECTION_FLAG_BARGE}. + */ + public void connect(@NonNull DestinationInfo destination, @DiscoveryFlags int flags) { + // todo + } + + /** + * Disconnects from the currently selected destination. + * <p> + * Does nothing if not currently connected. + * </p> + * + * @param reason The reason for the disconnection: one of + * {@link #DISCONNECTION_REASON_APPLICATION_REQUEST}, + * {@link #DISCONNECTION_REASON_USER_REQUEST}, or {@link #DISCONNECTION_REASON_ERROR}. + */ + public void disconnect(@DisconnectionReason int reason) { + // todo + } + } + + /** + * Describes immutable properties of a connection to a route. + */ + public static final class ConnectionInfo { + private final RouteInfo mRoute; + private final AudioAttributes mAudioAttributes; + private final Display mPresentationDisplay; + private final VolumeProvider mVolumeProvider; + private final IBinder[] mProtocolBinders; + private final Object[] mProtocolInstances; + private final Bundle mExtras; + private final ArrayList<Closeable> mCloseables; + + private static final Class<?>[] MEDIA_ROUTE_PROTOCOL_CTOR_PARAMETERS = + new Class<?>[] { IBinder.class }; + + ConnectionInfo(RouteInfo route, + AudioAttributes audioAttributes, Display display, + VolumeProvider volumeProvider, IBinder[] protocolBinders, + Bundle extras, ArrayList<Closeable> closeables) { + mRoute = route; + mAudioAttributes = audioAttributes; + mPresentationDisplay = display; + mVolumeProvider = volumeProvider; + mProtocolBinders = protocolBinders; + mProtocolInstances = new Object[mProtocolBinders.length]; + mExtras = extras; + mCloseables = closeables; + } + + /** + * Gets the route that is connected. + */ + public @NonNull RouteInfo getRoute() { + return mRoute; + } + + /** + * Gets the audio attributes which the client should use to stream audio + * to the destination, or null if the route does not support live audio streaming. + */ + public @Nullable AudioAttributes getAudioAttributes() { + return mAudioAttributes; + } + + /** + * Gets the display which the client should use to stream video to the + * destination using a {@link Presentation}, or null if the route does not + * support live video streaming. + */ + public @Nullable Display getPresentationDisplay() { + return mPresentationDisplay; + } + + /** + * Gets the route's volume provider, or null if none. + */ + public @Nullable VolumeProvider getVolumeProvider() { + return mVolumeProvider; + } + + /** + * Gets the set of supported route features. + */ + public @RouteFeatures int getFeatures() { + return mRoute.getFeatures(); + } + + /** + * Gets the list of supported route protocols. + * <p> + * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code> + * for more information. + * </p> + */ + public @NonNull List<String> getProtocols() { + return mRoute.getProtocols(); + } + + /** + * Gets an instance of a route protocol object that wraps the protocol binder + * and provides easy access to the protocol's functionality. + * <p> + * This is a convenience method which invokes {@link #getProtocolBinder(String)} + * using the name of the provided class then passes the resulting {@link IBinder} + * to a single-argument constructor of that class. + * </p><p> + * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code> + * for more information. + * </p> + */ + @SuppressWarnings("unchecked") + public @Nullable <T> T getProtocolObject(Class<T> clazz) { + int index = getProtocols().indexOf(clazz.getName()); + if (index < 0) { + return null; + } + if (mProtocolInstances[index] == null && mProtocolBinders[index] != null) { + final Constructor<T> ctor; + try { + ctor = clazz.getConstructor(MEDIA_ROUTE_PROTOCOL_CTOR_PARAMETERS); + } catch (NoSuchMethodException ex) { + throw new RuntimeException("Could not find public constructor " + + "with IBinder argument in protocol class: " + clazz.getName(), ex); + } + try { + mProtocolInstances[index] = ctor.newInstance(mProtocolBinders[index]); + } catch (InstantiationException | IllegalAccessException + | InvocationTargetException ex) { + throw new RuntimeException("Could create instance of protocol class: " + + clazz.getName(), ex); + } + } + return (T)mProtocolInstances[index]; + } + + /** + * Gets the {@link IBinder} that provides access to the specified route protocol + * or null if the protocol is not supported. + * <p> + * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code> + * for more information. + * </p> + */ + public @Nullable IBinder getProtocolBinder(@NonNull String name) { + int index = getProtocols().indexOf(name); + return index >= 0 ? mProtocolBinders[index] : null; + } + + /** + * Gets the {@link IBinder} that provides access to the specified route protocol + * at the given index in the protocol list or null if the protocol is not supported. + * <p> + * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code> + * for more information. + * </p> + */ + public @Nullable IBinder getProtocolBinder(int index) { + return mProtocolBinders[index]; + } + + /** + * Gets optional extra media route service or protocol specific information about + * the connection. Use the service or protocol name as the prefix for + * any extras to avoid namespace collisions. + */ + public @Nullable Bundle getExtras() { + return mExtras; + } + + /** + * Closes all closeables associated with the connection when the connection + * is being torn down. + */ + void close() { + final int count = mCloseables.size(); + for (int i = 0; i < count; i++) { + try { + mCloseables.get(i).close(); + } catch (IOException ex) { + } + } + } + + @Override + public @NonNull String toString() { + return "ConnectionInfo{ route=" + mRoute + + ", audioAttributes=" + mAudioAttributes + + ", presentationDisplay=" + mPresentationDisplay + + ", volumeProvider=" + mVolumeProvider + + ", protocolBinders=" + mProtocolBinders + " }"; + } + + /** + * Builds {@link ConnectionInfo} objects. + */ + public static final class Builder { + private final RouteInfo mRoute; + private AudioAttributes mAudioAttributes; + private Display mPresentationDisplay; + private VolumeProvider mVolumeProvider; + private final IBinder[] mProtocols; + private Bundle mExtras; + private final ArrayList<Closeable> mCloseables = new ArrayList<Closeable>(); + + /** + * Creates a builder for connection information. + * + * @param route The route that is connected. + */ + public Builder(@NonNull RouteInfo route) { + if (route == null) { + throw new IllegalArgumentException("route"); + } + mRoute = route; + mProtocols = new IBinder[route.getProtocols().size()]; + } + + /** + * Sets the audio attributes which the client should use to stream audio + * to the destination, or null if the route does not support live audio streaming. + */ + public @NonNull Builder setAudioAttributes( + @Nullable AudioAttributes audioAttributes) { + mAudioAttributes = audioAttributes; + return this; + } + + /** + * Sets the display which the client should use to stream video to the + * destination using a {@link Presentation}, or null if the route does not + * support live video streaming. + */ + public @NonNull Builder setPresentationDisplay(@Nullable Display display) { + mPresentationDisplay = display; + return this; + } + + /** + * Sets the route's volume provider, or null if none. + */ + public @NonNull Builder setVolumeProvider(@Nullable VolumeProvider provider) { + mVolumeProvider = provider; + return this; + } + + /** + * Sets the binder stub of a supported route protocol using + * the protocol's fully qualified class name. The protocol must be one + * of those that was indicated as being supported by the route. + * <p> + * If the stub implements {@link Closeable} then it will automatically + * be closed when the client disconnects from the route. + * </p><p> + * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code> + * for more information. + * </p> + */ + public @NonNull Builder setProtocolStub(@NonNull Class<?> clazz, + @NonNull IInterface stub) { + if (clazz == null) { + throw new IllegalArgumentException("clazz must not be null"); + } + if (stub == null) { + throw new IllegalArgumentException("stub must not be null"); + } + if (stub instanceof Closeable) { + mCloseables.add((Closeable)stub); + } + return setProtocolBinder(clazz.getName(), stub.asBinder()); + } + + /** + * Sets the binder interface of a supported route protocol by name. + * The protocol must be one of those that was indicated as being supported + * by the route. + * <p> + * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code> + * for more information. + * </p> + */ + public @NonNull Builder setProtocolBinder(@NonNull String name, + @NonNull IBinder binder) { + if (TextUtils.isEmpty(name)) { + throw new IllegalArgumentException("name must not be null or empty"); + } + if (binder == null) { + throw new IllegalArgumentException("binder must not be null"); + } + int index = mRoute.getProtocols().indexOf(name); + if (index < 0) { + throw new IllegalArgumentException("name must specify a protocol that " + + "the route actually declared that it supports: " + + "name=" + name + ", protocols=" + mRoute.getProtocols()); + } + mProtocols[index] = binder; + return this; + } + + /** + * Sets optional extra media route service or protocol specific information about + * the connection. Use the service or protocol name as the prefix for + * any extras to avoid namespace collisions. + */ + public @NonNull Builder setExtras(@Nullable Bundle extras) { + mExtras = extras; + return this; + } + + /** + * Builds the {@link ConnectionInfo} object. + */ + public @NonNull ConnectionInfo build() { + return new ConnectionInfo(mRoute, + mAudioAttributes, mPresentationDisplay, + mVolumeProvider, mProtocols, mExtras, mCloseables); + } + } + } + + /** + * Describes one particular way of routing media content to a destination + * according to the capabilities specified by a media route selector on behalf + * of an application. + */ + public static final class RouteInfo { + private final String mId; + private final DestinationInfo mDestination; + private final MediaRouteSelector mSelector; + private final int mFeatures; + private final ArrayList<String> mProtocols; + private final Bundle mExtras; + + RouteInfo(String id, DestinationInfo destination, MediaRouteSelector selector, + int features, ArrayList<String> protocols, Bundle extras) { + mId = id; + mDestination = destination; + mSelector = selector; + mFeatures = features; + mProtocols = protocols; + mExtras = extras; + } + + /** + * Gets the route's stable identifier. + * <p> + * The id is intended to uniquely identify the route among all routes that + * are offered by a particular destination in such a way that the client can + * refer to it at a later time. + * </p> + */ + public @NonNull String getId() { + return mId; + } + + /** + * Gets the destination that is offering this route. + */ + public @NonNull DestinationInfo getDestination() { + return mDestination; + } + + /** + * Gets the media route selector provided by the client for which this + * route was created. + * <p> + * It is implied that this route supports all of the required capabilities + * that were expressed in the selector. + * </p> + */ + public @NonNull MediaRouteSelector getSelector() { + return mSelector; + } + + /** + * Gets the set of supported route features. + */ + public @RouteFeatures int getFeatures() { + return mFeatures; + } + + /** + * Gets the list of supported route protocols. + * <p> + * Refer to <code>android.support.media.protocols.MediaRouteProtocol</code> + * for more information. + * </p> + */ + public @NonNull List<String> getProtocols() { + return mProtocols; + } + + /** + * Gets optional extra information about the route, or null if none. + */ + public @Nullable Bundle getExtras() { + return mExtras; + } + + @Override + public @NonNull String toString() { + return "RouteInfo{ id=" + mId + ", destination=" + mDestination + + ", features=0x" + Integer.toHexString(mFeatures) + + ", selector=" + mSelector + ", protocols=" + mProtocols + + ", extras=" + mExtras + " }"; + } + + /** + * Builds {@link RouteInfo} objects. + */ + public static final class Builder { + private final DestinationInfo mDestination; + private final String mId; + private final MediaRouteSelector mSelector; + private int mFeatures; + private final ArrayList<String> mProtocols = new ArrayList<String>(); + private Bundle mExtras; + + /** + * Creates a builder for route information. + * + * @param id The route's stable identifier. + * @param destination The destination of this route. + * @param selector The media route selector provided by the client for which + * this route was created. This must be one of the selectors that was + * included in the discovery request. + */ + public Builder(@NonNull String id, @NonNull DestinationInfo destination, + @NonNull MediaRouteSelector selector) { + if (TextUtils.isEmpty(id)) { + throw new IllegalArgumentException("id must not be null or empty"); + } + if (destination == null) { + throw new IllegalArgumentException("destination must not be null"); + } + if (selector == null) { + throw new IllegalArgumentException("selector must not be null"); + } + mDestination = destination; + mId = id; + mSelector = selector; + } + + /** + * Sets the set of supported route features. + */ + public @NonNull Builder setFeatures(@RouteFeatures int features) { + mFeatures = features; + return this; + } + + /** + * Adds a supported route protocol using its fully qualified class name. + * <p> + * If the protocol was not requested by the client in its selector + * then it will be silently discarded. + * </p> + */ + public @NonNull <T extends IInterface> Builder addProtocol(@NonNull Class<T> clazz) { + if (clazz == null) { + throw new IllegalArgumentException("clazz must not be null"); + } + return addProtocol(clazz.getName()); + } + + /** + * Adds a supported route protocol by name. + * <p> + * If the protocol was not requested by the client in its selector + * then it will be silently discarded. + * </p> + */ + public @NonNull Builder addProtocol(@NonNull String name) { + if (TextUtils.isEmpty(name)) { + throw new IllegalArgumentException("name must not be null"); + } + if (mSelector.containsProtocol(name)) { + mProtocols.add(name); + } + return this; + } + + /** + * Sets optional extra information about the route, or null if none. + */ + public @NonNull Builder setExtras(@Nullable Bundle extras) { + mExtras = extras; + return this; + } + + /** + * Builds the {@link RouteInfo} object. + * <p> + * Ensures that all required protocols have been supplied. + * </p> + */ + public @NonNull RouteInfo build() { + int missingFeatures = mSelector.getRequiredFeatures() & ~mFeatures; + if (missingFeatures != 0) { + throw new IllegalStateException("The media route selector " + + "specified required features which this route does " + + "not appear to support so it should not have been published: " + + "missing 0x" + Integer.toHexString(missingFeatures)); + } + for (String protocol : mSelector.getRequiredProtocols()) { + if (!mProtocols.contains(protocol)) { + throw new IllegalStateException("The media route selector " + + "specified required protocols which this route " + + "does not appear to support so it should not have " + + "been published: missing " + protocol); + } + } + return new RouteInfo(mId, mDestination, mSelector, + mFeatures, mProtocols, mExtras); + } + } + } + + /** + * Describes a destination for media content such as a device, + * an individual port on a device, or a group of devices. + */ + public static final class DestinationInfo { + private final String mId; + private final ServiceMetadata mService; + private final CharSequence mName; + private final CharSequence mDescription; + private final int mIconResourceId; + private final Bundle mExtras; + + DestinationInfo(String id, ServiceMetadata service, + CharSequence name, CharSequence description, + int iconResourceId, Bundle extras) { + mId = id; + mService = service; + mName = name; + mDescription = description; + mIconResourceId = iconResourceId; + mExtras = extras; + } + + /** + * Gets the destination's stable identifier. + * <p> + * The id is intended to uniquely identify the destination among all destinations + * provided by the media route service in such a way that the client can + * refer to it at a later time. Ideally, the id should be resilient to + * user-initiated actions such as changes to the name or description + * of the destination. + * </p> + */ + public @NonNull String getId() { + return mId; + } + + /** + * Gets metadata about the service that is providing access to this destination. + */ + public @NonNull ServiceMetadata getServiceMetadata() { + return mService; + } + + /** + * Gets the destination's name for display to the user. + */ + public @NonNull CharSequence getName() { + return mName; + } + + /** + * Gets the destination's description for display to the user, or null if none. + */ + public @Nullable CharSequence getDescription() { + return mDescription; + } + + /** + * Gets an icon resource from the service's package which is used + * to identify the destination, or -1 if none. + */ + public @DrawableRes int getIconResourceId() { + return mIconResourceId; + } + + /** + * Loads the icon drawable, or null if none. + */ + public @Nullable Drawable loadIcon(@NonNull PackageManager pm) { + return mIconResourceId >= 0 ? mService.getDrawable(pm, mIconResourceId) : null; + } + + /** + * Gets optional extra information about the destination, or null if none. + */ + public @Nullable Bundle getExtras() { + return mExtras; + } + + @Override + public @NonNull String toString() { + return "DestinationInfo{ id=" + mId + ", service=" + mService + ", name=" + mName + + ", description=" + mDescription + ", iconResourceId=" + mIconResourceId + + ", extras=" + mExtras + " }"; + } + + /** + * Builds {@link DestinationInfo} objects. + */ + public static final class Builder { + private final String mId; + private final ServiceMetadata mService; + private final CharSequence mName; + private CharSequence mDescription; + private int mIconResourceId = -1; + private Bundle mExtras; + + /** + * Creates a builder for destination information. + * + * @param id The destination's stable identifier. + * @param service Metatada about the service that is providing access to + * this destination. + * @param name The destination's name for display to the user. + */ + public Builder(@NonNull String id, @NonNull ServiceMetadata service, + @NonNull CharSequence name) { + if (TextUtils.isEmpty(id)) { + throw new IllegalArgumentException("id must not be null or empty"); + } + if (service == null) { + throw new IllegalArgumentException("service must not be null"); + } + if (TextUtils.isEmpty(name)) { + throw new IllegalArgumentException("name must not be null or empty"); + } + mId = id; + mService = service; + mName = name; + } + + /** + * Sets the destination's description for display to the user, or null if none. + */ + public @NonNull Builder setDescription(@Nullable CharSequence description) { + mDescription = description; + return this; + } + + /** + * Sets an icon resource from this package used to identify the destination, + * or -1 if none. + */ + public @NonNull Builder setIconResourceId(@DrawableRes int resid) { + mIconResourceId = resid; + return this; + } + + /** + * Gets optional extra information about the destination, or null if none. + */ + public @NonNull Builder setExtras(@Nullable Bundle extras) { + mExtras = extras; + return this; + } + + /** + * Builds the {@link DestinationInfo} object. + */ + public @NonNull DestinationInfo build() { + return new DestinationInfo(mId, mService, mName, mDescription, + mIconResourceId, mExtras); + } + } + } + + /** + * Describes metadata about a {@link MediaRouteService} which is providing + * access to certain kinds of destinations. + */ + public static final class ServiceMetadata { + private final ServiceInfo mService; + private CharSequence mLabel; + private Drawable mIcon; + + ServiceMetadata(Service service) throws NameNotFoundException { + mService = service.getPackageManager().getServiceInfo( + new ComponentName(service, service.getClass()), + PackageManager.GET_META_DATA); + } + + ServiceMetadata(ServiceInfo service) { + mService = service; + } + + /** + * Gets the service's component information including it name, label and icon. + */ + public @NonNull ServiceInfo getService() { + return mService; + } + + /** + * Gets the service's component name. + */ + public @NonNull ComponentName getComponentName() { + return new ComponentName(mService.packageName, mService.name); + } + + /** + * Gets the service's package name. + */ + public @NonNull String getPackageName() { + return mService.packageName; + } + + /** + * Gets the service's name for display to the user, or null if none. + */ + public @NonNull CharSequence getLabel(@NonNull PackageManager pm) { + if (mLabel == null) { + mLabel = mService.loadLabel(pm); + } + return mLabel; + } + + /** + * Gets the icon drawable, or null if none. + */ + public @Nullable Drawable getIcon(@NonNull PackageManager pm) { + if (mIcon == null) { + mIcon = mService.loadIcon(pm); + } + return mIcon; + } + + // TODO: add service metadata + + Drawable getDrawable(PackageManager pm, int resid) { + return pm.getDrawable(getPackageName(), resid, mService.applicationInfo); + } + + @Override + public @NonNull String toString() { + return "ServiceInfo{ service=" + getComponentName().toShortString() + " }"; + } + } + + /** + * Describes a request to discover routes on behalf of an application. + */ + public static final class DiscoveryRequest { + private final ArrayList<MediaRouteSelector> mSelectors = + new ArrayList<MediaRouteSelector>(); + private int mFlags; + + DiscoveryRequest(@NonNull List<MediaRouteSelector> selectors) { + setSelectors(selectors); + } + + /** + * Sets the list of media route selectors to consider during discovery. + */ + public void setSelectors(@NonNull List<MediaRouteSelector> selectors) { + if (selectors == null) { + throw new IllegalArgumentException("selectors"); + } + mSelectors.clear(); + mSelectors.addAll(selectors); + } + + /** + * Gets the list of media route selectors to consider during discovery. + */ + public @NonNull List<MediaRouteSelector> getSelectors() { + return mSelectors; + } + + /** + * Gets discovery flags, such as {@link MediaRouter#DISCOVERY_FLAG_BACKGROUND}. + */ + public @DiscoveryFlags int getFlags() { + return mFlags; + } + + /** + * Sets discovery flags, such as {@link MediaRouter#DISCOVERY_FLAG_BACKGROUND}. + */ + public void setFlags(@DiscoveryFlags int flags) { + mFlags = flags; + } + + @Override + public @NonNull String toString() { + return "DiscoveryRequest{ selectors=" + mSelectors + + ", flags=0x" + Integer.toHexString(mFlags) + + " }"; + } + } + + /** + * Describes a request to connect to a previously discovered route on + * behalf of an application. + */ + public static final class ConnectionRequest { + private RouteInfo mRoute; + private int mFlags; + private Bundle mExtras; + + ConnectionRequest(@NonNull RouteInfo route) { + setRoute(route); + } + + /** + * Gets the route to which to connect. + */ + public @NonNull RouteInfo getRoute() { + return mRoute; + } + + /** + * Sets the route to which to connect. + */ + public void setRoute(@NonNull RouteInfo route) { + if (route == null) { + throw new IllegalArgumentException("route must not be null"); + } + mRoute = route; + } + + /** + * Gets connection flags, such as {@link MediaRouter#CONNECTION_FLAG_BARGE}. + */ + public @ConnectionFlags int getFlags() { + return mFlags; + } + + /** + * Sets connection flags, such as {@link MediaRouter#CONNECTION_FLAG_BARGE}. + */ + public void setFlags(@ConnectionFlags int flags) { + mFlags = flags; + } + + /** + * Gets optional extras supplied by the application as part of the call to + * connect, or null if none. The media route service may use this + * information to configure the route during connection. + */ + public @Nullable Bundle getExtras() { + return mExtras; + } + + /** + * Sets optional extras supplied by the application as part of the call to + * connect, or null if none. The media route service may use this + * information to configure the route during connection. + */ + public void setExtras(@Nullable Bundle extras) { + mExtras = extras; + } + + @Override + public @NonNull String toString() { + return "ConnectionRequest{ route=" + mRoute + + ", flags=0x" + Integer.toHexString(mFlags) + + ", extras=" + mExtras + " }"; + } + } + + /** + * Callback interface to specify policy for route discovery, filtering, + * and connection establishment as well as observe media router state changes. + */ + public static abstract class RoutingCallback extends StateCallback { + /** + * Called to prepare a discovery request object to specify the desired + * media route selectors when the media router has been asked to start discovery. + * <p> + * By default, the discovery request contains all of the selectors which + * have been added to the media router. Subclasses may override the list of + * selectors by modifying the discovery request object before returning. + * </p> + * + * @param request The discovery request object which may be modified by + * this method to alter how discovery will be performed. + * @param selectors The immutable list of media route selectors which were + * added to the media router. + * @return True to allow discovery to proceed or false to abort it. + * By default, this methods returns true. + */ + public boolean onPrepareDiscoveryRequest(@NonNull DiscoveryRequest request, + @NonNull List<MediaRouteSelector> selectors) { + return true; + } + + /** + * Called to prepare a connection request object to specify the desired + * route and connection parameters when the media router has been asked to + * connect to a particular destination. + * <p> + * By default, the connection request specifies the first available route + * to the destination. Subclasses may override the route and destination + * or set additional connection parameters by modifying the connection request + * object before returning. + * </p> + * + * @param request The connection request object which may be modified by + * this method to alter how the connection will be established. + * @param destination The destination to which the media router was asked + * to connect. + * @param routes The list of routes that belong to that destination sorted + * in the same order as their matching media route selectors which were + * used during discovery. + * @return True to allow the connection to proceed or false to abort it. + * By default, this methods returns true. + */ + public boolean onPrepareConnectionRequest( + @NonNull ConnectionRequest request, + @NonNull DestinationInfo destination, @NonNull List<RouteInfo> routes) { + return true; + } + } + + /** + * Callback class to receive events from a {@link MediaRouter.Delegate}. + */ + public static abstract class StateCallback { + /** + * Called when the media router has been released. + */ + public void onReleased() { } + + /** + * Called when the discovery state has changed. + * + * @param state The new discovery state: one of + * {@link #DISCOVERY_STATE_STOPPED} or {@link #DISCOVERY_STATE_STARTED}. + */ + public void onDiscoveryStateChanged(@DiscoveryState int state) { } + + /** + * Called when the connection state has changed. + * + * @param state The new connection state: one of + * {@link #CONNECTION_STATE_DISCONNECTED}, {@link #CONNECTION_STATE_CONNECTING} + * or {@link #CONNECTION_STATE_CONNECTED}. + */ + public void onConnectionStateChanged(@ConnectionState int state) { } + + /** + * Called when the selected destination has changed. + * + * @param destination The new selected destination, or null if none. + */ + public void onSelectedDestinationChanged(@Nullable DestinationInfo destination) { } + + /** + * Called when route discovery has started. + */ + public void onDiscoveryStarted() { } + + /** + * Called when route discovery has stopped normally. + * <p> + * Abnormal termination is reported via {@link #onDiscoveryFailed}. + * </p> + */ + public void onDiscoveryStopped() { } + + /** + * Called when discovery has failed in a non-recoverable manner. + * + * @param error The error code: one of + * {@link MediaRouter#DISCOVERY_ERROR_UNKNOWN}, + * {@link MediaRouter#DISCOVERY_ERROR_ABORTED}, + * or {@link MediaRouter#DISCOVERY_ERROR_NO_CONNECTIVITY}. + * @param message The localized error message, or null if none. This message + * may be shown to the user. + * @param extras Additional information about the error which a client + * may use, or null if none. + */ + public void onDiscoveryFailed(@DiscoveryError int error, @Nullable CharSequence message, + @Nullable Bundle extras) { } + + /** + * Called when a new destination is found or has changed during discovery. + * <p> + * Certain destinations may be omitted because they have been filtered + * out by the media router's routing callback. + * </p> + * + * @param destination The destination that was found. + */ + public void onDestinationFound(@NonNull DestinationInfo destination) { } + + /** + * Called when a destination is no longer reachable or is no longer + * offering any routes that satisfy the discovery request. + * + * @param destination The destination that went away. + */ + public void onDestinationLost(@NonNull DestinationInfo destination) { } + + /** + * Called when a connection attempt begins. + */ + public void onConnecting() { } + + /** + * Called when the connection succeeds. + */ + public void onConnected() { } + + /** + * Called when the connection is terminated normally. + * <p> + * Abnormal termination is reported via {@link #onConnectionFailed}. + * </p> + */ + public void onDisconnected() { } + + /** + * Called when a connection attempt or connection in + * progress has failed in a non-recoverable manner. + * + * @param error The error code: one of + * {@link MediaRouter#CONNECTION_ERROR_ABORTED}, + * {@link MediaRouter#CONNECTION_ERROR_UNAUTHORIZED}, + * {@link MediaRouter#CONNECTION_ERROR_UNREACHABLE}, + * {@link MediaRouter#CONNECTION_ERROR_BUSY}, + * {@link MediaRouter#CONNECTION_ERROR_TIMEOUT}, + * {@link MediaRouter#CONNECTION_ERROR_BROKEN}, + * or {@link MediaRouter#CONNECTION_ERROR_BARGED}. + * @param message The localized error message, or null if none. This message + * may be shown to the user. + * @param extras Additional information about the error which a client + * may use, or null if none. + */ + public void onConnectionFailed(@ConnectionError int error, + @Nullable CharSequence message, @Nullable Bundle extras) { } + } +} diff --git a/media/java/android/media/session/RouteEvent.aidl b/media/java/android/media/routing/ParcelableConnectionInfo.aidl index 6966207..4a9ec94 100644 --- a/media/java/android/media/session/RouteEvent.aidl +++ b/media/java/android/media/routing/ParcelableConnectionInfo.aidl @@ -13,6 +13,6 @@ ** limitations under the License. */ -package android.media.session; +package android.media.routing; -parcelable RouteEvent; +parcelable ParcelableConnectionInfo; diff --git a/media/java/android/media/routing/ParcelableConnectionInfo.java b/media/java/android/media/routing/ParcelableConnectionInfo.java new file mode 100644 index 0000000..45cfe9f --- /dev/null +++ b/media/java/android/media/routing/ParcelableConnectionInfo.java @@ -0,0 +1,71 @@ +/* + * 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.routing; + +import android.media.AudioAttributes; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Internal parcelable representation of a media route connection. + */ +class ParcelableConnectionInfo implements Parcelable { + public AudioAttributes audioAttributes; + public int presentationDisplayId = -1; + // todo: volume + public IBinder[] protocolBinders; + public Bundle extras; + + public static final Parcelable.Creator<ParcelableConnectionInfo> CREATOR = + new Parcelable.Creator<ParcelableConnectionInfo>() { + @Override + public ParcelableConnectionInfo createFromParcel(Parcel source) { + ParcelableConnectionInfo info = new ParcelableConnectionInfo(); + if (source.readInt() != 0) { + info.audioAttributes = AudioAttributes.CREATOR.createFromParcel(source); + } + info.presentationDisplayId = source.readInt(); + info.protocolBinders = source.createBinderArray(); + info.extras = source.readBundle(); + return info; + } + + @Override + public ParcelableConnectionInfo[] newArray(int size) { + return new ParcelableConnectionInfo[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + if (audioAttributes != null) { + dest.writeInt(1); + audioAttributes.writeToParcel(dest, flags); + } else { + dest.writeInt(0); + } + dest.writeInt(presentationDisplayId); + dest.writeBinderArray(protocolBinders); + dest.writeBundle(extras); + } +} diff --git a/media/java/android/media/session/RouteCommand.aidl b/media/java/android/media/routing/ParcelableDestinationInfo.aidl index 725b308..bf1c198 100644 --- a/media/java/android/media/session/RouteCommand.aidl +++ b/media/java/android/media/routing/ParcelableDestinationInfo.aidl @@ -13,6 +13,6 @@ ** limitations under the License. */ -package android.media.session; +package android.media.routing; -parcelable RouteCommand; +parcelable ParcelableDestinationInfo; diff --git a/media/java/android/media/routing/ParcelableDestinationInfo.java b/media/java/android/media/routing/ParcelableDestinationInfo.java new file mode 100644 index 0000000..eca5eec --- /dev/null +++ b/media/java/android/media/routing/ParcelableDestinationInfo.java @@ -0,0 +1,65 @@ +/* + * 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.routing; + +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +/** + * Internal parcelable representation of a media destination. + */ +class ParcelableDestinationInfo implements Parcelable { + public String id; + public CharSequence name; + public CharSequence description; + public int iconResourceId; + public Bundle extras; + + public static final Parcelable.Creator<ParcelableDestinationInfo> CREATOR = + new Parcelable.Creator<ParcelableDestinationInfo>() { + @Override + public ParcelableDestinationInfo createFromParcel(Parcel source) { + ParcelableDestinationInfo info = new ParcelableDestinationInfo(); + info.id = source.readString(); + info.name = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); + info.description = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); + info.iconResourceId = source.readInt(); + info.extras = source.readBundle(); + return info; + } + + @Override + public ParcelableDestinationInfo[] newArray(int size) { + return new ParcelableDestinationInfo[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + TextUtils.writeToParcel(name, dest, flags); + TextUtils.writeToParcel(description, dest, flags); + dest.writeInt(iconResourceId); + dest.writeBundle(extras); + } +} diff --git a/media/java/android/media/routeprovider/RouteRequest.aidl b/media/java/android/media/routing/ParcelableRouteInfo.aidl index 7bc5722..126afaa 100644 --- a/media/java/android/media/routeprovider/RouteRequest.aidl +++ b/media/java/android/media/routing/ParcelableRouteInfo.aidl @@ -13,6 +13,6 @@ ** limitations under the License. */ -package android.media.routeprovider; +package android.media.routing; -parcelable RouteRequest; +parcelable ParcelableRouteInfo; diff --git a/media/java/android/media/routing/ParcelableRouteInfo.java b/media/java/android/media/routing/ParcelableRouteInfo.java new file mode 100644 index 0000000..fb1a547 --- /dev/null +++ b/media/java/android/media/routing/ParcelableRouteInfo.java @@ -0,0 +1,64 @@ +/* + * 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.routing; + +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Internal parcelable representation of a media route. + */ +class ParcelableRouteInfo implements Parcelable { + public String id; + public int selectorIndex; // index of selector within list used for discovery + public int features; + public String[] protocols; + public Bundle extras; + + public static final Parcelable.Creator<ParcelableRouteInfo> CREATOR = + new Parcelable.Creator<ParcelableRouteInfo>() { + @Override + public ParcelableRouteInfo createFromParcel(Parcel source) { + ParcelableRouteInfo info = new ParcelableRouteInfo(); + info.id = source.readString(); + info.selectorIndex = source.readInt(); + info.features = source.readInt(); + info.protocols = source.createStringArray(); + info.extras = source.readBundle(); + return info; + } + + @Override + public ParcelableRouteInfo[] newArray(int size) { + return new ParcelableRouteInfo[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(id); + dest.writeInt(selectorIndex); + dest.writeInt(features); + dest.writeStringArray(protocols); + dest.writeBundle(extras); + } +} diff --git a/media/java/android/media/session/ISession.aidl b/media/java/android/media/session/ISession.aidl index 5bc0de4..a92350b 100644 --- a/media/java/android/media/session/ISession.aidl +++ b/media/java/android/media/session/ISession.aidl @@ -17,10 +17,8 @@ package android.media.session; import android.content.ComponentName; import android.media.MediaMetadata; +import android.media.routing.IMediaRouter; import android.media.session.ISessionController; -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; @@ -34,17 +32,10 @@ interface ISession { ISessionController getController(); void setFlags(int flags); void setActive(boolean active); + void setMediaRouter(in IMediaRouter router); void setMediaButtonReceiver(in ComponentName mbr); 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 disconnectFromRoute(in RouteInfo route); - void sendRouteCommand(in RouteCommand event, in ResultReceiver cb); - // These commands are for the TransportPerformer void setMetadata(in MediaMetadata metadata); void setPlaybackState(in PlaybackState state); @@ -53,4 +44,4 @@ interface ISession { // These commands relate to volume handling void configureVolumeHandling(int type, int arg1, int arg2); void setCurrentVolume(int currentVolume); -}
\ No newline at end of file +} diff --git a/media/java/android/media/session/ISessionCallback.aidl b/media/java/android/media/session/ISessionCallback.aidl index 0316d1f..e554e27 100644 --- a/media/java/android/media/session/ISessionCallback.aidl +++ b/media/java/android/media/session/ISessionCallback.aidl @@ -16,9 +16,6 @@ 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; @@ -29,11 +26,6 @@ import android.os.ResultReceiver; oneway interface ISessionCallback { void onCommand(String command, in Bundle extras, in ResultReceiver cb); void onMediaButton(in Intent mediaButtonIntent, int sequenceNumber, in ResultReceiver cb); - void onRequestRouteChange(in RouteInfo route); - void onRouteConnected(in RouteInfo route, in RouteOptions options); - void onRouteDisconnected(in RouteInfo route, int reason); - void onRouteStateChange(int state); - void onRouteEvent(in RouteEvent event); // These callbacks are for the TransportPerformer void onPlay(); @@ -49,4 +41,4 @@ oneway interface ISessionCallback { // These callbacks are for volume handling void onAdjustVolumeBy(int delta); void onSetVolumeTo(int value); -}
\ No newline at end of file +} diff --git a/media/java/android/media/session/ISessionController.aidl b/media/java/android/media/session/ISessionController.aidl index b4c11f6..6cf5ef2 100644 --- a/media/java/android/media/session/ISessionController.aidl +++ b/media/java/android/media/session/ISessionController.aidl @@ -18,6 +18,8 @@ package android.media.session; import android.content.Intent; import android.media.MediaMetadata; import android.media.Rating; +import android.media.routing.IMediaRouterDelegate; +import android.media.routing.IMediaRouterStateCallback; import android.media.session.ISessionControllerCallback; import android.media.session.MediaSessionInfo; import android.media.session.ParcelableVolumeInfo; @@ -36,14 +38,15 @@ interface ISessionController { void registerCallbackListener(in ISessionControllerCallback cb); void unregisterCallbackListener(in ISessionControllerCallback cb); boolean isTransportControlEnabled(); - void showRoutePicker(); MediaSessionInfo getSessionInfo(); long getFlags(); ParcelableVolumeInfo getVolumeAttributes(); void adjustVolumeBy(int delta, int flags); void setVolumeTo(int value, int flags); - // These commands are for the TransportController + IMediaRouterDelegate createMediaRouterDelegate(IMediaRouterStateCallback callback); + + // These commands are for the TransportControls void play(); void pause(); void stop(); @@ -56,4 +59,4 @@ interface ISessionController { MediaMetadata getMetadata(); PlaybackState getPlaybackState(); int getRatingType(); -}
\ No newline at end of file +} diff --git a/media/java/android/media/session/ISessionControllerCallback.aidl b/media/java/android/media/session/ISessionControllerCallback.aidl index baa1379..64d2bc7 100644 --- a/media/java/android/media/session/ISessionControllerCallback.aidl +++ b/media/java/android/media/session/ISessionControllerCallback.aidl @@ -16,7 +16,6 @@ package android.media.session; import android.media.MediaMetadata; -import android.media.session.RouteInfo; import android.media.session.ParcelableVolumeInfo; import android.media.session.PlaybackState; import android.os.Bundle; @@ -26,10 +25,9 @@ import android.os.Bundle; */ oneway interface ISessionControllerCallback { void onEvent(String event, in Bundle extras); - void onRouteChanged(in RouteInfo route); // These callbacks are for the TransportController void onPlaybackStateChanged(in PlaybackState state); void onMetadataChanged(in MediaMetadata metadata); void onVolumeInfoChanged(in ParcelableVolumeInfo info); -}
\ No newline at end of file +} diff --git a/media/java/android/media/session/MediaController.java b/media/java/android/media/session/MediaController.java index edb69bc..cc8b31a 100644 --- a/media/java/android/media/session/MediaController.java +++ b/media/java/android/media/session/MediaController.java @@ -22,6 +22,7 @@ import android.media.AudioManager; import android.media.MediaMetadata; import android.media.Rating; import android.media.VolumeProvider; +import android.media.routing.MediaRouter; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -52,8 +53,7 @@ public final class MediaController { private static final int MSG_EVENT = 1; private static final int MSG_UPDATE_PLAYBACK_STATE = 2; private static final int MSG_UPDATE_METADATA = 3; - private static final int MSG_ROUTE = 4; - private static final int MSG_UPDATE_VOLUME = 5; + private static final int MSG_UPDATE_VOLUME = 4; private final ISessionController mSessionBinder; @@ -64,11 +64,11 @@ public final class MediaController { private boolean mCbRegistered = false; private MediaSessionInfo mInfo; - private TransportControls mTransportController; + private final TransportControls mTransportControls; private MediaController(ISessionController sessionBinder) { mSessionBinder = sessionBinder; - mTransportController = new TransportControls(); + mTransportControls = new TransportControls(); } /** @@ -91,12 +91,24 @@ public final class MediaController { } /** - * Get a {@link TransportControls} instance for this session. + * Get a {@link TransportControls} instance to send transport actions to + * the associated session. * - * @return A controls instance + * @return A transport controls instance. */ public @NonNull TransportControls getTransportControls() { - return mTransportController; + return mTransportControls; + } + + /** + * Creates a media router delegate through which the destination of the media + * router may be observed and controlled. + * + * @return The media router delegate, or null if the media session does + * not support media routing. + */ + public @Nullable MediaRouter.Delegate createMediaRouterDelegate() { + return new MediaRouter.Delegate(); } /** @@ -308,20 +320,6 @@ 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. - * - * @hide - */ - public void showRoutePicker() { - try { - mSessionBinder.showRoutePicker(); - } catch (RemoteException e) { - Log.d(TAG, "Dead object in showRoutePicker", e); - } - } - - /** * Get the info for the session this controller is connected to. * * @return The session info for the connected session. @@ -421,15 +419,6 @@ public final class MediaController { } /** - * Override to handle route changes for this session. - * - * @param route The new route - * @hide - */ - public void onRouteChanged(RouteInfo route) { - } - - /** * Override to handle changes in playback state. * * @param state The new playback state of the session @@ -670,14 +659,6 @@ public final class MediaController { } @Override - public void onRouteChanged(RouteInfo route) { - MediaController controller = mController.get(); - if (controller != null) { - controller.postMessage(MSG_ROUTE, route, null); - } - } - - @Override public void onPlaybackStateChanged(PlaybackState state) { MediaController controller = mController.get(); if (controller != null) { @@ -719,9 +700,6 @@ public final class MediaController { case MSG_EVENT: mCallback.onSessionEvent((String) msg.obj, msg.getData()); break; - case MSG_ROUTE: - mCallback.onRouteChanged((RouteInfo) msg.obj); - break; case MSG_UPDATE_PLAYBACK_STATE: mCallback.onPlaybackStateChanged((PlaybackState) msg.obj); break; diff --git a/media/java/android/media/session/MediaSession.java b/media/java/android/media/session/MediaSession.java index 2cbdc96..34997bd 100644 --- a/media/java/android/media/session/MediaSession.java +++ b/media/java/android/media/session/MediaSession.java @@ -25,6 +25,7 @@ import android.media.AudioManager; import android.media.MediaMetadata; import android.media.Rating; import android.media.VolumeProvider; +import android.media.routing.MediaRouter; import android.media.session.ISessionController; import android.media.session.ISession; import android.media.session.ISessionCallback; @@ -93,40 +94,6 @@ public final class MediaSession { public static final int FLAG_EXCLUSIVE_GLOBAL_PRIORITY = 1 << 16; /** - * Indicates the session was disconnected because the user that the session - * belonged to is stopping. - * - * @hide - */ - public static final int DISCONNECT_REASON_USER_STOPPING = 1; - - /** - * Indicates the session was disconnected because the provider disconnected - * the route. - * @hide - */ - public static final int DISCONNECT_REASON_PROVIDER_DISCONNECTED = 2; - - /** - * Indicates the session was disconnected because the route has changed. - * @hide - */ - public static final int DISCONNECT_REASON_ROUTE_CHANGED = 3; - - /** - * Indicates the session was disconnected because the session owner - * requested it disconnect. - * @hide - */ - public static final int DISCONNECT_REASON_SESSION_DISCONNECTED = 4; - - /** - * Indicates the session was disconnected because it was destroyed. - * @hide - */ - public static final int DISCONNECT_REASON_SESSION_DESTROYED = 5; - - /** * The session uses local playback. */ public static final int PLAYBACK_TYPE_LOCAL = 1; @@ -146,11 +113,7 @@ public final class MediaSession { = new ArrayList<CallbackMessageHandler>(); private final ArrayList<TransportMessageHandler> mTransportCallbacks = new ArrayList<TransportMessageHandler>(); - // TODO route interfaces - private final ArrayMap<String, RouteInterface.EventListener> mInterfaceListeners - = new ArrayMap<String, RouteInterface.EventListener>(); - private Route mRoute; private VolumeProvider mVolumeProvider; private boolean mActive = false; @@ -228,6 +191,23 @@ public final class MediaSession { } /** + * Associates a {@link MediaRouter} with this session to control the destination + * of media content. + * <p> + * A media router may only be associated with at most one session at a time. + * </p> + * + * @param router The media router, or null to remove the current association. + */ + public void setMediaRouter(@Nullable MediaRouter router) { + try { + mBinder.setMediaRouter(router != null ? router.getBinder() : null); + } catch (RemoteException e) { + Log.wtf(TAG, "Failure in setMediaButtonReceiver.", e); + } + } + + /** * Set a media button event receiver component to use to restart playback * after an app has been stopped. * @@ -377,90 +357,6 @@ public final class MediaSession { } /** - * 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. - * @hide - */ - 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. - * - * @hide - */ - public void disconnect() { - if (mRoute != null) { - try { - mBinder.disconnectFromRoute(mRoute.getRouteInfo()); - } catch (RemoteException e) { - Log.wtf(TAG, "Error disconnecting from route"); - } - } - } - - /** - * 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. - * @hide - */ - 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; - } - - /** * Add a callback to receive transport controls on, such as play, rewind, or * fast forward. * @@ -670,34 +566,6 @@ public final class MediaSession { } } - private void postRequestRouteChange(RouteInfo route) { - synchronized (mLock) { - for (int i = mCallbacks.size() - 1; i >= 0; i--) { - mCallbacks.get(i).post(CallbackMessageHandler.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(CallbackMessageHandler.MSG_ROUTE_CONNECTED, mRoute); - } - } - } - - private void postRouteDisconnected(RouteInfo route, int reason) { - synchronized (mLock) { - if (mRoute != null && TextUtils.equals(mRoute.getRouteInfo().getId(), route.getId())) { - for (int i = mCallbacks.size() - 1; i >= 0; i--) { - mCallbacks.get(i).post(CallbackMessageHandler.MSG_ROUTE_DISCONNECTED, mRoute, - reason); - } - } - } - } - /** * Return true if this is considered an active playback state. * @@ -796,47 +664,6 @@ public final class MediaSession { public void onControlCommand(@NonNull String command, @Nullable Bundle extras, @Nullable ResultReceiver cb) { } - - /** - * Called when the user has selected a different route to connect to. - * The app is responsible for connecting to the new route and migrating - * ongoing playback if necessary. - * - * @param route - * @hide - */ - 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 - * @hide - */ - 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> - * <li>{@link #DISCONNECT_REASON_USER_STOPPING}</li> - * <li>{@link #DISCONNECT_REASON_PROVIDER_DISCONNECTED}</li> - * <li>{@link #DISCONNECT_REASON_ROUTE_CHANGED}</li> - * <li>{@link #DISCONNECT_REASON_SESSION_DISCONNECTED}</li> - * <li>{@link #DISCONNECT_REASON_SESSION_DESTROYED}</li> - * </ul> - * - * @param route The route that disconnected - * @param reason The reason for the disconnect - * @hide - */ - public void onRouteDisconnected(Route route, int reason) { - } } /** @@ -902,17 +729,6 @@ public final class MediaSession { */ public void onSetRating(@NonNull Rating rating) { } - - /** - * Report that audio focus has changed on the app. This only happens if - * you have indicated you have started playing with - * {@link #setPlaybackState}. - * - * @param focusChange The type of focus change, TBD. - * @hide - */ - public void onRouteFocusChange(int focusChange) { - } } /** @@ -926,8 +742,7 @@ public final class MediaSession { } @Override - public void onCommand(String command, Bundle extras, ResultReceiver cb) - throws RemoteException { + public void onCommand(String command, Bundle extras, ResultReceiver cb) { MediaSession session = mMediaSession.get(); if (session != null) { session.postCommand(command, extras, cb); @@ -935,8 +750,8 @@ public final class MediaSession { } @Override - public void onMediaButton(Intent mediaButtonIntent, int sequenceNumber, ResultReceiver cb) - throws RemoteException { + public void onMediaButton(Intent mediaButtonIntent, int sequenceNumber, + ResultReceiver cb) { MediaSession session = mMediaSession.get(); try { if (session != null) { @@ -950,31 +765,7 @@ public final class MediaSession { } @Override - public void onRequestRouteChange(RouteInfo route) throws RemoteException { - MediaSession session = mMediaSession.get(); - if (session != null) { - session.postRequestRouteChange(route); - } - } - - @Override - public void onRouteConnected(RouteInfo route, RouteOptions options) { - MediaSession session = mMediaSession.get(); - if (session != null) { - session.postRouteConnected(route, options); - } - } - - @Override - public void onRouteDisconnected(RouteInfo route, int reason) { - MediaSession session = mMediaSession.get(); - if (session != null) { - session.postRouteDisconnected(route, reason); - } - } - - @Override - public void onPlay() throws RemoteException { + public void onPlay() { MediaSession session = mMediaSession.get(); if (session != null) { session.dispatchPlay(); @@ -982,7 +773,7 @@ public final class MediaSession { } @Override - public void onPause() throws RemoteException { + public void onPause() { MediaSession session = mMediaSession.get(); if (session != null) { session.dispatchPause(); @@ -990,7 +781,7 @@ public final class MediaSession { } @Override - public void onStop() throws RemoteException { + public void onStop() { MediaSession session = mMediaSession.get(); if (session != null) { session.dispatchStop(); @@ -998,7 +789,7 @@ public final class MediaSession { } @Override - public void onNext() throws RemoteException { + public void onNext() { MediaSession session = mMediaSession.get(); if (session != null) { session.dispatchNext(); @@ -1006,7 +797,7 @@ public final class MediaSession { } @Override - public void onPrevious() throws RemoteException { + public void onPrevious() { MediaSession session = mMediaSession.get(); if (session != null) { session.dispatchPrevious(); @@ -1014,7 +805,7 @@ public final class MediaSession { } @Override - public void onFastForward() throws RemoteException { + public void onFastForward() { MediaSession session = mMediaSession.get(); if (session != null) { session.dispatchFastForward(); @@ -1022,7 +813,7 @@ public final class MediaSession { } @Override - public void onRewind() throws RemoteException { + public void onRewind() { MediaSession session = mMediaSession.get(); if (session != null) { session.dispatchRewind(); @@ -1030,7 +821,7 @@ public final class MediaSession { } @Override - public void onSeekTo(long pos) throws RemoteException { + public void onSeekTo(long pos) { MediaSession session = mMediaSession.get(); if (session != null) { session.dispatchSeekTo(pos); @@ -1038,7 +829,7 @@ public final class MediaSession { } @Override - public void onRate(Rating rating) throws RemoteException { + public void onRate(Rating rating) { MediaSession session = mMediaSession.get(); if (session != null) { session.dispatchRate(rating); @@ -1046,26 +837,7 @@ public final class MediaSession { } @Override - public void onRouteEvent(RouteEvent event) throws RemoteException { - MediaSession 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 - } - - @Override - public void onAdjustVolumeBy(int delta) throws RemoteException { + public void onAdjustVolumeBy(int delta) { MediaSession session = mMediaSession.get(); if (session != null) { if (session.mVolumeProvider != null) { @@ -1075,7 +847,7 @@ public final class MediaSession { } @Override - public void onSetVolumeTo(int value) throws RemoteException { + public void onSetVolumeTo(int value) { MediaSession session = mMediaSession.get(); if (session != null) { if (session.mVolumeProvider != null) { @@ -1089,9 +861,6 @@ public final class MediaSession { private class CallbackMessageHandler extends Handler { 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 int MSG_ROUTE_DISCONNECTED = 5; private MediaSession.Callback mCallback; @@ -1114,15 +883,6 @@ public final class MediaSession { Command cmd = (Command) msg.obj; mCallback.onControlCommand(cmd.command, cmd.extras, cmd.stub); break; - case MSG_ROUTE_CHANGE: - mCallback.onRequestRouteChange((RouteInfo) msg.obj); - break; - case MSG_ROUTE_CONNECTED: - mCallback.onRouteConnected((Route) msg.obj); - break; - case MSG_ROUTE_DISCONNECTED: - mCallback.onRouteDisconnected((Route) msg.obj, msg.arg1); - break; } } } diff --git a/media/java/android/media/session/MediaSessionManager.java b/media/java/android/media/session/MediaSessionManager.java index c73a8d3..c477406 100644 --- a/media/java/android/media/session/MediaSessionManager.java +++ b/media/java/android/media/session/MediaSessionManager.java @@ -35,9 +35,8 @@ import java.util.ArrayList; import java.util.List; /** - * MediaSessionManager allows the creation and control of MediaSessions in the - * system. A MediaSession enables publishing information about ongoing media and - * interacting with MediaControllers and MediaRoutes. + * Provides support for interacting with {@link MediaSession media sessions} + * that applications have published to express their ongoing media playback state. * <p> * Use <code>Context.getSystemService(Context.MEDIA_SESSION_SERVICE)</code> to * get an instance of this class. @@ -256,8 +255,8 @@ public final class MediaSessionManager { } /** - * Dispatch an adjust volume request to the system. It will be routed to the - * most relevant stream/session. + * Dispatch an adjust volume request to the system. It will be sent to the + * most relevant audio stream or media session. * * @param suggestedStream The stream to fall back to if there isn't a * relevant stream @@ -292,8 +291,7 @@ public final class MediaSessionManager { private final IActiveSessionsListener.Stub mStub = new IActiveSessionsListener.Stub() { @Override - public void onActiveSessionsChanged(List<MediaSession.Token> tokens) - throws RemoteException { + public void onActiveSessionsChanged(List<MediaSession.Token> tokens) { ArrayList<MediaController> controllers = new ArrayList<MediaController>(); int size = tokens.size(); for (int i = 0; i < size; i++) { diff --git a/media/java/android/media/session/PlaybackState.java b/media/java/android/media/session/PlaybackState.java index 6125cb4..9ae2436 100644 --- a/media/java/android/media/session/PlaybackState.java +++ b/media/java/android/media/session/PlaybackState.java @@ -157,10 +157,11 @@ public final class PlaybackState implements Parcelable { /** * 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 #STATE_NONE}. If - * the connection failed {@link #STATE_ERROR} should be used. - * @hide + * new destination. Depending on the implementation you may return to the previous + * state when the connection finishes or enter {@link #STATE_NONE}. + * If the connection failed {@link #STATE_ERROR} should be used. + * + * @see #setState */ public final static int STATE_CONNECTING = 8; diff --git a/media/java/android/media/session/Route.java b/media/java/android/media/session/Route.java deleted file mode 100644 index 935eb5b..0000000 --- a/media/java/android/media/session/Route.java +++ /dev/null @@ -1,100 +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.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. - * @hide - */ -public final class Route { - private static final String TAG = "Route"; - private final RouteInfo mInfo; - private final MediaSession mSession; - private final RouteOptions mOptions; - - /** - * @hide - */ - public Route(RouteInfo info, RouteOptions options, MediaSession 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 - */ - MediaSession getSession() { - return mSession; - } -} diff --git a/media/java/android/media/session/RouteCommand.java b/media/java/android/media/session/RouteCommand.java deleted file mode 100644 index 358bc0a..0000000 --- a/media/java/android/media/session/RouteCommand.java +++ /dev/null @@ -1,117 +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.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.java b/media/java/android/media/session/RouteEvent.java deleted file mode 100644 index 918e410..0000000 --- a/media/java/android/media/session/RouteEvent.java +++ /dev/null @@ -1,120 +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.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.java b/media/java/android/media/session/RouteInfo.java deleted file mode 100644 index 02f78f9..0000000 --- a/media/java/android/media/session/RouteInfo.java +++ /dev/null @@ -1,234 +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.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. - * @hide - */ -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 deleted file mode 100644 index 8de4d89..0000000 --- a/media/java/android/media/session/RouteInterface.java +++ /dev/null @@ -1,213 +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.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.os.ResultReceiver; -import android.util.Log; - -import java.util.ArrayList; - -/** - * A route can support multiple interfaces for a {@link MediaSession} 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 RoutePlaybackControls for an example - * @hide - */ -public final class RouteInterface { - private static final String TAG = "RouteInterface"; - - /** - * 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 Route mRoute; - private final String mIface; - private final MediaSession mSession; - - private final Object mLock = new Object(); - private final ArrayList<EventHandler> mListeners = new ArrayList<EventHandler>(); - - /** - * @hide - */ - RouteInterface(Route route, String iface, MediaSession session) { - mRoute = route; - mIface = iface; - mSession = session; - mSession.addInterfaceListener(iface, mEventListener); - } - - /** - * 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) { - 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) { - if (listener == null) { - throw new IllegalArgumentException("listener may not be null"); - } - synchronized (mLock) { - int index = findIndexOfListenerLocked(listener); - if (index != -1) { - mListeners.remove(index); - } - } - } - - private int findIndexOfListenerLocked(EventListener listener) { - if (listener == null) { - throw new IllegalArgumentException("Callback cannot be null"); - } - for (int i = mListeners.size() - 1; i >= 0; i--) { - EventHandler handler = mListeners.get(i); - if (listener == handler.mListener) { - return i; - } - } - return -1; - } - - private EventListener mEventListener = new EventListener() { - @Override - public void onEvent(String event, Bundle args) { - synchronized (mLock) { - for (int i = mListeners.size() - 1; i >= 0; i--) { - mListeners.get(i).postEvent(event, args); - } - } - } - - }; - - /** - * An EventListener can be registered by an app with TODO to handle events - * sent by the session on a specific interface. - */ - public static abstract class EventListener { - /** - * This is called when an event is received from the interface. Events - * are sent by the session owner and will be delivered to all - * controllers that are listening to the interface. - * - * @param event The event that occurred. - * @param args Any extras that were included with the event. May be - * null. - */ - public abstract void onEvent(String event, Bundle args); - } - - private static final class EventHandler extends Handler { - - private final EventListener mListener; - - public EventHandler(Looper looper, EventListener cb) { - super(looper, null, true); - mListener = cb; - } - - @Override - public void handleMessage(Message msg) { - mListener.onEvent((String) msg.obj, msg.getData()); - } - - public void postEvent(String event, Bundle args) { - Message msg = obtainMessage(0, event); - msg.setData(args); - msg.sendToTarget(); - } - } -} diff --git a/media/java/android/media/session/RouteOptions.aidl b/media/java/android/media/session/RouteOptions.aidl deleted file mode 100644 index feaf517..0000000 --- a/media/java/android/media/session/RouteOptions.aidl +++ /dev/null @@ -1,18 +0,0 @@ -/* 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 deleted file mode 100644 index b4fb341..0000000 --- a/media/java/android/media/session/RouteOptions.java +++ /dev/null @@ -1,164 +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.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. - * @hide - */ -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 deleted file mode 100644 index 8211983..0000000 --- a/media/java/android/media/session/RoutePlaybackControls.java +++ /dev/null @@ -1,163 +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.MediaMetadata; -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. - * @hide - */ -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/services/core/java/com/android/server/media/MediaRouteProviderProxy.java b/services/core/java/com/android/server/media/MediaRouteProviderProxy.java deleted file mode 100644 index b31153b..0000000 --- a/services/core/java/com/android/server/media/MediaRouteProviderProxy.java +++ /dev/null @@ -1,419 +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 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.MediaSession; -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.io.PrintWriter; -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; - // Interfaces declared in the manifest - private final ArrayList<String> mInterfaces = new ArrayList<String>(); - private final ArrayList<RouteConnectionRecord> mConnections - = new ArrayList<RouteConnectionRecord>(); - // The sessions that have a route from this provider selected - private final ArrayList<MediaSessionRecord> mSessions = new ArrayList<MediaSessionRecord>(); - private final Handler mHandler = new Handler(); - - private Intent mBindIntent; - private IRouteProvider mBinder; - private boolean mRunning; - private boolean mPaused; - 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; - if (interfaces != null) { - mInterfaces.addAll(interfaces); - } - mBindIntent = new Intent(RouteProviderService.SERVICE_INTERFACE); - mBindIntent.setComponent(mComponentName); - } - - public void destroy() { - stop(); - mSessions.clear(); - updateBinding(); - } - - /** - * 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, mComponentName.getPackageName(), mUserId); - 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; - } - - public void addSession(MediaSessionRecord session) { - mSessions.add(session); - } - - public void removeSession(MediaSessionRecord session) { - mSessions.remove(session); - updateBinding(); - } - - public int getSessionCount() { - return mSessions.size(); - } - - public void dump(PrintWriter pw, String prefix) { - pw.println(prefix + mId + " " + this); - String indent = prefix + " "; - - pw.println(indent + "component=" + mComponentName.toString()); - pw.println(indent + "user id=" + mUserId); - pw.println(indent + "interfaces=" + mInterfaces.toString()); - pw.println(indent + "connections=" + mConnections.toString()); - pw.println(indent + "running=" + mRunning); - pw.println(indent + "interested=" + mInterested); - pw.println(indent + "bound=" + mBound); - } - - private void updateBinding() { - if (shouldBind()) { - bind(); - } else { - unbind(); - } - } - - // We want to bind as long as we're interested in this provider or there are - // sessions connected to it. - private boolean shouldBind() { - return (mRunning && mInterested) || (!mSessions.isEmpty()); - } - - 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 deleted file mode 100644 index 734eab9..0000000 --- a/services/core/java/com/android/server/media/MediaRouteProviderWatcher.java +++ /dev/null @@ -1,239 +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 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); - } - } - - // Stop discovering providers and routes. Providers that still have an - // active session connected to them will not unbind. - public void stop() { - if (mRunning) { - mRunning = false; - - mContext.unregisterReceiver(mScanPackagesReceiver); - mHandler.removeCallbacks(mScanPackagesRunnable); - - // Stop all inactive providers. - for (int i = mProviders.size() - 1; i >= 0; i--) { - mProviders.get(i).stop(); - } - } - } - - // Clean up the providers forcibly unbinding if necessary - public void destroy() { - for (int i = mProviders.size() - 1; i >= 0; i--) { - mProviders.get(i).destroy(); - mProviders.remove(i); - } - } - - 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 0fbcd7e..341c7a9 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecord.java +++ b/services/core/java/com/android/server/media/MediaSessionRecord.java @@ -19,19 +19,16 @@ package com.android.server.media; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.media.routeprovider.RouteRequest; +import android.media.routing.IMediaRouter; +import android.media.routing.IMediaRouterDelegate; +import android.media.routing.IMediaRouterStateCallback; import android.media.session.ISessionController; import android.media.session.ISessionControllerCallback; import android.media.session.ISession; import android.media.session.ISessionCallback; import android.media.session.MediaController; -import android.media.session.RouteCommand; -import android.media.session.RouteInfo; -import android.media.session.RouteOptions; -import android.media.session.RouteEvent; import android.media.session.MediaSession; import android.media.session.MediaSessionInfo; -import android.media.session.RouteInterface; import android.media.session.PlaybackState; import android.media.session.ParcelableVolumeInfo; import android.media.AudioManager; @@ -94,14 +91,9 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { private final Object mLock = new Object(); private final ArrayList<ISessionControllerCallback> mControllerCallbacks = new ArrayList<ISessionControllerCallback>(); - private final ArrayList<RouteRequest> mRequests = new ArrayList<RouteRequest>(); - private RouteInfo mRoute; - private RouteOptions mRequest; - private RouteConnectionRecord mConnection; - // TODO define a RouteState class with relevant info - private int mRouteState; private long mFlags; + private IMediaRouter mMediaRouter; private ComponentName mMediaButtonReceiver; // TransportPerformer fields @@ -160,24 +152,6 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { } /** - * 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. @@ -229,41 +203,6 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { } /** - * 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) { - disconnect(MediaSession.DISCONNECT_REASON_ROUTE_CHANGED); - } - 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); - } - - /** * Send a volume adjustment to the session owner. * * @param delta The amount to adjust the volume by. @@ -338,40 +277,6 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { } /** - * 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 (mDestroyed) { - Log.i(TAG, "setRouteConnected: session has been destroyed"); - connection.disconnect(); - return false; - } - if (mRoute == null || !TextUtils.equals(route.getId(), mRoute.getId())) { - Log.w(TAG, "setRouteConnected: connected route is stale"); - connection.disconnect(); - return false; - } - if (request != mRequest) { - Log.w(TAG, "setRouteConnected: connection request is stale"); - connection.disconnect(); - return false; - } - mConnection = connection; - mConnection.setListener(mConnectionListener); - mSessionCb.sendRouteConnected(); - } - return true; - } - - /** * Check if this session has been set to active by the app. * * @return True if the session is active, false otherwise. @@ -460,30 +365,6 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { return mOptimisticVolume; } - /** - * @return True if this session is currently connected to a route. - */ - public boolean isConnected() { - return mConnection != null; - } - - public void disconnect(int reason) { - synchronized (mLock) { - if (!mDestroyed) { - disconnectLocked(reason); - } - } - } - - private void disconnectLocked(int reason) { - if (mConnection != null) { - mConnection.setListener(null); - mConnection.disconnect(); - mConnection = null; - pushDisconnected(reason); - } - } - public boolean isTransportControlEnabled() { return hasFlag(MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); } @@ -502,11 +383,6 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { if (mDestroyed) { return; } - if (isConnected()) { - disconnectLocked(MediaSession.DISCONNECT_REASON_SESSION_DESTROYED); - } - mRoute = null; - mRequest = null; mDestroyed = true; } } @@ -532,15 +408,6 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { pw.println(indent + "controllers: " + mControllerCallbacks.size()); pw.println(indent + "state=" + (mPlaybackState == null ? null : mPlaybackState.toString())); pw.println(indent + "metadata:" + getShortMetadataString()); - pw.println(indent + "route requests {"); - int size = mRequests.size(); - for (int i = 0; i < size; i++) { - pw.println(indent + " " + mRequests.get(i).toString()); - } - pw.println(indent + "}"); - pw.println(indent + "route=" + (mRoute == null ? null : mRoute.toString())); - pw.println(indent + "connection=" + (mConnection == null ? null : mConnection.toString())); - pw.println(indent + "params=" + (mRequest == null ? null : mRequest.toString())); } private String getShortMetadataString() { @@ -550,12 +417,6 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { return "size=" + fields + ", title=" + title; } - private void pushDisconnected(int reason) { - synchronized (mLock) { - mSessionCb.sendRouteDisconnected(reason); - } - } - private void pushPlaybackStateUpdate() { synchronized (mLock) { if (mDestroyed) { @@ -613,25 +474,6 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { } } - private void pushRouteUpdate() { - synchronized (mLock) { - if (mDestroyed) { - return; - } - for (int i = mControllerCallbacks.size() - 1; i >= 0; i--) { - ISessionControllerCallback cb = mControllerCallbacks.get(i); - try { - cb.onRouteChanged(mRoute); - } catch (DeadObjectException e) { - Log.w(TAG, "Removing dead callback in pushRouteUpdate.", e); - mControllerCallbacks.remove(i); - } catch (RemoteException e) { - Log.w(TAG, "unexpected exception in pushRouteUpdate.", e); - } - } - } - } - private void pushEvent(String event, Bundle data) { synchronized (mLock) { if (mDestroyed) { @@ -651,25 +493,6 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { } } - private void pushRouteCommand(RouteCommand command, ResultReceiver cb) { - synchronized (mLock) { - if (mDestroyed) { - return; - } - 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 PlaybackState getStateWithUpdatedPosition() { PlaybackState state = mPlaybackState; long duration = -1; @@ -708,21 +531,6 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { return -1; } - private final RouteConnectionRecord.Listener mConnectionListener - = new RouteConnectionRecord.Listener() { - @Override - public void onEvent(RouteEvent event) { - RouteEvent eventForSession = new RouteEvent(null, event.getIface(), - event.getEvent(), event.getExtras()); - mSessionCb.sendRouteEvent(eventForSession); - } - - @Override - public void disconnect() { - MediaSessionRecord.this.disconnect(MediaSession.DISCONNECT_REASON_PROVIDER_DISCONNECTED); - } - }; - private final Runnable mClearOptimisticVolumeRunnable = new Runnable() { @Override public void run() { @@ -769,6 +577,12 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { } @Override + public void setMediaRouter(IMediaRouter router) { + mMediaRouter = router; + mHandler.post(MessageHandler.MSG_UPDATE_SESSION_STATE); + } + + @Override public void setMediaButtonReceiver(ComponentName mbr) { mMediaButtonReceiver = mbr; } @@ -797,46 +611,6 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { } @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 disconnectFromRoute(RouteInfo route) { - if (route != null && mRoute != null - && TextUtils.equals(route.getId(), mRoute.getId())) { - disconnect(MediaSession.DISCONNECT_REASON_SESSION_DISCONNECTED); - } - } - - @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); - } - } - - @Override public void setCurrentVolume(int volume) { mCurrentVolume = volume; mHandler.post(MessageHandler.MSG_UPDATE_VOLUME); @@ -903,46 +677,6 @@ 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 sendRouteDisconnected(int reason) { - try { - mCb.onRouteDisconnected(mRoute, reason); - } catch (RemoteException e) { - Slog.e(TAG, "Remote failure in sendRouteDisconnected"); - } - } - public void play() { try { mCb.onPlay(); @@ -1187,20 +921,19 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { } @Override - public void showRoutePicker() { - mService.showRoutePickerForSession(MediaSessionRecord.this); + public IMediaRouterDelegate createMediaRouterDelegate( + IMediaRouterStateCallback callback) { + // todo + return null; } } private class MessageHandler extends Handler { private static final int MSG_UPDATE_METADATA = 1; 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; - private static final int MSG_UPDATE_SESSION_STATE = 7; - private static final int MSG_UPDATE_VOLUME = 8; + private static final int MSG_SEND_EVENT = 3; + private static final int MSG_UPDATE_SESSION_STATE = 4; + private static final int MSG_UPDATE_VOLUME = 5; public MessageHandler(Looper looper) { super(looper); @@ -1214,17 +947,9 @@ public class MediaSessionRecord implements IBinder.DeathRecipient { case MSG_UPDATE_PLAYBACK_STATE: pushPlaybackStateUpdate(); break; - case MSG_UPDATE_ROUTE: - pushRouteUpdate(); - break; 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; case MSG_UPDATE_SESSION_STATE: // TODO add session state break; diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java index 310f3e9..4c475d9 100644 --- a/services/core/java/com/android/server/media/MediaSessionService.java +++ b/services/core/java/com/android/server/media/MediaSessionService.java @@ -30,14 +30,11 @@ import android.content.pm.PackageManager; import android.media.AudioManager; import android.media.IAudioService; import android.media.IRemoteVolumeController; -import android.media.routeprovider.RouteRequest; import android.media.session.IActiveSessionsListener; import android.media.session.ISession; import android.media.session.ISessionCallback; import android.media.session.ISessionManager; import android.media.session.MediaSession; -import android.media.session.RouteInfo; -import android.media.session.RouteOptions; import android.os.Binder; import android.os.Bundle; import android.os.Handler; @@ -74,15 +71,12 @@ public class MediaSessionService extends SystemService implements Monitor { private static final int WAKELOCK_TIMEOUT = 5000; private final SessionManagerImpl mSessionManagerImpl; - // private final MediaRouteProviderWatcher mRouteProviderWatcher; private final MediaSessionStack mPriorityStack; private final ArrayList<MediaSessionRecord> mAllSessions = new ArrayList<MediaSessionRecord>(); private final SparseArray<UserRecord> mUserRecords = new SparseArray<UserRecord>(); private final ArrayList<SessionsListenerRecord> mSessionsListeners = new ArrayList<SessionsListenerRecord>(); - // private final ArrayList<MediaRouteProviderProxy> mProviders - // = new ArrayList<MediaRouteProviderProxy>(); private final Object mLock = new Object(); private final MessageHandler mHandler = new MessageHandler(); private final PowerManager.WakeLock mMediaEventWakeLock; @@ -94,10 +88,6 @@ public class MediaSessionService extends SystemService implements Monitor { private MediaSessionRecord mPrioritySession; private int mCurrentUserId = -1; - // 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; - // Used to notify system UI when remote volume was changed. TODO find a // better way to handle this. private IRemoteVolumeController mRvc; @@ -126,69 +116,6 @@ public class MediaSessionService extends SystemService implements Monitor { return IAudioService.Stub.asInterface(b); } - /** - * 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) - synchronized (mLock) { - if (!mAllSessions.contains(record)) { - Log.d(TAG, "Unknown session tried to show route picker. Ignoring."); - return; - } - RouteInfo current = record.getRoute(); - UserRecord user = mUserRecords.get(record.getUserId()); - if (current != null) { - // For now send null to mean the local route - MediaRouteProviderProxy proxy = user.getProviderLocked(current.getProvider()); - if (proxy != null) { - proxy.removeSession(record); - } - record.selectRoute(null); - return; - } - ArrayList<MediaRouteProviderProxy> providers = user.getProvidersLocked(); - mShowRoutesRequestId++; - 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) { - if (!mAllSessions.contains(session)) { - Log.d(TAG, "Unknown session attempting to connect to route. Ignoring"); - return; - } - UserRecord user = mUserRecords.get(session.getUserId()); - if (user == null) { - Log.wtf(TAG, "connectToRoute: User " + session.getUserId() + " does not exist."); - return; - } - MediaRouteProviderProxy proxy = user.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); - proxy.connectToRoute(session, route, request); - } - } - public void updateSession(MediaSessionRecord record) { synchronized (mLock) { if (!mAllSessions.contains(record)) { @@ -552,110 +479,49 @@ public class MediaSessionService extends SystemService implements Monitor { } } - 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 = mAllSessions.get(index); - RouteInfo route = routes.get(0); - record.selectRoute(route); - UserRecord user = mUserRecords.get(record.getUserId()); - MediaRouteProviderProxy provider = user.getProviderLocked(route.getProvider()); - provider.addSession(record); - } - } - } - - @Override - public void onRouteConnected(String sessionId, RouteInfo route, - RouteRequest options, RouteConnectionRecord connection) { - synchronized (mLock) { - int index = findIndexOfSessionForIdLocked(sessionId); - if (index != -1) { - MediaSessionRecord session = mAllSessions.get(index); - session.setRouteConnected(route, options.getConnectionOptions(), connection); - } - } - } - }; - /** * Information about a particular user. The contents of this object is * guarded by mLock. */ final class UserRecord { private final int mUserId; - private final MediaRouteProviderWatcher mRouteProviderWatcher; - private final ArrayList<MediaRouteProviderProxy> mProviders - = new ArrayList<MediaRouteProviderProxy>(); private final ArrayList<MediaSessionRecord> mSessions = new ArrayList<MediaSessionRecord>(); public UserRecord(Context context, int userId) { mUserId = userId; - mRouteProviderWatcher = new MediaRouteProviderWatcher(context, - mProviderWatcherCallback, mHandler, userId); } public void startLocked() { - mRouteProviderWatcher.start(); + // nothing for now } public void stopLocked() { - mRouteProviderWatcher.stop(); - updateInterestLocked(); + // nothing for now } public void destroyLocked() { for (int i = mSessions.size() - 1; i >= 0; i--) { MediaSessionRecord session = mSessions.get(i); MediaSessionService.this.destroySessionLocked(session); - if (session.isConnected()) { - session.disconnect(MediaSession.DISCONNECT_REASON_USER_STOPPING); - } } } - public ArrayList<MediaRouteProviderProxy> getProvidersLocked() { - return mProviders; - } - public ArrayList<MediaSessionRecord> getSessionsLocked() { return mSessions; } public void addSessionLocked(MediaSessionRecord session) { mSessions.add(session); - updateInterestLocked(); } public void removeSessionLocked(MediaSessionRecord session) { mSessions.remove(session); - RouteInfo route = session.getRoute(); - if (route != null) { - MediaRouteProviderProxy provider = getProviderLocked(route.getProvider()); - if (provider != null) { - provider.removeSession(session); - } - } - updateInterestLocked(); } public void dumpLocked(PrintWriter pw, String prefix) { pw.println(prefix + "Record for user " + mUserId); String indent = prefix + " "; - int size = mProviders.size(); - pw.println(indent + size + " Providers:"); - for (int i = 0; i < size; i++) { - mProviders.get(i).dump(pw, indent); - } - pw.println(); - size = mSessions.size(); + int size = mSessions.size(); pw.println(indent + size + " Sessions:"); for (int i = 0; i < size; i++) { // Just print the session info, the full session dump will @@ -663,48 +529,6 @@ public class MediaSessionService extends SystemService implements Monitor { pw.println(indent + mSessions.get(i).getSessionInfo()); } } - - public void updateInterestLocked() { - // TODO go through the sessions and build up the set of interfaces - // we're interested in. Update the provider watcher. - // For now, just express interest in all providers for the current - // user - boolean interested = mUserId == mCurrentUserId; - for (int i = mProviders.size() - 1; i >= 0; i--) { - mProviders.get(i).setInterested(interested); - } - } - - 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 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); - } - } - }; } final class SessionsListenerRecord implements IBinder.DeathRecipient { diff --git a/services/core/java/com/android/server/media/MediaSessionStack.java b/services/core/java/com/android/server/media/MediaSessionStack.java index e26a2eb..3d1ecb3 100644 --- a/services/core/java/com/android/server/media/MediaSessionStack.java +++ b/services/core/java/com/android/server/media/MediaSessionStack.java @@ -25,7 +25,7 @@ import java.util.ArrayList; /** * Keeps track of media sessions and their priority for notifications, media - * button routing, etc. + * button dispatch, etc. */ public class MediaSessionStack { /** @@ -277,8 +277,8 @@ public class MediaSessionStack { lastActiveIndex++; lastPublishedIndex++; } else if (session.isPlaybackActive(true)) { - // TODO replace getRoute() == null with real local route check - if(session.getRoute() == null) { + // TODO this with real local route check + if (true) { // Active local sessions get top priority result.add(lastLocalIndex, session); lastLocalIndex++; diff --git a/services/core/java/com/android/server/media/RouteConnectionRecord.java b/services/core/java/com/android/server/media/RouteConnectionRecord.java deleted file mode 100644 index 90ddf29..0000000 --- a/services/core/java/com/android/server/media/RouteConnectionRecord.java +++ /dev/null @@ -1,118 +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 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 final String mPackageName; - private final int mUid; - private Listener mListener; - - public RouteConnectionRecord(IRouteConnection binder, String packageName, int uid) { - mBinder = binder; - mPackageName = packageName; - mUid = uid; - } - - /** - * 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(); - } - } - - @Override - public String toString() { - return "RouteConnection { binder=" + mBinder.toString() + ", package=" + mPackageName - + ", uid=" + mUid + "}"; - } - - /** - * 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/Android.mk b/tests/OneMedia/Android.mk index 93b9c9a..4feac68 100644 --- a/tests/OneMedia/Android.mk +++ b/tests/OneMedia/Android.mk @@ -10,8 +10,7 @@ LOCAL_PACKAGE_NAME := OneMedia LOCAL_CERTIFICATE := platform LOCAL_STATIC_JAVA_LIBRARIES := \ - android-support-v7-appcompat \ - android-support-v7-mediarouter + android-support-media-protocols LOCAL_PROGUARD_ENABLED := disabled diff --git a/tests/OneMedia/AndroidManifest.xml b/tests/OneMedia/AndroidManifest.xml index 504d471..9d78ca5 100644 --- a/tests/OneMedia/AndroidManifest.xml +++ b/tests/OneMedia/AndroidManifest.xml @@ -27,11 +27,11 @@ android:process="com.android.onemedia.service" /> <service android:name=".provider.OneMediaRouteProvider" - android:permission="android.permission.BIND_ROUTE_PROVIDER" + android:permission="android.permission.BIND_MEDIA_ROUTE_SERVICE" android:exported="true" android:process="com.android.onemedia.provider"> <intent-filter> - <action android:name="com.android.media.session.MediaRouteProvider" /> + <action android:name="android.media.routing.MediaRouteService" /> </intent-filter> </service> </application> diff --git a/tests/OneMedia/src/com/android/onemedia/OnePlayerActivity.java b/tests/OneMedia/src/com/android/onemedia/OnePlayerActivity.java index ee407ad..894377b 100644 --- a/tests/OneMedia/src/com/android/onemedia/OnePlayerActivity.java +++ b/tests/OneMedia/src/com/android/onemedia/OnePlayerActivity.java @@ -28,8 +28,6 @@ import android.widget.CheckBox; import android.widget.EditText; import android.widget.TextView; -import com.android.onemedia.playback.Renderer; - public class OnePlayerActivity extends Activity { private static final String TAG = "OnePlayerActivity"; diff --git a/tests/OneMedia/src/com/android/onemedia/PlayerController.java b/tests/OneMedia/src/com/android/onemedia/PlayerController.java index 145b389..802f473 100644 --- a/tests/OneMedia/src/com/android/onemedia/PlayerController.java +++ b/tests/OneMedia/src/com/android/onemedia/PlayerController.java @@ -18,7 +18,6 @@ package com.android.onemedia; import android.media.MediaMetadata; import android.media.session.MediaController; -import android.media.session.RouteInfo; import android.media.session.MediaSessionManager; import android.media.session.PlaybackState; import android.os.Bundle; @@ -121,7 +120,7 @@ public class PlayerController { } public void showRoutePicker() { - mController.showRoutePicker(); + // TODO } private void unbindFromService() { @@ -173,11 +172,6 @@ public class PlayerController { private class SessionCallback extends MediaController.Callback { @Override - public void onRouteChanged(RouteInfo route) { - // TODO - } - - @Override public void onPlaybackStateChanged(PlaybackState state) { if (state == null) { return; diff --git a/tests/OneMedia/src/com/android/onemedia/PlayerSession.java b/tests/OneMedia/src/com/android/onemedia/PlayerSession.java index a220107..7c0eabe 100644 --- a/tests/OneMedia/src/com/android/onemedia/PlayerSession.java +++ b/tests/OneMedia/src/com/android/onemedia/PlayerSession.java @@ -17,14 +17,17 @@ package com.android.onemedia; import android.content.Context; import android.content.Intent; -import android.media.session.Route; -import android.media.session.RouteInfo; -import android.media.session.RouteOptions; -import android.media.session.RoutePlaybackControls; +import android.media.routing.MediaRouteSelector; +import android.media.routing.MediaRouter; +import android.media.routing.MediaRouter.ConnectionRequest; +import android.media.routing.MediaRouter.DestinationInfo; +import android.media.routing.MediaRouter.RouteInfo; import android.media.session.MediaSession; import android.media.session.MediaSessionManager; import android.media.session.PlaybackState; import android.os.Bundle; +import android.support.media.protocols.MediaPlayerProtocol; +import android.support.media.protocols.MediaPlayerProtocol.MediaStatus; import android.util.Log; import android.view.KeyEvent; @@ -34,11 +37,13 @@ import com.android.onemedia.playback.Renderer; import com.android.onemedia.playback.RequestUtils; import java.util.ArrayList; +import java.util.List; public class PlayerSession { private static final String TAG = "PlayerSession"; protected MediaSession mSession; + protected MediaRouter mRouter; protected Context mContext; protected Renderer mRenderer; protected MediaSession.Callback mCallback; @@ -46,10 +51,6 @@ public class PlayerSession { protected PlaybackState mPlaybackState; protected Listener mListener; - protected ArrayList<RouteOptions> mRouteOptions; - protected Route mRoute; - protected RoutePlaybackControls mRouteControls; - protected RouteListener mRouteListener; private String mContent; @@ -63,41 +64,53 @@ public class PlayerSession { | 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(); - } + releaseSession(); + MediaSessionManager man = (MediaSessionManager) mContext .getSystemService(Context.MEDIA_SESSION_SERVICE); Log.d(TAG, "Creating session for package " + mContext.getBasePackageName()); + + mRouter = new MediaRouter(mContext); + mRouter.addSelector(new MediaRouteSelector.Builder() + .addRequiredProtocol(MediaPlayerProtocol.class) + .build()); + mRouter.addSelector(new MediaRouteSelector.Builder() + .setRequiredFeatures(MediaRouter.ROUTE_FEATURE_LIVE_AUDIO) + .setOptionalFeatures(MediaRouter.ROUTE_FEATURE_LIVE_VIDEO) + .build()); + mRouter.setRoutingCallback(new RoutingCallback(), null); + mSession = man.createSession("OneMedia"); mSession.addCallback(mCallback); mSession.addTransportControlsCallback(new TransportCallback()); mSession.setPlaybackState(mPlaybackState); mSession.setFlags(MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); - mSession.setRouteOptions(mRouteOptions); + mSession.setMediaRouter(mRouter); mSession.setActive(true); } public void onDestroy() { - if (mSession != null) { - mSession.release(); - } + releaseSession(); if (mRenderer != null) { mRenderer.unregisterListener(mRenderListener); mRenderer.onDestroy(); } } + private void releaseSession() { + if (mSession != null) { + mSession.release(); + mSession = null; + } + if (mRouter != null) { + mRouter.release(); + mRouter = null; + } + } + public void setListener(Listener listener) { mListener = listener; } @@ -218,40 +231,6 @@ 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.STATE_NONE); - } else { - // Use remote route - mSession.connect(route, mRouteOptions.get(0)); - mRenderer = null; - updateState(PlaybackState.STATE_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.STATE_NONE); - } - - @Override - public void onRouteDisconnected(Route route, int reason) { - - } } private class TransportCallback extends MediaSession.TransportControlsCallback { @@ -266,12 +245,62 @@ public class PlayerSession { } } - private class RouteListener extends RoutePlaybackControls.Listener { + private class RoutingCallback extends MediaRouter.RoutingCallback { @Override - public void onPlaybackStateChange(int state) { - Log.d(TAG, "Updating state to " + state); - updateState(state); + public void onConnectionStateChanged(int state) { + if (state == MediaRouter.CONNECTION_STATE_CONNECTING) { + if (mRenderer != null) { + mRenderer.onStop(); + } + mRenderer = null; + updateState(PlaybackState.STATE_CONNECTING); + return; + } + + MediaRouter.ConnectionInfo connection = mRouter.getConnection(); + if (connection != null) { + MediaPlayerProtocol protocol = + connection.getProtocolObject(MediaPlayerProtocol.class); + if (protocol != null) { + Log.d(TAG, "Connected to route using media player protocol"); + + protocol.setCallback(new PlayerCallback(), null); + mRenderer = new OneMRPRenderer(protocol); + updateState(PlaybackState.STATE_NONE); + return; + } + } + + // Use local route + mRenderer = new LocalRenderer(mContext, null); + mRenderer.registerListener(mRenderListener); + updateState(PlaybackState.STATE_NONE); } } + private class PlayerCallback extends MediaPlayerProtocol.Callback { + @Override + public void onStatusUpdated(MediaStatus status, Bundle extras) { + if (status != null) { + Log.d(TAG, "Received status update: " + status.toBundle()); + switch (status.getPlayerState()) { + case MediaStatus.PLAYER_STATE_BUFFERING: + updateState(PlaybackState.STATE_BUFFERING); + break; + case MediaStatus.PLAYER_STATE_IDLE: + updateState(PlaybackState.STATE_STOPPED); + break; + case MediaStatus.PLAYER_STATE_PAUSED: + updateState(PlaybackState.STATE_PAUSED); + break; + case MediaStatus.PLAYER_STATE_PLAYING: + updateState(PlaybackState.STATE_PLAYING); + break; + case MediaStatus.PLAYER_STATE_UNKNOWN: + updateState(PlaybackState.STATE_NONE); + break; + } + } + } + } } diff --git a/tests/OneMedia/src/com/android/onemedia/playback/MediaItem.java b/tests/OneMedia/src/com/android/onemedia/playback/MediaItem.java index 05516d2..c133325 100644 --- a/tests/OneMedia/src/com/android/onemedia/playback/MediaItem.java +++ b/tests/OneMedia/src/com/android/onemedia/playback/MediaItem.java @@ -15,10 +15,10 @@ */ package com.android.onemedia.playback; +import android.media.MediaMetadata; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; -import android.support.v7.media.MediaItemMetadata; /** * TODO: Insert description here. (generated by epastern) @@ -35,11 +35,11 @@ public class MediaItem implements Parcelable { } public String getTitle() { - return mBundle.getString(MediaItemMetadata.KEY_TITLE); + return mBundle.getString(MediaMetadata.METADATA_KEY_TITLE); } public String getArtist() { - return mBundle.getString(MediaItemMetadata.KEY_ALBUM_ARTIST); + return mBundle.getString(MediaMetadata.METADATA_KEY_ALBUM_ARTIST); } /* (non-Javadoc) diff --git a/tests/OneMedia/src/com/android/onemedia/playback/OneMRPRenderer.java b/tests/OneMedia/src/com/android/onemedia/playback/OneMRPRenderer.java index 9b0a2b2..55eb92c 100644 --- a/tests/OneMedia/src/com/android/onemedia/playback/OneMRPRenderer.java +++ b/tests/OneMedia/src/com/android/onemedia/playback/OneMRPRenderer.java @@ -1,39 +1,42 @@ package com.android.onemedia.playback; -import android.media.session.RoutePlaybackControls; import android.os.Bundle; +import android.support.media.protocols.MediaPlayerProtocol; +import android.support.media.protocols.MediaPlayerProtocol.MediaInfo; /** * Renderer for communicating with the OneMRP route */ public class OneMRPRenderer extends Renderer { - private final RoutePlaybackControls mControls; + private final MediaPlayerProtocol mProtocol; - public OneMRPRenderer(RoutePlaybackControls controls) { + public OneMRPRenderer(MediaPlayerProtocol protocol) { super(null, null); - mControls = controls; + mProtocol = protocol; } @Override public void setContent(Bundle request) { - mControls.playNow(request.getString(RequestUtils.EXTRA_KEY_SOURCE)); + MediaInfo mediaInfo = new MediaInfo(request.getString(RequestUtils.EXTRA_KEY_SOURCE), + MediaInfo.STREAM_TYPE_BUFFERED, "audio/mp3"); + mProtocol.load(mediaInfo, true, 0, null); } @Override public boolean onStop() { - mControls.pause(); + mProtocol.stop(null); return true; } @Override public boolean onPlay() { - mControls.resume(); + mProtocol.play(null); return true; } @Override public boolean onPause() { - mControls.pause(); + mProtocol.pause(null); return true; } diff --git a/tests/OneMedia/src/com/android/onemedia/playback/RequestUtils.java b/tests/OneMedia/src/com/android/onemedia/playback/RequestUtils.java index dd0d982..3778c5f 100644 --- a/tests/OneMedia/src/com/android/onemedia/playback/RequestUtils.java +++ b/tests/OneMedia/src/com/android/onemedia/playback/RequestUtils.java @@ -16,7 +16,6 @@ package com.android.onemedia.playback; import android.os.Bundle; -import android.support.v7.media.MediaItemMetadata; import java.util.HashMap; import java.util.Map; diff --git a/tests/OneMedia/src/com/android/onemedia/provider/OneMediaRouteProvider.java b/tests/OneMedia/src/com/android/onemedia/provider/OneMediaRouteProvider.java index f2d691c..2e1478b 100644 --- a/tests/OneMedia/src/com/android/onemedia/provider/OneMediaRouteProvider.java +++ b/tests/OneMedia/src/com/android/onemedia/provider/OneMediaRouteProvider.java @@ -15,19 +15,20 @@ */ 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.routing.MediaRouteSelector; +import android.media.routing.MediaRouteService; +import android.media.routing.MediaRouter.ConnectionInfo; +import android.media.routing.MediaRouter.ConnectionRequest; +import android.media.routing.MediaRouter.DestinationInfo; +import android.media.routing.MediaRouter.DiscoveryRequest; +import android.media.routing.MediaRouter.RouteInfo; import android.media.session.PlaybackState; import android.os.Bundle; import android.os.Handler; -import android.os.Looper; -import android.os.ResultReceiver; +import android.os.Process; +import android.support.media.protocols.MediaPlayerProtocol; +import android.support.media.protocols.MediaPlayerProtocol.MediaInfo; +import android.support.media.protocols.MediaPlayerProtocol.MediaStatus; import android.util.Log; import com.android.onemedia.playback.LocalRenderer; @@ -35,29 +36,28 @@ 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 { +public class OneMediaRouteProvider extends MediaRouteService { private static final String TAG = "OneMRP"; private static final boolean DEBUG = true; + private static final String TEST_DESTINATION_ID = "testDestination"; + private static final String TEST_ROUTE_ID = "testRoute"; + private Renderer mRenderer; private RenderListener mRenderListener; private PlaybackState mPlaybackState; - private RouteConnection mConnection; - private RoutePlaybackControlsHandler mControls; - private String mRouteId; private Handler mHandler; + private OneStub mStub; + @Override public void onCreate() { mHandler = new Handler(); - mRouteId = UUID.randomUUID().toString(); mRenderer = new LocalRenderer(this, null); mRenderListener = new RenderListener(); mPlaybackState = new PlaybackState(); @@ -65,81 +65,106 @@ public class OneMediaRouteProvider extends RouteProviderService { | PlaybackState.ACTION_PLAY); mRenderer.registerListener(mRenderListener); + } - if (DEBUG) { - Log.d(TAG, "onCreate, routeId is " + mRouteId); + @Override + public ClientSession onCreateClientSession(ClientInfo client) { + if (client.getUid() != Process.myUid()) { + // for testing purposes, only allow connections from this application + // since this provider is not fully featured + return null; } + return new OneSession(client); } - @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()); + private final class OneSession extends ClientSession { + private final ClientInfo mClient; + + public OneSession(ClientInfo client) { + mClient = client; + } + + @Override + public boolean onStartDiscovery(DiscoveryRequest req, DiscoveryCallback callback) { + for (MediaRouteSelector selector : req.getSelectors()) { + if (isMatch(selector)) { + DestinationInfo destination = new DestinationInfo.Builder( + TEST_DESTINATION_ID, getServiceMetadata(), "OneMedia") + .setDescription("Test route from OneMedia app.") + .build(); + ArrayList<RouteInfo> routes = new ArrayList<RouteInfo>(); + routes.add(new RouteInfo.Builder( + TEST_ROUTE_ID, destination, selector).build()); + callback.onDestinationFound(destination, routes); + return true; + } } + return false; } - ArrayList<RouteInfo> result = new ArrayList<RouteInfo>(); - if (bob.getOptionsSize() > 0) { - RouteInfo info = bob.build(); - result.add(info); + + @Override + public void onStopDiscovery() { } - if (DEBUG) { - Log.d(TAG, "getRoutes returning " + result.toString()); + + @Override + public boolean onConnect(ConnectionRequest req, ConnectionCallback callback) { + if (req.getRoute().getId().equals(TEST_ROUTE_ID)) { + mStub = new OneStub(); + ConnectionInfo connection = new ConnectionInfo.Builder(req.getRoute()) + .setProtocolStub(MediaPlayerProtocol.class, mStub) + .build(); + callback.onConnected(connection); + return true; + } + return false; } - return result; - } - @Override - public RouteConnection connect(RouteInfo route, RouteRequest request) { - if (mConnection != null) { - disconnect(mConnection); + @Override + public void onDisconnect() { + mStub = null; } - RouteConnection connection = new RouteConnection(this, route); - mControls = RoutePlaybackControlsHandler.addTo(connection); - mControls.addListener(new PlayHandler(mRouteId), mHandler); - if (DEBUG) { - Log.d(TAG, "Connected to route"); + + private boolean isMatch(MediaRouteSelector selector) { + if (!selector.containsProtocol(MediaPlayerProtocol.class)) { + return false; + } + for (String protocol : selector.getRequiredProtocols()) { + if (!protocol.equals(MediaPlayerProtocol.class.getName())) { + return false; + } + } + return true; } - return connection; } - private class PlayHandler extends RoutePlaybackControlsHandler.Listener { - private final String mRouteId; + private final class OneStub extends MediaPlayerProtocol.Stub { + MediaInfo mMediaInfo; - public PlayHandler(String routeId) { - mRouteId = routeId; + public OneStub() { + super(mHandler); } @Override - public void playNow(String content, ResultReceiver cb) { + public void onLoad(MediaInfo mediaInfo, boolean autoplay, long playPosition, + Bundle extras) { if (DEBUG) { - Log.d(TAG, "Attempting to play " + content); + Log.d(TAG, "Attempting to play " + mediaInfo.getContentId()); } // look up the route and send a play command to it + mMediaInfo = mediaInfo; Bundle bundle = new Bundle(); - bundle.putString(RequestUtils.EXTRA_KEY_SOURCE, content); + bundle.putString(RequestUtils.EXTRA_KEY_SOURCE, mediaInfo.getContentId()); mRenderer.setContent(bundle); - RouteInterfaceHandler.sendResult(cb, RouteInterface.RESULT_SUCCESS, null); } @Override - public boolean resume() { + public void onPlay(Bundle extras) { mRenderer.onPlay(); - return true; } @Override - public boolean pause() { + public void onPause(Bundle extras) { mRenderer.onPause(); - return true; } } @@ -148,9 +173,7 @@ public class OneMediaRouteProvider extends RouteProviderService { @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.STATE_ERROR); - } + sendStatusUpdate(PlaybackState.STATE_ERROR); } @Override @@ -186,7 +209,7 @@ public class OneMediaRouteProvider extends RouteProviderService { break; } - mControls.sendPlaybackChangeEvent(mPlaybackState.getState()); + sendStatusUpdate(mPlaybackState.getState()); } @Override @@ -203,5 +226,36 @@ public class OneMediaRouteProvider extends RouteProviderService { @Override public void onNextStarted() { } + + private void sendStatusUpdate(int state) { + if (mStub != null) { + MediaStatus status = new MediaStatus(1, mStub.mMediaInfo); + switch (state) { + case PlaybackState.STATE_BUFFERING: + case PlaybackState.STATE_FAST_FORWARDING: + case PlaybackState.STATE_REWINDING: + case PlaybackState.STATE_SKIPPING_TO_NEXT: + case PlaybackState.STATE_SKIPPING_TO_PREVIOUS: + status.setPlayerState(MediaStatus.PLAYER_STATE_BUFFERING); + break; + case PlaybackState.STATE_CONNECTING: + case PlaybackState.STATE_STOPPED: + status.setPlayerState(MediaStatus.PLAYER_STATE_IDLE); + break; + case PlaybackState.STATE_PAUSED: + status.setPlayerState(MediaStatus.PLAYER_STATE_PAUSED); + break; + case PlaybackState.STATE_PLAYING: + status.setPlayerState(MediaStatus.PLAYER_STATE_PLAYING); + break; + case PlaybackState.STATE_NONE: + case PlaybackState.STATE_ERROR: + default: + status.setPlayerState(MediaStatus.PLAYER_STATE_UNKNOWN); + break; + } + mStub.sendStatusUpdatedEvent(status, null); + } + } } } |