diff options
Diffstat (limited to 'media/java/android')
21 files changed, 3750 insertions, 0 deletions
diff --git a/media/java/android/media/MediaCodecInfo.java b/media/java/android/media/MediaCodecInfo.java index 01f8193..f470421 100644 --- a/media/java/android/media/MediaCodecInfo.java +++ b/media/java/android/media/MediaCodecInfo.java @@ -207,6 +207,7 @@ public final class MediaCodecInfo { // COLOR_FormatSurface indicates that the data will be a GraphicBuffer metadata reference. // In OMX this is called OMX_COLOR_FormatAndroidOpaque. public static final int COLOR_FormatSurface = 0x7F000789; + public static final int COLOR_Format32BitRGBA8888 = 0x7F00A000; // This corresponds to YUV_420_888 format public static final int COLOR_FormatYUV420Flexible = 0x7F420888; public static final int COLOR_QCOM_FormatYUV420SemiPlanar = 0x7fa30c00; 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/routing/IMediaRouter.aidl b/media/java/android/media/routing/IMediaRouter.aidl new file mode 100644 index 0000000..0abb258 --- /dev/null +++ b/media/java/android/media/routing/IMediaRouter.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 IMediaRouter { + +} + diff --git a/media/java/android/media/routing/IMediaRouterDelegate.aidl b/media/java/android/media/routing/IMediaRouterDelegate.aidl new file mode 100644 index 0000000..35f84c8 --- /dev/null +++ b/media/java/android/media/routing/IMediaRouterDelegate.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 IMediaRouterDelegate { + +} + 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/routing/MediaRouteSelector.aidl b/media/java/android/media/routing/MediaRouteSelector.aidl new file mode 100644 index 0000000..37bfa4a --- /dev/null +++ b/media/java/android/media/routing/MediaRouteSelector.aidl @@ -0,0 +1,18 @@ +/* Copyright 2014, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.media.routing; + +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/routing/ParcelableConnectionInfo.aidl b/media/java/android/media/routing/ParcelableConnectionInfo.aidl new file mode 100644 index 0000000..4a9ec94 --- /dev/null +++ b/media/java/android/media/routing/ParcelableConnectionInfo.aidl @@ -0,0 +1,18 @@ +/* Copyright 2014, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.media.routing; + +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/routing/ParcelableDestinationInfo.aidl b/media/java/android/media/routing/ParcelableDestinationInfo.aidl new file mode 100644 index 0000000..bf1c198 --- /dev/null +++ b/media/java/android/media/routing/ParcelableDestinationInfo.aidl @@ -0,0 +1,18 @@ +/* Copyright 2014, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.media.routing; + +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/routing/ParcelableRouteInfo.aidl b/media/java/android/media/routing/ParcelableRouteInfo.aidl new file mode 100644 index 0000000..126afaa --- /dev/null +++ b/media/java/android/media/routing/ParcelableRouteInfo.aidl @@ -0,0 +1,18 @@ +/* Copyright 2014, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.media.routing; + +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 bd0019f..af3b72e 100644 --- a/media/java/android/media/session/ISession.aidl +++ b/media/java/android/media/session/ISession.aidl @@ -19,6 +19,7 @@ import android.app.PendingIntent; import android.content.pm.ParceledListSlice; import android.media.AudioAttributes; import android.media.MediaMetadata; +import android.media.routing.IMediaRouter; import android.media.session.ISessionController; import android.media.session.PlaybackState; import android.media.session.MediaSession; @@ -34,6 +35,7 @@ interface ISession { ISessionController getController(); void setFlags(int flags); void setActive(boolean active); + void setMediaRouter(in IMediaRouter router); void setMediaButtonReceiver(in PendingIntent mbr); void setLaunchPendingIntent(in PendingIntent pi); void destroy(); diff --git a/media/java/android/media/session/ISessionController.aidl b/media/java/android/media/session/ISessionController.aidl index d684688..e2d06d3 100644 --- a/media/java/android/media/session/ISessionController.aidl +++ b/media/java/android/media/session/ISessionController.aidl @@ -20,6 +20,8 @@ import android.content.Intent; import android.content.pm.ParceledListSlice; 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.ParcelableVolumeInfo; import android.media.session.PlaybackState; @@ -49,6 +51,8 @@ interface ISessionController { void adjustVolume(int direction, int flags, String packageName); void setVolumeTo(int value, int flags, String packageName); + IMediaRouterDelegate createMediaRouterDelegate(IMediaRouterStateCallback callback); + // These commands are for the TransportControls void play(); void playFromMediaId(String uri, in Bundle extras); diff --git a/media/java/android/media/session/MediaController.java b/media/java/android/media/session/MediaController.java index e490c2b..def6f00 100644 --- a/media/java/android/media/session/MediaController.java +++ b/media/java/android/media/session/MediaController.java @@ -26,6 +26,7 @@ import android.media.AudioManager; import android.media.MediaMetadata; import android.media.Rating; import android.media.VolumeProvider; +import android.media.routing.MediaRouter; import android.net.Uri; import android.os.Bundle; import android.os.Handler; @@ -119,6 +120,17 @@ public final class MediaController { } /** + * 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(); + } + + /** * Send the specified media button event to the session. Only media keys can * be sent by this method, other keys will be ignored. * diff --git a/media/java/android/media/session/MediaSession.java b/media/java/android/media/session/MediaSession.java index 86da80a..ba9b2f0 100644 --- a/media/java/android/media/session/MediaSession.java +++ b/media/java/android/media/session/MediaSession.java @@ -29,6 +29,7 @@ import android.media.MediaDescription; import android.media.MediaMetadata; import android.media.Rating; import android.media.VolumeProvider; +import android.media.routing.MediaRouter; import android.net.Uri; import android.os.Bundle; import android.os.Handler; @@ -221,6 +222,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 pending intent for your media button receiver to allow restarting * playback after the session has been stopped. If your app is started in * this way an {@link Intent#ACTION_MEDIA_BUTTON} intent will be sent via |
