GlobalMediaRouter.java

/*
 * Copyright 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.mediarouter.media;

import static androidx.mediarouter.media.MediaRouter.AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE;
import static androidx.mediarouter.media.MediaRouter.AVAILABILITY_FLAG_REQUIRE_MATCH;
import static androidx.mediarouter.media.MediaRouter.CALLBACK_FLAG_FORCE_DISCOVERY;
import static androidx.mediarouter.media.MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN;
import static androidx.mediarouter.media.MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY;
import static androidx.mediarouter.media.MediaRouter.UNSELECT_REASON_ROUTE_CHANGED;
import static androidx.mediarouter.media.MediaRouter.UNSELECT_REASON_STOPPED;
import static androidx.mediarouter.media.MediaRouter.UNSELECT_REASON_UNKNOWN;

import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.support.v4.media.session.MediaSessionCompat;
import android.text.TextUtils;
import android.util.Log;
import android.view.Display;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.core.app.ActivityManagerCompat;
import androidx.core.content.ContextCompat;
import androidx.core.hardware.display.DisplayManagerCompat;
import androidx.core.util.Pair;
import androidx.media.VolumeProviderCompat;

import com.google.common.util.concurrent.ListenableFuture;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

/**
 * Global state for the media router.
 *
 * <p>Media routes and media route providers are global to the process; their state and the bulk of
 * the media router implementation lives here.
 */
/* package */ final class GlobalMediaRouter
        implements SystemMediaRouteProvider.SyncCallback,
        RegisteredMediaRouteProviderWatcher.Callback {

    static final String TAG = "GlobalMediaRouter";
    static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
    final Context mApplicationContext;
    SystemMediaRouteProvider mSystemProvider;
    @VisibleForTesting
    RegisteredMediaRouteProviderWatcher mRegisteredProviderWatcher;
    boolean mTransferReceiverDeclared;
    MediaRoute2Provider mMr2Provider;

    final ArrayList<WeakReference<MediaRouter>> mRouters = new ArrayList<>();
    private final ArrayList<MediaRouter.RouteInfo> mRoutes = new ArrayList<>();
    private final Map<Pair<String, String>, String> mUniqueIdMap = new HashMap<>();
    private final ArrayList<MediaRouter.ProviderInfo> mProviders = new ArrayList<>();
    private final ArrayList<RemoteControlClientRecord> mRemoteControlClients = new ArrayList<>();
    final RemoteControlClientCompat.PlaybackInfo mPlaybackInfo =
            new RemoteControlClientCompat.PlaybackInfo();
    private final ProviderCallback mProviderCallback = new ProviderCallback();
    final CallbackHandler mCallbackHandler = new CallbackHandler();
    private DisplayManagerCompat mDisplayManager;
    private final boolean mLowRam;
    private MediaRouterActiveScanThrottlingHelper mActiveScanThrottlingHelper;

    private MediaRouterParams mRouterParams;
    MediaRouter.RouteInfo mDefaultRoute;
    private MediaRouter.RouteInfo mBluetoothRoute;
    MediaRouter.RouteInfo mSelectedRoute;
    MediaRouteProvider.RouteController mSelectedRouteController;
    // Represents a route that are requested to be selected asynchronously.
    MediaRouter.RouteInfo mRequestedRoute;
    MediaRouteProvider.RouteController mRequestedRouteController;
    // A map from unique route ID to RouteController for the member routes in the currently
    // selected route group.
    final Map<String, MediaRouteProvider.RouteController> mRouteControllerMap = new HashMap<>();
    private MediaRouteDiscoveryRequest mDiscoveryRequest;
    private MediaRouteDiscoveryRequest mDiscoveryRequestForMr2Provider;
    private int mCallbackCount;
    MediaRouter.OnPrepareTransferListener mOnPrepareTransferListener;
    MediaRouter.PrepareTransferNotifier mTransferNotifier;
    private MediaSessionRecord mMediaSession;
    MediaSessionCompat mRccMediaSession;
    private MediaSessionCompat mCompatSession;
    private final MediaSessionCompat.OnActiveChangeListener mSessionActiveListener =
            new MediaSessionCompat.OnActiveChangeListener() {
                @Override
                public void onActiveChanged() {
                    if (mRccMediaSession != null) {
                        android.media.RemoteControlClient remoteControlClient =
                                (android.media.RemoteControlClient)
                                        mRccMediaSession.getRemoteControlClient();
                        if (mRccMediaSession.isActive()) {
                            addRemoteControlClient(remoteControlClient);
                        } else {
                            removeRemoteControlClient(remoteControlClient);
                        }
                    }
                }
            };

    /* package */ GlobalMediaRouter(Context applicationContext) {
        mApplicationContext = applicationContext;
        mLowRam =
                ActivityManagerCompat.isLowRamDevice(
                        (ActivityManager)
                                applicationContext.getSystemService(Context.ACTIVITY_SERVICE));

        mTransferReceiverDeclared =
                Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
                        && MediaTransferReceiver.isDeclared(mApplicationContext);
        mMr2Provider =
                Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && mTransferReceiverDeclared
                        ? new MediaRoute2Provider(mApplicationContext, new Mr2ProviderCallback())
                        : null;

        // Add the system media route provider for interoperating with
        // the framework media router.  This one is special and receives
        // synchronization messages from the media router.
        mSystemProvider = SystemMediaRouteProvider.obtain(mApplicationContext, this);
        start();
    }

    private void start() {
        mActiveScanThrottlingHelper =
                new MediaRouterActiveScanThrottlingHelper(this::updateDiscoveryRequest);
        addProvider(mSystemProvider, /* treatRouteDescriptorIdsAsUnique= */ true);
        if (mMr2Provider != null) {
            addProvider(mMr2Provider, /* treatRouteDescriptorIdsAsUnique= */ true);
        }

        // Start watching for routes published by registered media route
        // provider services.
        mRegisteredProviderWatcher =
                new RegisteredMediaRouteProviderWatcher(mApplicationContext, this);
        mRegisteredProviderWatcher.start();
    }

    /* package */ void reset() {
        mActiveScanThrottlingHelper.reset();

        setRouteListingPreference(null);
        setMediaSessionCompat(null);

        mRegisteredProviderWatcher.stop();

        for (RemoteControlClientRecord record : mRemoteControlClients) {
            record.disconnect();
        }

        List<MediaRouter.ProviderInfo> providers = new ArrayList<>(mProviders);
        for (MediaRouter.ProviderInfo providerInfo : providers) {
            removeProvider(providerInfo.mProviderInstance);
        }
        mCallbackHandler.removeCallbacksAndMessages(null);
    }

    /* package */ MediaRouter getRouter(Context context) {
        MediaRouter router;
        for (int i = mRouters.size(); --i >= 0; ) {
            router = mRouters.get(i).get();
            if (router == null) {
                mRouters.remove(i);
            } else if (router.mContext == context) {
                return router;
            }
        }
        router = new MediaRouter(context);
        mRouters.add(new WeakReference<>(router));
        return router;
    }

    /* package */ ContentResolver getContentResolver() {
        return mApplicationContext.getContentResolver();
    }

    /* package */ Display getDisplay(int displayId) {
        if (mDisplayManager == null) {
            mDisplayManager = DisplayManagerCompat.getInstance(mApplicationContext);
        }
        return mDisplayManager.getDisplay(displayId);
    }

    /* package */ void sendControlRequest(
            MediaRouter.RouteInfo route,
            Intent intent,
            MediaRouter.ControlRequestCallback callback) {
        if (route == mSelectedRoute && mSelectedRouteController != null) {
            if (mSelectedRouteController.onControlRequest(intent, callback)) {
                return;
            }
        }
        if (mTransferNotifier != null
                && route == mTransferNotifier.mToRoute
                && mTransferNotifier.mToRouteController != null) {
            if (mTransferNotifier.mToRouteController.onControlRequest(intent, callback)) {
                return;
            }
        }
        if (callback != null) {
            callback.onError(null, null);
        }
    }

    /* package */ void requestSetVolume(MediaRouter.RouteInfo route, int volume) {
        if (route == mSelectedRoute && mSelectedRouteController != null) {
            mSelectedRouteController.onSetVolume(volume);
        } else if (!mRouteControllerMap.isEmpty()) {
            MediaRouteProvider.RouteController controller =
                    mRouteControllerMap.get(route.mUniqueId);
            if (controller != null) {
                controller.onSetVolume(volume);
            }
        }
    }

    /* package */ void requestUpdateVolume(MediaRouter.RouteInfo route, int delta) {
        if (route == mSelectedRoute && mSelectedRouteController != null) {
            mSelectedRouteController.onUpdateVolume(delta);
        } else if (!mRouteControllerMap.isEmpty()) {
            MediaRouteProvider.RouteController controller =
                    mRouteControllerMap.get(route.mUniqueId);
            if (controller != null) {
                controller.onUpdateVolume(delta);
            }
        }
    }

    /* package */ MediaRouter.RouteInfo getRoute(String uniqueId) {
        for (MediaRouter.RouteInfo info : mRoutes) {
            if (info.mUniqueId.equals(uniqueId)) {
                return info;
            }
        }
        return null;
    }

    /* package */ List<MediaRouter.RouteInfo> getRoutes() {
        return mRoutes;
    }

    @Nullable
        /* package */ MediaRouterParams getRouterParams() {
        return mRouterParams;
    }

    // isMediaTransferEnabled() is true only on R+ device.
    @SuppressLint("NewApi")
    /* package */ void setRouterParams(@Nullable MediaRouterParams params) {
        MediaRouterParams oldParams = mRouterParams;
        mRouterParams = params;

        if (isMediaTransferEnabled()) {
            if (mMr2Provider == null) {
                mMr2Provider =
                        new MediaRoute2Provider(mApplicationContext, new Mr2ProviderCallback());
                addProvider(mMr2Provider, /* treatRouteDescriptorIdsAsUnique= */ true);
                // Make sure mDiscoveryRequestForMr2Provider is updated
                updateDiscoveryRequest();
                mRegisteredProviderWatcher.rescan();
            }

            boolean oldTransferToLocalEnabled =
                    oldParams != null && oldParams.isTransferToLocalEnabled();
            boolean newTransferToLocalEnabled = params != null && params.isTransferToLocalEnabled();

            if (oldTransferToLocalEnabled != newTransferToLocalEnabled) {
                // Since the discovery request itself is not changed,
                // call setDiscoveryRequestInternal to avoid the equality check.
                mMr2Provider.setDiscoveryRequestInternal(mDiscoveryRequestForMr2Provider);
            }
        } else {
            if (mMr2Provider != null) {
                removeProvider(mMr2Provider);
                mMr2Provider = null;
                mRegisteredProviderWatcher.rescan();
            }
        }
        mCallbackHandler.post(CallbackHandler.MSG_ROUTER_PARAMS_CHANGED, params);
    }

    /* package */ void setRouteListingPreference(
            @Nullable RouteListingPreference routeListingPreference) {
        if (mMr2Provider != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
            mMr2Provider.setRouteListingPreference(routeListingPreference);
        }
    }

    @NonNull
        /* package */ List<MediaRouter.ProviderInfo> getProviders() {
        return mProviders;
    }

    @NonNull
        /* package */ MediaRouter.RouteInfo getDefaultRoute() {
        if (mDefaultRoute == null) {
            // This should never happen once the media router has been fully
            // initialized but it is good to check for the error in case there
            // is a bug in provider initialization.
            throw new IllegalStateException(
                    "There is no default route.  "
                            + "The media router has not yet been fully initialized.");
        }
        return mDefaultRoute;
    }

    /* package */ MediaRouter.RouteInfo getBluetoothRoute() {
        return mBluetoothRoute;
    }

    @NonNull
        /* package */ MediaRouter.RouteInfo getSelectedRoute() {
        if (mSelectedRoute == null) {
            // This should never happen once the media router has been fully
            // initialized but it is good to check for the error in case there
            // is a bug in provider initialization.
            throw new IllegalStateException(
                    "There is no currently selected route.  "
                            + "The media router has not yet been fully initialized.");
        }
        return mSelectedRoute;
    }

    @Nullable
        /* package */ MediaRouter.RouteInfo.DynamicGroupState getDynamicGroupState(
            MediaRouter.RouteInfo route) {
        return mSelectedRoute.getDynamicGroupState(route);
    }

    /* package */ void addMemberToDynamicGroup(@NonNull MediaRouter.RouteInfo route) {
        if (!(mSelectedRouteController instanceof MediaRouteProvider.DynamicGroupRouteController)) {
            throw new IllegalStateException(
                    "There is no currently selected " + "dynamic group route.");
        }
        MediaRouter.RouteInfo.DynamicGroupState state = getDynamicGroupState(route);
        if (mSelectedRoute.getMemberRoutes().contains(route)
                || state == null
                || !state.isGroupable()) {
            Log.w(TAG, "Ignoring attempt to add a non-groupable route to dynamic group : " + route);
            return;
        }
        ((MediaRouteProvider.DynamicGroupRouteController) mSelectedRouteController)
                .onAddMemberRoute(route.getDescriptorId());
    }

    /* package */ void removeMemberFromDynamicGroup(@NonNull MediaRouter.RouteInfo route) {
        if (!(mSelectedRouteController instanceof MediaRouteProvider.DynamicGroupRouteController)) {
            throw new IllegalStateException(
                    "There is no currently selected " + "dynamic group route.");
        }
        MediaRouter.RouteInfo.DynamicGroupState state = getDynamicGroupState(route);
        if (!mSelectedRoute.getMemberRoutes().contains(route)
                || state == null
                || !state.isUnselectable()) {
            Log.w(TAG, "Ignoring attempt to remove a non-unselectable member route : " + route);
            return;
        }
        if (mSelectedRoute.getMemberRoutes().size() <= 1) {
            Log.w(TAG, "Ignoring attempt to remove the last member route.");
            return;
        }
        ((MediaRouteProvider.DynamicGroupRouteController) mSelectedRouteController)
                .onRemoveMemberRoute(route.getDescriptorId());
    }

    /* package */ void transferToRoute(@NonNull MediaRouter.RouteInfo route) {
        if (!(mSelectedRouteController instanceof MediaRouteProvider.DynamicGroupRouteController)) {
            throw new IllegalStateException(
                    "There is no currently selected dynamic group " + "route.");
        }
        MediaRouter.RouteInfo.DynamicGroupState state = getDynamicGroupState(route);
        if (state == null || !state.isTransferable()) {
            Log.w(TAG, "Ignoring attempt to transfer to a non-transferable route.");
            return;
        }
        ((MediaRouteProvider.DynamicGroupRouteController) mSelectedRouteController)
                .onUpdateMemberRoutes(Collections.singletonList(route.getDescriptorId()));
    }

    /* package */ void selectRoute(
            @NonNull MediaRouter.RouteInfo route, @MediaRouter.UnselectReason int unselectReason) {
        if (!mRoutes.contains(route)) {
            Log.w(TAG, "Ignoring attempt to select removed route: " + route);
            return;
        }
        if (!route.mEnabled) {
            Log.w(TAG, "Ignoring attempt to select disabled route: " + route);
            return;
        }

        // Check whether the route comes from MediaRouter2. The SDK check is required to avoid a
        // lint error but is not needed.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
                && route.getProviderInstance() == mMr2Provider
                && mSelectedRoute != route) {
            mMr2Provider.transferTo(route.getDescriptorId());
        } else {
            selectRouteInternal(route, unselectReason);
        }
    }

    /* package */ boolean isRouteAvailable(MediaRouteSelector selector, int flags) {
        if (selector.isEmpty()) {
            return false;
        }

        // On low-RAM devices, do not rely on actual discovery results unless asked to.
        if ((flags & AVAILABILITY_FLAG_REQUIRE_MATCH) == 0 && mLowRam) {
            return true;
        }

        boolean useOutputSwitcher =
                mRouterParams != null
                        && mRouterParams.isOutputSwitcherEnabled()
                        && isMediaTransferEnabled();
        // Check whether any existing routes match the selector.
        final int routeCount = mRoutes.size();
        for (int i = 0; i < routeCount; i++) {
            MediaRouter.RouteInfo route = mRoutes.get(i);
            if ((flags & AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE) != 0
                    && route.isDefaultOrBluetooth()) {
                continue;
            }
            // When using the output switcher, we only care about MR2 routes and system routes.
            if (useOutputSwitcher
                    && !route.isDefaultOrBluetooth()
                    && route.getProviderInstance() != mMr2Provider) {
                continue;
            }
            if (route.matchesSelector(selector)) {
                return true;
            }
        }

        // It doesn't look like we can find a matching route right now.
        return false;
    }

    /* package */ void updateDiscoveryRequest() {
        // Combine all of the callback selectors and active scan flags.
        boolean discover = false;
        MediaRouteSelector.Builder builder = new MediaRouteSelector.Builder();
        mActiveScanThrottlingHelper.reset();

        int callbackCount = 0;
        for (int i = mRouters.size(); --i >= 0; ) {
            MediaRouter router = mRouters.get(i).get();
            if (router == null) {
                mRouters.remove(i);
            } else {
                final int count = router.mCallbackRecords.size();
                callbackCount += count;
                for (int j = 0; j < count; j++) {
                    MediaRouter.CallbackRecord callback = router.mCallbackRecords.get(j);
                    builder.addSelector(callback.mSelector);
                    boolean callbackRequestingActiveScan =
                            (callback.mFlags & CALLBACK_FLAG_PERFORM_ACTIVE_SCAN) != 0;
                    mActiveScanThrottlingHelper.requestActiveScan(
                            callbackRequestingActiveScan, callback.mTimestamp);
                    if (callbackRequestingActiveScan) {
                        discover = true; // perform active scan implies request discovery
                    }
                    if ((callback.mFlags & CALLBACK_FLAG_REQUEST_DISCOVERY) != 0) {
                        if (!mLowRam) {
                            discover = true;
                        }
                    }
                    if ((callback.mFlags & CALLBACK_FLAG_FORCE_DISCOVERY) != 0) {
                        discover = true;
                    }
                }
            }
        }

        boolean activeScan =
                mActiveScanThrottlingHelper
                        .finalizeActiveScanAndScheduleSuppressActiveScanRunnable();

        mCallbackCount = callbackCount;
        MediaRouteSelector selector = discover ? builder.build() : MediaRouteSelector.EMPTY;

        // MediaRoute2Provider should keep registering discovery preference
        // even when the callback flag is zero.
        updateMr2ProviderDiscoveryRequest(builder.build(), activeScan);

        // Create a new discovery request.
        if (mDiscoveryRequest != null
                && mDiscoveryRequest.getSelector().equals(selector)
                && mDiscoveryRequest.isActiveScan() == activeScan) {
            return; // no change
        }
        if (selector.isEmpty() && !activeScan) {
            // Discovery is not needed.
            if (mDiscoveryRequest == null) {
                return; // no change
            }
            mDiscoveryRequest = null;
        } else {
            // Discovery is needed.
            mDiscoveryRequest = new MediaRouteDiscoveryRequest(selector, activeScan);
        }
        if (DEBUG) {
            Log.d(TAG, "Updated discovery request: " + mDiscoveryRequest);
        }
        if (discover && !activeScan && mLowRam) {
            Log.i(
                    TAG,
                    "Forcing passive route discovery on a low-RAM device, "
                            + "system performance may be affected.  Please consider using "
                            + "CALLBACK_FLAG_REQUEST_DISCOVERY instead of "
                            + "CALLBACK_FLAG_FORCE_DISCOVERY.");
        }

        // Notify providers.
        for (MediaRouter.ProviderInfo providerInfo : mProviders) {
            MediaRouteProvider provider = providerInfo.mProviderInstance;
            if (provider == mMr2Provider) {
                // MediaRoute2Provider is handled by updateMr2ProviderDiscoveryRequest().
                continue;
            }
            provider.setDiscoveryRequest(mDiscoveryRequest);
        }
    }

    private void updateMr2ProviderDiscoveryRequest(
            @NonNull MediaRouteSelector selector, boolean activeScan) {
        if (!isMediaTransferEnabled()) {
            return;
        }

        if (mDiscoveryRequestForMr2Provider != null
                && mDiscoveryRequestForMr2Provider.getSelector().equals(selector)
                && mDiscoveryRequestForMr2Provider.isActiveScan() == activeScan) {
            return; // no change
        }
        if (selector.isEmpty() && !activeScan) {
            // Discovery is not needed.
            if (mDiscoveryRequestForMr2Provider == null) {
                return; // no change
            }
            mDiscoveryRequestForMr2Provider = null;
        } else {
            // Discovery is needed.
            mDiscoveryRequestForMr2Provider = new MediaRouteDiscoveryRequest(selector, activeScan);
        }
        if (DEBUG) {
            Log.d(
                    TAG,
                    "Updated MediaRoute2Provider's discovery request: "
                            + mDiscoveryRequestForMr2Provider);
        }

        mMr2Provider.setDiscoveryRequest(mDiscoveryRequestForMr2Provider);
    }

    /* package */ int getCallbackCount() {
        return mCallbackCount;
    }

    /* package */ boolean isMediaTransferEnabled() {
        // The default value for isMediaTransferReceiverEnabled() is {@code true}.
        return mTransferReceiverDeclared
                && (mRouterParams == null || mRouterParams.isMediaTransferReceiverEnabled());
    }

    /* package */ boolean isTransferToLocalEnabled() {
        if (mRouterParams == null) {
            return false;
        }
        return mRouterParams.isTransferToLocalEnabled();
    }

    /**  */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    /* package */ boolean isGroupVolumeUxEnabled() {
        return mRouterParams == null
                || mRouterParams.mExtras == null
                || mRouterParams.mExtras.getBoolean(MediaRouterParams.ENABLE_GROUP_VOLUME_UX, true);
    }

    @Override
    public void addProvider(@NonNull MediaRouteProvider providerInstance) {
        addProvider(providerInstance, /* treatRouteDescriptorIdsAsUnique= */ false);
    }

    private void addProvider(
            @NonNull MediaRouteProvider providerInstance, boolean treatRouteDescriptorIdsAsUnique) {
        if (findProviderInfo(providerInstance) == null) {
            // 1. Add the provider to the list.
            MediaRouter.ProviderInfo provider =
                    new MediaRouter.ProviderInfo(providerInstance, treatRouteDescriptorIdsAsUnique);
            mProviders.add(provider);
            if (DEBUG) {
                Log.d(TAG, "Provider added: " + provider);
            }
            mCallbackHandler.post(CallbackHandler.MSG_PROVIDER_ADDED, provider);
            // 2. Create the provider's contents.
            updateProviderContents(provider, providerInstance.getDescriptor());
            // 3. Register the provider callback.
            providerInstance.setCallback(mProviderCallback);
            // 4. Set the discovery request.
            providerInstance.setDiscoveryRequest(mDiscoveryRequest);
        }
    }

    @Override
    public void removeProvider(@NonNull MediaRouteProvider providerInstance) {
        MediaRouter.ProviderInfo provider = findProviderInfo(providerInstance);
        if (provider != null) {
            // 1. Unregister the provider callback.
            providerInstance.setCallback(null);
            // 2. Clear the discovery request.
            providerInstance.setDiscoveryRequest(null);
            // 3. Delete the provider's contents.
            updateProviderContents(provider, null);
            // 4. Remove the provider from the list.
            if (DEBUG) {
                Log.d(TAG, "Provider removed: " + provider);
            }
            mCallbackHandler.post(CallbackHandler.MSG_PROVIDER_REMOVED, provider);
            mProviders.remove(provider);
        }
    }

    @Override
    public void releaseProviderController(
            @NonNull RegisteredMediaRouteProvider provider,
            @NonNull MediaRouteProvider.RouteController controller) {
        if (mSelectedRouteController == controller) {
            selectRoute(chooseFallbackRoute(), UNSELECT_REASON_STOPPED);
        }
        // TODO: Maybe release a member route controller if the given controller is a member of
        // the selected route.
    }

    /* package */ void updateProviderDescriptor(
            MediaRouteProvider providerInstance, MediaRouteProviderDescriptor descriptor) {
        MediaRouter.ProviderInfo provider = findProviderInfo(providerInstance);
        if (provider != null) {
            // Update the provider's contents.
            updateProviderContents(provider, descriptor);
        }
    }

    private MediaRouter.ProviderInfo findProviderInfo(MediaRouteProvider providerInstance) {
        for (MediaRouter.ProviderInfo providerInfo : mProviders) {
            if (providerInfo.mProviderInstance == providerInstance) {
                return providerInfo;
            }
        }
        return null;
    }

    private void updateProviderContents(
            MediaRouter.ProviderInfo provider, MediaRouteProviderDescriptor providerDescriptor) {
        if (!provider.updateDescriptor(providerDescriptor)) {
            // Nothing to update.
            return;
        }
        // Update all existing routes and reorder them to match
        // the order of their descriptors.
        int targetIndex = 0;
        boolean selectedRouteDescriptorChanged = false;
        if (providerDescriptor != null
                && (providerDescriptor.isValid()
                || providerDescriptor == mSystemProvider.getDescriptor())) {
            final List<MediaRouteDescriptor> routeDescriptors = providerDescriptor.getRoutes();
            // Updating route group's contents requires all member routes' information.
            // Add the groups to the lists and update them later.
            List<Pair<MediaRouter.RouteInfo, MediaRouteDescriptor>> addedGroups = new ArrayList<>();
            List<Pair<MediaRouter.RouteInfo, MediaRouteDescriptor>> updatedGroups =
                    new ArrayList<>();
            for (MediaRouteDescriptor routeDescriptor : routeDescriptors) {
                // SystemMediaRouteProvider may have invalid routes
                if (routeDescriptor == null || !routeDescriptor.isValid()) {
                    Log.w(TAG, "Ignoring invalid system route descriptor: " + routeDescriptor);
                    continue;
                }
                final String id = routeDescriptor.getId();
                final int sourceIndex = provider.findRouteIndexByDescriptorId(id);

                if (sourceIndex < 0) {
                    // 1. Add the route to the list.
                    String uniqueId = assignRouteUniqueId(provider, id);
                    MediaRouter.RouteInfo route = new MediaRouter.RouteInfo(provider, id, uniqueId);

                    provider.mRoutes.add(targetIndex++, route);
                    mRoutes.add(route);
                    // 2. Create the route's contents.
                    if (routeDescriptor.getGroupMemberIds().size() > 0) {
                        addedGroups.add(new Pair<>(route, routeDescriptor));
                    } else {
                        route.maybeUpdateDescriptor(routeDescriptor);
                        // 3. Notify clients about addition.
                        if (DEBUG) {
                            Log.d(TAG, "Route added: " + route);
                        }
                        mCallbackHandler.post(CallbackHandler.MSG_ROUTE_ADDED, route);
                    }
                } else if (sourceIndex < targetIndex) {
                    Log.w(TAG, "Ignoring route descriptor with duplicate id: " + routeDescriptor);
                } else {
                    MediaRouter.RouteInfo route = provider.mRoutes.get(sourceIndex);
                    // 1. Reorder the route within the list.
                    Collections.swap(provider.mRoutes, sourceIndex, targetIndex++);
                    // 2. Update the route's contents.
                    if (routeDescriptor.getGroupMemberIds().size() > 0) {
                        updatedGroups.add(new Pair<>(route, routeDescriptor));
                    } else {
                        // 3. Notify clients about changes.
                        if (updateRouteDescriptorAndNotify(route, routeDescriptor) != 0) {
                            if (route == mSelectedRoute) {
                                selectedRouteDescriptorChanged = true;
                            }
                        }
                    }
                }
            }
            // Update the new and/or existing groups.
            for (Pair<MediaRouter.RouteInfo, MediaRouteDescriptor> pair : addedGroups) {
                MediaRouter.RouteInfo route = pair.first;
                route.maybeUpdateDescriptor(pair.second);
                if (DEBUG) {
                    Log.d(TAG, "Route added: " + route);
                }
                mCallbackHandler.post(CallbackHandler.MSG_ROUTE_ADDED, route);
            }
            for (Pair<MediaRouter.RouteInfo, MediaRouteDescriptor> pair : updatedGroups) {
                MediaRouter.RouteInfo route = pair.first;
                if (updateRouteDescriptorAndNotify(route, pair.second) != 0) {
                    if (route == mSelectedRoute) {
                        selectedRouteDescriptorChanged = true;
                    }
                }
            }
        } else {
            Log.w(TAG, "Ignoring invalid provider descriptor: " + providerDescriptor);
        }

        // Dispose all remaining routes that do not have matching descriptors.
        for (int i = provider.mRoutes.size() - 1; i >= targetIndex; i--) {
            // 1. Delete the route's contents.
            MediaRouter.RouteInfo route = provider.mRoutes.get(i);
            route.maybeUpdateDescriptor(null);
            // 2. Remove the route from the list.
            mRoutes.remove(route);
        }

        // Update the selected route if needed.
        updateSelectedRouteIfNeeded(selectedRouteDescriptorChanged);

        // Now notify clients about routes that were removed.
        // We do this after updating the selected route to ensure
        // that the framework media router observes the new route
        // selection before the removal since removing the currently
        // selected route may have side-effects.
        for (int i = provider.mRoutes.size() - 1; i >= targetIndex; i--) {
            MediaRouter.RouteInfo route = provider.mRoutes.remove(i);
            if (DEBUG) {
                Log.d(TAG, "Route removed: " + route);
            }
            mCallbackHandler.post(CallbackHandler.MSG_ROUTE_REMOVED, route);
        }

        // Notify provider changed.
        if (DEBUG) {
            Log.d(TAG, "Provider changed: " + provider);
        }
        mCallbackHandler.post(CallbackHandler.MSG_PROVIDER_CHANGED, provider);
    }

    /* package */ int updateRouteDescriptorAndNotify(
            MediaRouter.RouteInfo route, MediaRouteDescriptor routeDescriptor) {
        int changes = route.maybeUpdateDescriptor(routeDescriptor);
        if (changes != 0) {
            if ((changes & MediaRouter.RouteInfo.CHANGE_GENERAL) != 0) {
                if (DEBUG) {
                    Log.d(TAG, "Route changed: " + route);
                }
                mCallbackHandler.post(CallbackHandler.MSG_ROUTE_CHANGED, route);
            }
            if ((changes & MediaRouter.RouteInfo.CHANGE_VOLUME) != 0) {
                if (DEBUG) {
                    Log.d(TAG, "Route volume changed: " + route);
                }
                mCallbackHandler.post(CallbackHandler.MSG_ROUTE_VOLUME_CHANGED, route);
            }
            if ((changes & MediaRouter.RouteInfo.CHANGE_PRESENTATION_DISPLAY) != 0) {
                if (DEBUG) {
                    Log.d(TAG, "Route presentation display changed: " + route);
                }
                mCallbackHandler.post(
                        CallbackHandler.MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED, route);
            }
        }
        return changes;
    }

    /* package */ String assignRouteUniqueId(
            MediaRouter.ProviderInfo provider, String routeDescriptorId) {
        // Although route descriptor ids are unique within a provider, it's
        // possible for there to be two providers with the same package name.
        // Therefore we must dedupe the composite id.
        String componentName = provider.getComponentName().flattenToShortString();
        String uniqueId =
                provider.mTreatRouteDescriptorIdsAsUnique
                        ? routeDescriptorId
                        : (componentName + ":" + routeDescriptorId);
        if (provider.mTreatRouteDescriptorIdsAsUnique || findRouteByUniqueId(uniqueId) < 0) {
            mUniqueIdMap.put(new Pair<>(componentName, routeDescriptorId), uniqueId);
            return uniqueId;
        }
        Log.w(
                TAG,
                "Either "
                        + routeDescriptorId
                        + " isn't unique in "
                        + componentName
                        + " or we're trying to assign a unique ID for an already added route");
        int i = 2;
        while (true) {
            String newUniqueId = String.format(Locale.US, "%s_%d", uniqueId, i);
            if (findRouteByUniqueId(newUniqueId) < 0) {
                mUniqueIdMap.put(new Pair<>(componentName, routeDescriptorId), newUniqueId);
                return newUniqueId;
            }
            i++;
        }
    }

    private int findRouteByUniqueId(String uniqueId) {
        final int count = mRoutes.size();
        for (int i = 0; i < count; i++) {
            if (mRoutes.get(i).mUniqueId.equals(uniqueId)) {
                return i;
            }
        }
        return -1;
    }

    /* package */ String getUniqueId(MediaRouter.ProviderInfo provider, String routeDescriptorId) {
        String componentName = provider.getComponentName().flattenToShortString();
        return mUniqueIdMap.get(new Pair<>(componentName, routeDescriptorId));
    }

    /* package */ void updateSelectedRouteIfNeeded(boolean selectedRouteDescriptorChanged) {
        // Update default route.
        if (mDefaultRoute != null && !mDefaultRoute.isSelectable()) {
            Log.i(
                    TAG,
                    "Clearing the default route because it "
                            + "is no longer selectable: "
                            + mDefaultRoute);
            mDefaultRoute = null;
        }
        if (mDefaultRoute == null && !mRoutes.isEmpty()) {
            for (MediaRouter.RouteInfo route : mRoutes) {
                if (isSystemDefaultRoute(route) && route.isSelectable()) {
                    mDefaultRoute = route;
                    Log.i(TAG, "Found default route: " + mDefaultRoute);
                    break;
                }
            }
        }

        // Update bluetooth route.
        if (mBluetoothRoute != null && !mBluetoothRoute.isSelectable()) {
            Log.i(
                    TAG,
                    "Clearing the bluetooth route because it "
                            + "is no longer selectable: "
                            + mBluetoothRoute);
            mBluetoothRoute = null;
        }
        if (mBluetoothRoute == null && !mRoutes.isEmpty()) {
            for (MediaRouter.RouteInfo route : mRoutes) {
                if (isSystemLiveAudioOnlyRoute(route) && route.isSelectable()) {
                    mBluetoothRoute = route;
                    Log.i(TAG, "Found bluetooth route: " + mBluetoothRoute);
                    break;
                }
            }
        }

        // Update selected route.
        if (mSelectedRoute == null || !mSelectedRoute.isEnabled()) {
            Log.i(
                    TAG,
                    "Unselecting the current route because it "
                            + "is no longer selectable: "
                            + mSelectedRoute);
            selectRouteInternal(chooseFallbackRoute(), UNSELECT_REASON_UNKNOWN);
        } else if (selectedRouteDescriptorChanged) {
            // In case the selected route is a route group, select/unselect route controllers
            // for the added/removed route members.
            maybeUpdateMemberRouteControllers();
            updatePlaybackInfoFromSelectedRoute();
        }
    }

    /* package */ MediaRouter.RouteInfo chooseFallbackRoute() {
        // When the current route is removed or no longer selectable,
        // we want to revert to a live audio route if there is
        // one (usually Bluetooth A2DP).  Failing that, use
        // the default route.
        for (MediaRouter.RouteInfo route : mRoutes) {
            if (route != mDefaultRoute
                    && isSystemLiveAudioOnlyRoute(route)
                    && route.isSelectable()) {
                return route;
            }
        }
        return mDefaultRoute;
    }

    private boolean isSystemLiveAudioOnlyRoute(MediaRouter.RouteInfo route) {
        return route.getProviderInstance() == mSystemProvider
                && route.supportsControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO)
                && !route.supportsControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
    }

    private boolean isSystemDefaultRoute(MediaRouter.RouteInfo route) {
        return route.getProviderInstance() == mSystemProvider
                && route.mDescriptorId.equals(SystemMediaRouteProvider.DEFAULT_ROUTE_ID);
    }

    /* package */ void selectRouteInternal(
            @NonNull MediaRouter.RouteInfo route, @MediaRouter.UnselectReason int unselectReason) {
        if (mSelectedRoute == route) {
            return;
        }

        // Cancel the previous asynchronous select if exists.
        if (mRequestedRoute != null) {
            mRequestedRoute = null;
            if (mRequestedRouteController != null) {
                mRequestedRouteController.onUnselect(UNSELECT_REASON_ROUTE_CHANGED);
                mRequestedRouteController.onRelease();
                mRequestedRouteController = null;
            }
        }

        // TODO: determine how to enable dynamic grouping on pre-R devices.
        if (isMediaTransferEnabled() && route.getProvider().supportsDynamicGroup()) {
            MediaRouteProvider.DynamicGroupRouteController dynamicGroupRouteController =
                    route.getProviderInstance()
                            .onCreateDynamicGroupRouteController(route.mDescriptorId);
            // Select route asynchronously.
            if (dynamicGroupRouteController != null) {
                dynamicGroupRouteController.setOnDynamicRoutesChangedListener(
                        ContextCompat.getMainExecutor(mApplicationContext), mDynamicRoutesListener);
                mRequestedRoute = route;
                mRequestedRouteController = dynamicGroupRouteController;
                mRequestedRouteController.onSelect();
                return;
            } else {
                Log.w(
                        TAG,
                        "setSelectedRouteInternal: Failed to create dynamic group route "
                                + "controller. route="
                                + route);
            }
        }

        MediaRouteProvider.RouteController routeController =
                route.getProviderInstance().onCreateRouteController(route.mDescriptorId);
        if (routeController != null) {
            routeController.onSelect();
        }

        if (DEBUG) {
            Log.d(TAG, "Route selected: " + route);
        }

        // Don't notify during the initialization.
        if (mSelectedRoute == null) {
            mSelectedRoute = route;
            mSelectedRouteController = routeController;
            mCallbackHandler.post(
                    GlobalMediaRouter.CallbackHandler.MSG_ROUTE_SELECTED,
                    new Pair<>(null, route),
                    unselectReason);
        } else {
            notifyTransfer(
                    this,
                    route,
                    routeController,
                    unselectReason,
                    /* requestedRoute= */ null,
                    /* memberRoutes= */ null);
        }
    }

    /* package */ void maybeUpdateMemberRouteControllers() {
        if (!mSelectedRoute.isGroup()) {
            return;
        }
        List<MediaRouter.RouteInfo> routes = mSelectedRoute.getMemberRoutes();
        // Build a set of descriptor IDs for the new route group.
        Set<String> idSet = new HashSet<>();
        for (MediaRouter.RouteInfo route : routes) {
            idSet.add(route.mUniqueId);
        }
        // Unselect route controllers for the removed routes.
        Iterator<Map.Entry<String, MediaRouteProvider.RouteController>> iter =
                mRouteControllerMap.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry<String, MediaRouteProvider.RouteController> entry = iter.next();
            if (!idSet.contains(entry.getKey())) {
                MediaRouteProvider.RouteController controller = entry.getValue();
                controller.onUnselect(UNSELECT_REASON_UNKNOWN);
                controller.onRelease();
                iter.remove();
            }
        }
        // Select route controllers for the added routes.
        for (MediaRouter.RouteInfo route : routes) {
            if (!mRouteControllerMap.containsKey(route.mUniqueId)) {
                MediaRouteProvider.RouteController controller =
                        route.getProviderInstance()
                                .onCreateRouteController(
                                        route.mDescriptorId, mSelectedRoute.mDescriptorId);
                controller.onSelect();
                mRouteControllerMap.put(route.mUniqueId, controller);
            }
        }
    }

    /* package */ void notifyTransfer(
            GlobalMediaRouter router,
            MediaRouter.RouteInfo route,
            @Nullable MediaRouteProvider.RouteController routeController,
            @MediaRouter.UnselectReason int reason,
            @Nullable MediaRouter.RouteInfo requestedRoute,
            @Nullable
            Collection<
                    MediaRouteProvider.DynamicGroupRouteController
                            .DynamicRouteDescriptor>
                    memberRoutes) {
        if (mTransferNotifier != null) {
            mTransferNotifier.cancel();
            mTransferNotifier = null;
        }
        mTransferNotifier =
                new MediaRouter.PrepareTransferNotifier(
                        router, route, routeController, reason, requestedRoute, memberRoutes);

        if (mTransferNotifier.mReason != UNSELECT_REASON_ROUTE_CHANGED
                || mOnPrepareTransferListener == null) {
            mTransferNotifier.finishTransfer();
        } else {
            ListenableFuture<Void> future =
                    mOnPrepareTransferListener.onPrepareTransfer(
                            mSelectedRoute, mTransferNotifier.mToRoute);
            if (future == null) {
                mTransferNotifier.finishTransfer();
            } else {
                mTransferNotifier.setFuture(future);
            }
        }
    }

    /* package */ MediaRouteProvider.DynamicGroupRouteController.OnDynamicRoutesChangedListener
            mDynamicRoutesListener =
            new MediaRouteProvider.DynamicGroupRouteController
                    .OnDynamicRoutesChangedListener() {
                @Override
                public void onRoutesChanged(
                        @NonNull MediaRouteProvider.DynamicGroupRouteController controller,
                        @Nullable MediaRouteDescriptor groupRouteDescriptor,
                        @NonNull
                        Collection<
                                MediaRouteProvider
                                        .DynamicGroupRouteController
                                        .DynamicRouteDescriptor>
                                routes) {
                    if (controller == mRequestedRouteController
                            && groupRouteDescriptor != null) {
                        MediaRouter.ProviderInfo provider = mRequestedRoute.getProvider();
                        String groupId = groupRouteDescriptor.getId();

                        String uniqueId = assignRouteUniqueId(provider, groupId);
                        MediaRouter.RouteInfo route =
                                new MediaRouter.RouteInfo(provider, groupId, uniqueId);
                        route.maybeUpdateDescriptor(groupRouteDescriptor);

                        if (mSelectedRoute == route) {
                            return;
                        }

                        notifyTransfer(
                                GlobalMediaRouter.this,
                                route,
                                mRequestedRouteController,
                                UNSELECT_REASON_ROUTE_CHANGED,
                                mRequestedRoute,
                                routes);

                        mRequestedRoute = null;
                        mRequestedRouteController = null;
                    } else if (controller == mSelectedRouteController) {
                        if (groupRouteDescriptor != null) {
                            updateRouteDescriptorAndNotify(
                                    mSelectedRoute, groupRouteDescriptor);
                        }
                        mSelectedRoute.updateDynamicDescriptors(routes);
                    }
                }
            };

    @Override
    public void onSystemRouteSelectedByDescriptorId(@NonNull String id) {
        // System route is selected, do not sync the route we selected before.
        mCallbackHandler.removeMessages(CallbackHandler.MSG_ROUTE_SELECTED);
        MediaRouter.ProviderInfo provider = findProviderInfo(mSystemProvider);
        if (provider != null) {
            MediaRouter.RouteInfo route = provider.findRouteByDescriptorId(id);
            if (route != null) {
                route.select();
            }
        }
    }

    /* package */ void addRemoteControlClient(android.media.RemoteControlClient rcc) {
        int index = findRemoteControlClientRecord(rcc);
        if (index < 0) {
            RemoteControlClientRecord record = new RemoteControlClientRecord(rcc);
            mRemoteControlClients.add(record);
        }
    }

    /* package */ void removeRemoteControlClient(android.media.RemoteControlClient rcc) {
        int index = findRemoteControlClientRecord(rcc);
        if (index >= 0) {
            RemoteControlClientRecord record = mRemoteControlClients.remove(index);
            record.disconnect();
        }
    }

    /* package */ void setMediaSession(Object session) {
        setMediaSessionRecord(session != null ? new MediaSessionRecord(session) : null);
    }

    /* package */ void setMediaSessionCompat(final MediaSessionCompat session) {
        mCompatSession = session;
        if (Build.VERSION.SDK_INT >= 21) {
            setMediaSessionRecord(session != null ? new MediaSessionRecord(session) : null);
        } else {
            if (mRccMediaSession != null) {
                removeRemoteControlClient(
                        (android.media.RemoteControlClient)
                                mRccMediaSession.getRemoteControlClient());
                mRccMediaSession.removeOnActiveChangeListener(mSessionActiveListener);
            }
            mRccMediaSession = session;
            if (session != null) {
                session.addOnActiveChangeListener(mSessionActiveListener);
                if (session.isActive()) {
                    addRemoteControlClient(
                            (android.media.RemoteControlClient) session.getRemoteControlClient());
                }
            }
        }
    }

    private void setMediaSessionRecord(MediaSessionRecord mediaSessionRecord) {
        if (mMediaSession != null) {
            mMediaSession.clearVolumeHandling();
        }
        mMediaSession = mediaSessionRecord;
        if (mediaSessionRecord != null) {
            updatePlaybackInfoFromSelectedRoute();
        }
    }

    /* package */ MediaSessionCompat.Token getMediaSessionToken() {
        if (mMediaSession != null) {
            return mMediaSession.getToken();
        } else if (mCompatSession != null) {
            return mCompatSession.getSessionToken();
        }
        return null;
    }

    private int findRemoteControlClientRecord(android.media.RemoteControlClient rcc) {
        final int count = mRemoteControlClients.size();
        for (int i = 0; i < count; i++) {
            RemoteControlClientRecord record = mRemoteControlClients.get(i);
            if (record.getRemoteControlClient() == rcc) {
                return i;
            }
        }
        return -1;
    }

    @SuppressLint("NewApi")
    void updatePlaybackInfoFromSelectedRoute() {
        if (mSelectedRoute != null) {
            mPlaybackInfo.volume = mSelectedRoute.getVolume();
            mPlaybackInfo.volumeMax = mSelectedRoute.getVolumeMax();
            mPlaybackInfo.volumeHandling = mSelectedRoute.getVolumeHandling();
            mPlaybackInfo.playbackStream = mSelectedRoute.getPlaybackStream();
            mPlaybackInfo.playbackType = mSelectedRoute.getPlaybackType();
            if (isMediaTransferEnabled() && mSelectedRoute.getProviderInstance() == mMr2Provider) {
                mPlaybackInfo.volumeControlId =
                        MediaRoute2Provider.getSessionIdForRouteController(
                                mSelectedRouteController);
            } else {
                mPlaybackInfo.volumeControlId = null;
            }

            for (RemoteControlClientRecord remoteControlClientRecord : mRemoteControlClients) {
                remoteControlClientRecord.updatePlaybackInfo();
            }
            if (mMediaSession != null) {
                if (mSelectedRoute == getDefaultRoute() || mSelectedRoute == getBluetoothRoute()) {
                    // Local route
                    mMediaSession.clearVolumeHandling();
                } else {
                    @VolumeProviderCompat.ControlType
                    int controlType = VolumeProviderCompat.VOLUME_CONTROL_FIXED;
                    if (mPlaybackInfo.volumeHandling
                            == MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE) {
                        controlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
                    }
                    mMediaSession.configureVolume(
                            controlType,
                            mPlaybackInfo.volumeMax,
                            mPlaybackInfo.volume,
                            mPlaybackInfo.volumeControlId);
                }
            }
        } else {
            if (mMediaSession != null) {
                mMediaSession.clearVolumeHandling();
            }
        }
    }

    private final class ProviderCallback extends MediaRouteProvider.Callback {
        ProviderCallback() {
        }

        @Override
        public void onDescriptorChanged(
                @NonNull MediaRouteProvider provider, MediaRouteProviderDescriptor descriptor) {
            updateProviderDescriptor(provider, descriptor);
        }
    }

    /* package */ final class Mr2ProviderCallback extends MediaRoute2Provider.Callback {
        @Override
        public void onSelectRoute(
                @NonNull String routeDescriptorId, @MediaRouter.UnselectReason int reason) {
            MediaRouter.RouteInfo routeToSelect = null;
            for (MediaRouter.RouteInfo routeInfo : getRoutes()) {
                if (routeInfo.getProviderInstance() != mMr2Provider) {
                    continue;
                }
                if (TextUtils.equals(routeDescriptorId, routeInfo.getDescriptorId())) {
                    routeToSelect = routeInfo;
                    break;
                }
            }

            if (routeToSelect == null) {
                Log.w(
                        TAG,
                        "onSelectRoute: The target RouteInfo is not found for descriptorId="
                                + routeDescriptorId);
                return;
            }

            selectRouteInternal(routeToSelect, reason);
        }

        @Override
        public void onSelectFallbackRoute(@MediaRouter.UnselectReason int reason) {
            selectRouteToFallbackRoute(reason);
        }

        @Override
        public void onReleaseController(@NonNull MediaRouteProvider.RouteController controller) {
            if (controller == mSelectedRouteController) {
                // Stop casting
                selectRouteToFallbackRoute(UNSELECT_REASON_STOPPED);
            } else if (DEBUG) {
                // 'Cast -> Phone' / 'Cast -> Cast(old)' cases triggered by selectRoute().
                // Nothing to do.
                Log.d(
                        TAG,
                        "A RouteController unrelated to the selected route is released."
                                + " controller="
                                + controller);
            }
        }

        /* package */ void selectRouteToFallbackRoute(@MediaRouter.UnselectReason int reason) {
            MediaRouter.RouteInfo fallbackRoute = chooseFallbackRoute();
            if (getSelectedRoute() != fallbackRoute) {
                selectRouteInternal(fallbackRoute, reason);
            }
            // Does nothing when the selected route is same with fallback route.
            // This is the difference between this and unselect().
        }
    }

    private final class MediaSessionRecord {
        private final MediaSessionCompat mMsCompat;

        private @VolumeProviderCompat.ControlType int mControlType;
        private int mMaxVolume;
        private VolumeProviderCompat mVpCompat;

        MediaSessionRecord(Object mediaSession) {
            this(MediaSessionCompat.fromMediaSession(mApplicationContext, mediaSession));
        }

        MediaSessionRecord(MediaSessionCompat mediaSessionCompat) {
            mMsCompat = mediaSessionCompat;
        }

        /* package */ void configureVolume(
                @VolumeProviderCompat.ControlType int controlType,
                int max,
                int current,
                @Nullable String volumeControlId) {
            if (mMsCompat != null) {
                if (mVpCompat != null && controlType == mControlType && max == mMaxVolume) {
                    // If we haven't changed control type or max just set the
                    // new current volume
                    mVpCompat.setCurrentVolume(current);
                } else {
                    // Otherwise create a new provider and update
                    mVpCompat =
                            new VolumeProviderCompat(controlType, max, current, volumeControlId) {
                                @Override
                                public void onSetVolumeTo(final int volume) {
                                    mCallbackHandler.post(
                                            () -> {
                                                if (mSelectedRoute != null) {
                                                    mSelectedRoute.requestSetVolume(volume);
                                                }
                                            });
                                }

                                @Override
                                public void onAdjustVolume(final int direction) {
                                    mCallbackHandler.post(
                                            () -> {
                                                if (mSelectedRoute != null) {
                                                    mSelectedRoute.requestUpdateVolume(direction);
                                                }
                                            });
                                }
                            };
                    mMsCompat.setPlaybackToRemote(mVpCompat);
                }
            }
        }

        /* package */ void clearVolumeHandling() {
            if (mMsCompat != null) {
                mMsCompat.setPlaybackToLocal(mPlaybackInfo.playbackStream);
                mVpCompat = null;
            }
        }

        /* package */ MediaSessionCompat.Token getToken() {
            if (mMsCompat != null) {
                return mMsCompat.getSessionToken();
            }
            return null;
        }
    }

    private final class RemoteControlClientRecord
            implements RemoteControlClientCompat.VolumeCallback {
        private final RemoteControlClientCompat mRccCompat;
        private boolean mDisconnected;

        RemoteControlClientRecord(android.media.RemoteControlClient rcc) {
            mRccCompat = RemoteControlClientCompat.obtain(mApplicationContext, rcc);
            mRccCompat.setVolumeCallback(this);
            updatePlaybackInfo();
        }

        /* package */ android.media.RemoteControlClient getRemoteControlClient() {
            return mRccCompat.getRemoteControlClient();
        }

        /* package */ void disconnect() {
            mDisconnected = true;
            mRccCompat.setVolumeCallback(null);
        }

        /* package */ void updatePlaybackInfo() {
            mRccCompat.setPlaybackInfo(mPlaybackInfo);
        }

        @Override
        public void onVolumeSetRequest(int volume) {
            if (!mDisconnected && mSelectedRoute != null) {
                mSelectedRoute.requestSetVolume(volume);
            }
        }

        @Override
        public void onVolumeUpdateRequest(int direction) {
            if (!mDisconnected && mSelectedRoute != null) {
                mSelectedRoute.requestUpdateVolume(direction);
            }
        }
    }

    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    /* package */ final class CallbackHandler extends Handler {
        private final ArrayList<MediaRouter.CallbackRecord> mTempCallbackRecords =
                new ArrayList<>();
        private final List<MediaRouter.RouteInfo> mDynamicGroupRoutes = new ArrayList<>();

        private static final int MSG_TYPE_MASK = 0xff00;
        private static final int MSG_TYPE_ROUTE = 0x0100;
        private static final int MSG_TYPE_PROVIDER = 0x0200;
        private static final int MSG_TYPE_ROUTER = 0x0300;

        public static final int MSG_ROUTE_ADDED = MSG_TYPE_ROUTE | 1;
        public static final int MSG_ROUTE_REMOVED = MSG_TYPE_ROUTE | 2;
        public static final int MSG_ROUTE_CHANGED = MSG_TYPE_ROUTE | 3;
        public static final int MSG_ROUTE_VOLUME_CHANGED = MSG_TYPE_ROUTE | 4;
        public static final int MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED = MSG_TYPE_ROUTE | 5;
        public static final int MSG_ROUTE_SELECTED = MSG_TYPE_ROUTE | 6;
        public static final int MSG_ROUTE_UNSELECTED = MSG_TYPE_ROUTE | 7;
        public static final int MSG_ROUTE_ANOTHER_SELECTED = MSG_TYPE_ROUTE | 8;

        public static final int MSG_PROVIDER_ADDED = MSG_TYPE_PROVIDER | 1;
        public static final int MSG_PROVIDER_REMOVED = MSG_TYPE_PROVIDER | 2;
        public static final int MSG_PROVIDER_CHANGED = MSG_TYPE_PROVIDER | 3;

        public static final int MSG_ROUTER_PARAMS_CHANGED = MSG_TYPE_ROUTER | 1;

        CallbackHandler() {
        }

        /* package */ void post(int msg, Object obj) {
            obtainMessage(msg, obj).sendToTarget();
        }

        /* package */ void post(int msg, Object obj, int arg) {
            Message message = obtainMessage(msg, obj);
            message.arg1 = arg;
            message.sendToTarget();
        }

        @Override
        public void handleMessage(Message msg) {
            final int what = msg.what;
            final Object obj = msg.obj;
            final int arg = msg.arg1;

            if (what == MSG_ROUTE_CHANGED
                    && getSelectedRoute().getId().equals(((MediaRouter.RouteInfo) obj).getId())) {
                updateSelectedRouteIfNeeded(true);
            }

            // Synchronize state with the system media router.
            syncWithSystemProvider(what, obj);

            // Invoke all registered callbacks.
            // Build a list of callbacks before invoking them in case callbacks
            // are added or removed during dispatch.
            try {
                for (int i = mRouters.size(); --i >= 0; ) {
                    MediaRouter router = mRouters.get(i).get();
                    if (router == null) {
                        mRouters.remove(i);
                    } else {
                        mTempCallbackRecords.addAll(router.mCallbackRecords);
                    }
                }

                for (MediaRouter.CallbackRecord tempCallbackRecord : mTempCallbackRecords) {
                    invokeCallback(tempCallbackRecord, what, obj, arg);
                }
            } finally {
                mTempCallbackRecords.clear();
            }
        }

        // Using Pair<RouteInfo, RouteInfo>
        @SuppressWarnings({"unchecked"})
        private void syncWithSystemProvider(int what, Object obj) {
            switch (what) {
                case MSG_ROUTE_ADDED:
                    mSystemProvider.onSyncRouteAdded((MediaRouter.RouteInfo) obj);
                    break;
                case MSG_ROUTE_REMOVED:
                    mSystemProvider.onSyncRouteRemoved((MediaRouter.RouteInfo) obj);
                    break;
                case MSG_ROUTE_CHANGED:
                    mSystemProvider.onSyncRouteChanged((MediaRouter.RouteInfo) obj);
                    break;
                case MSG_ROUTE_SELECTED: {
                    MediaRouter.RouteInfo selectedRoute =
                            ((Pair<MediaRouter.RouteInfo, MediaRouter.RouteInfo>) obj).second;
                    mSystemProvider.onSyncRouteSelected(selectedRoute);
                    // TODO(b/166794092): Remove this nullness check
                    if (mDefaultRoute != null && selectedRoute.isDefaultOrBluetooth()) {
                        for (MediaRouter.RouteInfo prevGroupRoute : mDynamicGroupRoutes) {
                            mSystemProvider.onSyncRouteRemoved(prevGroupRoute);
                        }
                        mDynamicGroupRoutes.clear();
                    }
                    break;
                }
                case MSG_ROUTE_ANOTHER_SELECTED: {
                    MediaRouter.RouteInfo groupRoute =
                            ((Pair<MediaRouter.RouteInfo, MediaRouter.RouteInfo>) obj).second;
                    mDynamicGroupRoutes.add(groupRoute);
                    mSystemProvider.onSyncRouteAdded(groupRoute);
                    mSystemProvider.onSyncRouteSelected(groupRoute);
                    break;
                }
            }
        }

        @SuppressWarnings("unchecked") // Using Pair<RouteInfo, RouteInfo>
        private void invokeCallback(
                MediaRouter.CallbackRecord record, int what, Object obj, int arg) {
            final MediaRouter router = record.mRouter;
            final MediaRouter.Callback callback = record.mCallback;
            switch (what & MSG_TYPE_MASK) {
                case MSG_TYPE_ROUTE: {
                    final MediaRouter.RouteInfo route =
                            (what == MSG_ROUTE_ANOTHER_SELECTED || what == MSG_ROUTE_SELECTED)
                                    ? ((Pair<MediaRouter.RouteInfo, MediaRouter.RouteInfo>) obj)
                                    .second
                                    : (MediaRouter.RouteInfo) obj;
                    final MediaRouter.RouteInfo optionalRoute =
                            (what == MSG_ROUTE_ANOTHER_SELECTED || what == MSG_ROUTE_SELECTED)
                                    ? ((Pair<MediaRouter.RouteInfo, MediaRouter.RouteInfo>) obj)
                                    .first
                                    : null;
                    if (route == null
                            || !record.filterRouteEvent(route, what, optionalRoute, arg)) {
                        break;
                    }
                    switch (what) {
                        case MSG_ROUTE_ADDED:
                            callback.onRouteAdded(router, route);
                            break;
                        case MSG_ROUTE_REMOVED:
                            callback.onRouteRemoved(router, route);
                            break;
                        case MSG_ROUTE_CHANGED:
                            callback.onRouteChanged(router, route);
                            break;
                        case MSG_ROUTE_VOLUME_CHANGED:
                            callback.onRouteVolumeChanged(router, route);
                            break;
                        case MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED:
                            callback.onRoutePresentationDisplayChanged(router, route);
                            break;
                        case MSG_ROUTE_SELECTED:
                            callback.onRouteSelected(router, route, arg, route);
                            break;
                        case MSG_ROUTE_UNSELECTED:
                            callback.onRouteUnselected(router, route, arg);
                            break;
                        case MSG_ROUTE_ANOTHER_SELECTED:
                            callback.onRouteSelected(router, route, arg, optionalRoute);
                            break;
                    }
                    break;
                }
                case MSG_TYPE_PROVIDER: {
                    final MediaRouter.ProviderInfo provider = (MediaRouter.ProviderInfo) obj;
                    switch (what) {
                        case MSG_PROVIDER_ADDED:
                            callback.onProviderAdded(router, provider);
                            break;
                        case MSG_PROVIDER_REMOVED:
                            callback.onProviderRemoved(router, provider);
                            break;
                        case MSG_PROVIDER_CHANGED:
                            callback.onProviderChanged(router, provider);
                            break;
                    }
                    break;
                }
                case MSG_TYPE_ROUTER: {
                    switch (what) {
                        case MSG_ROUTER_PARAMS_CHANGED:
                            final MediaRouterParams params = (MediaRouterParams) obj;
                            callback.onRouterParamsChanged(router, params);
                            break;
                    }
                    break;
                }
            }
        }
    }
}