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.Log;

import com.android.internal.annotations.GuardedBy;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
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.
 *
 * Media Router 2 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;
    @GuardedBy("mLock")
    private boolean mShouldUpdateRoutes = true;
    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())); mDiscoveryPreference = new RouteDiscoveryPreference.Builder( sManager.getPreferredFeatures(clientPackageName), true).build(); 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; } mShouldUpdateRoutes = true; } } 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; mShouldUpdateRoutes = true; 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) { if (mShouldUpdateRoutes) { mShouldUpdateRoutes = false; List filteredRoutes = new ArrayList<>(); for (MediaRoute2Info route : mRoutes.values()) { if (route.hasAnyFeatures(mDiscoveryPreference.getPreferredFeatures())) { filteredRoutes.add(route); } } mFilteredRoutes = Collections.unmodifiableList(filteredRoutes); } } 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; } List addedRoutes = new ArrayList<>(); List removedRoutes = new ArrayList<>(); List changedRoutes = new ArrayList<>(); synchronized (mLock) { List currentRoutesIds = currentRoutes.stream().map(MediaRoute2Info::getId) .collect(Collectors.toList()); for (String routeId : mRoutes.keySet()) { if (!currentRoutesIds.contains(routeId)) { // This route is removed while the callback is unregistered. MediaRoute2Info route = mRoutes.get(routeId); if (route.hasAnyFeatures(mDiscoveryPreference.getPreferredFeatures())) { removedRoutes.add(mRoutes.get(routeId)); } } } for (MediaRoute2Info route : currentRoutes) { if (mRoutes.containsKey(route.getId())) { if (!route.equals(mRoutes.get(route.getId()))) { // This route is changed while the callback is unregistered. if (route.hasAnyFeatures( mDiscoveryPreference.getPreferredFeatures())) { changedRoutes.add(route); } } } else { // This route is added while the callback is unregistered. if (route.hasAnyFeatures(mDiscoveryPreference.getPreferredFeatures())) { addedRoutes.add(route); } } } mRoutes.clear(); for (MediaRoute2Info route : currentRoutes) { mRoutes.put(route.getId(), route); } mShouldUpdateRoutes = true; } if (!addedRoutes.isEmpty()) { notifyRoutesAdded(addedRoutes); } if (!removedRoutes.isEmpty()) { notifyRoutesRemoved(removedRoutes); } if (!changedRoutes.isEmpty()) { notifyRoutesChanged(changedRoutes); } RoutingSessionInfo oldInfo = mSystemController.getRoutingSessionInfo(); mSystemController.setRoutingSessionInfo(currentSystemSessionInfo); if (!oldInfo.equals(currentSystemSessionInfo)) { notifyControllerUpdated(mSystemController); } } void addRoutesOnHandler(List routes) { List addedRoutes = new ArrayList<>(); synchronized (mLock) { for (MediaRoute2Info route : routes) { mRoutes.put(route.getId(), route); if (route.hasAnyFeatures(mDiscoveryPreference.getPreferredFeatures())) { addedRoutes.add(route); } } mShouldUpdateRoutes = true; } if (!addedRoutes.isEmpty()) { notifyRoutesAdded(addedRoutes); } } void removeRoutesOnHandler(List routes) { List removedRoutes = new ArrayList<>(); synchronized (mLock) { for (MediaRoute2Info route : routes) { mRoutes.remove(route.getId()); if (route.hasAnyFeatures(mDiscoveryPreference.getPreferredFeatures())) { removedRoutes.add(route); } } mShouldUpdateRoutes = true; } if (!removedRoutes.isEmpty()) { notifyRoutesRemoved(removedRoutes); } } void changeRoutesOnHandler(List routes) { List changedRoutes = new ArrayList<>(); synchronized (mLock) { for (MediaRoute2Info route : routes) { mRoutes.put(route.getId(), route); if (route.hasAnyFeatures(mDiscoveryPreference.getPreferredFeatures())) { changedRoutes.add(route); } } mShouldUpdateRoutes = true; } if (!changedRoutes.isEmpty()) { notifyRoutesChanged(changedRoutes); } } /** * 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 filterRoutes(List routes, RouteDiscoveryPreference discoveryRequest) { return routes.stream() .filter(route -> route.hasAnyFeatures(discoveryRequest.getPreferredFeatures())) .collect(Collectors.toList()); } private void updateAllRoutesFromManager() { if (!isSystemRouter()) { return; } synchronized (mLock) { mRoutes.clear(); for (MediaRoute2Info route : sManager.getAllRoutes()) { mRoutes.put(route.getId(), route); } mShouldUpdateRoutes = true; } } private void notifyRoutesAdded(List routes) { for (RouteCallbackRecord record: mRouteCallbackRecords) { List filteredRoutes = filterRoutes(routes, record.mPreference); if (!filteredRoutes.isEmpty()) { record.mExecutor.execute( () -> record.mRouteCallback.onRoutesAdded(filteredRoutes)); } } } private void notifyRoutesRemoved(List routes) { for (RouteCallbackRecord record: mRouteCallbackRecords) { List filteredRoutes = filterRoutes(routes, record.mPreference); if (!filteredRoutes.isEmpty()) { record.mExecutor.execute( () -> record.mRouteCallback.onRoutesRemoved(filteredRoutes)); } } } private void notifyRoutesChanged(List routes) { for (RouteCallbackRecord record: mRouteCallbackRecords) { List filteredRoutes = filterRoutes(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(); List filteredRoutes; synchronized (mLock) { filteredRoutes = filterRoutes(routes, mDiscoveryPreference); } if (filteredRoutes.isEmpty()) { return; } for (RouteCallbackRecord record: mRouteCallbackRecords) { record.mExecutor.execute( () -> record.mRouteCallback.onRoutesAdded(filteredRoutes)); } } @Override public void onRoutesRemoved(@NonNull List routes) { updateAllRoutesFromManager(); List filteredRoutes; synchronized (mLock) { filteredRoutes = filterRoutes(routes, mDiscoveryPreference); } if (filteredRoutes.isEmpty()) { return; } for (RouteCallbackRecord record: mRouteCallbackRecords) { record.mExecutor.execute( () -> record.mRouteCallback.onRoutesRemoved(filteredRoutes)); } } @Override public void onRoutesChanged(@NonNull List routes) { updateAllRoutesFromManager(); List filteredRoutes; synchronized (mLock) { filteredRoutes = filterRoutes(routes, mDiscoveryPreference); } if (filteredRoutes.isEmpty()) { return; } for (RouteCallbackRecord record: mRouteCallbackRecords) { record.mExecutor.execute( () -> record.mRouteCallback.onRoutesChanged(filteredRoutes)); } } @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 onPreferredFeaturesChanged(@NonNull String packageName, @NonNull List preferredFeatures) { if (!TextUtils.equals(mClientPackageName, packageName)) { return; } synchronized (mLock) { mDiscoveryPreference = new RouteDiscoveryPreference.Builder( preferredFeatures, true).build(); } updateAllRoutesFromManager(); notifyPreferredFeaturesChanged(preferredFeatures); } @Override public void onRequestFailed(int reason) { // Does nothing. } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy