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

Commit 3177a12f authored by Hall Liu's avatar Hall Liu
Browse files

Include BT hearing aid devices in Telecom

Allow Dialer to use Telecom APIs to route audio to BT hearing aid
devices.
Also do some light refactoring within the BT handling code.

Bug: 116725094
Test: manual, added unit tests
Change-Id: I7a958fe1afec7d827c8659a991363a11bd967bf9
parent cbbb2aab
Loading
Loading
Loading
Loading
+136 −35
Original line number Diff line number Diff line
@@ -18,27 +18,22 @@ package com.android.server.telecom.bluetooth;

import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothManager;
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.telecom.Log;

import com.android.server.telecom.BluetoothAdapterProxy;
import com.android.server.telecom.BluetoothHeadsetProxy;
import com.android.server.telecom.TelecomSystem;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.Set;

public class BluetoothDeviceManager {
    private final BluetoothProfile.ServiceListener mBluetoothProfileServiceListener =
@@ -52,8 +47,12 @@ public class BluetoothDeviceManager {
                                mBluetoothHeadsetService =
                                        new BluetoothHeadsetProxy((BluetoothHeadset) proxy);
                                Log.i(this, "- Got BluetoothHeadset: " + mBluetoothHeadsetService);
                            } else if (profile == BluetoothProfile.HEARING_AID) {
                                mBluetoothHearingAidService = (BluetoothHearingAid) proxy;
                                Log.i(this, "- Got BluetoothHearingAid: "
                                        + mBluetoothHearingAidService);
                            } else {
                                Log.w(this, "Connected to non-headset bluetooth service." +
                                Log.w(this, "Connected to non-requested bluetooth service." +
                                        " Not changing bluetooth headset.");
                            }
                        }
@@ -67,12 +66,25 @@ public class BluetoothDeviceManager {
                    Log.startSession("BMSL.oSD");
                    try {
                        synchronized (mLock) {
                            LinkedHashMap<String, BluetoothDevice> lostServiceDevices;
                            if (profile == BluetoothProfile.HEADSET) {
                                mBluetoothHeadsetService = null;
                            Log.i(BluetoothDeviceManager.this, "Lost BluetoothHeadset service. " +
                                Log.i(BluetoothDeviceManager.this,
                                        "Lost BluetoothHeadset service. " +
                                                "Removing all tracked devices.");
                                lostServiceDevices = mHfpDevicesByAddress;
                            } else if (profile == BluetoothProfile.HEARING_AID) {
                                mBluetoothHearingAidService = null;
                                Log.i(BluetoothDeviceManager.this,
                                        "Lost BluetoothHearingAid service. " +
                                                "Removing all tracked devices.");
                                lostServiceDevices = mHearingAidDevicesByAddress;
                            } else {
                                return;
                            }
                            List<BluetoothDevice> devicesToRemove = new LinkedList<>(
                                    mConnectedDevicesByAddress.values());
                            mConnectedDevicesByAddress.clear();
                                    lostServiceDevices.values());
                            lostServiceDevices.clear();
                            for (BluetoothDevice device : devicesToRemove) {
                                mBluetoothRouteManager.onDeviceLost(device.getAddress());
                            }
@@ -83,7 +95,11 @@ public class BluetoothDeviceManager {
                }
           };

    private final LinkedHashMap<String, BluetoothDevice> mConnectedDevicesByAddress =
    private final LinkedHashMap<String, BluetoothDevice> mHfpDevicesByAddress =
            new LinkedHashMap<>();
    private final LinkedHashMap<String, BluetoothDevice> mHearingAidDevicesByAddress =
            new LinkedHashMap<>();
    private final LinkedHashMap<BluetoothDevice, Long> mHearingAidDeviceSyncIds =
            new LinkedHashMap<>();

    // This lock only protects internal state -- it doesn't lock on anything going into Telecom.
@@ -91,12 +107,14 @@ public class BluetoothDeviceManager {

    private BluetoothRouteManager mBluetoothRouteManager;
    private BluetoothHeadsetProxy mBluetoothHeadsetService;
    private BluetoothHearingAid mBluetoothHearingAidService;

    public BluetoothDeviceManager(Context context, BluetoothAdapterProxy bluetoothAdapter) {

        if (bluetoothAdapter != null) {
            bluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener,
                    BluetoothProfile.HEADSET);
            bluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener,
                    BluetoothProfile.HEARING_AID);
        }
    }

@@ -106,58 +124,141 @@ public class BluetoothDeviceManager {

    public int getNumConnectedDevices() {
        synchronized (mLock) {
            return mConnectedDevicesByAddress.size();
            return mHfpDevicesByAddress.size() + mHearingAidDevicesByAddress.size();
        }
    }

    public Collection<BluetoothDevice> getConnectedDevices() {
        synchronized (mLock) {
            return Collections.unmodifiableCollection(
                    new ArrayList<>(mConnectedDevicesByAddress.values()));
            ArrayList<BluetoothDevice> result = new ArrayList<>(mHfpDevicesByAddress.values());
            result.addAll(mHearingAidDevicesByAddress.values());
            return Collections.unmodifiableCollection(result);
        }
    }

    public String getMostRecentlyConnectedDevice(String excludeAddress) {
        String result = null;
    // Same as getConnectedDevices except it filters out the hearing aid devices that are linked
    // together by their hiSyncId.
    public Collection<BluetoothDevice> getUniqueConnectedDevices() {
        ArrayList<BluetoothDevice> result = new ArrayList<>(mHfpDevicesByAddress.values());
        Set<Long> seenHiSyncIds = new LinkedHashSet<>();
        // Add the left-most active device to the seen list so that we match up with the list
        // generated in BluetoothRouteManager.
        if (mBluetoothHearingAidService != null) {
            for (BluetoothDevice device : mBluetoothHearingAidService.getActiveDevices()) {
                if (device != null) {
                    result.add(device);
                    seenHiSyncIds.add(mHearingAidDeviceSyncIds.getOrDefault(device, -1L));
                    break;
                }
            }
        }
        synchronized (mLock) {
            for (String addr : mConnectedDevicesByAddress.keySet()) {
                if (!Objects.equals(addr, excludeAddress)) {
                    result = addr;
            for (BluetoothDevice d : mHearingAidDevicesByAddress.values()) {
                long hiSyncId = mHearingAidDeviceSyncIds.getOrDefault(d, -1L);
                if (seenHiSyncIds.contains(hiSyncId)) {
                    continue;
                }
                result.add(d);
                seenHiSyncIds.add(hiSyncId);
            }
        }
        return result;
        return Collections.unmodifiableCollection(result);
    }

    public BluetoothHeadsetProxy getHeadsetService() {
        return mBluetoothHeadsetService;
    }

    public BluetoothHearingAid getHearingAidService() {
        return mBluetoothHearingAidService;
    }

    public void setHeadsetServiceForTesting(BluetoothHeadsetProxy bluetoothHeadset) {
        mBluetoothHeadsetService = bluetoothHeadset;
    }

    public BluetoothDevice getDeviceFromAddress(String address) {
        synchronized (mLock) {
            return mConnectedDevicesByAddress.get(address);
        }
    public void setHearingAidServiceForTesting(BluetoothHearingAid bluetoothHearingAid) {
        mBluetoothHearingAidService = bluetoothHearingAid;
    }

    void onDeviceConnected(BluetoothDevice device) {
    void onDeviceConnected(BluetoothDevice device, boolean isHearingAid) {
        synchronized (mLock) {
            if (!mConnectedDevicesByAddress.containsKey(device.getAddress())) {
                mConnectedDevicesByAddress.put(device.getAddress(), device);
            LinkedHashMap<String, BluetoothDevice> targetDeviceMap;
            if (isHearingAid) {
                long hiSyncId = mBluetoothHearingAidService.getHiSyncId(device);
                mHearingAidDeviceSyncIds.put(device, hiSyncId);
                targetDeviceMap = mHearingAidDevicesByAddress;
            } else {
                targetDeviceMap = mHfpDevicesByAddress;
            }
            if (!targetDeviceMap.containsKey(device.getAddress())) {
                targetDeviceMap.put(device.getAddress(), device);
                mBluetoothRouteManager.onDeviceAdded(device.getAddress());
            }
        }
    }

    void onDeviceDisconnected(BluetoothDevice device) {
    void onDeviceDisconnected(BluetoothDevice device, boolean isHearingAid) {
        synchronized (mLock) {
            if (mConnectedDevicesByAddress.containsKey(device.getAddress())) {
                mConnectedDevicesByAddress.remove(device.getAddress());
            LinkedHashMap<String, BluetoothDevice> targetDeviceMap;
            if (isHearingAid) {
                mHearingAidDeviceSyncIds.remove(device);
                targetDeviceMap = mHearingAidDevicesByAddress;
            } else {
                targetDeviceMap = mHfpDevicesByAddress;
            }
            if (targetDeviceMap.containsKey(device.getAddress())) {
                targetDeviceMap.remove(device.getAddress());
                mBluetoothRouteManager.onDeviceLost(device.getAddress());
            }
        }
    }

    public void disconnectAudio() {
        if (mBluetoothHearingAidService == null) {
            Log.w(this, "Trying to disconnect audio but no hearing aid service exists");
        } else {
            for (BluetoothDevice device : mBluetoothHearingAidService.getActiveDevices()) {
                if (device != null) {
                    mBluetoothHearingAidService.setActiveDevice(null);
                }
            }
        }
        if (mBluetoothHeadsetService == null) {
            Log.w(this, "Trying to disconnect audio but no headset service exists.");
        } else {
            mBluetoothHeadsetService.disconnectAudio();
        }
    }

    // Connect audio to the bluetooth device at address, checking to see whether it's a hearing aid
    // or a HFP device, and using the proper BT API.
    public boolean connectAudio(String address) {
        if (mHearingAidDevicesByAddress.containsKey(address)) {
            if (mBluetoothHearingAidService == null) {
                Log.w(this, "Attempting to turn on audio when the hearing aid service is null");
                return false;
            }
            return mBluetoothHearingAidService.setActiveDevice(
                    mHearingAidDevicesByAddress.get(address));
        } else if (mHfpDevicesByAddress.containsKey(address)) {
            BluetoothDevice device = mHfpDevicesByAddress.get(address);
            if (mBluetoothHeadsetService == null) {
                Log.w(this, "Attempting to turn on audio when the headset service is null");
                return false;
            }
            boolean success = mBluetoothHeadsetService.setActiveDevice(device);
            if (!success) {
                Log.w(this, "Couldn't set active device to %s", address);
                return false;
            }
            if (!mBluetoothHeadsetService.isAudioOn()) {
                return mBluetoothHeadsetService.connectAudio();
            }
            return true;
        } else {
            Log.w(this, "Attempting to turn on audio for a disconnected device");
            return false;
        }
    }
}
+68 −72
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.server.telecom.bluetooth;

import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHeadset;
import android.bluetooth.BluetoothHearingAid;
import android.content.Context;
import android.os.Message;
import android.telecom.Log;
@@ -150,7 +151,7 @@ public class BluetoothRouteManager extends StateMachine {
                        removeDevice((String) args.arg2);
                        break;
                    case CONNECT_HFP:
                        String actualAddress = connectHfpAudio((String) args.arg2);
                        String actualAddress = connectBtAudio((String) args.arg2);

                        if (actualAddress != null) {
                            transitionTo(getConnectingStateForAddress(actualAddress,
@@ -165,7 +166,7 @@ public class BluetoothRouteManager extends StateMachine {
                        break;
                    case RETRY_HFP_CONNECTION:
                        Log.i(LOG_TAG, "Retrying HFP connection to %s", (String) args.arg2);
                        String retryAddress = connectHfpAudio((String) args.arg2, args.argi1);
                        String retryAddress = connectBtAudio((String) args.arg2, args.argi1);

                        if (retryAddress != null) {
                            transitionTo(getConnectingStateForAddress(retryAddress,
@@ -252,7 +253,7 @@ public class BluetoothRouteManager extends StateMachine {
                            // Ignore repeated connection attempts to the same device
                            break;
                        }
                        String actualAddress = connectHfpAudio(address);
                        String actualAddress = connectBtAudio(address);

                        if (actualAddress != null) {
                            transitionTo(getConnectingStateForAddress(actualAddress,
@@ -263,13 +264,13 @@ public class BluetoothRouteManager extends StateMachine {
                        }
                        break;
                    case DISCONNECT_HFP:
                        disconnectAudio();
                        mDeviceManager.disconnectAudio();
                        break;
                    case RETRY_HFP_CONNECTION:
                        if (Objects.equals(address, mDeviceAddress)) {
                            Log.d(LOG_TAG, "Retry message came through while connecting.");
                        } else {
                            String retryAddress = connectHfpAudio(address, args.argi1);
                            String retryAddress = connectBtAudio(address, args.argi1);
                            if (retryAddress != null) {
                                transitionTo(getConnectingStateForAddress(retryAddress,
                                        "AudioConnecting/RETRY_HFP_CONNECTION"));
@@ -364,7 +365,7 @@ public class BluetoothRouteManager extends StateMachine {
                            // Ignore connection to already connected device.
                            break;
                        }
                        String actualAddress = connectHfpAudio(address);
                        String actualAddress = connectBtAudio(address);

                        if (actualAddress != null) {
                            transitionTo(getConnectingStateForAddress(address,
@@ -375,13 +376,13 @@ public class BluetoothRouteManager extends StateMachine {
                        }
                        break;
                    case DISCONNECT_HFP:
                        disconnectAudio();
                        mDeviceManager.disconnectAudio();
                        break;
                    case RETRY_HFP_CONNECTION:
                        if (Objects.equals(address, mDeviceAddress)) {
                            Log.d(LOG_TAG, "Retry message came through while connected.");
                        } else {
                            String retryAddress = connectHfpAudio(address, args.argi1);
                            String retryAddress = connectBtAudio(address, args.argi1);
                            if (retryAddress != null) {
                                transitionTo(getConnectingStateForAddress(retryAddress,
                                        "AudioConnected/RETRY_HFP_CONNECTION"));
@@ -436,8 +437,9 @@ public class BluetoothRouteManager extends StateMachine {

    private BluetoothStateListener mListener;
    private BluetoothDeviceManager mDeviceManager;
    // Tracks the active device in the BT stack.
    private BluetoothDevice mActiveDeviceCache = null;
    // Tracks the active devices in the BT stack (HFP or hearing aid).
    private BluetoothDevice mHfpActiveDeviceCache = null;
    private BluetoothDevice mHearingAidActiveDeviceCache = null;

    public BluetoothRouteManager(Context context, TelecomSystem.SyncRoot lock,
            BluetoothDeviceManager deviceManager, Timeouts.Adapter timeoutsAdapter) {
@@ -556,32 +558,38 @@ public class BluetoothRouteManager extends StateMachine {
        mListener.onBluetoothDeviceListChanged();
    }

    public void onActiveDeviceChanged(BluetoothDevice device) {
        BluetoothDevice oldActiveDevice = mActiveDeviceCache;
        mActiveDeviceCache = device;
        if ((oldActiveDevice == null) ^ (device == null)) {
            if (device == null) {
                mListener.onBluetoothActiveDeviceGone();
    public void onActiveDeviceChanged(BluetoothDevice device, boolean isHearingAid) {
        boolean wasActiveDevicePresent = mHearingAidActiveDeviceCache != null
                || mHfpActiveDeviceCache != null;
        if (isHearingAid) {
            mHearingAidActiveDeviceCache = device;
        } else {
                mListener.onBluetoothActiveDevicePresent();
            mHfpActiveDeviceCache = device;
        }
        boolean isActiveDevicePresent = mHearingAidActiveDeviceCache != null
                || mHfpActiveDeviceCache != null;

        if (wasActiveDevicePresent && !isActiveDevicePresent) {
            mListener.onBluetoothActiveDeviceGone();
        } else if (!wasActiveDevicePresent && isActiveDevicePresent) {
            mListener.onBluetoothActiveDevicePresent();
        }
    }

    public boolean hasBtActiveDevice() {
        return mActiveDeviceCache != null;
        return mHearingAidActiveDeviceCache != null || mHfpActiveDeviceCache != null;
    }

    public Collection<BluetoothDevice> getConnectedDevices() {
        return mDeviceManager.getConnectedDevices();
        return mDeviceManager.getUniqueConnectedDevices();
    }

    private String connectHfpAudio(String address) {
        return connectHfpAudio(address, 0);
    private String connectBtAudio(String address) {
        return connectBtAudio(address, 0);
    }

    /**
     * Initiates a HFP connection to the BT address specified.
     * Initiates a connection to the BT address specified.
     * Note: This method is not synchronized on the Telecom lock, so don't try and call back into
     * Telecom from within it.
     * @param address The address that should be tried first. May be null.
@@ -589,8 +597,8 @@ public class BluetoothRouteManager extends StateMachine {
     * @return The address of the device that's actually being connected to, or null if no
     * connection was successful.
     */
    private String connectHfpAudio(String address, int retryCount) {
        Collection<BluetoothDevice> deviceList = getConnectedDevices();
    private String connectBtAudio(String address, int retryCount) {
        Collection<BluetoothDevice> deviceList = mDeviceManager.getConnectedDevices();
        Optional<BluetoothDevice> matchingDevice = deviceList.stream()
                .filter(d -> Objects.equals(d.getAddress(), address))
                .findAny();
@@ -611,7 +619,7 @@ public class BluetoothRouteManager extends StateMachine {
            Log.i(this, "No device with address %s available. Using %s instead.",
                    address, actualAddress);
        }
        if (!connectAudio(actualAddress)) {
        if (!mDeviceManager.connectAudio(actualAddress)) {
            boolean shouldRetry = retryCount < MAX_CONNECTION_RETRIES;
            Log.w(LOG_TAG, "Could not connect to %s. Will %s", actualAddress,
                    shouldRetry ? "retry" : "not retry");
@@ -631,7 +639,13 @@ public class BluetoothRouteManager extends StateMachine {
    }

    private String getActiveDeviceAddress() {
        return mActiveDeviceCache == null ? null : mActiveDeviceCache.getAddress();
        if (mHfpActiveDeviceCache != null) {
            return mHfpActiveDeviceCache.getAddress();
        }
        if (mHearingAidActiveDeviceCache != null) {
            return mHearingAidActiveDeviceCache.getAddress();
        }
        return null;
    }

    private void transitionToActualState() {
@@ -652,14 +666,15 @@ public class BluetoothRouteManager extends StateMachine {
    @VisibleForTesting
    public BluetoothDevice getBluetoothAudioConnectedDevice() {
        BluetoothHeadsetProxy bluetoothHeadset = mDeviceManager.getHeadsetService();
        if (bluetoothHeadset == null) {
            Log.i(this, "getBluetoothAudioConnectedDevice: no headset service available.");
        BluetoothHearingAid bluetoothHearingAid = mDeviceManager.getHearingAidService();

        if (bluetoothHeadset == null && bluetoothHearingAid == null) {
            Log.i(this, "getBluetoothAudioConnectedDevice: no service available.");
            return null;
        }
        List<BluetoothDevice> deviceList = bluetoothHeadset.getConnectedDevices();

        for (int i = 0; i < deviceList.size(); i++) {
            BluetoothDevice device = deviceList.get(i);
        if (bluetoothHeadset != null) {
            for (BluetoothDevice device : bluetoothHeadset.getConnectedDevices()) {
                boolean isAudioOn = bluetoothHeadset.getAudioState(device)
                        != BluetoothHeadset.STATE_AUDIO_DISCONNECTED;
                Log.v(this, "isBluetoothAudioConnected: ==> isAudioOn = " + isAudioOn
@@ -668,6 +683,14 @@ public class BluetoothRouteManager extends StateMachine {
                    return device;
                }
            }
        }
        if (bluetoothHearingAid != null) {
            for (BluetoothDevice device : bluetoothHearingAid.getActiveDevices()) {
                if (device != null) {
                    return device;
                }
            }
        }
        return null;
    }

@@ -687,37 +710,6 @@ public class BluetoothRouteManager extends StateMachine {
        return bluetoothHeadset.isInbandRingingEnabled();
    }

    private boolean connectAudio(String address) {
        BluetoothHeadsetProxy bluetoothHeadset = mDeviceManager.getHeadsetService();
        if (bluetoothHeadset == null) {
            Log.w(this, "Trying to connect audio but no headset service exists.");
            return false;
        }
        BluetoothDevice device = mDeviceManager.getDeviceFromAddress(address);
        if (device == null) {
            Log.w(this, "Attempting to turn on audio for a disconnected device");
            return false;
        }
        boolean success = bluetoothHeadset.setActiveDevice(device);
        if (!success) {
            Log.w(LOG_TAG, "Couldn't set active device to %s", address);
            return false;
        }
        if (!bluetoothHeadset.isAudioOn()) {
            return bluetoothHeadset.connectAudio();
        }
        return true;
    }

    private void disconnectAudio() {
        BluetoothHeadsetProxy bluetoothHeadset = mDeviceManager.getHeadsetService();
        if (bluetoothHeadset == null) {
            Log.w(this, "Trying to disconnect audio but no headset service exists.");
        } else {
            bluetoothHeadset.disconnectAudio();
        }
    }

    private boolean addDevice(String address) {
        if (mAudioConnectingStates.containsKey(address)) {
            Log.i(this, "Attempting to add device %s twice.", address);
@@ -795,7 +787,11 @@ public class BluetoothRouteManager extends StateMachine {
    }

    @VisibleForTesting
    public void setActiveDeviceCacheForTesting(BluetoothDevice device) {
        mActiveDeviceCache = device;
    public void setActiveDeviceCacheForTesting(BluetoothDevice device, boolean isHearingAid) {
        if (isHearingAid) {
            mHearingAidActiveDeviceCache = device;
        } else {
            mHfpActiveDeviceCache = device;
        }
    }
}
+30 −7

File changed.

Preview size limit exceeded, changes collapsed.

+73 −37

File changed.

Preview size limit exceeded, changes collapsed.

+7 −23

File changed.

Preview size limit exceeded, changes collapsed.

Loading