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

Commit bb3ee6e0 authored by Sungsoo Lim's avatar Sungsoo Lim
Browse files

Update SystemMediaRoute2Provider for multiple BT devices

Bug: 147122575
Test: manually
Change-Id: I3188c5f4193128e1fb9505438fe5aad826704398
parent aa3d08a9
Loading
Loading
Loading
Loading
+321 −0
Original line number Diff line number Diff line
/*
 * 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 com.android.server.media;

import android.annotation.NonNull;
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.MediaRoute2Info;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseBooleanArray;

import com.android.internal.R;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

class BluetoothRouteProvider {
    private static final String TAG = "BTRouteProvider";
    private static BluetoothRouteProvider sInstance;

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    final Map<String, BluetoothRouteInfo> mBluetoothRoutes = new HashMap<>();
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    BluetoothA2dp mA2dpProfile;
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    BluetoothHearingAid mHearingAidProfile;

    private final Context mContext;
    private final BluetoothAdapter mBluetoothAdapter;
    private final BluetoothRoutesUpdatedListener mListener;
    private final Map<String, BluetoothEventReceiver> mEventReceiverMap = new HashMap<>();
    private final IntentFilter mIntentFilter = new IntentFilter();
    private final BroadcastReceiver mBroadcastReceiver = new BluetoothBroadcastReceiver();
    private final BluetoothProfileListener mProfileListener = new BluetoothProfileListener();

    private BluetoothDevice mActiveDevice = null;

    static synchronized BluetoothRouteProvider getInstance(@NonNull Context context,
            @NonNull BluetoothRoutesUpdatedListener listener) {
        Objects.requireNonNull(context);
        Objects.requireNonNull(listener);

        if (sInstance == null) {
            BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter();
            if (btAdapter == null) {
                return null;
            }
            sInstance = new BluetoothRouteProvider(context, btAdapter, listener);
        }
        return sInstance;
    }

    private BluetoothRouteProvider(Context context, BluetoothAdapter btAdapter,
            BluetoothRoutesUpdatedListener listener) {
        mContext = context;
        mBluetoothAdapter = btAdapter;
        mListener = listener;
        buildBluetoothRoutes();

        mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.A2DP);
        mBluetoothAdapter.getProfileProxy(mContext, mProfileListener, BluetoothProfile.HEARING_AID);

        // Bluetooth on/off broadcasts
        addEventReceiver(BluetoothAdapter.ACTION_STATE_CHANGED, new AdapterStateChangedReceiver());

        // Pairing broadcasts
        addEventReceiver(BluetoothDevice.ACTION_BOND_STATE_CHANGED, new BondStateChangedReceiver());

        DeviceStateChangedRecevier deviceStateChangedReceiver = new DeviceStateChangedRecevier();
        addEventReceiver(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED, deviceStateChangedReceiver);
        addEventReceiver(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED, deviceStateChangedReceiver);
        addEventReceiver(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED,
                deviceStateChangedReceiver);
        addEventReceiver(BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED,
                deviceStateChangedReceiver);

        mContext.registerReceiver(mBroadcastReceiver, mIntentFilter, null, null);
    }

    private void addEventReceiver(String action, BluetoothEventReceiver eventReceiver) {
        mEventReceiverMap.put(action, eventReceiver);
        mIntentFilter.addAction(action);
    }

    private void buildBluetoothRoutes() {
        mBluetoothRoutes.clear();
        for (BluetoothDevice device : mBluetoothAdapter.getBondedDevices()) {
            if (device.isConnected()) {
                BluetoothRouteInfo newBtRoute = createBluetoothRoute(device);
                mBluetoothRoutes.put(device.getAddress(), newBtRoute);
            }
        }
    }

    @NonNull List<MediaRoute2Info> getBluetoothRoutes() {
        ArrayList<MediaRoute2Info> routes = new ArrayList<>();
        for (BluetoothRouteInfo btRoute : mBluetoothRoutes.values()) {
            routes.add(btRoute.route);
        }
        return routes;
    }

    private void notifyBluetoothRoutesUpdated() {
        if (mListener != null) {
            mListener.onBluetoothRoutesUpdated(getBluetoothRoutes());
        }
    }

    private BluetoothRouteInfo createBluetoothRoute(BluetoothDevice device) {
        BluetoothRouteInfo newBtRoute = new BluetoothRouteInfo();
        newBtRoute.btDevice = device;
        newBtRoute.route = new MediaRoute2Info.Builder(device.getAddress(), device.getName())
                .addFeature(SystemMediaRoute2Provider.TYPE_LIVE_AUDIO)
                .setConnectionState(MediaRoute2Info.CONNECTION_STATE_DISCONNECTED)
                .setDescription(mContext.getResources().getText(
                        R.string.bluetooth_a2dp_audio_route_name).toString())
                .build();
        newBtRoute.connectedProfiles = new SparseBooleanArray();
        return newBtRoute;
    }

    private void setRouteConnectionStateForDevice(BluetoothDevice device,
            @MediaRoute2Info.ConnectionState int state) {
        if (device == null) {
            Log.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");
            return;
        }
        if (btRoute.route.getConnectionState() != state) {
            btRoute.route = new MediaRoute2Info.Builder(btRoute.route)
                    .setConnectionState(state).build();
        }
    }

    interface BluetoothRoutesUpdatedListener {
        void onBluetoothRoutesUpdated(@NonNull List<MediaRoute2Info> routes);
    }

    private class BluetoothRouteInfo {
        public BluetoothDevice btDevice;
        public MediaRoute2Info route;
        public SparseBooleanArray connectedProfiles;
    }

    // These callbacks run on the main thread.
    private final class BluetoothProfileListener implements BluetoothProfile.ServiceListener {
        public void onServiceConnected(int profile, BluetoothProfile proxy) {
            switch (profile) {
                case BluetoothProfile.A2DP:
                    mA2dpProfile = (BluetoothA2dp) proxy;
                    break;
                case BluetoothProfile.HEARING_AID:
                    mHearingAidProfile = (BluetoothHearingAid) proxy;
                    break;
                default:
                    return;
            }
            for (BluetoothDevice device : proxy.getConnectedDevices()) {
                BluetoothRouteInfo btRoute = mBluetoothRoutes.get(device.getAddress());
                if (btRoute == null) {
                    btRoute = createBluetoothRoute(device);
                    mBluetoothRoutes.put(device.getAddress(), btRoute);
                }
                btRoute.connectedProfiles.put(profile, true);
            }
        }

        public void onServiceDisconnected(int profile) {
            switch (profile) {
                case BluetoothProfile.A2DP:
                    mA2dpProfile = null;
                    break;
                case BluetoothProfile.HEARING_AID:
                    mHearingAidProfile = null;
                    break;
                default:
                    return;
            }
        }
    }
    private class BluetoothBroadcastReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);

            BluetoothEventReceiver receiver = mEventReceiverMap.get(action);
            if (receiver != null) {
                receiver.onReceive(context, intent, device);
            }
        }
    }

    private interface BluetoothEventReceiver {
        void onReceive(Context context, Intent intent, BluetoothDevice device);
    }

    private class AdapterStateChangedReceiver implements BluetoothEventReceiver {
        public void onReceive(Context context, Intent intent, BluetoothDevice device) {
            int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
            if (state == BluetoothAdapter.STATE_OFF
                    || state == BluetoothAdapter.STATE_TURNING_OFF) {
                mBluetoothRoutes.clear();
                notifyBluetoothRoutesUpdated();
            } else if (state == BluetoothAdapter.STATE_ON) {
                buildBluetoothRoutes();
                if (!mBluetoothRoutes.isEmpty()) {
                    notifyBluetoothRoutesUpdated();
                }
            }
        }
    }

    private class BondStateChangedReceiver implements BluetoothEventReceiver {
        public void onReceive(Context context, Intent intent, BluetoothDevice device) {
            int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
                    BluetoothDevice.ERROR);
            BluetoothRouteInfo btRoute = mBluetoothRoutes.get(device.getAddress());
            if (bondState == BluetoothDevice.BOND_BONDED && btRoute == null) {
                btRoute = createBluetoothRoute(device);
                if (mA2dpProfile != null && mA2dpProfile.getConnectedDevices().contains(device)) {
                    btRoute.connectedProfiles.put(BluetoothProfile.A2DP, true);
                }
                if (mHearingAidProfile != null
                        && mHearingAidProfile.getConnectedDevices().contains(device)) {
                    btRoute.connectedProfiles.put(BluetoothProfile.HEARING_AID, true);
                }
                mBluetoothRoutes.put(device.getAddress(), btRoute);
                notifyBluetoothRoutesUpdated();
            } else if (bondState == BluetoothDevice.BOND_NONE
                    && mBluetoothRoutes.remove(device.getAddress()) != null) {
                notifyBluetoothRoutesUpdated();
            }
        }
    }

    private class DeviceStateChangedRecevier implements BluetoothEventReceiver {
        @Override
        public void onReceive(Context context, Intent intent, BluetoothDevice device) {
            switch (intent.getAction()) {
                case BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED:
                    String prevActiveDeviceAddress =
                            (mActiveDevice == null) ? null : mActiveDevice.getAddress();
                    String curActiveDeviceAddress =
                            (device == null) ? null : device.getAddress();
                    if (!TextUtils.equals(prevActiveDeviceAddress, curActiveDeviceAddress)) {
                        if (mActiveDevice != null) {
                            setRouteConnectionStateForDevice(mActiveDevice,
                                    MediaRoute2Info.CONNECTION_STATE_DISCONNECTED);
                        }
                        if (device != null) {
                            setRouteConnectionStateForDevice(device,
                                    MediaRoute2Info.CONNECTION_STATE_CONNECTED);
                        }
                        notifyBluetoothRoutesUpdated();
                        mActiveDevice = device;
                    }
                    break;
                case BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED:
                    handleConnectionStateChanged(BluetoothProfile.A2DP, intent, device);
                    break;
            }
        }

        private void handleConnectionStateChanged(int profile, Intent intent,
                BluetoothDevice device) {
            int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
            BluetoothRouteInfo btRoute = mBluetoothRoutes.get(device.getAddress());
            if (state == BluetoothProfile.STATE_CONNECTED) {
                if (btRoute == null) {
                    btRoute = createBluetoothRoute(device);
                    mBluetoothRoutes.put(device.getAddress(), btRoute);
                    btRoute.connectedProfiles.put(profile, true);
                    notifyBluetoothRoutesUpdated();
                } else {
                    btRoute.connectedProfiles.put(profile, true);
                }
            } else if (state == BluetoothProfile.STATE_DISCONNECTING
                    || state == BluetoothProfile.STATE_DISCONNECTED) {
                btRoute.connectedProfiles.delete(profile);
                if (btRoute.connectedProfiles.size() == 0) {
                    mBluetoothRoutes.remove(device.getAddress());
                    if (mActiveDevice != null
                            && TextUtils.equals(mActiveDevice.getAddress(), device.getAddress())) {
                        mActiveDevice = null;
                    }
                    notifyBluetoothRoutesUpdated();
                }
            }
        }
    }
}
+9 −1
Original line number Diff line number Diff line
@@ -72,7 +72,7 @@ abstract class MediaRoute2Provider {
        return mSessionInfos;
    }

    void setAndNotifyProviderState(MediaRoute2ProviderInfo providerInfo,
    void setProviderState(MediaRoute2ProviderInfo providerInfo,
            List<RoutingSessionInfo> sessionInfos) {
        if (providerInfo == null) {
            mProviderInfo = null;
@@ -89,12 +89,20 @@ abstract class MediaRoute2Provider {
                            .build());
        }
        mSessionInfos = sessionInfoWithProviderId;
    }

    void notifyProviderState() {
        if (mCallback != null) {
            mCallback.onProviderStateChanged(this);
        }
    }

    void setAndNotifyProviderState(MediaRoute2ProviderInfo providerInfo,
            List<RoutingSessionInfo> sessionInfos) {
        setProviderState(providerInfo, sessionInfos);
        notifyProviderState();
    }

    public boolean hasComponentName(String packageName, String className) {
        return mComponentName.getPackageName().equals(packageName)
                && mComponentName.getClassName().equals(className);
+19 −23
Original line number Diff line number Diff line
@@ -30,12 +30,12 @@ import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.text.TextUtils;
import android.util.Log;

import com.android.internal.R;

import java.util.Collections;
import java.util.List;

/**
 * Provides routes for local playbacks such as phone speaker, wired headset, or Bluetooth speakers.
@@ -55,6 +55,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
    private final IAudioService mAudioService;
    private final Handler mHandler;
    private final Context mContext;
    private final BluetoothRouteProvider mBtRouteProvider;

    private static ComponentName sComponentName = new ComponentName(
            SystemMediaRoute2Provider.class.getPackageName$(),
@@ -62,7 +63,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {

    //TODO: Clean up these when audio manager support multiple bt devices
    MediaRoute2Info mDefaultRoute;
    MediaRoute2Info mBluetoothA2dpRoute;
    @NonNull List<MediaRoute2Info> mBluetoothRoutes = Collections.EMPTY_LIST;
    final AudioRoutesInfo mCurAudioRoutesInfo = new AudioRoutesInfo();

    final IAudioRoutesObserver.Stub mAudioRoutesObserver = new IAudioRoutesObserver.Stub() {
@@ -87,6 +88,10 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
        mAudioService = IAudioService.Stub.asInterface(
                ServiceManager.getService(Context.AUDIO_SERVICE));

        mBtRouteProvider = BluetoothRouteProvider.getInstance(context, (routes) -> {
            mBluetoothRoutes = routes;
            publishRoutes();
        });
        initializeRoutes();
    }

@@ -157,7 +162,15 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
            updateAudioRoutes(newAudioRoutes);
        }

        publishRoutes();
        mBluetoothRoutes = mBtRouteProvider.getBluetoothRoutes();

        MediaRoute2ProviderInfo.Builder builder = new MediaRoute2ProviderInfo.Builder();
        builder.addRoute(mDefaultRoute);
        for (MediaRoute2Info route : mBluetoothRoutes) {
            builder.addRoute(route);
        }
        setProviderState(builder.build(), Collections.emptyList());
        mHandler.post(() -> notifyProviderState());
    }

    void updateAudioRoutes(AudioRoutesInfo newRoutes) {
@@ -185,21 +198,6 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider {
                .addFeature(TYPE_LIVE_VIDEO)
                .build();

        if (!TextUtils.equals(newRoutes.bluetoothName, mCurAudioRoutesInfo.bluetoothName)) {
            mCurAudioRoutesInfo.bluetoothName = newRoutes.bluetoothName;
            if (mCurAudioRoutesInfo.bluetoothName != null) {
                //TODO: mark as bluetooth once MediaRoute2Info has device type
                mBluetoothA2dpRoute = new MediaRoute2Info.Builder(BLUETOOTH_ROUTE_ID,
                        mCurAudioRoutesInfo.bluetoothName)
                        .setDescription(mContext.getResources().getText(
                                R.string.bluetooth_a2dp_audio_route_name).toString())
                        .addFeature(TYPE_LIVE_AUDIO)
                        .build();
            } else {
                mBluetoothA2dpRoute = null;
            }
        }

        publishRoutes();
    }

@@ -207,15 +205,13 @@ 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.
     *
     * TODO: Support multiple BT devices
     */
    void publishRoutes() {
        MediaRoute2ProviderInfo.Builder builder = new MediaRoute2ProviderInfo.Builder();
        if (mBluetoothA2dpRoute != null) {
            builder.addRoute(mBluetoothA2dpRoute);
        }
        builder.addRoute(mDefaultRoute);
        for (MediaRoute2Info route : mBluetoothRoutes) {
            builder.addRoute(route);
        }
        setAndNotifyProviderState(builder.build(), Collections.emptyList());
    }
}