/* * Copyright (C) 2013 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.support.v7.media; import android.app.ActivityManager; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.app.ActivityManagerCompat; import android.support.v4.hardware.display.DisplayManagerCompat; import android.support.v4.media.VolumeProviderCompat; import android.support.v4.media.session.MediaSessionCompat; import android.support.v7.media.MediaRouteProvider.ProviderMetadata; import android.util.Log; import android.view.Display; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; /** * MediaRouter allows applications to control the routing of media channels * and streams from the current device to external speakers and destination devices. *
* A MediaRouter instance is retrieved through {@link #getInstance}. Applications * can query the media router about the currently selected route and its capabilities * to determine how to send content to the route's destination. Applications can * also {@link RouteInfo#sendControlRequest send control requests} to the route * to ask the route's destination to perform certain remote control functions * such as playing media. *
* See also {@link MediaRouteProvider} for information on how an application * can publish new media routes to the media router. *
* The media router API is not thread-safe; all interactions with it must be * done from the main thread of the process. *
*/ public final class MediaRouter { private static final String TAG = "MediaRouter"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); // Maintains global media router state for the process. // This field is initialized in MediaRouter.getInstance() before any // MediaRouter objects are instantiated so it is guaranteed to be // valid whenever any instance method is invoked. static GlobalMediaRouter sGlobal; // Context-bound state of the media router. final Context mContext; final ArrayList* When this flag is specified, the media router will actively scan for new * routes. Certain routes, such as wifi display routes, may not be discoverable * except when actively scanning. This flag is typically used when the route picker * dialog has been opened by the user to ensure that the route information is * up to date. *
* Active scanning may consume a significant amount of power and may have intrusive * effects on wireless connectivity. Therefore it is important that active scanning * only be requested when it is actually needed to satisfy a user request to * discover and select a new route. *
* This flag implies {@link #CALLBACK_FLAG_REQUEST_DISCOVERY} but performing * active scans is much more expensive than a normal discovery request. *
* * @see #CALLBACK_FLAG_REQUEST_DISCOVERY */ public static final int CALLBACK_FLAG_PERFORM_ACTIVE_SCAN = 1 << 0; /** * Flag for {@link #addCallback}: Do not filter route events. ** When this flag is specified, the callback will be invoked for events that affect any * route even if they do not match the callback's filter. *
*/ public static final int CALLBACK_FLAG_UNFILTERED_EVENTS = 1 << 1; /** * Flag for {@link #addCallback}: Request passive route discovery while this * callback is registered, except on {@link ActivityManager#isLowRamDevice low-RAM devices}. ** When this flag is specified, the media router will try to discover routes. * Although route discovery is intended to be efficient, checking for new routes may * result in some network activity and could slowly drain the battery. Therefore * applications should only specify {@link #CALLBACK_FLAG_REQUEST_DISCOVERY} when * they are running in the foreground and would like to provide the user with the * option of connecting to new routes. *
* Applications should typically add a callback using this flag in the * {@link android.app.Activity activity's} {@link android.app.Activity#onStart onStart} * method and remove it in the {@link android.app.Activity#onStop onStop} method. * The {@link android.support.v7.app.MediaRouteDiscoveryFragment} fragment may * also be used for this purpose. *
* On {@link ActivityManager#isLowRamDevice low-RAM devices} this flag * will be ignored. Refer to * {@link #addCallback(MediaRouteSelector, Callback, int) addCallback} for details. *
* * @see android.support.v7.app.MediaRouteDiscoveryFragment */ public static final int CALLBACK_FLAG_REQUEST_DISCOVERY = 1 << 2; /** * Flag for {@link #addCallback}: Request passive route discovery while this * callback is registered, even on {@link ActivityManager#isLowRamDevice low-RAM devices}. ** This flag has a significant performance impact on low-RAM devices * since it may cause many media route providers to be started simultaneously. * It is much better to use {@link #CALLBACK_FLAG_REQUEST_DISCOVERY} instead to avoid * performing passive discovery on these devices altogether. Refer to * {@link #addCallback(MediaRouteSelector, Callback, int) addCallback} for details. *
* * @see android.support.v7.app.MediaRouteDiscoveryFragment */ public static final int CALLBACK_FLAG_FORCE_DISCOVERY = 1 << 3; /** * Flag for {@link #isRouteAvailable}: Ignore the default route. ** This flag is used to determine whether a matching non-default route is available. * This constraint may be used to decide whether to offer the route chooser dialog * to the user. There is no point offering the chooser if there are no * non-default choices. *
*/ public static final int AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE = 1 << 0; /** * Flag for {@link #isRouteAvailable}: Require an actual route to be matched. ** If this flag is not set, then {@link #isRouteAvailable} will return true * if it is possible to discover a matching route even if discovery is not in * progress or if no matching route has yet been found. This feature is used to * save resources by removing the need to perform passive route discovery on * {@link ActivityManager#isLowRamDevice low-RAM devices}. *
* If this flag is set, then {@link #isRouteAvailable} will only return true if * a matching route has actually been discovered. *
*/ public static final int AVAILABILITY_FLAG_REQUIRE_MATCH = 1 << 1; MediaRouter(Context context) { mContext = context; } /** * Gets an instance of the media router service associated with the context. ** The application is responsible for holding a strong reference to the returned * {@link MediaRouter} instance, such as by storing the instance in a field of * the {@link android.app.Activity}, to ensure that the media router remains alive * as long as the application is using its features. *
* In other words, the support library only holds a {@link WeakReference weak reference} * to each media router instance. When there are no remaining strong references to the * media router instance, all of its callbacks will be removed and route discovery * will no longer be performed on its behalf. *
* * @return The media router instance for the context. The application must hold * a strong reference to this object as long as it is in use. */ public static MediaRouter getInstance(@NonNull Context context) { if (context == null) { throw new IllegalArgumentException("context must not be null"); } checkCallingThread(); if (sGlobal == null) { sGlobal = new GlobalMediaRouter(context.getApplicationContext()); sGlobal.start(); } return sGlobal.getRouter(context); } /** * Gets information about the {@link MediaRouter.RouteInfo routes} currently known to * this media router. */ public List* The system always provides a default route. *
* * @return The default route, which is guaranteed to never be null. */ @NonNull public RouteInfo getDefaultRoute() { checkCallingThread(); return sGlobal.getDefaultRoute(); } /** * Gets the currently selected route. ** The application should examine the route's * {@link RouteInfo#getControlFilters media control intent filters} to assess the * capabilities of the route before attempting to use it. *
* ** public boolean playMovie() { * MediaRouter mediaRouter = MediaRouter.getInstance(context); * MediaRouter.RouteInfo route = mediaRouter.getSelectedRoute(); * * // First try using the remote playback interface, if supported. * if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) { * // The route supports remote playback. * // Try to send it the Uri of the movie to play. * Intent intent = new Intent(MediaControlIntent.ACTION_PLAY); * intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK); * intent.setDataAndType("http://example.com/videos/movie.mp4", "video/mp4"); * if (route.supportsControlRequest(intent)) { * route.sendControlRequest(intent, null); * return true; // sent the request to play the movie * } * } * * // If remote playback was not possible, then play locally. * if (route.supportsControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO)) { * // The route supports live video streaming. * // Prepare to play content locally in a window or in a presentation. * return playMovieInWindow(); * } * * // Neither interface is supported, so we can't play the movie to this route. * return false; * } ** * @return The selected route, which is guaranteed to never be null. * * @see RouteInfo#getControlFilters * @see RouteInfo#supportsControlCategory * @see RouteInfo#supportsControlRequest */ @NonNull public RouteInfo getSelectedRoute() { checkCallingThread(); return sGlobal.getSelectedRoute(); } /** * Returns the selected route if it matches the specified selector, otherwise * selects the default route and returns it. * * @param selector The selector to match. * @return The previously selected route if it matched the selector, otherwise the * newly selected default route which is guaranteed to never be null. * * @see MediaRouteSelector * @see RouteInfo#matchesSelector * @see RouteInfo#isDefault */ @NonNull public RouteInfo updateSelectedRoute(@NonNull MediaRouteSelector selector) { if (selector == null) { throw new IllegalArgumentException("selector must not be null"); } checkCallingThread(); if (DEBUG) { Log.d(TAG, "updateSelectedRoute: " + selector); } RouteInfo route = sGlobal.getSelectedRoute(); if (!route.isDefault() && !route.matchesSelector(selector)) { route = sGlobal.getDefaultRoute(); sGlobal.selectRoute(route); } return route; } /** * Selects the specified route. * * @param route The route to select. */ public void selectRoute(@NonNull RouteInfo route) { if (route == null) { throw new IllegalArgumentException("route must not be null"); } checkCallingThread(); if (DEBUG) { Log.d(TAG, "selectRoute: " + route); } sGlobal.selectRoute(route); } /** * Returns true if there is a route that matches the specified selector. *
* This method returns true if there are any available routes that match the selector * regardless of whether they are enabled or disabled. If the * {@link #AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE} flag is specified, then * the method will only consider non-default routes. *
* On {@link ActivityManager#isLowRamDevice low-RAM devices} this method * will return true if it is possible to discover a matching route even if * discovery is not in progress or if no matching route has yet been found. * Use {@link #AVAILABILITY_FLAG_REQUIRE_MATCH} to require an actual match. *
* * @param selector The selector to match. * @param flags Flags to control the determination of whether a route may be available. * May be zero or some combination of {@link #AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE} * and {@link #AVAILABILITY_FLAG_REQUIRE_MATCH}. * @return True if a matching route may be available. */ public boolean isRouteAvailable(@NonNull MediaRouteSelector selector, int flags) { if (selector == null) { throw new IllegalArgumentException("selector must not be null"); } checkCallingThread(); return sGlobal.isRouteAvailable(selector, flags); } /** * Registers a callback to discover routes that match the selector and to receive * events when they change. ** This is a convenience method that has the same effect as calling * {@link #addCallback(MediaRouteSelector, Callback, int)} without flags. *
* * @param selector A route selector that indicates the kinds of routes that the * callback would like to discover. * @param callback The callback to add. * @see #removeCallback */ public void addCallback(MediaRouteSelector selector, Callback callback) { addCallback(selector, callback, 0); } /** * Registers a callback to discover routes that match the selector and to receive * events when they change. ** The selector describes the kinds of routes that the application wants to * discover. For example, if the application wants to use * live audio routes then it should include the * {@link MediaControlIntent#CATEGORY_LIVE_AUDIO live audio media control intent category} * in its selector when it adds a callback to the media router. * The selector may include any number of categories. *
* If the callback has already been registered, then the selector is added to * the set of selectors being monitored by the callback. *
* By default, the callback will only be invoked for events that affect routes * that match the specified selector. Event filtering may be disabled by specifying * the {@link #CALLBACK_FLAG_UNFILTERED_EVENTS} flag when the callback is registered. *
* Applications should use the {@link #isRouteAvailable} method to determine * whether is it possible to discover a route with the desired capabilities * and therefore whether the media route button should be shown to the user. *
* The {@link #CALLBACK_FLAG_REQUEST_DISCOVERY} flag should be used while the application * is in the foreground to request that passive discovery be performed if there are * sufficient resources to allow continuous passive discovery. * On {@link ActivityManager#isLowRamDevice low-RAM devices} this flag will be * ignored to conserve resources. *
* The {@link #CALLBACK_FLAG_FORCE_DISCOVERY} flag should be used when * passive discovery absolutely must be performed, even on low-RAM devices. * This flag has a significant performance impact on low-RAM devices * since it may cause many media route providers to be started simultaneously. * It is much better to use {@link #CALLBACK_FLAG_REQUEST_DISCOVERY} instead to avoid * performing passive discovery on these devices altogether. *
* The {@link #CALLBACK_FLAG_PERFORM_ACTIVE_SCAN} flag should be used when the * media route chooser dialog is showing to confirm the presence of available * routes that the user may connect to. This flag may use substantially more * power. *
* ** public class MyActivity extends Activity { * private MediaRouter mRouter; * private MediaRouter.Callback mCallback; * private MediaRouteSelector mSelector; * * protected void onCreate(Bundle savedInstanceState) { * super.onCreate(savedInstanceState); * * mRouter = Mediarouter.getInstance(this); * mCallback = new MyCallback(); * mSelector = new MediaRouteSelector.Builder() * .addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO) * .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK) * .build(); * } * * // Add the callback on start to tell the media router what kinds of routes * // the application is interested in so that it can try to discover suitable ones. * public void onStart() { * super.onStart(); * * mediaRouter.addCallback(mSelector, mCallback, * MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY); * * MediaRouter.RouteInfo route = mediaRouter.updateSelectedRoute(mSelector); * // do something with the route... * } * * // Remove the selector on stop to tell the media router that it no longer * // needs to invest effort trying to discover routes of these kinds for now. * public void onStop() { * super.onStop(); * * mediaRouter.removeCallback(mCallback); * } * * private final class MyCallback extends MediaRouter.Callback { * // Implement callback methods as needed. * } * } ** * @param selector A route selector that indicates the kinds of routes that the * callback would like to discover. * @param callback The callback to add. * @param flags Flags to control the behavior of the callback. * May be zero or a combination of {@link #CALLBACK_FLAG_PERFORM_ACTIVE_SCAN} and * {@link #CALLBACK_FLAG_UNFILTERED_EVENTS}. * @see #removeCallback */ public void addCallback(@NonNull MediaRouteSelector selector, @NonNull Callback callback, @CallbackFlags int flags) { if (selector == null) { throw new IllegalArgumentException("selector must not be null"); } if (callback == null) { throw new IllegalArgumentException("callback must not be null"); } checkCallingThread(); if (DEBUG) { Log.d(TAG, "addCallback: selector=" + selector + ", callback=" + callback + ", flags=" + Integer.toHexString(flags)); } CallbackRecord record; int index = findCallbackRecord(callback); if (index < 0) { record = new CallbackRecord(this, callback); mCallbackRecords.add(record); } else { record = mCallbackRecords.get(index); } boolean updateNeeded = false; if ((flags & ~record.mFlags) != 0) { record.mFlags |= flags; updateNeeded = true; } if (!record.mSelector.contains(selector)) { record.mSelector = new MediaRouteSelector.Builder(record.mSelector) .addSelector(selector) .build(); updateNeeded = true; } if (updateNeeded) { sGlobal.updateDiscoveryRequest(); } } /** * Removes the specified callback. It will no longer receive events about * changes to media routes. * * @param callback The callback to remove. * @see #addCallback */ public void removeCallback(@NonNull Callback callback) { if (callback == null) { throw new IllegalArgumentException("callback must not be null"); } checkCallingThread(); if (DEBUG) { Log.d(TAG, "removeCallback: callback=" + callback); } int index = findCallbackRecord(callback); if (index >= 0) { mCallbackRecords.remove(index); sGlobal.updateDiscoveryRequest(); } } private int findCallbackRecord(Callback callback) { final int count = mCallbackRecords.size(); for (int i = 0; i < count; i++) { if (mCallbackRecords.get(i).mCallback == callback) { return i; } } return -1; } /** * Registers a media route provider within this application process. *
* The provider will be added to the list of providers that all {@link MediaRouter} * instances within this process can use to discover routes. *
* * @param providerInstance The media route provider instance to add. * * @see MediaRouteProvider * @see #removeCallback */ public void addProvider(@NonNull MediaRouteProvider providerInstance) { if (providerInstance == null) { throw new IllegalArgumentException("providerInstance must not be null"); } checkCallingThread(); if (DEBUG) { Log.d(TAG, "addProvider: " + providerInstance); } sGlobal.addProvider(providerInstance); } /** * Unregisters a media route provider within this application process. ** The provider will be removed from the list of providers that all {@link MediaRouter} * instances within this process can use to discover routes. *
* * @param providerInstance The media route provider instance to remove. * * @see MediaRouteProvider * @see #addCallback */ public void removeProvider(@NonNull MediaRouteProvider providerInstance) { if (providerInstance == null) { throw new IllegalArgumentException("providerInstance must not be null"); } checkCallingThread(); if (DEBUG) { Log.d(TAG, "removeProvider: " + providerInstance); } sGlobal.removeProvider(providerInstance); } /** * Adds a remote control client to enable remote control of the volume * of the selected route. ** The remote control client must have previously been registered with * the audio manager using the {@link android.media.AudioManager#registerRemoteControlClient * AudioManager.registerRemoteControlClient} method. *
* * @param remoteControlClient The {@link android.media.RemoteControlClient} to register. */ public void addRemoteControlClient(@NonNull Object remoteControlClient) { if (remoteControlClient == null) { throw new IllegalArgumentException("remoteControlClient must not be null"); } checkCallingThread(); if (DEBUG) { Log.d(TAG, "addRemoteControlClient: " + remoteControlClient); } sGlobal.addRemoteControlClient(remoteControlClient); } /** * Removes a remote control client. * * @param remoteControlClient The {@link android.media.RemoteControlClient} * to unregister. */ public void removeRemoteControlClient(@NonNull Object remoteControlClient) { if (remoteControlClient == null) { throw new IllegalArgumentException("remoteControlClient must not be null"); } if (DEBUG) { Log.d(TAG, "removeRemoteControlClient: " + remoteControlClient); } sGlobal.removeRemoteControlClient(remoteControlClient); } /** * Sets the media session to enable remote control of the volume of the * selected route. This should be used instead of * {@link #addRemoteControlClient} when using media sessions. Set the * session to null to clear it. * * @param mediaSession The {@link android.media.session.MediaSession} to * use. */ public void setMediaSession(Object mediaSession) { if (DEBUG) { Log.d(TAG, "addMediaSession: " + mediaSession); } sGlobal.setMediaSession(mediaSession); } /** * Ensures that calls into the media router are on the correct thread. * It pays to be a little paranoid when global state invariants are at risk. */ static void checkCallingThread() { if (Looper.myLooper() != Looper.getMainLooper()) { throw new IllegalStateException("The media router service must only be " + "accessed on the application's main thread."); } } static* Each media route has a list of {@link MediaControlIntent media control} * {@link #getControlFilters intent filters} that describe the capabilities of the * route and the manner in which it is used and controlled. *
*/ public static final class RouteInfo { private final ProviderInfo mProvider; private final String mDescriptorId; private final String mUniqueId; private String mName; private String mDescription; private boolean mEnabled; private boolean mConnecting; private final ArrayList* The route unique id functions as a stable identifier by which the route is known. * For example, an application can use this id as a token to remember the * selected route across restarts or to communicate its identity to a service. *
* * @return The unique id of the route, never null. */ @NonNull public String getId() { return mUniqueId; } /** * Gets the user-visible name of the route. ** The route name identifies the destination represented by the route. * It may be a user-supplied name, an alias, or device serial number. *
* * @return The user-visible name of a media route. This is the string presented * to users who may select this as the active route. */ public String getName() { return mName; } /** * Gets the user-visible description of the route. ** The route description describes the kind of destination represented by the route. * It may be a user-supplied string, a model number or brand of device. *
* * @return The description of the route, or null if none. */ @Nullable public String getDescription() { return mDescription; } /** * Returns true if this route is enabled and may be selected. * * @return True if this route is enabled. */ public boolean isEnabled() { return mEnabled; } /** * Returns true if the route is in the process of connecting and is not * yet ready for use. * * @return True if this route is in the process of connecting. */ public boolean isConnecting() { return mConnecting; } /** * Returns true if this route is currently selected. * * @return True if this route is currently selected. * * @see MediaRouter#getSelectedRoute */ public boolean isSelected() { checkCallingThread(); return sGlobal.getSelectedRoute() == this; } /** * Returns true if this route is the default route. * * @return True if this route is the default route. * * @see MediaRouter#getDefaultRoute */ public boolean isDefault() { checkCallingThread(); return sGlobal.getDefaultRoute() == this; } /** * Gets a list of {@link MediaControlIntent media control intent} filters that * describe the capabilities of this route and the media control actions that * it supports. * * @return A list of intent filters that specifies the media control intents that * this route supports. * * @see MediaControlIntent * @see #supportsControlCategory * @see #supportsControlRequest */ public List* Media control categories describe the capabilities of this route * such as whether it supports live audio streaming or remote playback. *
* * @param category A {@link MediaControlIntent media control} category * such as {@link MediaControlIntent#CATEGORY_LIVE_AUDIO}, * {@link MediaControlIntent#CATEGORY_LIVE_VIDEO}, * {@link MediaControlIntent#CATEGORY_REMOTE_PLAYBACK}, or a provider-defined * media control category. * @return True if the route supports the specified intent category. * * @see MediaControlIntent * @see #getControlFilters */ public boolean supportsControlCategory(@NonNull String category) { if (category == null) { throw new IllegalArgumentException("category must not be null"); } checkCallingThread(); int count = mControlFilters.size(); for (int i = 0; i < count; i++) { if (mControlFilters.get(i).hasCategory(category)) { return true; } } return false; } /** * Returns true if the route supports the specified * {@link MediaControlIntent media control} category and action. ** Media control actions describe specific requests that an application * can ask a route to perform. *
* * @param category A {@link MediaControlIntent media control} category * such as {@link MediaControlIntent#CATEGORY_LIVE_AUDIO}, * {@link MediaControlIntent#CATEGORY_LIVE_VIDEO}, * {@link MediaControlIntent#CATEGORY_REMOTE_PLAYBACK}, or a provider-defined * media control category. * @param action A {@link MediaControlIntent media control} action * such as {@link MediaControlIntent#ACTION_PLAY}. * @return True if the route supports the specified intent action. * * @see MediaControlIntent * @see #getControlFilters */ public boolean supportsControlAction(@NonNull String category, @NonNull String action) { if (category == null) { throw new IllegalArgumentException("category must not be null"); } if (action == null) { throw new IllegalArgumentException("action must not be null"); } checkCallingThread(); int count = mControlFilters.size(); for (int i = 0; i < count; i++) { IntentFilter filter = mControlFilters.get(i); if (filter.hasCategory(category) && filter.hasAction(action)) { return true; } } return false; } /** * Returns true if the route supports the specified * {@link MediaControlIntent media control} request. ** Media control requests are used to request the route to perform * actions such as starting remote playback of a media item. *
* * @param intent A {@link MediaControlIntent media control intent}. * @return True if the route can handle the specified intent. * * @see MediaControlIntent * @see #getControlFilters */ public boolean supportsControlRequest(@NonNull Intent intent) { if (intent == null) { throw new IllegalArgumentException("intent must not be null"); } checkCallingThread(); ContentResolver contentResolver = sGlobal.getContentResolver(); int count = mControlFilters.size(); for (int i = 0; i < count; i++) { if (mControlFilters.get(i).match(contentResolver, intent, true, TAG) >= 0) { return true; } } return false; } /** * Sends a {@link MediaControlIntent media control} request to be performed * asynchronously by the route's destination. ** Media control requests are used to request the route to perform * actions such as starting remote playback of a media item. *
* This function may only be called on a selected route. Control requests * sent to unselected routes will fail. *
* * @param intent A {@link MediaControlIntent media control intent}. * @param callback A {@link ControlRequestCallback} to invoke with the result * of the request, or null if no result is required. * * @see MediaControlIntent */ public void sendControlRequest(@NonNull Intent intent, @Nullable ControlRequestCallback callback) { if (intent == null) { throw new IllegalArgumentException("intent must not be null"); } checkCallingThread(); sGlobal.sendControlRequest(this, intent, callback); } /** * Gets the type of playback associated with this route. * * @return The type of playback associated with this route: {@link #PLAYBACK_TYPE_LOCAL} * or {@link #PLAYBACK_TYPE_REMOTE}. */ @PlaybackType public int getPlaybackType() { return mPlaybackType; } /** * Gets the audio stream over which the playback associated with this route is performed. * * @return The stream over which the playback associated with this route is performed. */ public int getPlaybackStream() { return mPlaybackStream; } /** * Gets information about how volume is handled on the route. * * @return How volume is handled on the route: {@link #PLAYBACK_VOLUME_FIXED} * or {@link #PLAYBACK_VOLUME_VARIABLE}. */ @PlaybackVolume public int getVolumeHandling() { return mVolumeHandling; } /** * Gets the current volume for this route. Depending on the route, this may only * be valid if the route is currently selected. * * @return The volume at which the playback associated with this route is performed. */ public int getVolume() { return mVolume; } /** * Gets the maximum volume at which the playback associated with this route is performed. * * @return The maximum volume at which the playback associated with * this route is performed. */ public int getVolumeMax() { return mVolumeMax; } /** * Requests a volume change for this route asynchronously. ** This function may only be called on a selected route. It will have * no effect if the route is currently unselected. *
* * @param volume The new volume value between 0 and {@link #getVolumeMax}. */ public void requestSetVolume(int volume) { checkCallingThread(); sGlobal.requestSetVolume(this, Math.min(mVolumeMax, Math.max(0, volume))); } /** * Requests an incremental volume update for this route asynchronously. ** This function may only be called on a selected route. It will have * no effect if the route is currently unselected. *
* * @param delta The delta to add to the current volume. */ public void requestUpdateVolume(int delta) { checkCallingThread(); if (delta != 0) { sGlobal.requestUpdateVolume(this, delta); } } /** * Gets the {@link Display} that should be used by the application to show * a {@link android.app.Presentation} on an external display when this route is selected. * Depending on the route, this may only be valid if the route is currently * selected. ** The preferred presentation display may change independently of the route * being selected or unselected. For example, the presentation display * of the default system route may change when an external HDMI display is connected * or disconnected even though the route itself has not changed. *
* This method may return null if there is no external display associated with * the route or if the display is not ready to show UI yet. *
* The application should listen for changes to the presentation display * using the {@link Callback#onRoutePresentationDisplayChanged} callback and * show or dismiss its {@link android.app.Presentation} accordingly when the display * becomes available or is removed. *
* This method only makes sense for * {@link MediaControlIntent#CATEGORY_LIVE_VIDEO live video} routes. *
* * @return The preferred presentation display to use when this route is * selected or null if none. * * @see MediaControlIntent#CATEGORY_LIVE_VIDEO * @see android.app.Presentation */ @Nullable public Display getPresentationDisplay() { checkCallingThread(); if (mPresentationDisplayId >= 0 && mPresentationDisplay == null) { mPresentationDisplay = sGlobal.getDisplay(mPresentationDisplayId); } return mPresentationDisplay; } /** * Gets a collection of extra properties about this route that were supplied * by its media route provider, or null if none. */ @Nullable public Bundle getExtras() { return mExtras; } /** * Selects this media route. */ public void select() { checkCallingThread(); sGlobal.selectRoute(this); } @Override public String toString() { return "MediaRouter.RouteInfo{ uniqueId=" + mUniqueId + ", name=" + mName + ", description=" + mDescription + ", enabled=" + mEnabled + ", connecting=" + mConnecting + ", playbackType=" + mPlaybackType + ", playbackStream=" + mPlaybackStream + ", volumeHandling=" + mVolumeHandling + ", volume=" + mVolume + ", volumeMax=" + mVolumeMax + ", presentationDisplayId=" + mPresentationDisplayId + ", extras=" + mExtras + ", providerPackageName=" + mProvider.getPackageName() + " }"; } int updateDescriptor(MediaRouteDescriptor descriptor) { int changes = 0; if (mDescriptor != descriptor) { mDescriptor = descriptor; if (descriptor != null) { if (!equal(mName, descriptor.getName())) { mName = descriptor.getName(); changes |= CHANGE_GENERAL; } if (!equal(mDescription, descriptor.getDescription())) { mDescription = descriptor.getDescription(); changes |= CHANGE_GENERAL; } if (mEnabled != descriptor.isEnabled()) { mEnabled = descriptor.isEnabled(); changes |= CHANGE_GENERAL; } if (mConnecting != descriptor.isConnecting()) { mConnecting = descriptor.isConnecting(); changes |= CHANGE_GENERAL; } if (!mControlFilters.equals(descriptor.getControlFilters())) { mControlFilters.clear(); mControlFilters.addAll(descriptor.getControlFilters()); changes |= CHANGE_GENERAL; } if (mPlaybackType != descriptor.getPlaybackType()) { mPlaybackType = descriptor.getPlaybackType(); changes |= CHANGE_GENERAL; } if (mPlaybackStream != descriptor.getPlaybackStream()) { mPlaybackStream = descriptor.getPlaybackStream(); changes |= CHANGE_GENERAL; } if (mVolumeHandling != descriptor.getVolumeHandling()) { mVolumeHandling = descriptor.getVolumeHandling(); changes |= CHANGE_GENERAL | CHANGE_VOLUME; } if (mVolume != descriptor.getVolume()) { mVolume = descriptor.getVolume(); changes |= CHANGE_GENERAL | CHANGE_VOLUME; } if (mVolumeMax != descriptor.getVolumeMax()) { mVolumeMax = descriptor.getVolumeMax(); changes |= CHANGE_GENERAL | CHANGE_VOLUME; } if (mPresentationDisplayId != descriptor.getPresentationDisplayId()) { mPresentationDisplayId = descriptor.getPresentationDisplayId(); mPresentationDisplay = null; changes |= CHANGE_GENERAL | CHANGE_PRESENTATION_DISPLAY; } if (!equal(mExtras, descriptor.getExtras())) { mExtras = descriptor.getExtras(); changes |= CHANGE_GENERAL; } } } return changes; } String getDescriptorId() { return mDescriptorId; } MediaRouteProvider getProviderInstance() { return mProvider.getProviderInstance(); } } /** * Provides information about a media route provider. ** This object may be used to determine which media route provider has * published a particular route. *
*/ public static final class ProviderInfo { private final MediaRouteProvider mProviderInstance; private final ArrayList* A Callback will only receive events relevant to routes that the callback * was registered for unless the {@link MediaRouter#CALLBACK_FLAG_UNFILTERED_EVENTS} * flag was specified in {@link MediaRouter#addCallback(MediaRouteSelector, Callback, int)}. *
* * @see MediaRouter#addCallback(MediaRouteSelector, Callback, int) * @see MediaRouter#removeCallback(Callback) */ public static abstract class Callback { /** * Called when the supplied media route becomes selected as the active route. * * @param router The media router reporting the event. * @param route The route that has been selected. */ public void onRouteSelected(MediaRouter router, RouteInfo route) { } /** * Called when the supplied media route becomes unselected as the active route. * * @param router The media router reporting the event. * @param route The route that has been unselected. */ public void onRouteUnselected(MediaRouter router, RouteInfo route) { } /** * Called when a media route has been added. * * @param router The media router reporting the event. * @param route The route that has become available for use. */ public void onRouteAdded(MediaRouter router, RouteInfo route) { } /** * Called when a media route has been removed. * * @param router The media router reporting the event. * @param route The route that has been removed from availability. */ public void onRouteRemoved(MediaRouter router, RouteInfo route) { } /** * Called when a property of the indicated media route has changed. * * @param router The media router reporting the event. * @param route The route that was changed. */ public void onRouteChanged(MediaRouter router, RouteInfo route) { } /** * Called when a media route's volume changes. * * @param router The media router reporting the event. * @param route The route whose volume changed. */ public void onRouteVolumeChanged(MediaRouter router, RouteInfo route) { } /** * Called when a media route's presentation display changes. ** This method is called whenever the route's presentation display becomes * available, is removed or has changes to some of its properties (such as its size). *
* * @param router The media router reporting the event. * @param route The route whose presentation display changed. * * @see RouteInfo#getPresentationDisplay() */ public void onRoutePresentationDisplayChanged(MediaRouter router, RouteInfo route) { } /** * Called when a media route provider has been added. * * @param router The media router reporting the event. * @param provider The provider that has become available for use. */ public void onProviderAdded(MediaRouter router, ProviderInfo provider) { } /** * Called when a media route provider has been removed. * * @param router The media router reporting the event. * @param provider The provider that has been removed from availability. */ public void onProviderRemoved(MediaRouter router, ProviderInfo provider) { } /** * Called when a property of the indicated media route provider has changed. * * @param router The media router reporting the event. * @param provider The provider that was changed. */ public void onProviderChanged(MediaRouter router, ProviderInfo provider) { } } /** * Callback which is invoked with the result of a media control request. * * @see RouteInfo#sendControlRequest */ public static abstract class ControlRequestCallback { /** * Called when a media control request succeeds. * * @param data Result data, or null if none. * Contents depend on the {@link MediaControlIntent media control action}. */ public void onResult(Bundle data) { } /** * Called when a media control request fails. * * @param error A localized error message which may be shown to the user, or null * if the cause of the error is unclear. * @param data Error data, or null if none. * Contents depend on the {@link MediaControlIntent media control action}. */ public void onError(String error, Bundle data) { } } private static final class CallbackRecord { public final MediaRouter mRouter; public final Callback mCallback; public MediaRouteSelector mSelector; public int mFlags; public CallbackRecord(MediaRouter router, Callback callback) { mRouter = router; mCallback = callback; mSelector = MediaRouteSelector.EMPTY; } public boolean filterRouteEvent(RouteInfo route) { return (mFlags & CALLBACK_FLAG_UNFILTERED_EVENTS) != 0 || route.matchesSelector(mSelector); } } /** * Global state for the media router. ** Media routes and media route providers are global to the process; their * state and the bulk of the media router implementation lives here. *
*/ private static final class GlobalMediaRouter implements SystemMediaRouteProvider.SyncCallback, RegisteredMediaRouteProviderWatcher.Callback { private final Context mApplicationContext; private final ArrayList