[go: nahoru, domu]

blob: f05837849105787589698d1b80fff5d03044ada8 [file] [log] [blame]
/*
* 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 androidx.mediarouter.media;
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
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.IntentSender;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Bundle;
import android.os.Looper;
import android.os.SystemClock;
import android.support.v4.media.session.MediaSessionCompat;
import android.text.TextUtils;
import android.util.Log;
import android.view.Display;
import androidx.annotation.IntDef;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.collection.ArrayMap;
import androidx.core.util.ObjectsCompat;
import androidx.core.util.Pair;
import androidx.mediarouter.app.MediaRouteDiscoveryFragment;
import androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController;
import androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor;
import androidx.mediarouter.media.MediaRouteProvider.ProviderMetadata;
import androidx.mediarouter.media.MediaRouteProvider.RouteController;
import com.google.common.util.concurrent.ListenableFuture;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
/**
* MediaRouter allows applications to control the routing of media channels
* and streams from the current device to external speakers and destination devices.
* <p>
* 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.
* </p><p>
* See also {@link MediaRouteProvider} for information on how an application
* can publish new media routes to the media router.
* </p><p>
* The media router API is not thread-safe; all interactions with it must be
* done from the main thread of the process.
* </p>
*/
// TODO: Add the javadoc for manifest requirements about 'Package visibility' in Android 11
public final class MediaRouter {
// The "Ax" prefix disambiguates from the platform's MediaRouter.
static final String TAG = "AxMediaRouter";
static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
@IntDef({
UNSELECT_REASON_UNKNOWN,
UNSELECT_REASON_DISCONNECTED,
UNSELECT_REASON_STOPPED,
UNSELECT_REASON_ROUTE_CHANGED
})
@Retention(RetentionPolicy.SOURCE)
@interface UnselectReason {}
/**
* Passed to {@link MediaRouteProvider.RouteController#onUnselect(int)},
* {@link Callback#onRouteUnselected(MediaRouter, RouteInfo, int)} and
* {@link Callback#onRouteSelected(MediaRouter, RouteInfo, int)} when the reason the route
* was unselected is unknown.
*/
public static final int UNSELECT_REASON_UNKNOWN = 0;
/**
* Passed to {@link MediaRouteProvider.RouteController#onUnselect(int)},
* {@link Callback#onRouteUnselected(MediaRouter, RouteInfo, int)} and
* {@link Callback#onRouteSelected(MediaRouter, RouteInfo, int)} when the user pressed
* the disconnect button to disconnect and keep playing.
* <p>
*
* @see MediaRouteDescriptor#canDisconnectAndKeepPlaying()
*/
public static final int UNSELECT_REASON_DISCONNECTED = 1;
/**
* Passed to {@link MediaRouteProvider.RouteController#onUnselect(int)},
* {@link Callback#onRouteUnselected(MediaRouter, RouteInfo, int)} and
* {@link Callback#onRouteSelected(MediaRouter, RouteInfo, int)} when the user pressed
* the stop casting button.
* <p>
* Media should stop when this reason is passed.
*/
public static final int UNSELECT_REASON_STOPPED = 2;
/**
* Passed to {@link MediaRouteProvider.RouteController#onUnselect(int)},
* {@link Callback#onRouteUnselected(MediaRouter, RouteInfo, int)} and
* {@link Callback#onRouteSelected(MediaRouter, RouteInfo, int)} when the user selected
* a different route.
*/
public static final int UNSELECT_REASON_ROUTE_CHANGED = 3;
/** Maintains global media router state for the process. */
static GlobalMediaRouter sGlobal;
// Context-bound state of the media router.
final Context mContext;
final ArrayList<CallbackRecord> mCallbackRecords = new ArrayList<>();
@IntDef(
flag = true,
value = {
CALLBACK_FLAG_PERFORM_ACTIVE_SCAN,
CALLBACK_FLAG_REQUEST_DISCOVERY,
CALLBACK_FLAG_UNFILTERED_EVENTS,
CALLBACK_FLAG_FORCE_DISCOVERY
})
@Retention(RetentionPolicy.SOURCE)
private @interface CallbackFlags {}
/**
* Flag for {@link #addCallback}: Actively scan for routes while this callback
* is registered.
* <p>
* 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.
* </p><p>
* 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.
* </p><p>
* This flag implies {@link #CALLBACK_FLAG_REQUEST_DISCOVERY} but performing
* active scans is much more expensive than a normal discovery request.
* </p>
*
* @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.
* <p>
* 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.
* </p>
*/
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}.
* <p>
* 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 this flag when
* they are running in the foreground and would like to provide the user with the
* option of connecting to new routes.
* </p><p>
* 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 MediaRouteDiscoveryFragment} fragment may
* also be used for this purpose.
* </p><p class="note">
* On {@link ActivityManager#isLowRamDevice low-RAM devices} this flag
* will be ignored. Refer to
* {@link #addCallback(MediaRouteSelector, Callback, int) addCallback} for details.
* </p>
*
* @see 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}.
* <p class="note">
* 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.
* </p>
*
* @see MediaRouteDiscoveryFragment
*/
public static final int CALLBACK_FLAG_FORCE_DISCOVERY = 1 << 3;
/**
* Flag for {@link #isRouteAvailable}: Ignore the default route.
* <p>
* 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.
* </p>
*/
public static final int AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE = 1 << 0;
/**
* Flag for {@link #isRouteAvailable}: Require an actual route to be matched.
* <p>
* 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}.
* </p><p>
* If this flag is set, then {@link #isRouteAvailable} will only return true if
* a matching route has actually been discovered.
* </p>
*/
public static final int AVAILABILITY_FLAG_REQUIRE_MATCH = 1 << 1;
/* package */ MediaRouter(Context context) {
mContext = context;
}
/**
* Gets an instance of the media router service associated with the context.
* <p>
* 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.
* </p><p>
* 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.
* </p>
*
* <p>Must be called on the main thread.
*
* @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.
*/
@MainThread
@NonNull
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());
}
// Use sGlobal directly to avoid initialization.
return sGlobal.getRouter(context);
}
/**
* Resets all internal state for testing. Should be only used for testing purpose.
* <p>
* After calling this method, the caller should stop using the existing media router instances.
* Instead, the caller should create a new media router instance again by calling
* {@link #getInstance(Context)}.
* <p>
* Note that the following classes' instances need to be recreated after calling this method,
* as these classes store the media router instance on their constructor:
* <ul>
* <li>{@link androidx.mediarouter.app.MediaRouteActionProvider}
* <li>{@link androidx.mediarouter.app.MediaRouteButton}
* <li>{@link androidx.mediarouter.app.MediaRouteChooserDialog}
* <li>{@link androidx.mediarouter.app.MediaRouteControllerDialog}
* <li>{@link androidx.mediarouter.app.MediaRouteDiscoveryFragment}
* </ul>
*/
@RestrictTo(LIBRARY_GROUP)
public static void resetGlobalRouter() {
if (sGlobal == null) {
return;
}
sGlobal.reset();
sGlobal = null;
}
/** Gets the initialized global router. */
@RestrictTo(LIBRARY_GROUP)
@NonNull
static GlobalMediaRouter getGlobalRouter() {
if (sGlobal == null) {
throw new IllegalStateException(
"getGlobalRouter cannot be called when sGlobal is " + "null");
}
return sGlobal;
}
/**
* Gets information about the {@link MediaRouter.RouteInfo routes} currently known to
* this media router.
*
* <p>Must be called on the main thread.
*/
@MainThread
@NonNull
public List<RouteInfo> getRoutes() {
checkCallingThread();
return getGlobalRouter().getRoutes();
}
/**
* Gets information about the {@link MediaRouter.ProviderInfo route providers}
* currently known to this media router.
*
* <p>Must be called on the main thread.
*/
@MainThread
@NonNull
public List<ProviderInfo> getProviders() {
checkCallingThread();
return getGlobalRouter().getProviders();
}
/**
* Gets the default route for playing media content on the system.
* <p>
* The system always provides a default route.
* </p>
*
* <p>Must be called on the main thread.
*
* @return The default route, which is guaranteed to never be null.
*/
@MainThread
@NonNull
public RouteInfo getDefaultRoute() {
checkCallingThread();
return getGlobalRouter().getDefaultRoute();
}
/**
* Gets a bluetooth route for playing media content on the system.
*
* <p>Must be called on the main thread.
*
* @return A bluetooth route, if exist, otherwise null.
*/
@MainThread
@Nullable
public RouteInfo getBluetoothRoute() {
checkCallingThread();
return getGlobalRouter().getBluetoothRoute();
}
/**
* Gets the currently selected route.
* <p>
* 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.
* </p>
*
* <h3>Example</h3>
* <pre>
* 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;
* }
* </pre>
*
* <p>Must be called on the main thread.
*
* @return The selected route, which is guaranteed to never be null.
* @see RouteInfo#getControlFilters
* @see RouteInfo#supportsControlCategory
* @see RouteInfo#supportsControlRequest
*/
@MainThread
@NonNull
public RouteInfo getSelectedRoute() {
checkCallingThread();
return getGlobalRouter().getSelectedRoute();
}
/**
* Returns the selected route if it matches the specified selector, otherwise
* selects the default route and returns it. If there is one live audio route
* (usually Bluetooth A2DP), it will be selected instead of default route.
*
* <p>Must be called on the main thread.
*
* @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
*/
@MainThread
@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);
}
GlobalMediaRouter globalRouter = getGlobalRouter();
RouteInfo route = globalRouter.getSelectedRoute();
if (!route.isDefaultOrBluetooth() && !route.matchesSelector(selector)) {
route = globalRouter.chooseFallbackRoute();
globalRouter.selectRoute(route, MediaRouter.UNSELECT_REASON_ROUTE_CHANGED);
}
return route;
}
/**
* Selects the specified route.
*
* <p>Must be called on the main thread.
*
* @param route The route to select.
*/
@MainThread
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);
}
getGlobalRouter().selectRoute(route, MediaRouter.UNSELECT_REASON_ROUTE_CHANGED);
}
/**
* Unselects the current route and selects the default route instead.
* <p>
* The reason given must be one of:
* <ul>
* <li>{@link MediaRouter#UNSELECT_REASON_UNKNOWN}</li>
* <li>{@link MediaRouter#UNSELECT_REASON_DISCONNECTED}</li>
* <li>{@link MediaRouter#UNSELECT_REASON_STOPPED}</li>
* <li>{@link MediaRouter#UNSELECT_REASON_ROUTE_CHANGED}</li>
* </ul>
*
* <p>Must be called on the main thread.
*
* @param reason The reason for disconnecting the current route.
*/
@MainThread
public void unselect(@UnselectReason int reason) {
if (reason < MediaRouter.UNSELECT_REASON_UNKNOWN ||
reason > MediaRouter.UNSELECT_REASON_ROUTE_CHANGED) {
throw new IllegalArgumentException("Unsupported reason to unselect route");
}
checkCallingThread();
// Choose the fallback route if it's not already selected.
// Otherwise, select the default route.
GlobalMediaRouter globalRouter = getGlobalRouter();
RouteInfo fallbackRoute = globalRouter.chooseFallbackRoute();
if (globalRouter.getSelectedRoute() != fallbackRoute) {
globalRouter.selectRoute(fallbackRoute, reason);
}
}
/**
* Adds the specified route as a member to the current dynamic group.
*/
@RestrictTo(LIBRARY)
@MainThread
public void addMemberToDynamicGroup(@NonNull RouteInfo route) {
if (route == null) {
throw new NullPointerException("route must not be null");
}
checkCallingThread();
getGlobalRouter().addMemberToDynamicGroup(route);
}
/**
* Removes the specified route from the current dynamic group.
*/
@RestrictTo(LIBRARY)
@MainThread
public void removeMemberFromDynamicGroup(@NonNull RouteInfo route) {
if (route == null) {
throw new NullPointerException("route must not be null");
}
checkCallingThread();
getGlobalRouter().removeMemberFromDynamicGroup(route);
}
/**
* Transfers the current dynamic group to the specified route.
*/
@RestrictTo(LIBRARY)
@MainThread
public void transferToRoute(@NonNull RouteInfo route) {
if (route == null) {
throw new NullPointerException("route must not be null");
}
checkCallingThread();
getGlobalRouter().transferToRoute(route);
}
/**
* Returns true if there is a route that matches the specified selector.
*
* <p>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.
*
* <p class="note">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.
*
* <p>Must be called on the main thread.
*
* @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.
*/
@MainThread
public boolean isRouteAvailable(@NonNull MediaRouteSelector selector, int flags) {
if (selector == null) {
throw new IllegalArgumentException("selector must not be null");
}
checkCallingThread();
return getGlobalRouter().isRouteAvailable(selector, flags);
}
/**
* Registers a callback to discover routes that match the selector and to receive events when
* they change.
*
* <p>This is a convenience method that has the same effect as calling {@link
* #addCallback(MediaRouteSelector, Callback, int)} without flags.
*
* <p>Must be called on the main thread.
*
* @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
*/
@MainThread
public void addCallback(@NonNull MediaRouteSelector selector, @NonNull Callback callback) {
addCallback(selector, callback, 0);
}
/**
* Registers a callback to discover routes that match the selector and to receive events when
* they change.
*
* <p>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.
*
* <p>If the callback has already been registered, then the selector is added to the set of
* selectors being monitored by the callback.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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. Once active scan is requested, it
* will be effective for 30 seconds and will be suppressed after the delay. If you need active
* scan after this duration, you have to add your callback again with the {@link
* #CALLBACK_FLAG_PERFORM_ACTIVE_SCAN} flag.
*
* <h3>Example</h3>
*
* <pre>
* public class MyActivity extends Activity {
* private MediaRouter mRouter;
* private MediaRouter.Callback mCallback;
* private MediaRouteSelector mSelector;
*
* // Add the callback on start to tell the media router what kinds of routes
* // the application is interested in so that it can get events about media routing changes
* // from the system.
* 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();
* mRouter.addCallback(mSelector, mCallback, &#47;* flags= *&#47; 0);
* }
*
* // Add the callback flag CALLBACK_FLAG_REQUEST_DISCOVERY on start by calling
* // addCallback() again so that the media router can try to discover suitable ones.
* public void onStart() {
* super.onStart();
*
* mRouter.addCallback(mSelector, mCallback,
* MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
*
* MediaRouter.RouteInfo route = mRouter.updateSelectedRoute(mSelector);
* // do something with the route...
* }
*
* // Remove the callback flag CALLBACK_FLAG_REQUEST_DISCOVERY on stop by calling
* // addCallback() again in order 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() {
* mRouter.addCallback(mSelector, mCallback, &#47;* flags= *&#47; 0);
*
* super.onStop();
* }
*
* // Remove the callback when the activity is destroyed.
* public void onDestroy() {
* mRouter.removeCallback(mCallback);
*
* super.onDestroy();
* }
*
* private final class MyCallback extends MediaRouter.Callback {
* // Implement callback methods as needed.
* }
* }
* </pre>
*
* <p>Must be called on the main thread.
*
* @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
*/
// TODO: Change the usages of addCallback() for changing flags when setCallbackFlags() is added.
@MainThread
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) {
record.mFlags = flags;
updateNeeded = true;
}
long currentTime = SystemClock.elapsedRealtime();
if ((flags & CALLBACK_FLAG_PERFORM_ACTIVE_SCAN) != 0) {
// If the flag has active scan, the active scan might be suppressed previously if the
// previous change is too long ago. In this case, the discovery request needs to be
// updated so that the active scan state can be true again.
updateNeeded = true;
}
record.mTimestamp = currentTime;
if (!record.mSelector.contains(selector)) {
record.mSelector = new MediaRouteSelector.Builder(record.mSelector)
.addSelector(selector)
.build();
updateNeeded = true;
}
if (updateNeeded) {
getGlobalRouter().updateDiscoveryRequest();
}
}
/**
* Removes the specified callback. It will no longer receive events about
* changes to media routes.
*
* <p>Must be called on the main thread.
*
* @param callback The callback to remove.
* @see #addCallback
*/
@MainThread
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);
getGlobalRouter().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;
}
/**
* Sets a listener for receiving events when the selected route is about to be changed.
*
* <p>Must be called on the main thread.
*/
@MainThread
public void setOnPrepareTransferListener(@Nullable OnPrepareTransferListener listener) {
checkCallingThread();
getGlobalRouter().mOnPrepareTransferListener = listener;
}
/**
* Registers a media route provider within this application process.
* <p>
* The provider will be added to the list of providers that all {@link MediaRouter}
* instances within this process can use to discover routes.
* </p>
*
* <p>Must be called on the main thread.
*
* @param providerInstance The media route provider instance to add.
* @see MediaRouteProvider
* @see #removeCallback
*/
@MainThread
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);
}
getGlobalRouter().addProvider(providerInstance);
}
/**
* Unregisters a media route provider within this application process.
* <p>
* The provider will be removed from the list of providers that all {@link MediaRouter}
* instances within this process can use to discover routes.
* </p>
*
* <p>Must be called on the main thread.
*
* @param providerInstance The media route provider instance to remove.
* @see MediaRouteProvider
* @see #addCallback
*/
@MainThread
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);
}
getGlobalRouter().removeProvider(providerInstance);
}
/**
* Adds a remote control client to enable remote control of the volume of the selected route.
*
* <p>The remote control client must have previously been registered with the audio manager
* using the {@link android.media.AudioManager#registerRemoteControlClient
* AudioManager.registerRemoteControlClient} method.
*
* <p>Must be called on the main thread.
*
* @param remoteControlClient The {@link android.media.RemoteControlClient} to register.
* @deprecated Use {@link #setMediaSessionCompat} instead.
*/
@MainThread
@Deprecated
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);
}
getGlobalRouter()
.addRemoteControlClient((android.media.RemoteControlClient) remoteControlClient);
}
/**
* Removes a remote control client.
*
* <p>Must be called on the main thread.
*
* @param remoteControlClient The {@link android.media.RemoteControlClient} to unregister.
*/
@MainThread
public void removeRemoteControlClient(@NonNull Object remoteControlClient) {
if (remoteControlClient == null) {
throw new IllegalArgumentException("remoteControlClient must not be null");
}
checkCallingThread();
if (DEBUG) {
Log.d(TAG, "removeRemoteControlClient: " + remoteControlClient);
}
getGlobalRouter()
.removeRemoteControlClient((android.media.RemoteControlClient) remoteControlClient);
}
/**
* Equivalent to {@link #setMediaSessionCompat}, except it takes an {@link
* android.media.session.MediaSession}.
*/
@MainThread
public void setMediaSession(@Nullable Object mediaSession) {
checkCallingThread();
if (DEBUG) {
Log.d(TAG, "setMediaSession: " + mediaSession);
}
getGlobalRouter().setMediaSession(mediaSession);
}
/**
* Associates the provided {@link MediaSessionCompat} to this router.
*
* <p>Maintains the internal state of the provided session to signal it's linked to the
* currently selected route at any given time. This guarantees that the system UI shows the
* correct route name when applicable.
*
* <p>Must be called on the main thread.
*
* @param mediaSession The {@link MediaSessionCompat} to associate to this media router, or null
* to clear the existing association.
*/
@MainThread
public void setMediaSessionCompat(@Nullable MediaSessionCompat mediaSession) {
checkCallingThread();
if (DEBUG) {
Log.d(TAG, "setMediaSessionCompat: " + mediaSession);
}
getGlobalRouter().setMediaSessionCompat(mediaSession);
}
@Nullable
public MediaSessionCompat.Token getMediaSessionToken() {
return sGlobal == null ? null : sGlobal.getMediaSessionToken();
// Use sGlobal exceptionally due to unchecked thread.
}
/**
* Gets {@link MediaRouterParams parameters} of the media router service associated with this
* media router.
*
* <p>Must be called on the main thread.
*/
@MainThread
@Nullable
public MediaRouterParams getRouterParams() {
checkCallingThread();
return getGlobalRouter().getRouterParams();
}
/**
* Sets {@link MediaRouterParams parameters} of the media router service associated with this
* media router.
*
* <p>Must be called on the main thread.
*
* @param params The parameter to set
*/
@MainThread
public void setRouterParams(@Nullable MediaRouterParams params) {
checkCallingThread();
getGlobalRouter().setRouterParams(params);
}
/**
* Sets the {@link RouteListingPreference} of the app associated to this media router.
*
* <p>This method does nothing on devices running API 33 or older.
*
* <p>Use this method to inform the system UI of the routes that you would like to list for
* media routing, via the Output Switcher.
*
* <p>You should call this method immediately after creating an instance and immediately after
* receiving any {@link Callback route list changes} in order to keep the system UI in a
* consistent state. You can also call this method at any other point to update the listing
* preference dynamically (which reflect in the system's Output Switcher).
*
* <p>Notes:
*
* <ul>
* <li>You should not include the ids of two or more routes with a match in their {@link
* MediaRouteDescriptor#getDeduplicationIds() deduplication ids}. If you do, the system
* will deduplicate them using its own criteria.
* <li>You can use this method to rank routes in the output switcher, placing the more
* important routes first. The system might override the proposed ranking.
* <li>You can use this method to change how routes are listed using dynamic criteria. For
* example, you can disable routing while an {@link
* RouteListingPreference.Item#SUBTEXT_AD_ROUTING_DISALLOWED ad is playing}).
* </ul>
*
* @param routeListingPreference The {@link RouteListingPreference} for the system to use for
* route listing. When null, the system uses its default listing criteria.
*/
@MainThread
public void setRouteListingPreference(@Nullable RouteListingPreference routeListingPreference) {
checkCallingThread();
getGlobalRouter().setRouteListingPreference(routeListingPreference);
}
/**
* Throws an {@link IllegalStateException} if the calling thread is not the main thread.
*/
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.");
}
}
/**
* Returns whether the media transfer feature is enabled.
*
* @see MediaRouter
*/
@RestrictTo(LIBRARY)
public static boolean isMediaTransferEnabled() {
if (sGlobal == null) {
return false;
}
return getGlobalRouter().isMediaTransferEnabled();
}
@RestrictTo(LIBRARY)
public static boolean isGroupVolumeUxEnabled() {
if (sGlobal == null) {
return false;
}
return getGlobalRouter().isGroupVolumeUxEnabled();
}
/**
* Returns how many {@link MediaRouter.Callback callbacks} are registered throughout the all
* {@link MediaRouter media routers} in this process.
*/
static int getGlobalCallbackCount() {
if (sGlobal == null) {
return 0;
}
return getGlobalRouter().getCallbackCount();
}
/**
* Returns whether transferring media from remote to local is enabled.
*/
static boolean isTransferToLocalEnabled() {
return getGlobalRouter().isTransferToLocalEnabled();
}
/**
* Provides information about a media route.
* <p>
* 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.
* </p>
*/
public static class RouteInfo {
private final ProviderInfo mProvider;
final String mDescriptorId;
final String mUniqueId;
private String mName;
private String mDescription;
private Uri mIconUri;
boolean mEnabled;
private @ConnectionState int mConnectionState;
private boolean mCanDisconnect;
private final ArrayList<IntentFilter> mControlFilters = new ArrayList<>();
private int mPlaybackType;
private int mPlaybackStream;
private @DeviceType int mDeviceType;
private int mVolumeHandling;
private int mVolume;
private int mVolumeMax;
private Display mPresentationDisplay;
private int mPresentationDisplayId = PRESENTATION_DISPLAY_ID_NONE;
private Bundle mExtras;
private IntentSender mSettingsIntent;
MediaRouteDescriptor mDescriptor;
private List<RouteInfo> mMemberRoutes = new ArrayList<>();
private Map<String, DynamicRouteDescriptor> mDynamicGroupDescriptors;
@IntDef({
CONNECTION_STATE_DISCONNECTED,
CONNECTION_STATE_CONNECTING,
CONNECTION_STATE_CONNECTED
})
@Retention(RetentionPolicy.SOURCE)
private @interface ConnectionState {}
/**
* The default connection state indicating the route is disconnected.
*
* @see #getConnectionState
*/
public static final int CONNECTION_STATE_DISCONNECTED = 0;
/**
* A connection state indicating the route is in the process of connecting and is not yet
* ready for use.
*
* @see #getConnectionState
*/
public static final int CONNECTION_STATE_CONNECTING = 1;
/**
* A connection state indicating the route is connected.
*
* @see #getConnectionState
*/
public static final int CONNECTION_STATE_CONNECTED = 2;
@IntDef({PLAYBACK_TYPE_LOCAL, PLAYBACK_TYPE_REMOTE})
@Retention(RetentionPolicy.SOURCE)
private @interface PlaybackType {}
/**
* The default playback type, "local", indicating the presentation of the media
* is happening on the same device (e.g. a phone, a tablet) as where it is
* controlled from.
*
* @see #getPlaybackType
*/
public static final int PLAYBACK_TYPE_LOCAL = 0;
/**
* A playback type indicating the presentation of the media is happening on
* a different device (i.e. the remote device) than where it is controlled from.
*
* @see #getPlaybackType
*/
public static final int PLAYBACK_TYPE_REMOTE = 1;
@RestrictTo(LIBRARY)
@IntDef({
DEVICE_TYPE_UNKNOWN,
DEVICE_TYPE_TV,
DEVICE_TYPE_SPEAKER,
DEVICE_TYPE_BLUETOOTH,
DEVICE_TYPE_AUDIO_VIDEO_RECEIVER,
DEVICE_TYPE_TABLET,
DEVICE_TYPE_TABLET_DOCKED,
DEVICE_TYPE_COMPUTER,
DEVICE_TYPE_GAME_CONSOLE,
DEVICE_TYPE_CAR,
DEVICE_TYPE_SMARTWATCH,
DEVICE_TYPE_SMARTPHONE,
DEVICE_TYPE_GROUP
})
@Retention(RetentionPolicy.SOURCE)
public @interface DeviceType {}
/**
* The default receiver device type of the route indicating the type is unknown.
*
* @see #getDeviceType
*/
@RestrictTo(LIBRARY)
public static final int DEVICE_TYPE_UNKNOWN = 0;
/**
* A receiver device type of the route indicating the presentation of the media is happening
* on a TV.
*
* @see #getDeviceType
*/
public static final int DEVICE_TYPE_TV = 1;
/**
* A receiver device type of the route indicating the presentation of the media is happening
* on a speaker.
*
* @see #getDeviceType
*/
public static final int DEVICE_TYPE_SPEAKER = 2;
/**
* A receiver device type of the route indicating the presentation of the media is happening
* on a bluetooth device such as a bluetooth speaker.
*
* @see #getDeviceType
*/
@RestrictTo(LIBRARY)
public static final int DEVICE_TYPE_BLUETOOTH = 3;
/**
* A receiver device type indicating that the presentation of the media is happening on an
* Audio/Video receiver (AVR).
*
* @see #getDeviceType
*/
public static final int DEVICE_TYPE_AUDIO_VIDEO_RECEIVER = 4;
/**
* A receiver device type indicating that the presentation of the media is happening on a
* tablet.
*
* @see #getDeviceType
*/
public static final int DEVICE_TYPE_TABLET = 5;
/**
* A receiver device type indicating that the presentation of the media is happening on a
* docked tablet.
*
* @see #getDeviceType
*/
public static final int DEVICE_TYPE_TABLET_DOCKED = 6;
/**
* A receiver device type indicating that the presentation of the media is happening on a
* computer.
*
* @see #getDeviceType
*/
public static final int DEVICE_TYPE_COMPUTER = 7;
/**
* A receiver device type indicating that the presentation of the media is happening on a
* gaming console.
*
* @see #getDeviceType
*/
public static final int DEVICE_TYPE_GAME_CONSOLE = 8;
/**
* A receiver device type indicating that the presentation of the media is happening on a
* car.
*
* @see #getDeviceType
*/
public static final int DEVICE_TYPE_CAR = 9;
/**
* A receiver device type indicating that the presentation of the media is happening on a
* smartwatch.
*
* @see #getDeviceType
*/
public static final int DEVICE_TYPE_SMARTWATCH = 10;
/**
* A receiver device type indicating that the presentation of the media is happening on a
* smartphone.
*
* @see #getDeviceType
*/
public static final int DEVICE_TYPE_SMARTPHONE = 11;
/**
* A receiver device type indicating that the presentation of the media is happening on a
* group of devices.
*
* @see #getDeviceType
*/
public static final int DEVICE_TYPE_GROUP = 1000;
@IntDef({PLAYBACK_VOLUME_FIXED, PLAYBACK_VOLUME_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
private @interface PlaybackVolume {}
/**
* Playback information indicating the playback volume is fixed, i.e. it cannot be
* controlled from this object. An example of fixed playback volume is a remote player,
* playing over HDMI where the user prefers to control the volume on the HDMI sink, rather
* than attenuate at the source.
*
* @see #getVolumeHandling
*/
public static final int PLAYBACK_VOLUME_FIXED = 0;
/**
* Playback information indicating the playback volume is variable and can be controlled
* from this object.
*
* @see #getVolumeHandling
*/
public static final int PLAYBACK_VOLUME_VARIABLE = 1;
/**
* The default presentation display id indicating no presentation display is associated
* with the route.
*/
@RestrictTo(LIBRARY)
public static final int PRESENTATION_DISPLAY_ID_NONE = -1;
static final int CHANGE_GENERAL = 1 << 0;
static final int CHANGE_VOLUME = 1 << 1;
static final int CHANGE_PRESENTATION_DISPLAY = 1 << 2;
// Should match to SystemMediaRouteProvider.PACKAGE_NAME.
static final String SYSTEM_MEDIA_ROUTE_PROVIDER_PACKAGE_NAME = "android";
RouteInfo(ProviderInfo provider, String descriptorId, String uniqueId) {
mProvider = provider;
mDescriptorId = descriptorId;
mUniqueId = uniqueId;
}
/**
* Gets information about the provider of this media route.
*/
@NonNull
public ProviderInfo getProvider() {
return mProvider;
}
/**
* Gets the unique id of the route.
* <p>
* 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.
* </p>
*
* @return The unique id of the route, never null.
*/
@NonNull
public String getId() {
return mUniqueId;
}
/**
* Gets the user-visible name of the route.
* <p>
* The route name identifies the destination represented by the route.
* It may be a user-supplied name, an alias, or device serial number.
* </p>
*
* @return The user-visible name of a media route. This is the string presented
* to users who may select this as the active route.
*/
@NonNull
public String getName() {
return mName;
}
/**
* Gets the user-visible description of the route.
* <p>
* 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.
* </p>
*
* @return The description of the route, or null if none.
*/
@Nullable
public String getDescription() {
return mDescription;
}
/**
* Gets the URI of the icon representing this route.
* <p>
* This icon will be used in picker UIs if available.
* </p>
*
* @return The URI of the icon representing this route, or null if none.
*/
@Nullable
public Uri getIconUri() {
return mIconUri;
}
/**
* 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.
* @deprecated use {@link #getConnectionState} instead.
*/
@Deprecated
public boolean isConnecting() {
return mConnectionState == CONNECTION_STATE_CONNECTING;
}
/**
* Gets the connection state of the route.
*
* @return The connection state of this route: {@link #CONNECTION_STATE_DISCONNECTED},
* {@link #CONNECTION_STATE_CONNECTING}, or {@link #CONNECTION_STATE_CONNECTED}.
*/
@ConnectionState
public int getConnectionState() {
return mConnectionState;
}
/**
* Returns true if this route is currently selected.
*
* <p>Must be called on the main thread.
*
* @return True if this route is currently selected.
* @see MediaRouter#getSelectedRoute
*/
// Note: Only one representative route can return true. For instance:
// - If this route is a selected (non-group) route, it returns true.
// - If this route is a selected group route, it returns true.
// - If this route is a selected member route of a group, it returns false.
@MainThread
public boolean isSelected() {
checkCallingThread();
return getGlobalRouter().getSelectedRoute() == this;
}
/**
* Returns true if this route is the default route.
*
* <p>Must be called on the main thread.
*
* @return True if this route is the default route.
* @see MediaRouter#getDefaultRoute
*/
@MainThread
public boolean isDefault() {
checkCallingThread();
return getGlobalRouter().getDefaultRoute() == this;
}
/**
* Returns true if this route is a bluetooth route.
*
* <p>Must be called on the main thread.
*
* @return True if this route is a bluetooth route.
* @see MediaRouter#getBluetoothRoute
*/
@MainThread
public boolean isBluetooth() {
checkCallingThread();
return getGlobalRouter().getBluetoothRoute() == this;
}
/**
* Returns true if this route is the default route and the device speaker.
*
* @return True if this route is the default route and the device speaker.
*/
public boolean isDeviceSpeaker() {
int defaultAudioRouteNameResourceId = Resources.getSystem().getIdentifier(
"default_audio_route_name", "string", "android");
return isDefault() && TextUtils.equals(
Resources.getSystem().getText(defaultAudioRouteNameResourceId), mName);
}
/**
* 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
*/
@NonNull
public List<IntentFilter> getControlFilters() {
return mControlFilters;
}
/**
* Returns true if the route supports at least one of the capabilities
* described by a media route selector.
*
* <p>Must be called on the main thread.
*
* @param selector The selector that specifies the capabilities to check.
* @return True if the route supports at least one of the capabilities
* described in the media route selector.
*/
@MainThread
public boolean matchesSelector(@NonNull MediaRouteSelector selector) {
if (selector == null) {
throw new IllegalArgumentException("selector must not be null");
}
checkCallingThread();
return selector.matchesControlFilters(mControlFilters);
}
/**
* Returns true if the route supports the specified {@link MediaControlIntent media control}
* category.
*
* <p>Media control categories describe the capabilities of this route such as whether it
* supports live audio streaming or remote playback.
*
* <p>Must be called on the main thread.
*
* @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
*/
@MainThread
public boolean supportsControlCategory(@NonNull String category) {
if (category == null) {
throw new IllegalArgumentException("category must not be null");
}
checkCallingThread();
for (IntentFilter intentFilter : mControlFilters) {
if (intentFilter.hasCategory(category)) {
return true;
}
}
return false;
}
/**
* Returns true if the route supports the specified {@link MediaControlIntent media control}
* category and action.
*
* <p>Media control actions describe specific requests that an application can ask a route
* to perform.
*
* <p>Must be called on the main thread.
*
* @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
*/
@MainThread
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();
for (IntentFilter intentFilter : mControlFilters) {
if (intentFilter.hasCategory(category) && intentFilter.hasAction(action)) {
return true;
}
}
return false;
}
/**
* Returns true if the route supports the specified
* {@link MediaControlIntent media control} request.
* <p>
* Media control requests are used to request the route to perform
* actions such as starting remote playback of a media item.
* </p>
*
* <p>Must be called on the main thread.
*
* @param intent A {@link MediaControlIntent media control intent}.
* @return True if the route can handle the specified intent.
* @see MediaControlIntent
* @see #getControlFilters
*/
@MainThread
public boolean supportsControlRequest(@NonNull Intent intent) {
if (intent == null) {
throw new IllegalArgumentException("intent must not be null");
}
checkCallingThread();
ContentResolver contentResolver = getGlobalRouter().getContentResolver();
for (IntentFilter intentFilter : mControlFilters) {
if (intentFilter.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.
*
* <p>Media control requests are used to request the route to perform actions such as
* starting remote playback of a media item.
*
* <p>This function may only be called on a selected route. Control requests sent to
* unselected routes will fail.
*
* <p>Must be called on the main thread.
*
* @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
*/
@MainThread
public void sendControlRequest(
@NonNull Intent intent, @Nullable ControlRequestCallback callback) {
if (intent == null) {
throw new IllegalArgumentException("intent must not be null");
}
checkCallingThread();
getGlobalRouter().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 the type of the receiver device associated with this route.
*
* @return The type of the receiver device associated with this route.
*/
@DeviceType
public int getDeviceType() {
return mDeviceType;
}
/** */
@RestrictTo(LIBRARY)
public boolean isDefaultOrBluetooth() {
if (isDefault() || mDeviceType == DEVICE_TYPE_BLUETOOTH) {
return true;
}
// This is a workaround for platform version 23 or below where the system route
// provider doesn't specify device type for bluetooth media routes.
return isSystemMediaRouteProvider(this)
&& supportsControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO)
&& !supportsControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
}
/**
* Returns {@code true} if the route is selectable.
*/
boolean isSelectable() {
// This tests whether the route is still valid and enabled.
// The route descriptor field is set to null when the route is removed.
return mDescriptor != null && mEnabled;
}
private static boolean isSystemMediaRouteProvider(MediaRouter.RouteInfo route) {
return TextUtils.equals(route.getProviderInstance().getMetadata().getPackageName(),
SYSTEM_MEDIA_ROUTE_PROVIDER_PACKAGE_NAME);
}
/**
* 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() {
if (isGroup() && !isGroupVolumeUxEnabled()) {
return PLAYBACK_VOLUME_FIXED;
}
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;
}
/**
* Gets whether this route supports disconnecting without interrupting playback.
*
* @return True if this route can disconnect without stopping playback, false otherwise.
*/
public boolean canDisconnect() {
return mCanDisconnect;
}
/**
* Requests a volume change for this route asynchronously.
* <p>
* This function may only be called on a selected route. It will have
* no effect if the route is currently unselected.
* </p>
*
* <p>Must be called on the main thread.
*
* @param volume The new volume value between 0 and {@link #getVolumeMax}.
*/
@MainThread
public void requestSetVolume(int volume) {
checkCallingThread();
getGlobalRouter().requestSetVolume(this, Math.min(mVolumeMax, Math.max(0, volume)));
}
/**
* Requests an incremental volume update for this route asynchronously.
* <p>
* This function may only be called on a selected route. It will have
* no effect if the route is currently unselected.
* </p>
*
* <p>Must be called on the main thread.
*
* @param delta The delta to add to the current volume.
*/
@MainThread
public void requestUpdateVolume(int delta) {
checkCallingThread();
if (delta != 0) {
getGlobalRouter().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.
* <p>
* 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.
* </p><p>
* 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.
* </p><p>
* 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.
* </p><p>
* This method only makes sense for
* {@link MediaControlIntent#CATEGORY_LIVE_VIDEO live video} routes.
* </p>
*
* <p>Must be called on the main thread.
*
* @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
*/
@MainThread
@Nullable
public Display getPresentationDisplay() {
checkCallingThread();
if (mPresentationDisplayId >= 0 && mPresentationDisplay == null) {
mPresentationDisplay = getGlobalRouter().getDisplay(mPresentationDisplayId);
}
return mPresentationDisplay;
}
/**
* Gets the route's presentation display id, or -1 if none.
*/
@RestrictTo(LIBRARY)
public int getPresentationDisplayId() {
return mPresentationDisplayId;
}
/**
* 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;
}
/**
* Gets an intent sender for launching a settings activity for this
* route.
*/
@Nullable
public IntentSender getSettingsIntent() {
return mSettingsIntent;
}
/**
* Selects this media route.
*
* <p>Must be called on the main thread.
*/
@MainThread
public void select() {
checkCallingThread();
getGlobalRouter().selectRoute(this, MediaRouter.UNSELECT_REASON_ROUTE_CHANGED);
}
/**
* Returns true if the route has one or more members
*/
@RestrictTo(LIBRARY)
public boolean isGroup() {
return getMemberRoutes().size() >= 1;
}
/**
* Gets the dynamic group state of the given route.
*/
@RestrictTo(LIBRARY)
@Nullable
public DynamicGroupState getDynamicGroupState(@NonNull RouteInfo route) {
if (route == null) {
throw new NullPointerException("route must not be null");
}
if (mDynamicGroupDescriptors != null
&& mDynamicGroupDescriptors.containsKey(route.mUniqueId)) {
return new DynamicGroupState(mDynamicGroupDescriptors.get(route.mUniqueId));
}
return null;
}
/**
* Returns the routes in this group
*
* @return The list of the routes in this group
*/
@RestrictTo(LIBRARY)
@NonNull
public List<RouteInfo> getMemberRoutes() {
return Collections.unmodifiableList(mMemberRoutes);
}
/**
*
*/
@MainThread
@RestrictTo(LIBRARY)
@Nullable
public DynamicGroupRouteController getDynamicGroupController() {
checkCallingThread();
//TODO: handle multiple controllers case
RouteController controller = getGlobalRouter().mSelectedRouteController;
if (controller instanceof DynamicGroupRouteController) {
return (DynamicGroupRouteController) controller;
}
return null;
}
@Override
@NonNull
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("MediaRouter.RouteInfo{ uniqueId=").append(mUniqueId)
.append(", name=").append(mName)
.append(", description=").append(mDescription)
.append(", iconUri=").append(mIconUri)
.append(", enabled=").append(mEnabled)
.append(", connectionState=").append(mConnectionState)
.append(", canDisconnect=").append(mCanDisconnect)
.append(", playbackType=").append(mPlaybackType)
.append(", playbackStream=").append(mPlaybackStream)
.append(", deviceType=").append(mDeviceType)
.append(", volumeHandling=").append(mVolumeHandling)
.append(", volume=").append(mVolume)
.append(", volumeMax=").append(mVolumeMax)
.append(", presentationDisplayId=").append(mPresentationDisplayId)
.append(", extras=").append(mExtras)
.append(", settingsIntent=").append(mSettingsIntent)
.append(", providerPackageName=").append(mProvider.getPackageName());
if (isGroup()) {
sb.append(", members=[");
final int count = mMemberRoutes.size();
for (int i = 0; i < count; i++) {
if (i > 0) sb.append(", ");
if (mMemberRoutes.get(i) != this) {
sb.append(mMemberRoutes.get(i).getId());
}
}
sb.append(']');
}
sb.append(" }");
return sb.toString();
}
int maybeUpdateDescriptor(MediaRouteDescriptor descriptor) {
int changes = 0;
if (mDescriptor != descriptor) {
changes = updateDescriptor(descriptor);
}
return changes;
}
private boolean isSameControlFilters(List<IntentFilter> filters1,
List<IntentFilter> filters2) {
if (filters1 == filters2) {
return true;
}
if (filters1 == null || filters2 == null) {
return false;
}
ListIterator<IntentFilter> e1 = filters1.listIterator();
ListIterator<IntentFilter> e2 = filters2.listIterator();
while (e1.hasNext() && e2.hasNext()) {
if (!isSameControlFilter(e1.next(), e2.next())) {
return false;
}
}
return !(e1.hasNext() || e2.hasNext());
}
private boolean isSameControlFilter(IntentFilter filter1, IntentFilter filter2) {
if (filter1 == filter2) {
return true;
}
if (filter1 == null || filter2 == null) {
return false;
}
// Check only actions and categories for a brief comparison
int countActions = filter1.countActions();
if (countActions != filter2.countActions()) {
return false;
}
for (int i = 0; i < countActions; ++i) {
if (!filter1.getAction(i).equals(filter2.getAction(i))) {
return false;
}
}
int countCategories = filter1.countCategories();
if (countCategories != filter2.countCategories()) {
return false;
}
for (int i = 0; i < countCategories; ++i) {
if (!filter1.getCategory(i).equals(filter2.getCategory(i))) {
return false;
}
}
return true;
}
int updateDescriptor(MediaRouteDescriptor descriptor) {
int changes = 0;
mDescriptor = descriptor;
if (descriptor != null) {
if (!ObjectsCompat.equals(mName, descriptor.getName())) {
mName = descriptor.getName();
changes |= CHANGE_GENERAL;
}
if (!ObjectsCompat.equals(mDescription, descriptor.getDescription())) {
mDescription = descriptor.getDescription();
changes |= CHANGE_GENERAL;
}
if (!ObjectsCompat.equals(mIconUri, descriptor.getIconUri())) {
mIconUri = descriptor.getIconUri();
changes |= CHANGE_GENERAL;
}
if (mEnabled != descriptor.isEnabled()) {
mEnabled = descriptor.isEnabled();
changes |= CHANGE_GENERAL;
}
if (mConnectionState != descriptor.getConnectionState()) {
mConnectionState = descriptor.getConnectionState();
changes |= CHANGE_GENERAL;
}
// Use custom method to compare two control filters to confirm it is changed.
if (!isSameControlFilters(mControlFilters, 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 (mDeviceType != descriptor.getDeviceType()) {
mDeviceType = descriptor.getDeviceType();
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 (!ObjectsCompat.equals(mExtras, descriptor.getExtras())) {
mExtras = descriptor.getExtras();
changes |= CHANGE_GENERAL;
}
if (!ObjectsCompat.equals(mSettingsIntent, descriptor.getSettingsActivity())) {
mSettingsIntent = descriptor.getSettingsActivity();
changes |= CHANGE_GENERAL;
}
if (mCanDisconnect != descriptor.canDisconnectAndKeepPlaying()) {
mCanDisconnect = descriptor.canDisconnectAndKeepPlaying();
changes |= CHANGE_GENERAL | CHANGE_PRESENTATION_DISPLAY;
}
boolean memberChanged = false;
List<String> groupMemberIds = descriptor.getGroupMemberIds();
List<RouteInfo> routes = new ArrayList<>();
if (groupMemberIds.size() != mMemberRoutes.size()) {
memberChanged = true;
}
//TODO: Clean this up not to reference the global router
if (!groupMemberIds.isEmpty()) {
GlobalMediaRouter globalRouter = getGlobalRouter();
for (String groupMemberId : groupMemberIds) {
String uniqueId = globalRouter.getUniqueId(getProvider(), groupMemberId);
RouteInfo groupMember = globalRouter.getRoute(uniqueId);
if (groupMember != null) {
routes.add(groupMember);
if (!memberChanged && !mMemberRoutes.contains(groupMember)) {
memberChanged = true;
}
}
}
}
if (memberChanged) {
mMemberRoutes = routes;
changes |= CHANGE_GENERAL;
}
}
return changes;
}
String getDescriptorId() {
return mDescriptorId;
}
@RestrictTo(LIBRARY)
@NonNull
public MediaRouteProvider getProviderInstance() {
return mProvider.getProviderInstance();
}
void updateDynamicDescriptors(Collection<DynamicRouteDescriptor> dynamicDescriptors) {
mMemberRoutes.clear();
if (mDynamicGroupDescriptors == null) {
mDynamicGroupDescriptors = new ArrayMap<>();
}
mDynamicGroupDescriptors.clear();
for (DynamicRouteDescriptor dynamicDescriptor : dynamicDescriptors) {
RouteInfo route = findRouteByDynamicRouteDescriptor(dynamicDescriptor);
if (route == null) {
continue;
}
mDynamicGroupDescriptors.put(route.mUniqueId, dynamicDescriptor);
if ((dynamicDescriptor.getSelectionState() == DynamicRouteDescriptor.SELECTING)
|| (dynamicDescriptor.getSelectionState()
== DynamicRouteDescriptor.SELECTED)) {
mMemberRoutes.add(route);
}
}
getGlobalRouter().mCallbackHandler.post(
GlobalMediaRouter.CallbackHandler.MSG_ROUTE_CHANGED, this);
}
RouteInfo findRouteByDynamicRouteDescriptor(DynamicRouteDescriptor dynamicDescriptor) {
String descriptorId = dynamicDescriptor.getRouteDescriptor().getId();
return getProvider().findRouteByDescriptorId(descriptorId);
}
/**
* Represents the dynamic group state of the {@link RouteInfo}.
*/
@RestrictTo(LIBRARY)
public static final class DynamicGroupState {
final DynamicRouteDescriptor mDynamicDescriptor;
DynamicGroupState(DynamicRouteDescriptor descriptor) {
mDynamicDescriptor = descriptor;
}
/**
* Gets the selection state of the route when the {@link MediaRouteProvider} of the
* route supports
* {@link MediaRouteProviderDescriptor#supportsDynamicGroupRoute() dynamic group}.
*
* @return The selection state of the route: {@link DynamicRouteDescriptor#UNSELECTED},
* {@link DynamicRouteDescriptor#SELECTING}, or {@link DynamicRouteDescriptor#SELECTED}.
*/
@RestrictTo(LIBRARY)
public int getSelectionState() {
return (mDynamicDescriptor != null) ? mDynamicDescriptor.getSelectionState()
: DynamicRouteDescriptor.UNSELECTED;
}
@RestrictTo(LIBRARY)
public boolean isUnselectable() {
return mDynamicDescriptor == null || mDynamicDescriptor.isUnselectable();
}
@RestrictTo(LIBRARY)
public boolean isGroupable() {
return mDynamicDescriptor != null && mDynamicDescriptor.isGroupable();
}
@RestrictTo(LIBRARY)
public boolean isTransferable() {
return mDynamicDescriptor != null && mDynamicDescriptor.isTransferable();
}
}
}
/**
* Provides information about a media route provider.
* <p>
* This object may be used to determine which media route provider has
* published a particular route.
* </p>
*/
public static final class ProviderInfo {
// Package private fields to avoid use of a synthetic accessor.
final MediaRouteProvider mProviderInstance;
final List<RouteInfo> mRoutes = new ArrayList<>();
final boolean mTreatRouteDescriptorIdsAsUnique;
private final ProviderMetadata mMetadata;
private MediaRouteProviderDescriptor mDescriptor;
ProviderInfo(MediaRouteProvider provider) {
this(provider, /* treatRouteDescriptorIdsAsUnique= */ false);
}
ProviderInfo(MediaRouteProvider provider, boolean treatRouteDescriptorIdsAsUnique) {
mProviderInstance = provider;
mMetadata = provider.getMetadata();
mTreatRouteDescriptorIdsAsUnique = treatRouteDescriptorIdsAsUnique;
}
/**
* Gets the provider's underlying {@link MediaRouteProvider} instance.
*
* <p>Must be called on the main thread.
*/
@NonNull
@MainThread
public MediaRouteProvider getProviderInstance() {
checkCallingThread();
return mProviderInstance;
}
/**
* Gets the package name of the media route provider.
*/
@NonNull
public String getPackageName() {
return mMetadata.getPackageName();
}
/**
* Gets the component name of the media route provider.
*/
@NonNull
public ComponentName getComponentName() {
return mMetadata.getComponentName();
}
/**
* Gets the {@link MediaRouter.RouteInfo routes} published by this route provider.
*
* <p>Must be called on the main thread.
*/
@MainThread
@NonNull
public List<RouteInfo> getRoutes() {
checkCallingThread();
return Collections.unmodifiableList(mRoutes);
}
boolean updateDescriptor(MediaRouteProviderDescriptor descriptor) {
if (mDescriptor != descriptor) {
mDescriptor = descriptor;
return true;
}
return false;
}
int findRouteIndexByDescriptorId(String id) {
final int count = mRoutes.size();
for (int i = 0; i < count; i++) {
if (mRoutes.get(i).mDescriptorId.equals(id)) {
return i;
}
}
return -1;
}
RouteInfo findRouteByDescriptorId(String id) {
for (RouteInfo route : mRoutes) {
if (route.mDescriptorId.equals(id)) {
return route;
}
}
return null;
}
boolean supportsDynamicGroup() {
return mDescriptor != null && mDescriptor.supportsDynamicGroupRoute();
}
@NonNull
@Override
public String toString() {
return "MediaRouter.RouteProviderInfo{ packageName=" + getPackageName() + " }";
}
}
/**
* Interface for receiving events about media routing changes.
* All methods of this interface will be called from the application's main thread.
* <p>
* 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)}.
* </p>
*
* @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.
* @deprecated Use {@link #onRouteSelected(MediaRouter, RouteInfo, int)} instead.
*/
@Deprecated
public void onRouteSelected(@NonNull MediaRouter router, @NonNull RouteInfo route) {}
/**
* Called when the supplied media route becomes selected as the active route.
*
* <p>The reason provided will be one of the following:
*
* <ul>
* <li>{@link MediaRouter#UNSELECT_REASON_UNKNOWN}
* <li>{@link MediaRouter#UNSELECT_REASON_DISCONNECTED}
* <li>{@link MediaRouter#UNSELECT_REASON_STOPPED}
* <li>{@link MediaRouter#UNSELECT_REASON_ROUTE_CHANGED}
* </ul>
*
* @param router The media router reporting the event.
* @param route The route that has been selected.
* @param reason The reason for unselecting the previous route.
*/
public void onRouteSelected(
@NonNull MediaRouter router, @NonNull RouteInfo route, @UnselectReason int reason) {
onRouteSelected(router, route);
}
// TODO: Revise the comment when we have a feature that enables dynamic grouping on pre-R
// devices.
/**
* Called when the supplied media route becomes selected as the active route, which may be
* different from the route requested by {@link #selectRoute(RouteInfo)}. That can happen
* when {@link MediaTransferReceiver media transfer feature} is enabled. The default
* implementation calls {@link #onRouteSelected(MediaRouter, RouteInfo, int)} with the
* actually selected route.
*
* @param router The media router reporting the event.
* @param selectedRoute The route that has been selected.
* @param reason The reason for unselecting the previous route.
* @param requestedRoute The route that was requested to be selected.
*/
public void onRouteSelected(
@NonNull MediaRouter router,
@NonNull RouteInfo selectedRoute,
@UnselectReason int reason,
@NonNull RouteInfo requestedRoute) {
onRouteSelected(router, selectedRoute, reason);
}
/**
* Called when the supplied media route becomes unselected as the active route. For detailed
* reason, override {@link #onRouteUnselected(MediaRouter, RouteInfo, int)} instead.
*
* @param router The media router reporting the event.
* @param route The route that has been unselected.
* @deprecated Use {@link #onRouteUnselected(MediaRouter, RouteInfo, int)} instead.
*/
@Deprecated
public void onRouteUnselected(@NonNull MediaRouter router, @NonNull RouteInfo route) {}
/**
* Called when the supplied media route becomes unselected as the active route. The default
* implementation calls {@link #onRouteUnselected}.
*
* <p>The reason provided will be one of the following:
*
* <ul>
* <li>{@link MediaRouter#UNSELECT_REASON_UNKNOWN}
* <li>{@link MediaRouter#UNSELECT_REASON_DISCONNECTED}
* <li>{@link MediaRouter#UNSELECT_REASON_STOPPED}
* <li>{@link MediaRouter#UNSELECT_REASON_ROUTE_CHANGED}
* </ul>
*
* @param router The media router reporting the event.
* @param route The route that has been unselected.
* @param reason The reason for unselecting the route.
*/
public void onRouteUnselected(
@NonNull MediaRouter router, @NonNull RouteInfo route, @UnselectReason int reason) {
onRouteUnselected(router, 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(@NonNull MediaRouter router, @NonNull 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(@NonNull MediaRouter router, @NonNull 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(@NonNull MediaRouter router, @NonNull 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(@NonNull MediaRouter router, @NonNull RouteInfo route) {}
/**
* Called when a media route's presentation display changes.
*
* <p>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(
@NonNull MediaRouter router, @NonNull 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(@NonNull MediaRouter router, @NonNull 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(
@NonNull MediaRouter router, @NonNull 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(
@NonNull MediaRouter router, @NonNull ProviderInfo provider) {}
/** */
@RestrictTo(LIBRARY)
public void onRouterParamsChanged(
@NonNull MediaRouter router, @Nullable MediaRouterParams params) {}
}
/**
* Listener for receiving events when the selected route is about to be changed.
*
* @see #setOnPrepareTransferListener(OnPrepareTransferListener)
*/
public interface OnPrepareTransferListener {
/**
* Implement this to handle transfer seamlessly.
*
* <p>Setting the listener will defer stopping the previous route, from which you may get
* the media status to resume media seamlessly on the new route. When the transfer is
* prepared, set the returned future to stop media being played on the previous route and
* release resources. This method is called on the main thread.
*
* <p>{@link Callback#onRouteUnselected(MediaRouter, RouteInfo, int)} and {@link
* Callback#onRouteSelected(MediaRouter, RouteInfo, int)} are called after the future is
* done.
*
* @param fromRoute The route that is about to be unselected.
* @param toRoute The route that is about to be selected.
* @return A {@link ListenableFuture} whose completion indicates that the transfer is
* prepared or {@code null} to indicate that no preparation is needed. If a future is
* returned, until the future is completed, the media continues to be played on the
* previous route.
*/
@MainThread
@Nullable
ListenableFuture<Void> onPrepareTransfer(
@NonNull RouteInfo fromRoute, @NonNull RouteInfo toRoute);
}
/**
* 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(@Nullable 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(@Nullable String error, @Nullable Bundle data) {}
}
@RestrictTo(LIBRARY_GROUP)
static final class CallbackRecord {
public final MediaRouter mRouter;
public final Callback mCallback;
public MediaRouteSelector mSelector;
public int mFlags;
public long mTimestamp;
public CallbackRecord(MediaRouter router, Callback callback) {
mRouter = router;
mCallback = callback;
mSelector = MediaRouteSelector.EMPTY;
}
public boolean filterRouteEvent(RouteInfo route, int what, RouteInfo optionalRoute,
int reason) {
if ((mFlags & CALLBACK_FLAG_UNFILTERED_EVENTS) != 0
|| route.matchesSelector(mSelector)) {
return true;
}
// In order to notify the app of cast-to-phone event, the onRouteSelected(phone)
// should be called regaredless of the callbakck's control category.
if (isTransferToLocalEnabled() && route.isDefaultOrBluetooth()
&& what == GlobalMediaRouter.CallbackHandler.MSG_ROUTE_SELECTED
&& reason == UNSELECT_REASON_ROUTE_CHANGED
&& optionalRoute != null) {
// Check the previously selected route is remote route.
return !optionalRoute.isDefaultOrBluetooth();
}
return false;
}
}
/**
* Class to notify events about transfer.
*/
static final class PrepareTransferNotifier {
private static final long TRANSFER_TIMEOUT_MS = 15_000;
final RouteController mToRouteController;
final @UnselectReason int mReason;
private final RouteInfo mFromRoute;
final RouteInfo mToRoute;
private final RouteInfo mRequestedRoute;
@Nullable
final List<DynamicRouteDescriptor> mMemberRoutes;
private final WeakReference<GlobalMediaRouter> mRouter;
private ListenableFuture<Void> mFuture = null;
private boolean mFinished = false;
private boolean mCanceled = false;
PrepareTransferNotifier(GlobalMediaRouter router, RouteInfo route,
@Nullable RouteController routeController, @UnselectReason int reason,
@Nullable RouteInfo requestedRoute,
@Nullable Collection<DynamicRouteDescriptor> memberRoutes) {
mRouter = new WeakReference<>(router);
mToRoute = route;
mToRouteController = routeController;
mReason = reason;
mFromRoute = router.mSelectedRoute;
mRequestedRoute = requestedRoute;
mMemberRoutes = (memberRoutes == null) ? null : new ArrayList<>(memberRoutes);
// For the case it's not handled properly
router.mCallbackHandler.postDelayed(this::finishTransfer,
TRANSFER_TIMEOUT_MS);
}
void setFuture(ListenableFuture<Void> future) {
GlobalMediaRouter router = mRouter.get();
if (router == null || router.mTransferNotifier != this) {
Log.w(TAG, "Router is released. Cancel transfer");
cancel();
return;
}
if (mFuture != null) {
throw new IllegalStateException("future is already set");
}
mFuture = future;
future.addListener(this::finishTransfer, router.mCallbackHandler::post);
}
/**
* Notifies that preparation for transfer is finished.
*/
@MainThread
void finishTransfer() {
checkCallingThread();
if (mFinished || mCanceled) {
return;
}
GlobalMediaRouter router = mRouter.get();
if (router == null || router.mTransferNotifier != this
|| (mFuture != null && mFuture.isCancelled())) {
cancel();
return;
}
mFinished = true;
router.mTransferNotifier = null;
unselectFromRouteAndNotify();
selectToRouteAndNotify();
}
void cancel() {
if (mFinished || mCanceled) {
return;
}
mCanceled = true;
if (mToRouteController != null) {
mToRouteController.onUnselect(UNSELECT_REASON_UNKNOWN);
mToRouteController.onRelease();
}
}
private void unselectFromRouteAndNotify() {
GlobalMediaRouter router = mRouter.get();
if (router == null || router.mSelectedRoute != mFromRoute) {
return;
}
router.mCallbackHandler.post(GlobalMediaRouter.CallbackHandler.MSG_ROUTE_UNSELECTED,
mFromRoute, mReason);
if (router.mSelectedRouteController != null) {
router.mSelectedRouteController.onUnselect(mReason);
router.mSelectedRouteController.onRelease();
}
// Release member route controllers
if (!router.mRouteControllerMap.isEmpty()) {
for (RouteController controller : router.mRouteControllerMap.values()) {
controller.onUnselect(mReason);
controller.onRelease();
}
router.mRouteControllerMap.clear();
}
router.mSelectedRouteController = null;
}
private void selectToRouteAndNotify() {
GlobalMediaRouter router = mRouter.get();
if (router == null) {
return;
}
router.mSelectedRoute = mToRoute;
router.mSelectedRouteController = mToRouteController;
if (mRequestedRoute == null) {
router.mCallbackHandler.post(GlobalMediaRouter.CallbackHandler.MSG_ROUTE_SELECTED,
new Pair<>(mFromRoute, mToRoute), mReason);
} else {
router.mCallbackHandler.post(
GlobalMediaRouter.CallbackHandler.MSG_ROUTE_ANOTHER_SELECTED,
new Pair<>(mRequestedRoute, mToRoute), mReason);
}
router.mRouteControllerMap.clear();
router.maybeUpdateMemberRouteControllers();
router.updatePlaybackInfoFromSelectedRoute();
if (mMemberRoutes != null) {
router.mSelectedRoute.updateDynamicDescriptors(mMemberRoutes);
}
}
}
}