All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.android.media.MediaRouter2 Maven / Gradle / Ivy

Go to download

A library jar that provides APIs for Applications written for the Google Android Platform.

There is a newer version: 15-robolectric-12650502
Show newest version
/*
 * 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.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 sSystemMediaRouter2Map = new ArrayMap<>(); private static MediaRouter2Manager sManager; @GuardedBy("sRouterLock") private static MediaRouter2 sInstance; private final Context mContext; private final IMediaRouterService mMediaRouterService; private final Object mLock = new Object(); private final CopyOnWriteArrayList mRouteCallbackRecords = new CopyOnWriteArrayList<>(); private final CopyOnWriteArrayList mTransferCallbackRecords = new CopyOnWriteArrayList<>(); private final CopyOnWriteArrayList mControllerCallbackRecords = new CopyOnWriteArrayList<>(); private final CopyOnWriteArrayList mControllerCreationRequests = new CopyOnWriteArrayList<>(); // TODO: Specify the fields that are only used (or not used) by system media router. private final String mClientPackageName; final ManagerCallback mManagerCallback; private final String mPackageName; @GuardedBy("mLock") final Map mRoutes = new ArrayMap<>(); final RoutingController mSystemController; @GuardedBy("mLock") private RouteDiscoveryPreference mDiscoveryPreference = RouteDiscoveryPreference.EMPTY; // TODO: Make MediaRouter2 is always connected to the MediaRouterService. @GuardedBy("mLock") MediaRouter2Stub mStub; @GuardedBy("mLock") private final Map mNonSystemRoutingControllers = new ArrayMap<>(); private final AtomicInteger mNextRequestId = new AtomicInteger(1); final Handler mHandler; private volatile ArrayMap mPreviousRoutes = new ArrayMap<>(); private volatile List mFilteredRoutes = Collections.emptyList(); private volatile OnGetControllerHintsListener mOnGetControllerHintsListener; /** Gets an instance of the media router associated with the context. */ @NonNull public static MediaRouter2 getInstance(@NonNull Context context) { Objects.requireNonNull(context, "context must not be null"); synchronized (sRouterLock) { if (sInstance == null) { sInstance = new MediaRouter2(context.getApplicationContext()); } return sInstance; } } /** * Gets an instance of the system media router which controls the app's media routing. Returns * {@code null} if the given package name is invalid. There are several things to note when * using the media routers created with this method. * *

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: * *

    *
  • {@link #getControllers()} *
  • {@link #getController(String)}} *
  • {@link TransferCallback#onTransfer(RoutingController, RoutingController)} *
  • {@link TransferCallback#onStop(RoutingController)} *
  • {@link ControllerCallback#onControllerUpdated(RoutingController)} *
* * 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. * *

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()) { sManager.startScan(); } } /** * 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()) { sManager.stopScan(); } } 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 currentSystemRoutes = null; RoutingSessionInfo currentSystemSessionInfo = null; try { currentSystemRoutes = mMediaRouterService.getSystemRoutes(); currentSystemSessionInfo = mMediaRouterService.getSystemSessionInfo(); } catch (RemoteException ex) { Log.e(TAG, "Unable to get current system's routes / session info", ex); } if (currentSystemRoutes == null || currentSystemRoutes.isEmpty()) { throw new RuntimeException("Null or empty currentSystemRoutes. Something is wrong."); } if (currentSystemSessionInfo == null) { throw new RuntimeException("Null currentSystemSessionInfo. Something is wrong."); } for (MediaRoute2Info route : currentSystemRoutes) { mRoutes.put(route.getId(), route); } mSystemController = new SystemRoutingController(currentSystemSessionInfo); // Only used by system MediaRouter2. mClientPackageName = null; mManagerCallback = null; } private MediaRouter2(Context context, String clientPackageName) { mContext = context; mClientPackageName = clientPackageName; mManagerCallback = new ManagerCallback(); mHandler = new Handler(Looper.getMainLooper()); mSystemController = new SystemRoutingController( ensureClientPackageNameForSystemSession( sManager.getSystemRoutingSession(clientPackageName))); mDiscoveryPreference = sManager.getDiscoveryPreference(clientPackageName); updateAllRoutesFromManager(); // Only used by non-system MediaRouter2. mMediaRouterService = null; mPackageName = null; } /** * Returns whether any route in {@code routeList} has a same unique ID with given route. * * @hide */ static boolean checkRouteListContainsRouteId( @NonNull List routeList, @NonNull String routeId) { for (MediaRoute2Info info : routeList) { if (TextUtils.equals(routeId, info.getId())) { return true; } } return false; } /** * Gets the client package name of the app which this media router controls. * *

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 getAllRoutes() { if (isSystemRouter()) { return sManager.getAllRoutes(); } return Collections.emptyList(); } /** * Gets the unmodifiable list of {@link MediaRoute2Info routes} currently known to the media * router. * *

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 getRoutes() { synchronized (mLock) { return mFilteredRoutes; } } /** * Registers a callback to get the result of {@link #transferTo(MediaRoute2Info)}. * If you register the same callback twice or more, it will be ignored. * * @param executor the executor to execute the callback on * @param callback the callback to register * @see #unregisterTransferCallback */ public void registerTransferCallback( @NonNull @CallbackExecutor Executor executor, @NonNull TransferCallback callback) { Objects.requireNonNull(executor, "executor must not be null"); Objects.requireNonNull(callback, "callback must not be null"); TransferCallbackRecord record = new TransferCallbackRecord(executor, callback); if (!mTransferCallbackRecords.addIfAbsent(record)) { Log.w(TAG, "registerTransferCallback: Ignoring the same callback"); return; } } /** * 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 callback the callback to unregister * @see #registerTransferCallback */ public void unregisterTransferCallback(@NonNull TransferCallback callback) { Objects.requireNonNull(callback, "callback must not be null"); if (!mTransferCallbackRecords.remove(new TransferCallbackRecord(null, callback))) { Log.w(TAG, "unregisterTransferCallback: Ignoring an unknown callback"); return; } } /** * Registers a {@link ControllerCallback}. If you register the same callback twice or more, it * will be ignored. * * @see #unregisterControllerCallback(ControllerCallback) */ public void registerControllerCallback( @NonNull @CallbackExecutor Executor executor, @NonNull ControllerCallback callback) { Objects.requireNonNull(executor, "executor must not be null"); Objects.requireNonNull(callback, "callback must not be null"); ControllerCallbackRecord record = new ControllerCallbackRecord(executor, callback); if (!mControllerCallbackRecords.addIfAbsent(record)) { Log.w(TAG, "registerControllerCallback: Ignoring the same callback"); return; } } /** * Unregisters a {@link ControllerCallback}. The callback will no longer receive events. * If the callback has not been added or been removed already, it is ignored. * * @see #registerControllerCallback(Executor, ControllerCallback) */ public void unregisterControllerCallback(@NonNull ControllerCallback callback) { Objects.requireNonNull(callback, "callback must not be null"); if (!mControllerCallbackRecords.remove(new ControllerCallbackRecord(null, callback))) { Log.w(TAG, "unregisterControllerCallback: Ignoring an unknown callback"); return; } } /** * Sets an {@link OnGetControllerHintsListener} to send hints when creating a * {@link RoutingController}. To send the hints, listener should be set BEFORE calling * {@link #transferTo(MediaRoute2Info)}. * * @param listener A listener to send optional app-specific hints when creating a controller. * {@code null} for unset. */ public void setOnGetControllerHintsListener(@Nullable OnGetControllerHintsListener listener) { if (isSystemRouter()) { return; } mOnGetControllerHintsListener = listener; } /** * Transfers the current media to the given route. If it's necessary a new * {@link RoutingController} is created or it is handled within the current routing controller. * * @param route the route you want to transfer the current media to. Pass {@code null} to * stop routing of the current media. * @see TransferCallback#onTransfer * @see TransferCallback#onTransferFailure */ public void transferTo(@NonNull MediaRoute2Info route) { if (isSystemRouter()) { sManager.selectRoute(mClientPackageName, route); return; } Log.v(TAG, "Transferring to route: " + route); boolean routeFound; synchronized (mLock) { // TODO: Check thread-safety routeFound = mRoutes.containsKey(route.getId()); } if (!routeFound) { notifyTransferFailure(route); return; } RoutingController controller = getCurrentController(); if (controller.getRoutingSessionInfo().getTransferableRoutes().contains(route.getId())) { controller.transferToRoute(route); return; } requestCreateController(controller, route, MANAGER_REQUEST_ID_NONE); } /** * Stops the current media routing. If the {@link #getSystemController() system controller} * controls the media routing, this method is a no-op. */ public void stop() { if (isSystemRouter()) { List sessionInfos = sManager.getRoutingSessions(mClientPackageName); RoutingSessionInfo sessionToRelease = sessionInfos.get(sessionInfos.size() - 1); sManager.releaseSession(sessionToRelease); return; } getCurrentController().release(); } /** * Transfers the media of a routing controller to the given route. * *

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 controllers = getControllers(); return controllers.get(controllers.size() - 1); } /** * Gets a {@link RoutingController} which can control the routes provided by system. * e.g. Phone speaker, wired headset, Bluetooth, etc. * *

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 getControllers() { List result = new ArrayList<>(); if (isSystemRouter()) { // Unlike non-system MediaRouter2, controller instances cannot be kept, // since the transfer events initiated from other apps will not come through manager. List sessions = sManager.getRoutingSessions(mClientPackageName); for (RoutingSessionInfo session : sessions) { RoutingController controller; if (session.isSystemSession()) { mSystemController.setRoutingSessionInfo( ensureClientPackageNameForSystemSession(session)); controller = mSystemController; } else { controller = new RoutingController(session); } result.add(controller); } return result; } result.add(0, mSystemController); synchronized (mLock) { result.addAll(mNonSystemRoutingControllers.values()); } return result; } /** * Requests a volume change for the route asynchronously. * It may have no effect if the route is currently not selected. * *

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 currentRoutes, RoutingSessionInfo currentSystemSessionInfo) { if (currentRoutes == null || currentRoutes.isEmpty() || currentSystemSessionInfo == null) { Log.e(TAG, "syncRoutesOnHandler: Received wrong data. currentRoutes=" + currentRoutes + ", currentSystemSessionInfo=" + currentSystemSessionInfo); return; } synchronized (mLock) { mRoutes.clear(); for (MediaRoute2Info route : currentRoutes) { mRoutes.put(route.getId(), route); } updateFilteredRoutesLocked(); } RoutingSessionInfo oldInfo = mSystemController.getRoutingSessionInfo(); mSystemController.setRoutingSessionInfo(currentSystemSessionInfo); if (!oldInfo.equals(currentSystemSessionInfo)) { notifyControllerUpdated(mSystemController); } } void dispatchFilteredRoutesChangedLocked(List newRoutes) { List addedRoutes = new ArrayList<>(); List removedRoutes = new ArrayList<>(); List changedRoutes = new ArrayList<>(); Set newRouteIds = newRoutes.stream().map(MediaRoute2Info::getId).collect(Collectors.toSet()); for (MediaRoute2Info route : newRoutes) { MediaRoute2Info prevRoute = mPreviousRoutes.get(route.getId()); if (prevRoute == null) { addedRoutes.add(route); } else if (!prevRoute.equals(route)) { changedRoutes.add(route); } } for (int i = 0; i < mPreviousRoutes.size(); i++) { if (!newRouteIds.contains(mPreviousRoutes.keyAt(i))) { removedRoutes.add(mPreviousRoutes.valueAt(i)); } } // update previous routes for (MediaRoute2Info route : removedRoutes) { mPreviousRoutes.remove(route.getId()); } for (MediaRoute2Info route : addedRoutes) { mPreviousRoutes.put(route.getId(), route); } for (MediaRoute2Info route : changedRoutes) { mPreviousRoutes.put(route.getId(), route); } if (!addedRoutes.isEmpty()) { notifyRoutesAdded(addedRoutes); } if (!removedRoutes.isEmpty()) { notifyRoutesRemoved(removedRoutes); } if (!changedRoutes.isEmpty()) { notifyRoutesChanged(changedRoutes); } } void addRoutesOnHandler(List routes) { synchronized (mLock) { for (MediaRoute2Info route : routes) { mRoutes.put(route.getId(), route); } updateFilteredRoutesLocked(); } } void removeRoutesOnHandler(List routes) { synchronized (mLock) { for (MediaRoute2Info route : routes) { mRoutes.remove(route.getId()); } updateFilteredRoutesLocked(); } } void changeRoutesOnHandler(List routes) { List changedRoutes = new ArrayList<>(); synchronized (mLock) { for (MediaRoute2Info route : routes) { mRoutes.put(route.getId(), route); } updateFilteredRoutesLocked(); } } /** Updates filtered routes and dispatch callbacks */ @GuardedBy("mLock") void updateFilteredRoutesLocked() { mFilteredRoutes = Collections.unmodifiableList( filterRoutesWithCompositePreferenceLocked(List.copyOf(mRoutes.values()))); mHandler.sendMessage( obtainMessage(MediaRouter2::dispatchFilteredRoutesChangedLocked, this, mFilteredRoutes)); } /** * Creates a controller and calls the {@link TransferCallback#onTransfer}. If the controller * creation has failed, then it calls {@link TransferCallback#onTransferFailure}. * *

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 getSortedRoutes( List routes, List packageOrder) { if (packageOrder.isEmpty()) { return routes; } Map packagePriority = new ArrayMap<>(); int count = packageOrder.size(); for (int i = 0; i < count; i++) { // the last package will have 1 as the priority packagePriority.put(packageOrder.get(i), count - i); } ArrayList sortedRoutes = new ArrayList<>(routes); // take the negative for descending order sortedRoutes.sort( Comparator.comparingInt(r -> -packagePriority.getOrDefault(r.getPackageName(), 0))); return sortedRoutes; } @GuardedBy("mLock") private List filterRoutesWithCompositePreferenceLocked( List routes) { Set deduplicationIdSet = new ArraySet<>(); List filteredRoutes = new ArrayList<>(); for (MediaRoute2Info route : getSortedRoutes(routes, mDiscoveryPreference.getDeduplicationPackageOrder())) { if (!route.hasAnyFeatures(mDiscoveryPreference.getPreferredFeatures())) { continue; } if (!mDiscoveryPreference.getAllowedPackages().isEmpty() && (route.getPackageName() == null || !mDiscoveryPreference .getAllowedPackages() .contains(route.getPackageName()))) { continue; } if (mDiscoveryPreference.shouldRemoveDuplicates()) { if (!Collections.disjoint(deduplicationIdSet, route.getDeduplicationIds())) { continue; } deduplicationIdSet.addAll(route.getDeduplicationIds()); } filteredRoutes.add(route); } return filteredRoutes; } private List filterRoutesWithIndividualPreference( List routes, RouteDiscoveryPreference discoveryPreference) { List filteredRoutes = new ArrayList<>(); if (isSystemRouter()) { // Individual discovery preferences do not apply for the system router. filteredRoutes.addAll(routes); return filteredRoutes; } for (MediaRoute2Info route : routes) { if (!route.hasAnyFeatures(discoveryPreference.getPreferredFeatures())) { continue; } if (!discoveryPreference.getAllowedPackages().isEmpty() && (route.getPackageName() == null || !discoveryPreference .getAllowedPackages() .contains(route.getPackageName()))) { continue; } filteredRoutes.add(route); } return filteredRoutes; } private void updateAllRoutesFromManager() { if (!isSystemRouter()) { return; } synchronized (mLock) { mRoutes.clear(); for (MediaRoute2Info route : sManager.getAllRoutes()) { mRoutes.put(route.getId(), route); } updateFilteredRoutesLocked(); } } private void notifyRoutesAdded(List routes) { for (RouteCallbackRecord record : mRouteCallbackRecords) { List filteredRoutes = filterRoutesWithIndividualPreference(routes, record.mPreference); if (!filteredRoutes.isEmpty()) { record.mExecutor.execute(() -> record.mRouteCallback.onRoutesAdded(filteredRoutes)); } } } private void notifyRoutesRemoved(List routes) { for (RouteCallbackRecord record : mRouteCallbackRecords) { List filteredRoutes = filterRoutesWithIndividualPreference(routes, record.mPreference); if (!filteredRoutes.isEmpty()) { record.mExecutor.execute( () -> record.mRouteCallback.onRoutesRemoved(filteredRoutes)); } } } private void notifyRoutesChanged(List routes) { for (RouteCallbackRecord record : mRouteCallbackRecords) { List filteredRoutes = filterRoutesWithIndividualPreference(routes, record.mPreference); if (!filteredRoutes.isEmpty()) { record.mExecutor.execute( () -> record.mRouteCallback.onRoutesChanged(filteredRoutes)); } } } private void notifyPreferredFeaturesChanged(List features) { for (RouteCallbackRecord record : mRouteCallbackRecords) { record.mExecutor.execute( () -> record.mRouteCallback.onPreferredFeaturesChanged(features)); } } private void notifyTransfer(RoutingController oldController, RoutingController newController) { for (TransferCallbackRecord record : mTransferCallbackRecords) { record.mExecutor.execute( () -> record.mTransferCallback.onTransfer(oldController, newController)); } } private void notifyTransferFailure(MediaRoute2Info route) { for (TransferCallbackRecord record : mTransferCallbackRecords) { record.mExecutor.execute(() -> record.mTransferCallback.onTransferFailure(route)); } } private void notifyStop(RoutingController controller) { for (TransferCallbackRecord record : mTransferCallbackRecords) { record.mExecutor.execute(() -> record.mTransferCallback.onStop(controller)); } } private void notifyControllerUpdated(RoutingController controller) { for (ControllerCallbackRecord record : mControllerCallbackRecords) { record.mExecutor.execute(() -> record.mCallback.onControllerUpdated(controller)); } } /** Callback for receiving events about media route discovery. */ public abstract static class RouteCallback { /** * Called when routes are added. Whenever you registers a callback, this will be invoked * with known routes. * * @param routes the list of routes that have been added. It's never empty. */ public void onRoutesAdded(@NonNull List routes) {} /** * Called when routes are removed. * * @param routes the list of routes that have been removed. It's never empty. */ public void onRoutesRemoved(@NonNull List routes) {} /** * Called when routes are changed. For example, it is called when the route's name or volume * have been changed. * * @param routes the list of routes that have been changed. It's never empty. */ public void onRoutesChanged(@NonNull List routes) {} /** * Called when the client app's preferred features are changed. When this is called, it is * recommended to {@link #getRoutes()} to get the routes that are currently available to the * app. * * @param preferredFeatures the new preferred features set by the application * @hide */ @SystemApi public void onPreferredFeaturesChanged(@NonNull List preferredFeatures) {} } /** Callback for receiving events on media transfer. */ public abstract static class TransferCallback { /** * Called when a media is transferred between two different routing controllers. This can * happen by calling {@link #transferTo(MediaRoute2Info)}. * *

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 getSelectedRoutes() { List selectedRouteIds; synchronized (mControllerLock) { selectedRouteIds = mSessionInfo.getSelectedRoutes(); } return getRoutesWithIds(selectedRouteIds); } /** * @return the unmodifiable list of selectable routes for the session. */ @NonNull public List getSelectableRoutes() { List selectableRouteIds; synchronized (mControllerLock) { selectableRouteIds = mSessionInfo.getSelectableRoutes(); } return getRoutesWithIds(selectableRouteIds); } /** * @return the unmodifiable list of deselectable routes for the session. */ @NonNull public List getDeselectableRoutes() { List deselectableRouteIds; synchronized (mControllerLock) { deselectableRouteIds = mSessionInfo.getDeselectableRoutes(); } return getRoutesWithIds(deselectableRouteIds); } /** * Gets the information about how volume is handled on the session. * *

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: * *

    *
  • It should not be included in {@link #getSelectedRoutes()} *
  • It should be included in {@link #getSelectableRoutes()} *
* * 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 selectedRoutes = getSelectedRoutes(); if (checkRouteListContainsRouteId(selectedRoutes, route.getId())) { Log.w(TAG, "Ignoring selecting a route that is already selected. route=" + route); return; } List selectableRoutes = getSelectableRoutes(); if (!checkRouteListContainsRouteId(selectableRoutes, route.getId())) { Log.w(TAG, "Ignoring selecting a non-selectable route=" + route); return; } if (isSystemRouter()) { sManager.selectRoute(getRoutingSessionInfo(), route); return; } MediaRouter2Stub stub; synchronized (mLock) { stub = mStub; } if (stub != null) { try { mMediaRouterService.selectRouteWithRouter2(stub, getId(), route); } catch (RemoteException ex) { Log.e(TAG, "Unable to select route for session.", ex); } } } /** * Deselects a route from the remote session. After a route is deselected, the media is * expected to be stopped on the deselected route. * *

The given route must satisfy all of the following conditions: * *

    *
  • It should be included in {@link #getSelectedRoutes()} *
  • It should be included in {@link #getDeselectableRoutes()} *
* * 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 selectedRoutes = getSelectedRoutes(); if (!checkRouteListContainsRouteId(selectedRoutes, route.getId())) { Log.w(TAG, "Ignoring deselecting a route that is not selected. route=" + route); return; } List deselectableRoutes = getDeselectableRoutes(); if (!checkRouteListContainsRouteId(deselectableRoutes, route.getId())) { Log.w(TAG, "Ignoring deselecting a non-deselectable route=" + route); return; } if (isSystemRouter()) { sManager.deselectRoute(getRoutingSessionInfo(), route); return; } MediaRouter2Stub stub; synchronized (mLock) { stub = mStub; } if (stub != null) { try { mMediaRouterService.deselectRouteWithRouter2(stub, getId(), route); } catch (RemoteException ex) { Log.e(TAG, "Unable to deselect route from session.", ex); } } } /** * Transfers to a given route for the remote session. The given route must be included in * {@link RoutingSessionInfo#getTransferableRoutes()}. * * @see RoutingSessionInfo#getSelectedRoutes() * @see RoutingSessionInfo#getTransferableRoutes() * @see ControllerCallback#onControllerUpdated */ void transferToRoute(@NonNull MediaRoute2Info route) { Objects.requireNonNull(route, "route must not be null"); synchronized (mControllerLock) { if (isReleased()) { Log.w(TAG, "transferToRoute: Called on released controller. Ignoring."); return; } if (!mSessionInfo.getTransferableRoutes().contains(route.getId())) { Log.w(TAG, "Ignoring transferring to a non-transferable route=" + route); return; } } MediaRouter2Stub stub; synchronized (mLock) { stub = mStub; } if (stub != null) { try { mMediaRouterService.transferToRouteWithRouter2(stub, getId(), route); } catch (RemoteException ex) { Log.e(TAG, "Unable to transfer to route for session.", ex); } } } /** * Requests a volume change for the remote session asynchronously. * * @param volume The new volume value between 0 and {@link RoutingController#getVolumeMax} * (inclusive). * @see #getVolume() */ public void setVolume(int volume) { if (getVolumeHandling() == MediaRoute2Info.PLAYBACK_VOLUME_FIXED) { Log.w(TAG, "setVolume: The routing session has fixed volume. Ignoring."); return; } if (volume < 0 || volume > getVolumeMax()) { Log.w(TAG, "setVolume: The target volume is out of range. Ignoring"); return; } if (isReleased()) { Log.w(TAG, "setVolume: Called on released controller. Ignoring."); return; } if (isSystemRouter()) { sManager.setSessionVolume(getRoutingSessionInfo(), volume); return; } MediaRouter2Stub stub; synchronized (mLock) { stub = mStub; } if (stub != null) { try { mMediaRouterService.setSessionVolumeWithRouter2(stub, getId(), volume); } catch (RemoteException ex) { Log.e(TAG, "setVolume: Failed to deliver request.", ex); } } } /** * Releases this controller and the corresponding session. Any operations on this controller * after calling this method will be ignored. The devices that are playing media will stop * playing it. */ public void release() { releaseInternal(/* shouldReleaseSession= */ true); } /** * Schedules release of the controller. * * @return {@code true} if it's successfully scheduled, {@code false} if it's already * scheduled to be released or released. */ boolean scheduleRelease() { synchronized (mControllerLock) { if (mState != CONTROLLER_STATE_ACTIVE) { return false; } mState = CONTROLLER_STATE_RELEASING; } synchronized (mLock) { // It could happen if the controller is released by the another thread // in between two locks if (!mNonSystemRoutingControllers.remove(getId(), this)) { // In that case, onStop isn't called so we return true to call onTransfer. // It's also consistent with that the another thread acquires the lock later. return true; } } mHandler.postDelayed(this::release, TRANSFER_TIMEOUT_MS); return true; } void releaseInternal(boolean shouldReleaseSession) { boolean shouldNotifyStop; synchronized (mControllerLock) { if (mState == CONTROLLER_STATE_RELEASED) { if (DEBUG) { Log.d(TAG, "releaseInternal: Called on released controller. Ignoring."); } return; } shouldNotifyStop = (mState == CONTROLLER_STATE_ACTIVE); mState = CONTROLLER_STATE_RELEASED; } if (isSystemRouter()) { sManager.releaseSession(getRoutingSessionInfo()); return; } synchronized (mLock) { mNonSystemRoutingControllers.remove(getId(), this); if (shouldReleaseSession && mStub != null) { try { mMediaRouterService.releaseSessionWithRouter2(mStub, getId()); } catch (RemoteException ex) { Log.e(TAG, "Unable to release session", ex); } } if (shouldNotifyStop) { mHandler.sendMessage( obtainMessage( MediaRouter2::notifyStop, MediaRouter2.this, RoutingController.this)); } if (mRouteCallbackRecords.isEmpty() && mNonSystemRoutingControllers.isEmpty() && mStub != null) { try { mMediaRouterService.unregisterRouter2(mStub); } catch (RemoteException ex) { Log.e(TAG, "releaseInternal: Unable to unregister media router.", ex); } mStub = null; } } } @Override public String toString() { // To prevent logging spam, we only print the ID of each route. List selectedRoutes = getSelectedRoutes().stream() .map(MediaRoute2Info::getId) .collect(Collectors.toList()); List selectableRoutes = getSelectableRoutes().stream() .map(MediaRoute2Info::getId) .collect(Collectors.toList()); List deselectableRoutes = getDeselectableRoutes().stream() .map(MediaRoute2Info::getId) .collect(Collectors.toList()); StringBuilder result = new StringBuilder() .append("RoutingController{ ") .append("id=") .append(getId()) .append(", selectedRoutes={") .append(selectedRoutes) .append("}") .append(", selectableRoutes={") .append(selectableRoutes) .append("}") .append(", deselectableRoutes={") .append(deselectableRoutes) .append("}") .append(" }"); return result.toString(); } @NonNull RoutingSessionInfo getRoutingSessionInfo() { synchronized (mControllerLock) { return mSessionInfo; } } void setRoutingSessionInfo(@NonNull RoutingSessionInfo info) { synchronized (mControllerLock) { mSessionInfo = info; } } private List getRoutesWithIds(List routeIds) { if (isSystemRouter()) { return getRoutes().stream() .filter(r -> routeIds.contains(r.getId())) .collect(Collectors.toList()); } synchronized (mLock) { return routeIds.stream() .map(mRoutes::get) .filter(Objects::nonNull) .collect(Collectors.toList()); } } } class SystemRoutingController extends RoutingController { SystemRoutingController(@NonNull RoutingSessionInfo sessionInfo) { super(sessionInfo); } @Override public boolean isReleased() { // SystemRoutingController will never be released return false; } @Override boolean scheduleRelease() { // SystemRoutingController can be always transferred return true; } @Override void releaseInternal(boolean shouldReleaseSession) { // Do nothing. SystemRoutingController will never be released } } static final class RouteCallbackRecord { public final Executor mExecutor; public final RouteCallback mRouteCallback; public final RouteDiscoveryPreference mPreference; RouteCallbackRecord( @Nullable Executor executor, @NonNull RouteCallback routeCallback, @Nullable RouteDiscoveryPreference preference) { mRouteCallback = routeCallback; mExecutor = executor; mPreference = preference; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof RouteCallbackRecord)) { return false; } return mRouteCallback == ((RouteCallbackRecord) obj).mRouteCallback; } @Override public int hashCode() { return mRouteCallback.hashCode(); } } static final class TransferCallbackRecord { public final Executor mExecutor; public final TransferCallback mTransferCallback; TransferCallbackRecord( @NonNull Executor executor, @NonNull TransferCallback transferCallback) { mTransferCallback = transferCallback; mExecutor = executor; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof TransferCallbackRecord)) { return false; } return mTransferCallback == ((TransferCallbackRecord) obj).mTransferCallback; } @Override public int hashCode() { return mTransferCallback.hashCode(); } } static final class ControllerCallbackRecord { public final Executor mExecutor; public final ControllerCallback mCallback; ControllerCallbackRecord( @Nullable Executor executor, @NonNull ControllerCallback callback) { mCallback = callback; mExecutor = executor; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof ControllerCallbackRecord)) { return false; } return mCallback == ((ControllerCallbackRecord) obj).mCallback; } @Override public int hashCode() { return mCallback.hashCode(); } } static final class ControllerCreationRequest { public final int mRequestId; public final long mManagerRequestId; public final MediaRoute2Info mRoute; public final RoutingController mOldController; ControllerCreationRequest( int requestId, long managerRequestId, @NonNull MediaRoute2Info route, @NonNull RoutingController oldController) { mRequestId = requestId; mManagerRequestId = managerRequestId; mRoute = Objects.requireNonNull(route, "route must not be null"); mOldController = Objects.requireNonNull(oldController, "oldController must not be null"); } } class MediaRouter2Stub extends IMediaRouter2.Stub { @Override public void notifyRouterRegistered( List currentRoutes, RoutingSessionInfo currentSystemSessionInfo) { mHandler.sendMessage( obtainMessage( MediaRouter2::syncRoutesOnHandler, MediaRouter2.this, currentRoutes, currentSystemSessionInfo)); } @Override public void notifyRoutesAdded(List routes) { mHandler.sendMessage( obtainMessage(MediaRouter2::addRoutesOnHandler, MediaRouter2.this, routes)); } @Override public void notifyRoutesRemoved(List routes) { mHandler.sendMessage( obtainMessage(MediaRouter2::removeRoutesOnHandler, MediaRouter2.this, routes)); } @Override public void notifyRoutesChanged(List routes) { mHandler.sendMessage( obtainMessage(MediaRouter2::changeRoutesOnHandler, MediaRouter2.this, routes)); } @Override public void notifySessionCreated(int requestId, @Nullable RoutingSessionInfo sessionInfo) { mHandler.sendMessage( obtainMessage( MediaRouter2::createControllerOnHandler, MediaRouter2.this, requestId, sessionInfo)); } @Override public void notifySessionInfoChanged(@Nullable RoutingSessionInfo sessionInfo) { mHandler.sendMessage( obtainMessage( MediaRouter2::updateControllerOnHandler, MediaRouter2.this, sessionInfo)); } @Override public void notifySessionReleased(RoutingSessionInfo sessionInfo) { mHandler.sendMessage( obtainMessage( MediaRouter2::releaseControllerOnHandler, MediaRouter2.this, sessionInfo)); } @Override public void requestCreateSessionByManager( long managerRequestId, RoutingSessionInfo oldSession, MediaRoute2Info route) { mHandler.sendMessage( obtainMessage( MediaRouter2::onRequestCreateControllerByManagerOnHandler, MediaRouter2.this, oldSession, route, managerRequestId)); } } // Note: All methods are run on main thread. class ManagerCallback implements MediaRouter2Manager.Callback { @Override public void onRoutesAdded(@NonNull List routes) { updateAllRoutesFromManager(); } @Override public void onRoutesRemoved(@NonNull List routes) { updateAllRoutesFromManager(); } @Override public void onRoutesChanged(@NonNull List routes) { updateAllRoutesFromManager(); } @Override public void onTransferred( @NonNull RoutingSessionInfo oldSession, @NonNull RoutingSessionInfo newSession) { if (!oldSession.isSystemSession() && !TextUtils.equals(mClientPackageName, oldSession.getClientPackageName())) { return; } if (!newSession.isSystemSession() && !TextUtils.equals(mClientPackageName, newSession.getClientPackageName())) { return; } // For successful in-session transfer, onControllerUpdated() handles it. if (TextUtils.equals(oldSession.getId(), newSession.getId())) { return; } RoutingController oldController; if (oldSession.isSystemSession()) { mSystemController.setRoutingSessionInfo( ensureClientPackageNameForSystemSession(oldSession)); oldController = mSystemController; } else { oldController = new RoutingController(oldSession); } RoutingController newController; if (newSession.isSystemSession()) { mSystemController.setRoutingSessionInfo( ensureClientPackageNameForSystemSession(newSession)); newController = mSystemController; } else { newController = new RoutingController(newSession); } notifyTransfer(oldController, newController); } @Override public void onTransferFailed( @NonNull RoutingSessionInfo session, @NonNull MediaRoute2Info route) { if (!session.isSystemSession() && !TextUtils.equals(mClientPackageName, session.getClientPackageName())) { return; } notifyTransferFailure(route); } @Override public void onSessionUpdated(@NonNull RoutingSessionInfo session) { if (!session.isSystemSession() && !TextUtils.equals(mClientPackageName, session.getClientPackageName())) { return; } RoutingController controller; if (session.isSystemSession()) { mSystemController.setRoutingSessionInfo( ensureClientPackageNameForSystemSession(session)); controller = mSystemController; } else { controller = new RoutingController(session); } notifyControllerUpdated(controller); } @Override public void onSessionReleased(@NonNull RoutingSessionInfo session) { if (session.isSystemSession()) { Log.e(TAG, "onSessionReleased: Called on system session. Ignoring."); return; } if (!TextUtils.equals(mClientPackageName, session.getClientPackageName())) { return; } notifyStop(new RoutingController(session, RoutingController.CONTROLLER_STATE_RELEASED)); } @Override public void onDiscoveryPreferenceChanged( @NonNull String packageName, @NonNull RouteDiscoveryPreference preference) { if (!TextUtils.equals(mClientPackageName, packageName)) { return; } synchronized (mLock) { mDiscoveryPreference = preference; } updateAllRoutesFromManager(); notifyPreferredFeaturesChanged(preference.getPreferredFeatures()); } @Override public void onRequestFailed(int reason) { // Does nothing. } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy