/* * Copyright 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.media; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; import android.Manifest; import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.TestApi; import android.content.Context; import android.content.pm.PackageManager; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.RemoteException; import android.os.ServiceManager; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; import com.android.internal.annotations.GuardedBy; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; /** * This API is not generally intended for third party application developers. Use the * AndroidX * Media Router * Library for consistent behavior across all devices. * *
MediaRouter2 allows applications to control the routing of media channels and streams from
* the current device to remote speakers and devices.
*/
// TODO(b/157873330): Add method names at the beginning of log messages. (e.g. selectRoute)
// Not only MediaRouter2, but also to service / manager / provider.
// TODO: ensure thread-safe and document it
public final class MediaRouter2 {
private static final String TAG = "MR2";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final Object sSystemRouterLock = new Object();
private static final Object sRouterLock = new Object();
// The maximum time for the old routing controller available after transfer.
private static final int TRANSFER_TIMEOUT_MS = 30_000;
// The manager request ID representing that no manager is involved.
private static final long MANAGER_REQUEST_ID_NONE = MediaRoute2ProviderService.REQUEST_ID_NONE;
@GuardedBy("sSystemRouterLock")
private static Map First of all, the discovery preference passed to {@link #registerRouteCallback} will have
* no effect. The callback will be called accordingly with the client app's discovery
* preference. Therefore, it is recommended to pass {@link RouteDiscoveryPreference#EMPTY}
* there.
*
* Also, do not keep/compare the instances of the {@link RoutingController}, since they are
* always newly created with the latest session information whenever below methods are called:
*
* Finally, it will have no effect to call {@link #setOnGetControllerHintsListener}.
*
* @param clientPackageName the package name of the app to control
* @throws SecurityException if the caller doesn't have MODIFY_AUDIO_ROUTING permission.
* @hide
*/
@SystemApi
@RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
@Nullable
public static MediaRouter2 getInstance(
@NonNull Context context, @NonNull String clientPackageName) {
Objects.requireNonNull(context, "context must not be null");
Objects.requireNonNull(clientPackageName, "clientPackageName must not be null");
// Note: Even though this check could be somehow bypassed, the other permission checks
// in system server will not allow MediaRouter2Manager to be registered.
IMediaRouterService serviceBinder =
IMediaRouterService.Stub.asInterface(
ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE));
try {
// SecurityException will be thrown if there's no permission.
serviceBinder.enforceMediaContentControlPermission();
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
PackageManager pm = context.getPackageManager();
try {
pm.getPackageInfo(clientPackageName, 0);
} catch (PackageManager.NameNotFoundException ex) {
Log.e(TAG, "Package " + clientPackageName + " not found. Ignoring.");
return null;
}
synchronized (sSystemRouterLock) {
MediaRouter2 instance = sSystemMediaRouter2Map.get(clientPackageName);
if (instance == null) {
if (sManager == null) {
sManager = MediaRouter2Manager.getInstance(context.getApplicationContext());
}
instance = new MediaRouter2(context, clientPackageName);
sSystemMediaRouter2Map.put(clientPackageName, instance);
// Using direct executor here, since MediaRouter2Manager also posts
// to the main handler.
sManager.registerCallback(Runnable::run, instance.mManagerCallback);
}
return instance;
}
}
/**
* Starts scanning remote routes.
*
* Route discovery can happen even when the {@link #startScan()} is not called. This is
* because the scanning could be started before by other apps. Therefore, calling this method
* after calling {@link #stopScan()} does not necessarily mean that the routes found before are
* removed and added again.
*
* Use {@link RouteCallback} to get the route related events.
*
* Note that calling start/stopScan is applied to all system routers in the same process.
*
* This will be no-op for non-system media routers.
*
* @see #stopScan()
* @see #getInstance(Context, String)
* @hide
*/
@SystemApi
@RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
public void startScan() {
if (isSystemRouter()) {
if (!mIsScanning.getAndSet(true)) {
sManager.registerScanRequest();
}
}
}
/**
* Stops scanning remote routes to reduce resource consumption.
*
* Route discovery can be continued even after this method is called. This is because the
* scanning is only turned off when all the apps stop scanning. Therefore, calling this method
* does not necessarily mean the routes are removed. Also, for the same reason it does not mean
* that {@link RouteCallback#onRoutesAdded(List)} is not called afterwards.
*
* Use {@link RouteCallback} to get the route related events.
*
* Note that calling start/stopScan is applied to all system routers in the same process.
*
* This will be no-op for non-system media routers.
*
* @see #startScan()
* @see #getInstance(Context, String)
* @hide
*/
@SystemApi
@RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
public void stopScan() {
if (isSystemRouter()) {
if (mIsScanning.getAndSet(false)) {
sManager.unregisterScanRequest();
}
}
}
private MediaRouter2(Context appContext) {
mContext = appContext;
mMediaRouterService =
IMediaRouterService.Stub.asInterface(
ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE));
mPackageName = mContext.getPackageName();
mHandler = new Handler(Looper.getMainLooper());
List This will return null for non-system media routers.
*
* @see #getInstance(Context, String)
* @hide
*/
@SystemApi
@Nullable
public String getClientPackageName() {
return mClientPackageName;
}
/**
* Registers a callback to discover routes and to receive events when they change.
*
* If the specified callback is already registered, its registration will be updated for the
* given {@link Executor executor} and {@link RouteDiscoveryPreference discovery preference}.
*/
public void registerRouteCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull RouteCallback routeCallback,
@NonNull RouteDiscoveryPreference preference) {
Objects.requireNonNull(executor, "executor must not be null");
Objects.requireNonNull(routeCallback, "callback must not be null");
Objects.requireNonNull(preference, "preference must not be null");
if (isSystemRouter()) {
preference = RouteDiscoveryPreference.EMPTY;
}
RouteCallbackRecord record = new RouteCallbackRecord(executor, routeCallback, preference);
mRouteCallbackRecords.remove(record);
// It can fail to add the callback record if another registration with the same callback
// is happening but it's okay because either this or the other registration should be done.
mRouteCallbackRecords.addIfAbsent(record);
if (isSystemRouter()) {
return;
}
synchronized (mLock) {
if (mStub == null) {
MediaRouter2Stub stub = new MediaRouter2Stub();
try {
mMediaRouterService.registerRouter2(stub, mPackageName);
mStub = stub;
} catch (RemoteException ex) {
Log.e(TAG, "registerRouteCallback: Unable to register MediaRouter2.", ex);
}
}
if (mStub != null && updateDiscoveryPreferenceIfNeededLocked()) {
try {
mMediaRouterService.setDiscoveryRequestWithRouter2(mStub, mDiscoveryPreference);
} catch (RemoteException ex) {
Log.e(TAG, "registerRouteCallback: Unable to set discovery request.", ex);
}
}
}
}
/**
* Unregisters the given callback. The callback will no longer receive events. If the callback
* has not been added or been removed already, it is ignored.
*
* @param routeCallback the callback to unregister
* @see #registerRouteCallback
*/
public void unregisterRouteCallback(@NonNull RouteCallback routeCallback) {
Objects.requireNonNull(routeCallback, "callback must not be null");
if (!mRouteCallbackRecords.remove(new RouteCallbackRecord(null, routeCallback, null))) {
Log.w(TAG, "unregisterRouteCallback: Ignoring unknown callback");
return;
}
if (isSystemRouter()) {
return;
}
synchronized (mLock) {
if (mStub == null) {
return;
}
if (updateDiscoveryPreferenceIfNeededLocked()) {
try {
mMediaRouterService.setDiscoveryRequestWithRouter2(mStub, mDiscoveryPreference);
} catch (RemoteException ex) {
Log.e(TAG, "unregisterRouteCallback: Unable to set discovery request.", ex);
}
}
if (mRouteCallbackRecords.isEmpty() && mNonSystemRoutingControllers.isEmpty()) {
try {
mMediaRouterService.unregisterRouter2(mStub);
} catch (RemoteException ex) {
Log.e(TAG, "Unable to unregister media router.", ex);
}
mStub = null;
}
}
}
@GuardedBy("mLock")
private boolean updateDiscoveryPreferenceIfNeededLocked() {
RouteDiscoveryPreference newDiscoveryPreference = new RouteDiscoveryPreference.Builder(
mRouteCallbackRecords.stream().map(record -> record.mPreference).collect(
Collectors.toList())).build();
if (Objects.equals(mDiscoveryPreference, newDiscoveryPreference)) {
return false;
}
mDiscoveryPreference = newDiscoveryPreference;
updateFilteredRoutesLocked();
return true;
}
/**
* Gets the list of all discovered routes. This list includes the routes that are not related to
* the client app.
*
* This will return an empty list for non-system media routers.
*
* @hide
*/
@SystemApi
@NonNull
public List Please note that the list can be changed before callbacks are invoked.
*
* @return the list of routes that contains at least one of the route features in discovery
* preferences registered by the application
*/
@NonNull
public List This will be no-op for non-system media routers.
*
* @param controller a routing controller controlling media routing.
* @param route the route you want to transfer the media to.
* @hide
*/
@SystemApi
@RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
public void transfer(@NonNull RoutingController controller, @NonNull MediaRoute2Info route) {
if (isSystemRouter()) {
sManager.transfer(controller.getRoutingSessionInfo(), route);
return;
}
}
void requestCreateController(
@NonNull RoutingController controller,
@NonNull MediaRoute2Info route,
long managerRequestId) {
final int requestId = mNextRequestId.getAndIncrement();
ControllerCreationRequest request =
new ControllerCreationRequest(requestId, managerRequestId, route, controller);
mControllerCreationRequests.add(request);
OnGetControllerHintsListener listener = mOnGetControllerHintsListener;
Bundle controllerHints = null;
if (listener != null) {
controllerHints = listener.onGetControllerHints(route);
if (controllerHints != null) {
controllerHints = new Bundle(controllerHints);
}
}
MediaRouter2Stub stub;
synchronized (mLock) {
stub = mStub;
}
if (stub != null) {
try {
mMediaRouterService.requestCreateSessionWithRouter2(
stub,
requestId,
managerRequestId,
controller.getRoutingSessionInfo(),
route,
controllerHints);
} catch (RemoteException ex) {
Log.e(TAG, "createControllerForTransfer: "
+ "Failed to request for creating a controller.", ex);
mControllerCreationRequests.remove(request);
if (managerRequestId == MANAGER_REQUEST_ID_NONE) {
notifyTransferFailure(route);
}
}
}
}
@NonNull
private RoutingController getCurrentController() {
List Note: The system controller can't be released. Calling {@link RoutingController#release()}
* will be ignored.
*
* This method always returns the same instance.
*/
@NonNull
public RoutingController getSystemController() {
return mSystemController;
}
/**
* Gets a {@link RoutingController} whose ID is equal to the given ID.
* Returns {@code null} if there is no matching controller.
*/
@Nullable
public RoutingController getController(@NonNull String id) {
Objects.requireNonNull(id, "id must not be null");
for (RoutingController controller : getControllers()) {
if (TextUtils.equals(id, controller.getId())) {
return controller;
}
}
return null;
}
/**
* Gets the list of currently active {@link RoutingController routing controllers} on which
* media can be played.
*
* Note: The list returned here will never be empty. The first element in the list is
* always the {@link #getSystemController() system controller}.
*/
@NonNull
public List This will be no-op for non-system media routers.
*
* @param volume The new volume value between 0 and {@link MediaRoute2Info#getVolumeMax}.
* @see #getInstance(Context, String)
* @hide
*/
@SystemApi
@RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
public void setRouteVolume(@NonNull MediaRoute2Info route, int volume) {
Objects.requireNonNull(route, "route must not be null");
if (isSystemRouter()) {
sManager.setRouteVolume(route, volume);
return;
}
// If this API needs to be public, use IMediaRouterService#setRouteVolumeWithRouter2()
}
void syncRoutesOnHandler(
List Pass {@code null} to sessionInfo for the failure case.
*/
void createControllerOnHandler(int requestId, @Nullable RoutingSessionInfo sessionInfo) {
ControllerCreationRequest matchingRequest = null;
for (ControllerCreationRequest request : mControllerCreationRequests) {
if (request.mRequestId == requestId) {
matchingRequest = request;
break;
}
}
if (matchingRequest == null) {
Log.w(TAG, "createControllerOnHandler: Ignoring an unknown request.");
return;
}
mControllerCreationRequests.remove(matchingRequest);
MediaRoute2Info requestedRoute = matchingRequest.mRoute;
// TODO: Notify the reason for failure.
if (sessionInfo == null) {
notifyTransferFailure(requestedRoute);
return;
} else if (!TextUtils.equals(requestedRoute.getProviderId(), sessionInfo.getProviderId())) {
Log.w(
TAG,
"The session's provider ID does not match the requested route's. "
+ "(requested route's providerId="
+ requestedRoute.getProviderId()
+ ", actual providerId="
+ sessionInfo.getProviderId()
+ ")");
notifyTransferFailure(requestedRoute);
return;
}
RoutingController oldController = matchingRequest.mOldController;
// When the old controller is released before transferred, treat it as a failure.
// This could also happen when transfer is requested twice or more.
if (!oldController.scheduleRelease()) {
Log.w(
TAG,
"createControllerOnHandler: "
+ "Ignoring controller creation for released old controller. "
+ "oldController="
+ oldController);
if (!sessionInfo.isSystemSession()) {
new RoutingController(sessionInfo).release();
}
notifyTransferFailure(requestedRoute);
return;
}
RoutingController newController;
if (sessionInfo.isSystemSession()) {
newController = getSystemController();
newController.setRoutingSessionInfo(sessionInfo);
} else {
newController = new RoutingController(sessionInfo);
synchronized (mLock) {
mNonSystemRoutingControllers.put(newController.getId(), newController);
}
}
notifyTransfer(oldController, newController);
}
void updateControllerOnHandler(RoutingSessionInfo sessionInfo) {
if (sessionInfo == null) {
Log.w(TAG, "updateControllerOnHandler: Ignoring null sessionInfo.");
return;
}
if (sessionInfo.isSystemSession()) {
// The session info is sent from SystemMediaRoute2Provider.
RoutingController systemController = getSystemController();
systemController.setRoutingSessionInfo(sessionInfo);
notifyControllerUpdated(systemController);
return;
}
RoutingController matchingController;
synchronized (mLock) {
matchingController = mNonSystemRoutingControllers.get(sessionInfo.getId());
}
if (matchingController == null) {
Log.w(
TAG,
"updateControllerOnHandler: Matching controller not found. uniqueSessionId="
+ sessionInfo.getId());
return;
}
RoutingSessionInfo oldInfo = matchingController.getRoutingSessionInfo();
if (!TextUtils.equals(oldInfo.getProviderId(), sessionInfo.getProviderId())) {
Log.w(
TAG,
"updateControllerOnHandler: Provider IDs are not matched. old="
+ oldInfo.getProviderId()
+ ", new="
+ sessionInfo.getProviderId());
return;
}
matchingController.setRoutingSessionInfo(sessionInfo);
notifyControllerUpdated(matchingController);
}
void releaseControllerOnHandler(RoutingSessionInfo sessionInfo) {
if (sessionInfo == null) {
Log.w(TAG, "releaseControllerOnHandler: Ignoring null sessionInfo.");
return;
}
RoutingController matchingController;
synchronized (mLock) {
matchingController = mNonSystemRoutingControllers.get(sessionInfo.getId());
}
if (matchingController == null) {
if (DEBUG) {
Log.d(
TAG,
"releaseControllerOnHandler: Matching controller not found. "
+ "uniqueSessionId="
+ sessionInfo.getId());
}
return;
}
RoutingSessionInfo oldInfo = matchingController.getRoutingSessionInfo();
if (!TextUtils.equals(oldInfo.getProviderId(), sessionInfo.getProviderId())) {
Log.w(
TAG,
"releaseControllerOnHandler: Provider IDs are not matched. old="
+ oldInfo.getProviderId()
+ ", new="
+ sessionInfo.getProviderId());
return;
}
matchingController.releaseInternal(/* shouldReleaseSession= */ false);
}
void onRequestCreateControllerByManagerOnHandler(
RoutingSessionInfo oldSession, MediaRoute2Info route, long managerRequestId) {
RoutingController controller;
if (oldSession.isSystemSession()) {
controller = getSystemController();
} else {
synchronized (mLock) {
controller = mNonSystemRoutingControllers.get(oldSession.getId());
}
}
if (controller == null) {
return;
}
requestCreateController(controller, route, managerRequestId);
}
/**
* Returns whether this router is created with {@link #getInstance(Context, String)}. This kind
* of router can control the target app's media routing.
*/
private boolean isSystemRouter() {
return mClientPackageName != null;
}
/**
* Returns a {@link RoutingSessionInfo} which has the client package name. The client package
* name is set only when the given sessionInfo doesn't have it. Should only used for system
* media routers.
*/
private RoutingSessionInfo ensureClientPackageNameForSystemSession(
@NonNull RoutingSessionInfo sessionInfo) {
if (!sessionInfo.isSystemSession()
|| !TextUtils.isEmpty(sessionInfo.getClientPackageName())) {
return sessionInfo;
}
return new RoutingSessionInfo.Builder(sessionInfo)
.setClientPackageName(mClientPackageName)
.build();
}
private List Override this to start playback with {@code newController}. You may want to get the
* status of the media that is being played with {@code oldController} and resume it
* continuously with {@code newController}. After this is called, any callbacks with {@code
* oldController} will not be invoked unless {@code oldController} is the {@link
* #getSystemController() system controller}. You need to {@link RoutingController#release()
* release} {@code oldController} before playing the media with {@code newController}.
*
* @param oldController the previous controller that controlled routing
* @param newController the new controller to control routing
* @see #transferTo(MediaRoute2Info)
*/
public void onTransfer(
@NonNull RoutingController oldController,
@NonNull RoutingController newController) {}
/**
* Called when {@link #transferTo(MediaRoute2Info)} failed.
*
* @param requestedRoute the route info which was used for the transfer
*/
public void onTransferFailure(@NonNull MediaRoute2Info requestedRoute) {}
/**
* Called when a media routing stops. It can be stopped by a user or a provider. App should
* not continue playing media locally when this method is called. The {@code controller} is
* released before this method is called.
*
* @param controller the controller that controlled the stopped media routing
*/
public void onStop(@NonNull RoutingController controller) {}
}
/**
* A listener interface to send optional app-specific hints when creating a {@link
* RoutingController}.
*/
public interface OnGetControllerHintsListener {
/**
* Called when the {@link MediaRouter2} or the system is about to request a media route
* provider service to create a controller with the given route. The {@link Bundle} returned
* here will be sent to media route provider service as a hint.
*
* Since controller creation can be requested by the {@link MediaRouter2} and the system,
* set the listener as soon as possible after acquiring {@link MediaRouter2} instance. The
* method will be called on the same thread that calls {@link #transferTo(MediaRoute2Info)}
* or the main thread if it is requested by the system.
*
* @param route the route to create a controller with
* @return An optional bundle of app-specific arguments to send to the provider, or {@code
* null} if none. The contents of this bundle may affect the result of controller
* creation.
* @see MediaRoute2ProviderService#onCreateSession(long, String, String, Bundle)
*/
@Nullable
Bundle onGetControllerHints(@NonNull MediaRoute2Info route);
}
/** Callback for receiving {@link RoutingController} updates. */
public abstract static class ControllerCallback {
/**
* Called when a controller is updated. (e.g., when the selected routes of the controller is
* changed or when the volume of the controller is changed.)
*
* @param controller the updated controller. It may be the {@link #getSystemController()
* system controller}.
* @see #getSystemController()
*/
public void onControllerUpdated(@NonNull RoutingController controller) {}
}
/**
* A class to control media routing session in media route provider. For example,
* selecting/deselecting/transferring to routes of a session can be done through this. Instances
* are created when {@link TransferCallback#onTransfer(RoutingController, RoutingController)} is
* called, which is invoked after {@link #transferTo(MediaRoute2Info)} is called.
*/
public class RoutingController {
private final Object mControllerLock = new Object();
private static final int CONTROLLER_STATE_UNKNOWN = 0;
private static final int CONTROLLER_STATE_ACTIVE = 1;
private static final int CONTROLLER_STATE_RELEASING = 2;
private static final int CONTROLLER_STATE_RELEASED = 3;
@GuardedBy("mControllerLock")
private RoutingSessionInfo mSessionInfo;
@GuardedBy("mControllerLock")
private int mState;
RoutingController(@NonNull RoutingSessionInfo sessionInfo) {
mSessionInfo = sessionInfo;
mState = CONTROLLER_STATE_ACTIVE;
}
RoutingController(@NonNull RoutingSessionInfo sessionInfo, int state) {
mSessionInfo = sessionInfo;
mState = state;
}
/**
* @return the ID of the controller. It is globally unique.
*/
@NonNull
public String getId() {
synchronized (mControllerLock) {
return mSessionInfo.getId();
}
}
/**
* Gets the original session ID set by {@link RoutingSessionInfo.Builder#Builder(String,
* String)}.
*
* @hide
*/
@NonNull
@TestApi
public String getOriginalId() {
synchronized (mControllerLock) {
return mSessionInfo.getOriginalId();
}
}
/**
* Gets the control hints used to control routing session if available. It is set by the
* media route provider.
*/
@Nullable
public Bundle getControlHints() {
synchronized (mControllerLock) {
return mSessionInfo.getControlHints();
}
}
/**
* @return the unmodifiable list of currently selected routes
*/
@NonNull
public List Please note that you may not control the volume of the session even when you can
* control the volume of each selected route in the session.
*
* @return {@link MediaRoute2Info#PLAYBACK_VOLUME_FIXED} or {@link
* MediaRoute2Info#PLAYBACK_VOLUME_VARIABLE}
*/
@MediaRoute2Info.PlaybackVolume
public int getVolumeHandling() {
synchronized (mControllerLock) {
return mSessionInfo.getVolumeHandling();
}
}
/** Gets the maximum volume of the session. */
public int getVolumeMax() {
synchronized (mControllerLock) {
return mSessionInfo.getVolumeMax();
}
}
/**
* Gets the current volume of the session.
*
* When it's available, it represents the volume of routing session, which is a group of
* selected routes. Use {@link MediaRoute2Info#getVolume()} to get the volume of a route,
*
* @see MediaRoute2Info#getVolume()
*/
public int getVolume() {
synchronized (mControllerLock) {
return mSessionInfo.getVolume();
}
}
/**
* Returns true if this controller is released, false otherwise. If it is released, then all
* other getters from this instance may return invalid values. Also, any operations to this
* instance will be ignored once released.
*
* @see #release
*/
public boolean isReleased() {
synchronized (mControllerLock) {
return mState == CONTROLLER_STATE_RELEASED;
}
}
/**
* Selects a route for the remote session. After a route is selected, the media is expected
* to be played to the all the selected routes. This is different from {@link
* MediaRouter2#transferTo(MediaRoute2Info)} transferring to a route}, where the media is
* expected to 'move' from one route to another.
*
* The given route must satisfy all of the following conditions:
*
* The given route must satisfy all of the following conditions:
*
*
*
*
* Therefore, in order to track the current routing status, keep the controller's ID instead,
* and use {@link #getController(String)} and {@link #getSystemController()} for getting
* controllers.
*
*
*
*
* If the route doesn't meet any of above conditions, it will be ignored.
*
* @see #deselectRoute(MediaRoute2Info)
* @see #getSelectedRoutes()
* @see #getSelectableRoutes()
* @see ControllerCallback#onControllerUpdated
*/
public void selectRoute(@NonNull MediaRoute2Info route) {
Objects.requireNonNull(route, "route must not be null");
if (isReleased()) {
Log.w(TAG, "selectRoute: Called on released controller. Ignoring.");
return;
}
List
*
*
* If the route doesn't meet any of above conditions, it will be ignored.
*
* @see #getSelectedRoutes()
* @see #getDeselectableRoutes()
* @see ControllerCallback#onControllerUpdated
*/
public void deselectRoute(@NonNull MediaRoute2Info route) {
Objects.requireNonNull(route, "route must not be null");
if (isReleased()) {
Log.w(TAG, "deselectRoute: called on released controller. Ignoring.");
return;
}
List