MediaRoute2ProviderServiceAdapter.java

/*
 * Copyright 2020 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.MediaRouteProviderProtocol.CLIENT_DATA_ROUTE_ID;
import static androidx.mediarouter.media.MediaRouteProviderProtocol.CLIENT_DATA_VOLUME;
import static androidx.mediarouter.media.MediaRouteProviderProtocol.CLIENT_MSG_ROUTE_CONTROL_REQUEST;
import static androidx.mediarouter.media.MediaRouteProviderProtocol.CLIENT_MSG_SET_ROUTE_VOLUME;
import static androidx.mediarouter.media.MediaRouteProviderProtocol.CLIENT_MSG_UPDATE_ROUTE_VOLUME;
import static androidx.mediarouter.media.MediaRouteProviderProtocol.SERVICE_DATA_ERROR;
import static androidx.mediarouter.media.MediaRouteProviderProtocol.SERVICE_MSG_CONTROL_REQUEST_FAILED;
import static androidx.mediarouter.media.MediaRouteProviderProtocol.SERVICE_MSG_CONTROL_REQUEST_SUCCEEDED;
import static androidx.mediarouter.media.MediaRouter.UNSELECT_REASON_UNKNOWN;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.media.MediaRoute2ProviderService;
import android.media.RouteDiscoveryPreference;
import android.media.RoutingSessionInfo;
import android.os.Build;
import android.os.Bundle;
import android.os.DeadObjectException;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;

import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.collection.ArrayMap;
import androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController;
import androidx.mediarouter.media.MediaRouteProvider.DynamicGroupRouteController.DynamicRouteDescriptor;
import androidx.mediarouter.media.MediaRouteProvider.RouteController;
import androidx.mediarouter.media.MediaRouteProviderService.MediaRouteProviderServiceImplApi30;
import androidx.mediarouter.media.MediaRouteProviderService.MediaRouteProviderServiceImplApi30.ClientRecord;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;

@RequiresApi(api = Build.VERSION_CODES.R)
class MediaRoute2ProviderServiceAdapter extends MediaRoute2ProviderService {
    private static final String TAG = "MR2ProviderService";
    static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);

    private final Object mLock = new Object();

    final MediaRouteProviderServiceImplApi30 mServiceImpl;
    // Maps session ID to SessionRecord.
    @GuardedBy("mLock")
    final Map<String, SessionRecord> mSessionRecords = new ArrayMap<>();
    // Maps controller ID to Session ID.
    final SparseArray<String> mSessionIdMap = new SparseArray<>();

    private volatile MediaRouteProviderDescriptor mProviderDescriptor;

    @SuppressLint("InlinedApi")
    public static final String SERVICE_INTERFACE = MediaRoute2ProviderService.SERVICE_INTERFACE;

    MediaRoute2ProviderServiceAdapter(MediaRouteProviderServiceImplApi30 serviceImpl) {
        mServiceImpl = serviceImpl;
    }

    @Override
    public void attachBaseContext(Context context) {
        super.attachBaseContext(context);
    }

    @Override
    public void onSetRouteVolume(long requestId, @NonNull String routeId, int volume) {
        RouteController controller = findControllerByRouteId(routeId);

        if (controller == null) {
            Log.w(TAG, "onSetRouteVolume: Couldn't find a controller for routeId=" + routeId);
            notifyRequestFailed(requestId, REASON_ROUTE_NOT_AVAILABLE);
            return;
        }
        controller.onSetVolume(volume);
    }

    @Override
    public void onSetSessionVolume(long requestId, @NonNull String sessionId, int volume) {
        RoutingSessionInfo sessionInfo = getSessionInfo(sessionId);
        if (sessionInfo == null) {
            Log.w(TAG, "onSetSessionVolume: Couldn't find a session");
            notifyRequestFailed(requestId, REASON_INVALID_COMMAND);
            return;
        }

        DynamicGroupRouteController controller = findControllerBySessionId(sessionId);
        if (controller == null) {
            Log.w(TAG, "onSetSessionVolume: Couldn't find a controller");
            notifyRequestFailed(requestId, REASON_ROUTE_NOT_AVAILABLE);
            return;
        }
        controller.onSetVolume(volume);
    }

    @Override
    public void onCreateSession(long requestId, @NonNull String packageName,
            @NonNull String routeId, @Nullable Bundle sessionHints) {
        MediaRouteProvider provider = getMediaRouteProvider();
        MediaRouteDescriptor selectedRoute = getRouteDescriptor(routeId, "onCreateSession");
        if (selectedRoute == null) {
            notifyRequestFailed(requestId, REASON_ROUTE_NOT_AVAILABLE);
            return;
        }

        int sessionFlags = SessionRecord.SESSION_FLAG_MR2;
        DynamicGroupRouteController controller;
        if (mProviderDescriptor.supportsDynamicGroupRoute()) {
            controller = provider.onCreateDynamicGroupRouteController(routeId);
            sessionFlags |= SessionRecord.SESSION_FLAG_GROUP | SessionRecord.SESSION_FLAG_DYNAMIC;
            if (controller == null) {
                Log.w(TAG, "onCreateSession: Couldn't create a dynamic controller");
                notifyRequestFailed(requestId, REASON_REJECTED);
                return;
            }
        } else {
            RouteController routeController = provider.onCreateRouteController(routeId);
            if (routeController == null) {
                Log.w(TAG, "onCreateSession: Couldn't create a controller");
                notifyRequestFailed(requestId, REASON_REJECTED);
                return;
            }
            if (!selectedRoute.getGroupMemberIds().isEmpty()) {
                sessionFlags |= SessionRecord.SESSION_FLAG_GROUP;
            }
            controller = new DynamicGroupRouteControllerProxy(routeId, routeController);
        }

        SessionRecord sessionRecord = new SessionRecord(controller, sessionFlags);
        String sessionId = assignSessionId(sessionRecord);

        RoutingSessionInfo.Builder builder =
                new RoutingSessionInfo.Builder(sessionId, packageName)
                        .setName(selectedRoute.getName())
                        .setVolumeHandling(selectedRoute.getVolumeHandling())
                        .setVolume(selectedRoute.getVolume())
                        .setVolumeMax(selectedRoute.getVolumeMax());

        if (selectedRoute.getGroupMemberIds().isEmpty()) {
            builder.addSelectedRoute(routeId);
        } else {
            for (String memberId : selectedRoute.getGroupMemberIds()) {
                builder.addSelectedRoute(memberId);
            }
        }

        RoutingSessionInfo sessionInfo = builder.build();

        if ((sessionFlags & SessionRecord.SESSION_FLAG_GROUP) != 0) {
            sessionRecord.updateMemberRouteControllers(routeId, /*oldSession=*/null,
                    sessionInfo);
        }

        sessionRecord.notifySessionCreated(requestId, sessionInfo);

        mServiceImpl.setDynamicRoutesChangedListener(controller);
    }

    @Override
    public void onReleaseSession(long requestId, @NonNull String sessionId) {
        RoutingSessionInfo sessionInfo = getSessionInfo(sessionId);
        if (sessionInfo == null) {
            return;
        }

        SessionRecord sessionRecord;
        synchronized (mLock) {
            sessionRecord = mSessionRecords.remove(sessionId);
        }
        if (sessionRecord == null) {
            Log.w(TAG, "onReleaseSession: Couldn't find a session");
            notifyRequestFailed(requestId, REASON_INVALID_COMMAND);
            return;
        }
        sessionRecord.release();
    }

    @Override
    public void onSelectRoute(long requestId, @NonNull String sessionId,
            @NonNull String routeId) {
        RoutingSessionInfo sessionInfo = getSessionInfo(sessionId);
        if (sessionInfo == null) {
            Log.w(TAG, "onSelectRoute: Couldn't find a session");
            notifyRequestFailed(requestId, REASON_INVALID_COMMAND);
            return;
        }
        if (getRouteDescriptor(routeId, "onSelectRoute") == null) {
            notifyRequestFailed(requestId, REASON_ROUTE_NOT_AVAILABLE);
            return;
        }

        DynamicGroupRouteController controller = findControllerBySessionId(sessionId);
        if (controller == null) {
            Log.w(TAG, "onSelectRoute: Couldn't find a controller");
            notifyRequestFailed(requestId, REASON_ROUTE_NOT_AVAILABLE);
            return;
        }
        controller.onAddMemberRoute(routeId);
    }

    @Override
    public void onDeselectRoute(long requestId, @NonNull String sessionId,
            @NonNull String routeId) {
        RoutingSessionInfo sessionInfo = getSessionInfo(sessionId);
        if (sessionInfo == null) {
            Log.w(TAG, "onDeselectRoute: Couldn't find a session");
            notifyRequestFailed(requestId, REASON_INVALID_COMMAND);
            return;
        }
        if (getRouteDescriptor(routeId, "onDeselectRoute") == null) {
            notifyRequestFailed(requestId, REASON_ROUTE_NOT_AVAILABLE);
            return;
        }

        DynamicGroupRouteController controller = findControllerBySessionId(sessionId);
        if (controller == null) {
            Log.w(TAG, "onDeselectRoute: Couldn't find a controller");
            notifyRequestFailed(requestId, REASON_ROUTE_NOT_AVAILABLE);
            return;
        }
        controller.onRemoveMemberRoute(routeId);
    }

    @Override
    public void onTransferToRoute(long requestId, @NonNull String sessionId,
            @NonNull String routeId) {
        RoutingSessionInfo sessionInfo = getSessionInfo(sessionId);
        if (sessionInfo == null) {
            Log.w(TAG, "onTransferToRoute: Couldn't find a session");
            notifyRequestFailed(requestId, REASON_INVALID_COMMAND);
            return;
        }
        if (getRouteDescriptor(routeId, "onTransferToRoute") == null) {
            notifyRequestFailed(requestId, REASON_ROUTE_NOT_AVAILABLE);
            return;
        }

        DynamicGroupRouteController controller = findControllerBySessionId(sessionId);
        if (controller == null) {
            Log.w(TAG, "onTransferToRoute: Couldn't find a controller");
            notifyRequestFailed(requestId, REASON_ROUTE_NOT_AVAILABLE);
            return;
        }
        controller.onUpdateMemberRoutes(Collections.singletonList(routeId));
    }

    @Override
    public void onDiscoveryPreferenceChanged(@NonNull RouteDiscoveryPreference preference) {
        MediaRouteSelector selector = new MediaRouteSelector.Builder()
                .addControlCategories(preference.getPreferredFeatures().stream()
                        .map(MediaRouter2Utils::toControlCategory)
                        .collect(Collectors.toList())).build();
        mServiceImpl.setBaseDiscoveryRequest(new MediaRouteDiscoveryRequest(selector,
                preference.shouldPerformActiveScan()));
    }

    public void setProviderDescriptor(@Nullable MediaRouteProviderDescriptor descriptor) {
        mProviderDescriptor = descriptor;
        List<MediaRouteDescriptor> routeDescriptors =
                (descriptor == null) ? Collections.emptyList() : descriptor.getRoutes();

        Map<String, MediaRouteDescriptor> descriptorMap =
                routeDescriptors.stream().filter(Objects::nonNull)
                        // Ignores duplicated route IDs.
                        .collect(Collectors.toMap(r -> r.getId(), r -> r, (fst, snd) -> fst));

        updateStaticSessions(descriptorMap);
        // Handle duplicated IDs
        notifyRoutes(descriptorMap.values().stream()
                .map(MediaRouter2Utils::toFwkMediaRoute2Info)
                .filter(Objects::nonNull)
                .collect(Collectors.toList()));
    }

    private DynamicGroupRouteController findControllerBySessionId(String sessionId) {
        synchronized (mLock) {
            SessionRecord sessionRecord = mSessionRecords.get(sessionId);
            return (sessionRecord == null) ? null : sessionRecord.getGroupController();
        }
    }

    private MediaRouteDescriptor getRouteDescriptor(String routeId, String description) {
        MediaRouteProvider provider = getMediaRouteProvider();
        if (provider == null || mProviderDescriptor == null) {
            Log.w(TAG, description + ": no provider info");
            return null;
        }

        List<MediaRouteDescriptor> routes = mProviderDescriptor.getRoutes();
        for (MediaRouteDescriptor route : routes) {
            if (TextUtils.equals(route.getId(), routeId)) {
                return route;
            }
        }
        Log.w(TAG, description + ": Couldn't find a route : " + routeId);
        return null;
    }

    private SessionRecord findSessionRecordByController(DynamicGroupRouteController controller) {
        synchronized (mLock) {
            for (Map.Entry<String, SessionRecord> entry : mSessionRecords.entrySet()) {
                SessionRecord sessionRecord = entry.getValue();
                if (sessionRecord.getGroupController() == controller) {
                    return sessionRecord;
                }
            }
        }
        return null;
    }

    public void setDynamicRouteDescriptor(DynamicGroupRouteController controller,
            MediaRouteDescriptor groupRoute,
            Collection<DynamicRouteDescriptor> descriptors) {
        SessionRecord sessionRecord = findSessionRecordByController(controller);
        if (sessionRecord == null) {
            Log.w(TAG, "setDynamicRouteDescriptor: Ignoring unknown controller");
            return;
        }

        sessionRecord.updateSessionInfo(groupRoute, descriptors);
    }

    void updateStaticSessions(Map<String, MediaRouteDescriptor> routeDescriptors) {
        List<SessionRecord> staticSessions;
        synchronized (mLock) {
            staticSessions = mSessionRecords.values().stream()
                    .filter(r -> (r.getFlags() & SessionRecord.SESSION_FLAG_DYNAMIC) == 0)
                    .collect(Collectors.toList());
        }
        for (SessionRecord sessionRecord : staticSessions) {
            DynamicGroupRouteControllerProxy controller =
                    (DynamicGroupRouteControllerProxy) sessionRecord.getGroupController();
            if (routeDescriptors.containsKey(controller.getRouteId())) {
                sessionRecord.updateSessionInfo(routeDescriptors.get(controller.getRouteId()),
                        /*dynamicRouteDescriptors=*/null);
            }
        }
    }

    //TODO: Remove this
    void onControlRequest(Messenger messenger, int requestId, String sessionId,
            Intent intent) {
        RoutingSessionInfo sessionInfo = getSessionInfo(sessionId);
        if (sessionInfo == null) {
            Log.w(TAG, "onCustomCommand: Couldn't find a session");
            return;
        }

        DynamicGroupRouteController controller = findControllerBySessionId(sessionId);
        if (controller == null) {
            Log.w(TAG, "onControlRequest: Couldn't find a controller");
            notifyRequestFailed(requestId, REASON_ROUTE_NOT_AVAILABLE);
            return;
        }

        MediaRouter.ControlRequestCallback callback = new MediaRouter.ControlRequestCallback() {
            @Override
            public void onResult(Bundle data) {
                if (DEBUG) {
                    Log.d(TAG, "Route control request succeeded"
                            + ", sessionId=" + sessionId
                            + ", intent=" + intent
                            + ", data=" + data);
                }

                sendReply(messenger, SERVICE_MSG_CONTROL_REQUEST_SUCCEEDED,
                        requestId, 0, data, null);
            }

            @Override
            public void onError(String error, Bundle data) {
                if (DEBUG) {
                    Log.d(TAG, "Route control request failed"
                            + ", sessionId=" + sessionId
                            + ", intent=" + intent
                            + ", error=" + error + ", data=" + data);
                }
                if (error != null) {
                    Bundle bundle = new Bundle();
                    bundle.putString(SERVICE_DATA_ERROR, error);
                    sendReply(messenger, SERVICE_MSG_CONTROL_REQUEST_FAILED,
                            requestId, 0, data, bundle);
                } else {
                    sendReply(messenger, SERVICE_MSG_CONTROL_REQUEST_FAILED,
                            requestId, 0, data, null);
                }
            }

            void sendReply(Messenger messenger, int what,
                    int requestId, int arg, Object obj, Bundle data) {
                Message msg = Message.obtain();
                msg.what = what;
                msg.arg1 = requestId;
                msg.arg2 = arg;
                msg.obj = obj;
                msg.setData(data);
                try {
                    messenger.send(msg);
                } catch (DeadObjectException ex) {
                    // The client died.
                } catch (RemoteException ex) {
                    Log.e(TAG, "Could not send message to the client.", ex);
                }
            }
        };

        controller.onControlRequest(intent, callback);
    }

    void setRouteVolume(@NonNull String routeId, int volume) {
        RouteController controller = findControllerByRouteId(routeId);
        if (controller == null) {
            Log.w(TAG, "setRouteVolume: Couldn't find a controller for routeId=" + routeId);
            return;
        }
        controller.onSetVolume(volume);
    }

    void updateRouteVolume(@NonNull String routeId, int delta) {
        RouteController controller = findControllerByRouteId(routeId);
        if (controller == null) {
            Log.w(TAG, "updateRouteVolume: Couldn't find a controller for routeId=" + routeId);
            return;
        }
        controller.onUpdateVolume(delta);
    }

    void notifyRouteControllerAdded(ClientRecord clientRecord,
            RouteController routeController, int controllerId, String packageName, String routeId) {
        MediaRouteDescriptor descriptor = getRouteDescriptor(routeId, "notifyRouteControllerAdded");
        if (descriptor == null) {
            return;
        }

        int sessionFlags = 0;
        DynamicGroupRouteController controller;
        if (routeController instanceof DynamicGroupRouteController) {
            sessionFlags |= SessionRecord.SESSION_FLAG_DYNAMIC | SessionRecord.SESSION_FLAG_GROUP;
            controller = (DynamicGroupRouteController) routeController;
        } else {
            if (!descriptor.getGroupMemberIds().isEmpty()) {
                sessionFlags |= SessionRecord.SESSION_FLAG_GROUP;
            }
            controller = new DynamicGroupRouteControllerProxy(routeId, routeController);
        }

        SessionRecord sessionRecord = new SessionRecord(controller, sessionFlags, clientRecord);

        String sessionId = assignSessionId(sessionRecord);
        mSessionIdMap.put(controllerId, sessionId);

        RoutingSessionInfo.Builder builder =
                new RoutingSessionInfo.Builder(sessionId, packageName)
                        .addSelectedRoute(routeId)
                        .setName(descriptor.getName())
                        .setVolumeHandling(descriptor.getVolumeHandling())
                        .setVolume(descriptor.getVolume())
                        .setVolumeMax(descriptor.getVolumeMax());

        sessionRecord.notifySessionCreated(REQUEST_ID_NONE, builder.build());
    }

    void notifyRouteControllerRemoved(int controllerId) {
        String sessionId = mSessionIdMap.get(controllerId);
        if (sessionId == null) {
            return;
        }
        mSessionIdMap.remove(controllerId);

        SessionRecord sessionRecord;
        synchronized (mLock) {
            sessionRecord = mSessionRecords.remove(sessionId);
        }
        if (sessionRecord != null) {
            sessionRecord.release();
        }
    }

    private RouteController findControllerByRouteId(String routeId) {
        List<SessionRecord> sessionRecords = new ArrayList<>();
        synchronized (mLock) {
            sessionRecords.addAll(mSessionRecords.values());
        }
        for (SessionRecord sessionRecord : sessionRecords) {
            RouteController controller = sessionRecord.findControllerByRouteId(routeId);
            if (controller != null) {
                return controller;
            }
        }
        return null;
    }

    MediaRouteProvider getMediaRouteProvider() {
        MediaRouteProviderService service = mServiceImpl.getService();
        if (service == null) {
            return null;
        }
        return service.getMediaRouteProvider();
    }

    private String assignSessionId(SessionRecord sessionRecord) {
        String sessionId;
        synchronized (mLock) {
            do {
                //TODO: Consider a better way to create a session ID.
                sessionId = UUID.randomUUID().toString();
            } while (mSessionRecords.containsKey(sessionId));
            sessionRecord.mSessionId = sessionId;
            mSessionRecords.put(sessionId, sessionRecord);
            return sessionId;
        }
    }

    private static class DynamicGroupRouteControllerProxy
            extends DynamicGroupRouteController {
        private final String mRouteId;
        private final RouteController mRouteController;

        DynamicGroupRouteControllerProxy(String routeId, RouteController routeController) {
            mRouteId = routeId;
            mRouteController = routeController;
        }

        public String getRouteId() {
            return mRouteId;
        }

        @Override
        public void onRelease() {
            mRouteController.onRelease();
        }

        @Override
        public void onSelect() {
            mRouteController.onSelect();
        }

        @Override
        public void onUnselect(@MediaRouter.UnselectReason int reason) {
            mRouteController.onUnselect(reason);
        }

        @Override
        public void onSetVolume(int volume) {
            mRouteController.onSetVolume(volume);
        }

        @Override
        public void onUpdateVolume(int delta) {
            mRouteController.onUpdateVolume(delta);
        }

        @Override
        public boolean onControlRequest(Intent intent,
                MediaRouter.ControlRequestCallback callback) {
            return mRouteController.onControlRequest(intent, callback);
        }

        @Override
        public void onUpdateMemberRoutes(@Nullable List<String> routeIds) {
            // Do nothing.
        }

        @Override
        public void onAddMemberRoute(@NonNull String routeId) {
            // Do nothing.
        }

        @Override
        public void onRemoveMemberRoute(String routeId) {
            // Do nothing.
        }
    }

    final class SessionRecord {
        /**
         * A flag indicating whether the session is created from
         * {@link MediaRoute2ProviderService#onCreateSession(long, String, String, Bundle)}.
         */
        static final int SESSION_FLAG_MR2 = 1 << 0;

        /**
         * A flag indicating whether the session is a group.
         * If true, route controllers for members should be managed.
         */
        static final int SESSION_FLAG_GROUP = 1 << 1;

        /**
         * A flag indicating whether the members of session can be dynamically changed.
         */
        static final int SESSION_FLAG_DYNAMIC = 1 << 2;

        private final DynamicGroupRouteController mController;
        private final Map<String, RouteController> mRouteIdToControllerMap = new ArrayMap<>();
        private final WeakReference<ClientRecord> mClientRecord;

        private int mFlags;
        private boolean mIsReleased;
        private RoutingSessionInfo mSessionInfo;
        String mSessionId;

        SessionRecord(DynamicGroupRouteController controller, int flags) {
            this(controller, flags, null);
        }

        SessionRecord(DynamicGroupRouteController controller, int flags,
                ClientRecord clientRecord) {
            mController = controller;
            mFlags = flags;
            mClientRecord = new WeakReference<>(clientRecord);
        }

        public int getFlags() {
            return mFlags;
        }

        DynamicGroupRouteController getGroupController() {
            return mController;
        }

        RouteController findControllerByRouteId(String routeId) {
            ClientRecord clientRecord = mClientRecord.get();
            // Controllers are managed by the client.
            if (clientRecord != null) {
                return clientRecord.findControllerByRouteId(routeId);
            }
            return mRouteIdToControllerMap.get(routeId);
        }

        public void notifySessionCreated(long requestId, RoutingSessionInfo sessionInfo) {
            Messenger messenger = new Messenger(new IncomingHandler(
                    MediaRoute2ProviderServiceAdapter.this, mSessionId));

            RoutingSessionInfo.Builder builder = new RoutingSessionInfo.Builder(sessionInfo);

            Bundle controlHints = new Bundle();
            controlHints.putParcelable(MediaRouter2Utils.KEY_MESSENGER, messenger);
            controlHints.putString(MediaRouter2Utils.KEY_SESSION_NAME,
                    sessionInfo.getName().toString());

            mSessionInfo = builder.setControlHints(controlHints).build();
            MediaRoute2ProviderServiceAdapter.this.notifySessionCreated(requestId, mSessionInfo);
        }

        public void updateSessionInfo(@Nullable MediaRouteDescriptor groupRoute,
                @Nullable Collection<DynamicRouteDescriptor> dynamicRouteDescriptors) {
            RoutingSessionInfo sessionInfo = mSessionInfo;

            if (sessionInfo == null) {
                Log.w(TAG, "updateSessionInfo: mSessionInfo is null. This shouldn't happen.");
                return;
            }

            if (groupRoute != null && !groupRoute.isEnabled()) {
                onReleaseSession(REQUEST_ID_NONE, mSessionId);
                return;
            }

            RoutingSessionInfo.Builder builder = new RoutingSessionInfo.Builder(sessionInfo);
            if (groupRoute != null) {
                builder.setName(groupRoute.getName())
                        .setVolume(groupRoute.getVolume())
                        .setVolumeMax(groupRoute.getVolumeMax())
                        .setVolumeHandling(groupRoute.getVolumeHandling());

                Bundle controlHints = sessionInfo.getControlHints();
                if (controlHints == null) {
                    Log.w(TAG, "updateSessionInfo: controlHints is null. "
                            + "This shouldn't happen.");
                    controlHints = new Bundle();
                }
                controlHints.putString(MediaRouter2Utils.KEY_SESSION_NAME, groupRoute.getName());
                builder.setControlHints(controlHints);
            }

            if (dynamicRouteDescriptors != null && !dynamicRouteDescriptors.isEmpty()) {
                builder.clearSelectedRoutes();
                builder.clearSelectableRoutes();
                builder.clearDeselectableRoutes();
                builder.clearTransferableRoutes();

                for (DynamicRouteDescriptor descriptor : dynamicRouteDescriptors) {
                    String routeId = descriptor.getRouteDescriptor().getId();
                    if (descriptor.mSelectionState == DynamicRouteDescriptor.SELECTING
                            || descriptor.mSelectionState == DynamicRouteDescriptor.SELECTED) {
                        builder.addSelectedRoute(routeId);
                    }
                    if (descriptor.isGroupable()) {
                        builder.addSelectableRoute(routeId);
                    }
                    if (descriptor.isUnselectable()) {
                        builder.addDeselectableRoute(routeId);
                    }
                    if (descriptor.isTransferable()) {
                        builder.addTransferableRoute(routeId);
                    }
                }
            }

            RoutingSessionInfo newSessionInfo = builder.build();
            if ((mFlags & (SESSION_FLAG_MR2 | SESSION_FLAG_DYNAMIC))
                    == (SESSION_FLAG_MR2 | SESSION_FLAG_DYNAMIC) && groupRoute != null) {
                updateMemberRouteControllers(groupRoute.getId(), sessionInfo, newSessionInfo);
            }
            notifySessionUpdated(newSessionInfo);
        }

        public void release() {
            if (!mIsReleased) {
                // Release member controllers
                if ((mFlags & (SESSION_FLAG_MR2 | SESSION_FLAG_GROUP))
                        == (SESSION_FLAG_MR2 | SESSION_FLAG_GROUP)) {
                    updateMemberRouteControllers(null, mSessionInfo, null);
                }

                if ((mFlags & SESSION_FLAG_MR2) != 0) {
                    mController.onUnselect(MediaRouter.UNSELECT_REASON_STOPPED);
                    mController.onRelease();
                }
                mIsReleased = true;
                notifySessionReleased(mSessionId);
            }
        }

        public void updateMemberRouteControllers(String groupId, RoutingSessionInfo oldSession,
                RoutingSessionInfo newSession) {
            List<String> oldRouteIds = (oldSession == null) ? Collections.emptyList() :
                    oldSession.getSelectedRoutes();
            List<String> newRouteIds = (newSession == null) ? Collections.emptyList() :
                    newSession.getSelectedRoutes();

            for (String routeId : newRouteIds) {
                RouteController controller = findControllerByRouteId(routeId);
                if (controller == null) {
                    controller = getOrCreateRouteController(routeId, groupId);
                    controller.onSelect();
                }
            }
            for (String routeId : oldRouteIds) {
                if (!newRouteIds.contains(routeId)) {
                    releaseRouteControllerByRouteId(routeId);
                }
            }
        }

        private RouteController getOrCreateRouteController(String routeId, String routeGroupId) {
            RouteController controller = mRouteIdToControllerMap.get(routeId);
            if (controller != null) {
                return controller;
            }

            controller = routeGroupId == null
                    ? getMediaRouteProvider().onCreateRouteController(routeId)
                    : getMediaRouteProvider().onCreateRouteController(routeId, routeGroupId);
            if (controller != null) {
                mRouteIdToControllerMap.put(routeId, controller);
            }
            return controller;
        }

        private boolean releaseRouteControllerByRouteId(String routeId) {
            RouteController controller = mRouteIdToControllerMap.remove(routeId);
            if (controller != null) {
                controller.onUnselect(UNSELECT_REASON_UNKNOWN);
                controller.onRelease();
                return true;
            }
            return false;
        }
    }

    static class IncomingHandler extends Handler {
        private final MediaRoute2ProviderServiceAdapter mServiceAdapter;
        private final String mSessionId;

        IncomingHandler(MediaRoute2ProviderServiceAdapter serviceAdapter, String sessionId) {
            super(Looper.myLooper());
            mServiceAdapter = serviceAdapter;
            mSessionId = sessionId;
        }

        @Override
        public void handleMessage(Message msg) {
            final Messenger messenger = msg.replyTo;
            final int what = msg.what;
            final int requestId = msg.arg1;
            final Object obj = msg.obj;
            final Bundle data = msg.getData();

            switch (what) {
                case CLIENT_MSG_ROUTE_CONTROL_REQUEST:
                    if (obj instanceof Intent) {
                        mServiceAdapter.onControlRequest(messenger, requestId,
                                mSessionId, (Intent) obj);
                    }
                    break;

                case CLIENT_MSG_SET_ROUTE_VOLUME: {
                    int volume = data.getInt(CLIENT_DATA_VOLUME, -1);
                    String routeId = data.getString(CLIENT_DATA_ROUTE_ID);
                    if (volume >= 0 && routeId != null) {
                        mServiceAdapter.setRouteVolume(routeId, volume);
                    }
                    break;
                }

                case CLIENT_MSG_UPDATE_ROUTE_VOLUME: {
                    int delta = data.getInt(CLIENT_DATA_VOLUME, 0);
                    String routeId = data.getString(CLIENT_DATA_ROUTE_ID);
                    if (delta != 0 && routeId != null) {
                        mServiceAdapter.updateRouteVolume(routeId, delta);
                    }
                    break;
                }
            }
        }
    }
}