Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 581fc98d authored by Kyunglyul Hyun's avatar Kyunglyul Hyun
Browse files

MediaRouter: enable transferring from/to BT devices.

The main objective of this CL is to enable selecting BT device.
For that it updates MRM and SystemMediaRoute2Provider such that
  - MRM.getRoutingControllers() returns a list including the system session
  - SystemMediaRoute2Provider marks routes as "transferable"
  - SystemMediaRoute2Provider sets provider id correctly
  - SystemMediaRoute2Provider handles transferToRoute()

Bug: 147979868
Bug: 147122575

Test: atest mediaroutertest
&& manually selecting phone / bt devices from output switcher

Change-Id: I2e2032fd6677f79b9f864c313c40846daa87f113
parent 0e4d731b
Loading
Loading
Loading
Loading
+10 −5
Original line number Diff line number Diff line
@@ -178,8 +178,9 @@ public class MediaRouter2Manager {

    /**
     * Gets routing controllers of an application with the given package name.
     * If the application isn't running or it doesn't use {@link MediaRouter2}, an empty list
     * will be returned.
     * The first element of the returned list is the system routing controller.
     *
     * @see MediaRouter2#getSystemController()
     */
    @NonNull
    public List<RoutingController> getRoutingControllers(@NonNull String packageName) {
@@ -188,7 +189,8 @@ public class MediaRouter2Manager {
        List<RoutingController> controllers = new ArrayList<>();

        for (RoutingSessionInfo sessionInfo : getActiveSessions()) {
            if (TextUtils.equals(sessionInfo.getClientPackageName(), packageName)) {
            if (sessionInfo.isSystemSession()
                    || TextUtils.equals(sessionInfo.getClientPackageName(), packageName)) {
                controllers.add(new RoutingController(sessionInfo));
            }
        }
@@ -196,8 +198,11 @@ public class MediaRouter2Manager {
    }

    /**
     * Gets the list of all active routing sessions. It doesn't include default routing sessions
     * of applications.
     * Gets the list of all active routing sessions.
     * The first element of the list is the system routing session containing
     * phone speakers, wired headset, Bluetooth devices.
     * The system routing session is shared by apps such that controlling it will affect
     * all apps.
     */
    @NonNull
    public List<RoutingSessionInfo> getActiveSessions() {
+6 −6
Original line number Diff line number Diff line
@@ -246,7 +246,7 @@ public class MediaRouterManagerTest {
            }
        });

        assertEquals(0, mManager.getRoutingControllers(mPackageName).size());
        assertEquals(1, mManager.getRoutingControllers(mPackageName).size());

        mManager.selectRoute(mPackageName, routes.get(ROUTE_ID1));
        latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
@@ -254,14 +254,14 @@ public class MediaRouterManagerTest {
        List<MediaRouter2Manager.RoutingController> controllers =
                mManager.getRoutingControllers(mPackageName);

        assertEquals(1, controllers.size());
        assertEquals(2, controllers.size());

        MediaRouter2Manager.RoutingController routingController = controllers.get(0);
        MediaRouter2Manager.RoutingController routingController = controllers.get(1);
        awaitOnRouteChangedManager(
                () -> routingController.release(),
                ROUTE_ID1,
                route -> TextUtils.equals(route.getClientPackageName(), null));
        assertEquals(0, mManager.getRoutingControllers(mPackageName).size());
        assertEquals(1, mManager.getRoutingControllers(mPackageName).size());
    }

    /**
@@ -290,8 +290,8 @@ public class MediaRouterManagerTest {
        List<MediaRouter2Manager.RoutingController> controllers =
                mManager.getRoutingControllers(mPackageName);

        assertEquals(1, controllers.size());
        MediaRouter2Manager.RoutingController routingController = controllers.get(0);
        assertEquals(2, controllers.size());
        MediaRouter2Manager.RoutingController routingController = controllers.get(1);

        awaitOnRouteChangedManager(
                () -> mManager.selectRoute(mPackageName, routes.get(ROUTE_ID5_TO_TRANSFER_TO)),
+53 −4
Original line number Diff line number Diff line
@@ -29,12 +29,13 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.media.MediaRoute2Info;
import android.text.TextUtils;
import android.util.Log;
import android.util.Slog;
import android.util.SparseBooleanArray;

import com.android.internal.R;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -59,7 +60,6 @@ class BluetoothRouteProvider {
    private final BroadcastReceiver mBroadcastReceiver = new BluetoothBroadcastReceiver();
    private final BluetoothProfileListener mProfileListener = new BluetoothProfileListener();

    // TODO: The mActiveDevice should be set when BluetoothRouteProvider is created.
    private BluetoothDevice mActiveDevice = null;

    static synchronized BluetoothRouteProvider getInstance(@NonNull Context context,
@@ -104,6 +104,43 @@ class BluetoothRouteProvider {
        mContext.registerReceiver(mBroadcastReceiver, mIntentFilter, null, null);
    }

    /**
     * Clears the active device for all known profiles.
     */
    public void clearActiveDevices() {
        BluetoothA2dp a2dpProfile = mA2dpProfile;
        BluetoothHearingAid hearingAidProfile = mHearingAidProfile;
        if (a2dpProfile != null) {
            a2dpProfile.setActiveDevice(null);
        }
        if (hearingAidProfile != null) {
            hearingAidProfile.setActiveDevice(null);
        }
    }

    /**
     * Sets the active device.
     * @param deviceId the id of the Bluetooth device
     */
    public void setActiveDevice(@NonNull String deviceId) {
        BluetoothRouteInfo btRouteInfo = mBluetoothRoutes.get(deviceId);
        if (btRouteInfo == null) {
            Slog.w(TAG, "setActiveDevice: unknown device id=" + deviceId);
            return;
        }
        BluetoothA2dp a2dpProfile = mA2dpProfile;
        BluetoothHearingAid hearingAidProfile = mHearingAidProfile;

        if (a2dpProfile != null
                && btRouteInfo.connectedProfiles.get(BluetoothProfile.A2DP, false)) {
            a2dpProfile.setActiveDevice(btRouteInfo.btDevice);
        }
        if (hearingAidProfile != null
                && btRouteInfo.connectedProfiles.get(BluetoothProfile.HEARING_AID, false)) {
            hearingAidProfile.setActiveDevice(btRouteInfo.btDevice);
        }
    }

    private void addEventReceiver(String action, BluetoothEventReceiver eventReceiver) {
        mEventReceiverMap.put(action, eventReceiver);
        mIntentFilter.addAction(action);
@@ -157,12 +194,12 @@ class BluetoothRouteProvider {
    private void setRouteConnectionStateForDevice(BluetoothDevice device,
            @MediaRoute2Info.ConnectionState int state) {
        if (device == null) {
            Log.w(TAG, "setRouteConnectionStateForDevice: device shouldn't be null");
            Slog.w(TAG, "setRouteConnectionStateForDevice: device shouldn't be null");
            return;
        }
        BluetoothRouteInfo btRoute = mBluetoothRoutes.get(device.getAddress());
        if (btRoute == null) {
            Log.w(TAG, "setRouteConnectionStateForDevice: route shouldn't be null");
            Slog.w(TAG, "setRouteConnectionStateForDevice: route shouldn't be null");
            return;
        }
        if (btRoute.route.getConnectionState() != state) {
@@ -184,24 +221,36 @@ class BluetoothRouteProvider {
    // These callbacks run on the main thread.
    private final class BluetoothProfileListener implements BluetoothProfile.ServiceListener {
        public void onServiceConnected(int profile, BluetoothProfile proxy) {
            List<BluetoothDevice> activeDevices;
            switch (profile) {
                case BluetoothProfile.A2DP:
                    mA2dpProfile = (BluetoothA2dp) proxy;
                    // It may contain null.
                    activeDevices = Collections.singletonList(mA2dpProfile.getActiveDevice());
                    break;
                case BluetoothProfile.HEARING_AID:
                    mHearingAidProfile = (BluetoothHearingAid) proxy;
                    activeDevices = mHearingAidProfile.getActiveDevices();
                    break;
                default:
                    return;
            }
            //TODO: Check a pair of HAP devices whether there exist two or more active devices.
            for (BluetoothDevice device : proxy.getConnectedDevices()) {
                BluetoothRouteInfo btRoute = mBluetoothRoutes.get(device.getAddress());
                if (btRoute == null) {
                    btRoute = createBluetoothRoute(device);
                    mBluetoothRoutes.put(device.getAddress(), btRoute);
                }
                if (activeDevices.contains(device)) {
                    mActiveDevice = device;
                    setRouteConnectionStateForDevice(device,
                            MediaRoute2Info.CONNECTION_STATE_CONNECTED);
                }

                btRoute.connectedProfiles.put(profile, true);
            }
            notifyBluetoothRoutesUpdated();
        }

        public void onServiceDisconnected(int profile) {
+21 −25
Original line number Diff line number Diff line
@@ -760,17 +760,12 @@ class MediaRouter2ServiceImpl {
            Slog.w(TAG, "selectClientRouteLocked: Ignoring unknown manager.");
            return;
        }
        //TODO: we shouldn't ignore selecting request for unknown clients. (RCN?)
        Client2Record clientRecord = managerRecord.mUserRecord.mHandler
                .findClientforSessionLocked(sessionId);
        if (clientRecord == null) {
            Slog.w(TAG, "selectClientRouteLocked: Ignoring unknown session.");
            return;
        }

        clientRecord.mUserRecord.mHandler.sendMessage(
        managerRecord.mUserRecord.mHandler.sendMessage(
                obtainMessage(UserHandler::selectRouteOnHandler,
                            clientRecord.mUserRecord.mHandler,
                            managerRecord.mUserRecord.mHandler,
                            clientRecord, sessionId, route));
    }

@@ -783,17 +778,12 @@ class MediaRouter2ServiceImpl {
            Slog.w(TAG, "deselectClientRouteLocked: Ignoring unknown manager.");
            return;
        }
        //TODO: we shouldn't ignore selecting request for unknown clients. (RCN?)
        Client2Record clientRecord = managerRecord.mUserRecord.mHandler
                .findClientforSessionLocked(sessionId);
        if (clientRecord == null) {
            Slog.w(TAG, "deslectClientRouteLocked: Ignoring unknown session.");
            return;
        }

        clientRecord.mUserRecord.mHandler.sendMessage(
        managerRecord.mUserRecord.mHandler.sendMessage(
                obtainMessage(UserHandler::deselectRouteOnHandler,
                        clientRecord.mUserRecord.mHandler,
                        managerRecord.mUserRecord.mHandler,
                        clientRecord, sessionId, route));
    }

@@ -806,17 +796,12 @@ class MediaRouter2ServiceImpl {
            Slog.w(TAG, "transferClientRouteLocked: Ignoring unknown manager.");
            return;
        }
        //TODO: we shouldn't ignore selecting request for unknown clients. (RCN?)
        Client2Record clientRecord = managerRecord.mUserRecord.mHandler
                .findClientforSessionLocked(sessionId);
        if (clientRecord == null) {
            Slog.w(TAG, "transferClientRouteLocked: Ignoring unknown session.");
            return;
        }

        clientRecord.mUserRecord.mHandler.sendMessage(
        managerRecord.mUserRecord.mHandler.sendMessage(
                obtainMessage(UserHandler::transferToRouteOnHandler,
                        clientRecord.mUserRecord.mHandler,
                        managerRecord.mUserRecord.mHandler,
                        clientRecord, sessionId, route));
    }

@@ -1166,7 +1151,7 @@ class MediaRouter2ServiceImpl {
                    requestId, sessionHints);
        }

        private void selectRouteOnHandler(@NonNull Client2Record clientRecord,
        private void selectRouteOnHandler(@Nullable Client2Record clientRecord,
                String uniqueSessionId, MediaRoute2Info route) {
            if (!checkArgumentsForSessionControl(clientRecord, uniqueSessionId, route,
                    "selecting")) {
@@ -1182,7 +1167,7 @@ class MediaRouter2ServiceImpl {
            provider.selectRoute(getOriginalId(uniqueSessionId), route.getOriginalId());
        }

        private void deselectRouteOnHandler(@NonNull Client2Record clientRecord,
        private void deselectRouteOnHandler(@Nullable Client2Record clientRecord,
                String uniqueSessionId, MediaRoute2Info route) {
            if (!checkArgumentsForSessionControl(clientRecord, uniqueSessionId, route,
                    "deselecting")) {
@@ -1198,7 +1183,7 @@ class MediaRouter2ServiceImpl {
            provider.deselectRoute(getOriginalId(uniqueSessionId), route.getOriginalId());
        }

        private void transferToRouteOnHandler(@NonNull Client2Record clientRecord,
        private void transferToRouteOnHandler(Client2Record clientRecord,
                String uniqueSessionId, MediaRoute2Info route) {
            if (!checkArgumentsForSessionControl(clientRecord, uniqueSessionId, route,
                    "transferring to")) {
@@ -1215,7 +1200,7 @@ class MediaRouter2ServiceImpl {
                    route.getOriginalId());
        }

        private boolean checkArgumentsForSessionControl(@NonNull Client2Record clientRecord,
        private boolean checkArgumentsForSessionControl(@Nullable Client2Record clientRecord,
                String uniqueSessionId, MediaRoute2Info route, @NonNull String description) {
            if (route == null) {
                Slog.w(TAG, "Ignoring " + description + " null route");
@@ -1236,6 +1221,17 @@ class MediaRouter2ServiceImpl {
                return false;
            }

            // Bypass checking client if it's the system session (clientRecord should be null)
            if (TextUtils.equals(getProviderId(uniqueSessionId), mSystemProvider.getUniqueId())) {
                return true;
            }

            //TODO: Handle RCN case.
            if (clientRecord == null) {
                Slog.w(TAG, "Ignoring " + description + " route from unknown client.");
                return false;
            }

            Client2Record matchingRecord = mSessionToClientMap.get(uniqueSessionId);
            if (matchingRecord != clientRecord) {
                Slog.w(TAG, "Ignoring " + description + " route from non-matching client. "
+29 −18
Original line number Diff line number Diff line
@@ -64,7 +64,6 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
            SystemMediaRoute2Provider.class.getPackageName$(),
            SystemMediaRoute2Provider.class.getName());

    //TODO: Clean up these when audio manager support multiple bt devices
    MediaRoute2Info mDefaultRoute;
    @NonNull List<MediaRoute2Info> mBluetoothRoutes = Collections.EMPTY_LIST;
    final AudioRoutesInfo mCurAudioRoutesInfo = new AudioRoutesInfo();
@@ -91,6 +90,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
        mAudioService = IAudioService.Stub.asInterface(
                ServiceManager.getService(Context.AUDIO_SERVICE));

        initializeDefaultRoute();
        mBtRouteProvider = BluetoothRouteProvider.getInstance(context, (routes) -> {
            mBluetoothRoutes = routes;
            publishRoutes();
@@ -103,7 +103,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
                notifySessionInfoUpdated();
            }
        });
        initializeRoutes();
        initializeSessionInfo();
    }

    @Override
@@ -119,17 +119,21 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {

    @Override
    public void selectRoute(String sessionId, String routeId) {
        //TODO: implement method
        // Do nothing since we don't support multiple BT yet.
    }

    @Override
    public void deselectRoute(String sessionId, String routeId) {
        //TODO: implement method
        // Do nothing since we don't support multiple BT yet.
    }

    @Override
    public void transferToRoute(String sessionId, String routeId) {
        //TODO: implement method
        if (TextUtils.equals(routeId, mDefaultRoute.getId())) {
            mBtRouteProvider.clearActiveDevices();
        } else {
            mBtRouteProvider.setActiveDevice(routeId);
        }
    }

    //TODO: implement method
@@ -147,8 +151,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
    public void requestUpdateVolume(String routeId, int delta) {
    }

    void initializeRoutes() {
        //TODO: adds necessary info
    private void initializeDefaultRoute() {
        mDefaultRoute = new MediaRoute2Info.Builder(
                DEFAULT_ROUTE_ID,
                mContext.getResources().getText(R.string.default_audio_route_name).toString())
@@ -172,7 +175,9 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
            // route yet.
            updateAudioRoutes(newAudioRoutes);
        }
    }

    private void initializeSessionInfo() {
        mBluetoothRoutes = mBtRouteProvider.getBluetoothRoutes();

        MediaRoute2ProviderInfo.Builder builder = new MediaRoute2ProviderInfo.Builder();
@@ -183,11 +188,15 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
        setProviderState(builder.build());
        mHandler.post(() -> notifyProviderState());

        // Note: No lock needed when initializing.
        //TODO: clean up this
        // This is required because it is not instantiated in the main thread and
        // BluetoothRoutesUpdatedListener can be called before this function
        synchronized (mLock) {
            updateSessionInfosIfNeededLocked();
        }
    }

    void updateAudioRoutes(AudioRoutesInfo newRoutes) {
    private void updateAudioRoutes(AudioRoutesInfo newRoutes) {
        int name = R.string.default_audio_route_name;
        mCurAudioRoutesInfo.mainType = newRoutes.mainType;
        if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADPHONES) != 0
@@ -226,15 +235,22 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
                .setSystemSession(true);
        String activeBtDeviceAddress = mBtRouteProvider.getActiveDeviceAddress();

        RoutingSessionInfo newSessionInfo;
        if (!TextUtils.isEmpty(activeBtDeviceAddress)) {
            // Bluetooth route. Set the route ID with the device's address.
            newSessionInfo = builder.addSelectedRoute(activeBtDeviceAddress).build();
            builder.addSelectedRoute(activeBtDeviceAddress);
            builder.addTransferrableRoute(mDefaultRoute.getId());
        } else {
            // Default device
            newSessionInfo = builder.addSelectedRoute(mDefaultRoute.getId()).build();
            builder.addSelectedRoute(mDefaultRoute.getId());
        }

        for (MediaRoute2Info route : mBluetoothRoutes) {
            if (!TextUtils.equals(activeBtDeviceAddress, route.getId())) {
                builder.addTransferrableRoute(route.getId());
            }
        }

        RoutingSessionInfo newSessionInfo = builder.setProviderId(mUniqueId).build();
        if (Objects.equals(oldSessionInfo, newSessionInfo)) {
            return false;
        } else {
@@ -244,11 +260,6 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
        }
    }

    /**
     * The first route should be the currently selected system route.
     * For example, if there are two system routes (BT and device speaker),
     * BT will be the first route in the list.
     */
    void publishRoutes() {
        MediaRoute2ProviderInfo.Builder builder = new MediaRoute2ProviderInfo.Builder();
        builder.addRoute(mDefaultRoute);