/* * Copyright (C) 2016 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.telecom.bluetooth; import static com.android.server.telecom.AudioRoute.TYPE_BLUETOOTH_HA; import static com.android.server.telecom.AudioRoute.TYPE_BLUETOOTH_SCO; import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_REMOVED; import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BASELINE_ROUTE; import static com.android.server.telecom.CallAudioRouteController.INCLUDE_BLUETOOTH_IN_BASELINE; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadset; import android.bluetooth.BluetoothHearingAid; import android.bluetooth.BluetoothLeAudio; import android.bluetooth.BluetoothLeAudioCodecStatus; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothStatusCodes; import android.content.Context; import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.os.Bundle; import android.telecom.Log; import android.util.ArraySet; import android.util.LocalLog; import android.util.Pair; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.IndentingPrintWriter; import com.android.server.telecom.AudioRoute; import com.android.server.telecom.CallAudioCommunicationDeviceTracker; import com.android.server.telecom.CallAudioRouteAdapter; import com.android.server.telecom.CallAudioRouteController; import com.android.server.telecom.flags.FeatureFlags; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; 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.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; public class BluetoothDeviceManager { public static final int DEVICE_TYPE_HEADSET = 0; public static final int DEVICE_TYPE_HEARING_AID = 1; public static final int DEVICE_TYPE_LE_AUDIO = 2; private static final Map PROFILE_TO_AUDIO_ROUTE_MAP = new HashMap<>(); static { PROFILE_TO_AUDIO_ROUTE_MAP.put(BluetoothProfile.HEADSET, AudioRoute.TYPE_BLUETOOTH_SCO); PROFILE_TO_AUDIO_ROUTE_MAP.put(BluetoothProfile.LE_AUDIO, AudioRoute.TYPE_BLUETOOTH_LE); PROFILE_TO_AUDIO_ROUTE_MAP.put(BluetoothProfile.HEARING_AID, TYPE_BLUETOOTH_HA); } private BluetoothLeAudio.Callback mLeAudioCallbacks = new BluetoothLeAudio.Callback() { @Override public void onCodecConfigChanged(int groupId, BluetoothLeAudioCodecStatus status) {} @Override public void onGroupStatusChanged(int groupId, int groupStatus) {} @Override public void onGroupNodeAdded(BluetoothDevice device, int groupId) { Log.i(this, (device == null ? "device is null" : device.getAddress()) + " group added " + groupId); if (device == null || groupId == BluetoothLeAudio.GROUP_ID_INVALID) { Log.w(this, "invalid parameter"); return; } synchronized (mLock) { mGroupsByDevice.put(device, groupId); } } @Override public void onGroupNodeRemoved(BluetoothDevice device, int groupId) { Log.i(this, (device == null ? "device is null" : device.getAddress()) + " group removed " + groupId); if (device == null || groupId == BluetoothLeAudio.GROUP_ID_INVALID) { Log.w(this, "invalid parameter"); return; } synchronized (mLock) { mGroupsByDevice.remove(device); } } }; private final BluetoothProfile.ServiceListener mBluetoothProfileServiceListener = new BluetoothProfile.ServiceListener() { @Override public void onServiceConnected(int profile, BluetoothProfile proxy) { Log.startSession("BPSL.oSC"); try { synchronized (mLock) { String logString; if (profile == BluetoothProfile.HEADSET) { if (mFeatureFlags.useRefactoredAudioRouteSwitching()) { mBluetoothHeadsetFuture.complete((BluetoothHeadset) proxy); } mBluetoothHeadset = (BluetoothHeadset) proxy; logString = "Got BluetoothHeadset: " + mBluetoothHeadset; } else if (profile == BluetoothProfile.HEARING_AID) { mBluetoothHearingAid = (BluetoothHearingAid) proxy; logString = "Got BluetoothHearingAid: " + mBluetoothHearingAid; } else if (profile == BluetoothProfile.LE_AUDIO) { mBluetoothLeAudioService = (BluetoothLeAudio) proxy; logString = ("Got BluetoothLeAudio: " + mBluetoothLeAudioService ) + (", mLeAudioCallbackRegistered: " + mLeAudioCallbackRegistered); if (!mLeAudioCallbackRegistered) { if (mFeatureFlags.postponeRegisterToLeaudio()) { mExecutor.execute(this::registerToLeAudio); } else { registerToLeAudio(); } } } else { logString = "Connected to non-requested bluetooth service." + " Not changing bluetooth headset."; } Log.i(BluetoothDeviceManager.this, logString); mLocalLog.log(logString); } } finally { Log.endSession(); } } private void registerToLeAudio() { synchronized (mLock) { String logString = "Register to leAudio"; if (mLeAudioCallbackRegistered) { logString += ", but already registered"; Log.i(BluetoothDeviceManager.this, logString); mLocalLog.log(logString); return; } try { mLeAudioCallbackRegistered = true; mBluetoothLeAudioService.registerCallback( mExecutor, mLeAudioCallbacks); } catch (IllegalStateException e) { mLeAudioCallbackRegistered = false; logString += ", but failed: " + e; } Log.i(BluetoothDeviceManager.this, logString); mLocalLog.log(logString); } } @Override public void onServiceDisconnected(int profile) { Log.startSession("BPSL.oSD"); try { synchronized (mLock) { LinkedHashMap lostServiceDevices; String logString; if (profile == BluetoothProfile.HEADSET) { if (mFeatureFlags.useRefactoredAudioRouteSwitching()) { mBluetoothHeadsetFuture.complete(null); } mBluetoothHeadset = null; lostServiceDevices = mHfpDevicesByAddress; mBluetoothRouteManager.onActiveDeviceChanged(null, DEVICE_TYPE_HEADSET); logString = "Lost BluetoothHeadset service. " + "Removing all tracked devices"; } else if (profile == BluetoothProfile.HEARING_AID) { mBluetoothHearingAid = null; logString = "Lost BluetoothHearingAid service. " + "Removing all tracked devices."; lostServiceDevices = mHearingAidDevicesByAddress; mBluetoothRouteManager.onActiveDeviceChanged(null, DEVICE_TYPE_HEARING_AID); } else if (profile == BluetoothProfile.LE_AUDIO) { mBluetoothLeAudioService = null; logString = "Lost BluetoothLeAudio service. " + "Removing all tracked devices."; lostServiceDevices = mLeAudioDevicesByAddress; mBluetoothRouteManager.onActiveDeviceChanged(null, DEVICE_TYPE_LE_AUDIO); } else { return; } Log.i(BluetoothDeviceManager.this, logString); mLocalLog.log(logString); if (mFeatureFlags.useRefactoredAudioRouteSwitching()) { handleAudioRefactoringServiceDisconnected(profile); } else { List devicesToRemove = new LinkedList<>( lostServiceDevices.values()); lostServiceDevices.clear(); for (BluetoothDevice device : devicesToRemove) { mBluetoothRouteManager.onDeviceLost(device.getAddress()); } } } } finally { Log.endSession(); } } }; private void handleAudioRefactoringServiceDisconnected(int profile) { CallAudioRouteController controller = (CallAudioRouteController) mCallAudioRouteAdapter; Map btRoutes = controller .getBluetoothRoutes(); List> btRoutesToRemove = new ArrayList<>(); for (AudioRoute route: btRoutes.keySet()) { if (route.getType() != PROFILE_TO_AUDIO_ROUTE_MAP.get(profile)) { continue; } BluetoothDevice device = btRoutes.get(route); // Prevent concurrent modification exception by just iterating through keys instead of // simultaneously removing them. btRoutesToRemove.add(new Pair<>(route, device)); } for (Pair routeToRemove: btRoutesToRemove) { AudioRoute route = routeToRemove.first; BluetoothDevice device = routeToRemove.second; mCallAudioRouteAdapter.sendMessageWithSessionInfo( BT_DEVICE_REMOVED, route.getType(), device); } mCallAudioRouteAdapter.sendMessageWithSessionInfo( SWITCH_BASELINE_ROUTE, INCLUDE_BLUETOOTH_IN_BASELINE, (String) null); } private final LinkedHashMap mHfpDevicesByAddress = new LinkedHashMap<>(); private final LinkedHashMap mHearingAidDevicesByAddress = new LinkedHashMap<>(); private final LinkedHashMap mHearingAidDeviceSyncIds = new LinkedHashMap<>(); private final LinkedHashMap mLeAudioDevicesByAddress = new LinkedHashMap<>(); private final LinkedHashMap mGroupsByDevice = new LinkedHashMap<>(); private final ArrayList> mDevicesByAddressMaps = new ArrayList>(); { mDevicesByAddressMaps.add(mHfpDevicesByAddress); mDevicesByAddressMaps.add(mHearingAidDevicesByAddress); mDevicesByAddressMaps.add(mLeAudioDevicesByAddress); } private int mGroupIdActive = BluetoothLeAudio.GROUP_ID_INVALID; private int mGroupIdPending = BluetoothLeAudio.GROUP_ID_INVALID; private final LocalLog mLocalLog = new LocalLog(20); // This lock only protects internal state -- it doesn't lock on anything going into Telecom. private final Object mLock = new Object(); private BluetoothRouteManager mBluetoothRouteManager; private BluetoothHeadset mBluetoothHeadset; private CompletableFuture mBluetoothHeadsetFuture; private BluetoothHearingAid mBluetoothHearingAid; private boolean mLeAudioCallbackRegistered = false; private BluetoothLeAudio mBluetoothLeAudioService; private boolean mLeAudioSetAsCommunicationDevice = false; private String mLeAudioDevice; private String mHearingAidDevice; private boolean mHearingAidSetAsCommunicationDevice = false; private BluetoothDevice mBluetoothHearingAidActiveDeviceCache; private BluetoothAdapter mBluetoothAdapter; private AudioManager mAudioManager; private Executor mExecutor; private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker; private CallAudioRouteAdapter mCallAudioRouteAdapter; private FeatureFlags mFeatureFlags; public BluetoothDeviceManager(Context context, BluetoothAdapter bluetoothAdapter, CallAudioCommunicationDeviceTracker communicationDeviceTracker, FeatureFlags featureFlags) { mFeatureFlags = featureFlags; if (bluetoothAdapter != null) { mBluetoothAdapter = bluetoothAdapter; if (mFeatureFlags.useRefactoredAudioRouteSwitching()) { mBluetoothHeadsetFuture = new CompletableFuture<>(); } bluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener, BluetoothProfile.HEADSET); bluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener, BluetoothProfile.HEARING_AID); bluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener, BluetoothProfile.LE_AUDIO); mAudioManager = context.getSystemService(AudioManager.class); mExecutor = context.getMainExecutor(); mCommunicationDeviceTracker = communicationDeviceTracker; } } public void setBluetoothRouteManager(BluetoothRouteManager brm) { mBluetoothRouteManager = brm; } private List getLeAudioConnectedDevices() { synchronized (mLock) { // Let's get devices which are a group leaders ArrayList devices = new ArrayList<>(); if (mGroupsByDevice.isEmpty() || mBluetoothLeAudioService == null) { return devices; } for (LinkedHashMap.Entry entry : mGroupsByDevice.entrySet()) { if (Objects.equals(entry.getKey(), mBluetoothLeAudioService.getConnectedGroupLeadDevice(entry.getValue()))) { devices.add(entry.getKey()); } } devices.removeIf(device -> !mLeAudioDevicesByAddress.containsValue(device)); return devices; } } public int getNumConnectedDevices() { return getConnectedDevices().size(); } public Collection getConnectedDevices() { synchronized (mLock) { ArraySet result = new ArraySet<>(); // Set storing the group ids of all dual mode audio devices to de-dupe them Set dualModeGroupIds = new ArraySet<>(); for (BluetoothDevice hfpDevice: mHfpDevicesByAddress.values()) { result.add(hfpDevice); if (mBluetoothLeAudioService == null) { continue; } int groupId = mBluetoothLeAudioService.getGroupId(hfpDevice); if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) { dualModeGroupIds.add(groupId); } } result.addAll(mHearingAidDevicesByAddress.values()); if (mBluetoothLeAudioService == null) { return Collections.unmodifiableCollection(result); } for (BluetoothDevice leAudioDevice: getLeAudioConnectedDevices()) { // Exclude dual mode audio devices included from the HFP devices list int groupId = mBluetoothLeAudioService.getGroupId(leAudioDevice); if (groupId != BluetoothLeAudio.GROUP_ID_INVALID && !dualModeGroupIds.contains(groupId)) { result.add(leAudioDevice); } } return Collections.unmodifiableCollection(result); } } // Same as getConnectedDevices except it filters out the hearing aid devices that are linked // together by their hiSyncId. public Collection getUniqueConnectedDevices() { ArraySet result; synchronized (mLock) { result = new ArraySet<>(mHfpDevicesByAddress.values()); } Set 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 (mBluetoothAdapter != null) { for (BluetoothDevice device : mBluetoothAdapter.getActiveDevices( BluetoothProfile.HEARING_AID)) { if (device != null) { result.add(device); seenHiSyncIds.add(mHearingAidDeviceSyncIds.getOrDefault(device, -1L)); break; } } } synchronized (mLock) { for (BluetoothDevice d : mHearingAidDevicesByAddress.values()) { long hiSyncId = mHearingAidDeviceSyncIds.getOrDefault(d, -1L); if (seenHiSyncIds.contains(hiSyncId)) { continue; } result.add(d); seenHiSyncIds.add(hiSyncId); } } if (mBluetoothLeAudioService != null) { result.addAll(getLeAudioConnectedDevices()); } return Collections.unmodifiableCollection(result); } public BluetoothHeadset getBluetoothHeadset() { if (mFeatureFlags.useRefactoredAudioRouteSwitching()) { try { mBluetoothHeadset = mBluetoothHeadsetFuture.get(500L, TimeUnit.MILLISECONDS); return mBluetoothHeadset; } catch (TimeoutException | InterruptedException | ExecutionException e) { // ignore Log.w(this, "Acquire BluetoothHeadset service failed due to: " + e); return null; } } else { return mBluetoothHeadset; } } public BluetoothAdapter getBluetoothAdapter() { return mBluetoothAdapter; } public BluetoothHearingAid getBluetoothHearingAid() { return mBluetoothHearingAid; } public BluetoothLeAudio getLeAudioService() { return mBluetoothLeAudioService; } public void setHeadsetServiceForTesting(BluetoothHeadset bluetoothHeadset) { mBluetoothHeadset = bluetoothHeadset; } public void setHearingAidServiceForTesting(BluetoothHearingAid bluetoothHearingAid) { mBluetoothHearingAid = bluetoothHearingAid; } public void setLeAudioServiceForTesting(BluetoothLeAudio bluetoothLeAudio) { mBluetoothLeAudioService = bluetoothLeAudio; mBluetoothLeAudioService.registerCallback(mExecutor, mLeAudioCallbacks); } public static String getDeviceTypeString(int deviceType) { switch (deviceType) { case DEVICE_TYPE_LE_AUDIO: return "LeAudio"; case DEVICE_TYPE_HEARING_AID: return "HearingAid"; case DEVICE_TYPE_HEADSET: return "HFP"; default: return "unknown type"; } } @VisibleForTesting public void onDeviceConnected(BluetoothDevice device, int deviceType) { synchronized (mLock) { clearDeviceFromDeviceMaps(device.getAddress()); LinkedHashMap targetDeviceMap; if (deviceType == DEVICE_TYPE_LE_AUDIO) { if (mBluetoothLeAudioService == null) { Log.w(this, "LE audio service null when receiving device added broadcast"); return; } /* Check if group is known. */ if (!mGroupsByDevice.containsKey(device)) { int groupId = mBluetoothLeAudioService.getGroupId(device); /* If it is not yet assigned, then it will be provided in the callback */ if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) { mGroupsByDevice.put(device, groupId); } } targetDeviceMap = mLeAudioDevicesByAddress; } else if (deviceType == DEVICE_TYPE_HEARING_AID) { if (mBluetoothHearingAid == null) { Log.w(this, "Hearing aid service null when receiving device added broadcast"); return; } long hiSyncId = mBluetoothHearingAid.getHiSyncId(device); mHearingAidDeviceSyncIds.put(device, hiSyncId); targetDeviceMap = mHearingAidDevicesByAddress; } else if (deviceType == DEVICE_TYPE_HEADSET) { if (getBluetoothHeadset() == null) { Log.w(this, "Headset service null when receiving device added broadcast"); return; } targetDeviceMap = mHfpDevicesByAddress; } else { Log.w(this, "Device: " + device.getAddress() + " with invalid type: " + getDeviceTypeString(deviceType)); return; } if (!targetDeviceMap.containsKey(device.getAddress())) { Log.i(this, "Adding device with address: " + device + " and devicetype=" + getDeviceTypeString(deviceType)); targetDeviceMap.put(device.getAddress(), device); mBluetoothRouteManager.onDeviceAdded(device.getAddress()); } } } void clearDeviceFromDeviceMaps(String deviceAddress) { for (LinkedHashMap deviceMap : mDevicesByAddressMaps) { deviceMap.remove(deviceAddress); } } void onDeviceDisconnected(BluetoothDevice device, int deviceType) { mLocalLog.log("Device disconnected -- address: " + device.getAddress() + " deviceType: " + deviceType); synchronized (mLock) { LinkedHashMap targetDeviceMap; if (deviceType == DEVICE_TYPE_LE_AUDIO) { targetDeviceMap = mLeAudioDevicesByAddress; } else if (deviceType == DEVICE_TYPE_HEARING_AID) { mHearingAidDeviceSyncIds.remove(device); targetDeviceMap = mHearingAidDevicesByAddress; } else if (deviceType == DEVICE_TYPE_HEADSET) { targetDeviceMap = mHfpDevicesByAddress; } else { Log.w(this, "Device: " + device.getAddress() + " with invalid type: " + getDeviceTypeString(deviceType)); return; } if (targetDeviceMap.containsKey(device.getAddress())) { Log.i(this, "Removing device with address: " + device + " and devicetype=" + getDeviceTypeString(deviceType)); targetDeviceMap.remove(device.getAddress()); mBluetoothRouteManager.onDeviceLost(device.getAddress()); } } } public void disconnectAudio() { if (mFeatureFlags.callAudioCommunicationDeviceRefactor()) { mCommunicationDeviceTracker.clearBtCommunicationDevice(); disconnectSco(); } else { disconnectSco(); clearLeAudioCommunicationDevice(); clearHearingAidCommunicationDevice(); } } public int disconnectSco() { int result = BluetoothStatusCodes.ERROR_UNKNOWN; if (getBluetoothHeadset() == null) { Log.w(this, "Trying to disconnect audio but no headset service exists."); } else { result = mBluetoothHeadset.disconnectAudio(); } return result; } public boolean isLeAudioCommunicationDevice() { return mLeAudioSetAsCommunicationDevice; } public boolean isHearingAidSetAsCommunicationDevice() { return mHearingAidSetAsCommunicationDevice; } public void clearLeAudioCommunicationDevice() { Log.i(this, "clearLeAudioCommunicationDevice: mLeAudioSetAsCommunicationDevice = " + mLeAudioSetAsCommunicationDevice + " device = " + mLeAudioDevice); if (!mLeAudioSetAsCommunicationDevice) { return; } mLeAudioSetAsCommunicationDevice = false; if (mLeAudioDevice != null) { mBluetoothRouteManager.onAudioLost(mLeAudioDevice); mLeAudioDevice = null; } if (mAudioManager == null) { Log.i(this, "clearLeAudioCommunicationDevice: mAudioManager is null"); return; } AudioDeviceInfo audioDeviceInfo = mAudioManager.getCommunicationDevice(); if (audioDeviceInfo != null && audioDeviceInfo.getType() == AudioDeviceInfo.TYPE_BLE_HEADSET) { mBluetoothRouteManager.onAudioLost(audioDeviceInfo.getAddress()); mAudioManager.clearCommunicationDevice(); } } public void clearHearingAidCommunicationDevice() { Log.i(this, "clearHearingAidCommunicationDevice: mHearingAidSetAsCommunicationDevice = " + mHearingAidSetAsCommunicationDevice); if (!mHearingAidSetAsCommunicationDevice) { return; } mHearingAidSetAsCommunicationDevice = false; if (mHearingAidDevice != null) { mBluetoothRouteManager.onAudioLost(mHearingAidDevice); mHearingAidDevice = null; } if (mAudioManager == null) { Log.i(this, "clearHearingAidCommunicationDevice: mAudioManager is null"); return; } AudioDeviceInfo audioDeviceInfo = mAudioManager.getCommunicationDevice(); if (audioDeviceInfo != null && audioDeviceInfo.getType() == AudioDeviceInfo.TYPE_HEARING_AID) { mAudioManager.clearCommunicationDevice(); } } public boolean setLeAudioCommunicationDevice() { Log.i(this, "setLeAudioCommunicationDevice"); if (mLeAudioSetAsCommunicationDevice) { Log.i(this, "setLeAudioCommunicationDevice already set"); return true; } if (mAudioManager == null) { Log.w(this, " mAudioManager is null"); return false; } AudioDeviceInfo bleHeadset = null; List devices = mAudioManager.getAvailableCommunicationDevices(); if (devices.size() == 0) { Log.w(this, " No communication devices available."); return false; } for (AudioDeviceInfo device : devices) { Log.i(this, " Available device type: " + device.getType()); if (device.getType() == AudioDeviceInfo.TYPE_BLE_HEADSET) { bleHeadset = device; break; } } if (bleHeadset == null) { Log.w(this, " No bleHeadset device available"); return false; } // clear hearing aid communication device if set clearHearingAidCommunicationDevice(); // Turn BLE_OUT_HEADSET ON. boolean result = mAudioManager.setCommunicationDevice(bleHeadset); if (!result) { Log.w(this, " Could not set bleHeadset device"); } else { Log.i(this, " bleHeadset device set"); mBluetoothRouteManager.onAudioOn(bleHeadset.getAddress()); mLeAudioSetAsCommunicationDevice = true; mLeAudioDevice = bleHeadset.getAddress(); } return result; } public boolean setHearingAidCommunicationDevice() { Log.i(this, "setHearingAidCommunicationDevice"); if (mHearingAidSetAsCommunicationDevice) { Log.i(this, "mHearingAidSetAsCommunicationDevice already set"); return true; } if (mAudioManager == null) { Log.w(this, " mAudioManager is null"); return false; } AudioDeviceInfo hearingAid = null; List devices = mAudioManager.getAvailableCommunicationDevices(); if (devices.size() == 0) { Log.w(this, " No communication devices available."); return false; } for (AudioDeviceInfo device : devices) { Log.i(this, " Available device type: " + device.getType()); if (device.getType() == AudioDeviceInfo.TYPE_HEARING_AID) { hearingAid = device; break; } } if (hearingAid == null) { Log.w(this, " No hearingAid device available"); return false; } // clear LE audio communication device if set clearLeAudioCommunicationDevice(); // Turn hearing aid ON. boolean result = mAudioManager.setCommunicationDevice(hearingAid); if (!result) { Log.w(this, " Could not set hearingAid device"); } else { Log.i(this, " hearingAid device set"); mHearingAidDevice = hearingAid.getAddress(); mHearingAidSetAsCommunicationDevice = true; } return result; } public boolean setCommunicationDeviceForAddress(String address) { AudioDeviceInfo deviceInfo = null; List devices = mAudioManager.getAvailableCommunicationDevices(); if (devices.size() == 0) { Log.w(this, " No communication devices available."); return false; } for (AudioDeviceInfo device : devices) { Log.i(this, " Available device type: " + device.getType()); if (device.getAddress().equals(address)) { deviceInfo = device; break; } } if (!mAudioManager.getCommunicationDevice().equals(deviceInfo)) { return mAudioManager.setCommunicationDevice(deviceInfo); } return true; } // Connect audio to the bluetooth device at address, checking to see whether it's // le audio, hearing aid or a HFP device, and using the proper BT API. public boolean connectAudio(String address, boolean switchingBtDevices) { int callProfile = BluetoothProfile.LE_AUDIO; Log.i(this, "Telecomm connecting audio to device: " + address); BluetoothDevice device = null; if (mLeAudioDevicesByAddress.containsKey(address)) { Log.i(this, "Telecomm found LE Audio device for address: " + address); if (mBluetoothLeAudioService == null) { Log.w(this, "Attempting to turn on audio when the le audio service is null"); return false; } device = mLeAudioDevicesByAddress.get(address); callProfile = BluetoothProfile.LE_AUDIO; } else if (mHearingAidDevicesByAddress.containsKey(address)) { Log.i(this, "Telecomm found hearing aid device for address: " + address); if (mBluetoothHearingAid == null) { Log.w(this, "Attempting to turn on audio when the hearing aid service is null"); return false; } device = mHearingAidDevicesByAddress.get(address); callProfile = BluetoothProfile.HEARING_AID; } else if (mHfpDevicesByAddress.containsKey(address)) { Log.i(this, "Telecomm found HFP device for address: " + address); if (getBluetoothHeadset() == null) { Log.w(this, "Attempting to turn on audio when the headset service is null"); return false; } device = mHfpDevicesByAddress.get(address); callProfile = BluetoothProfile.HEADSET; } if (device == null) { Log.w(this, "No active profiles for Bluetooth address=" + address); return false; } Bundle preferredAudioProfiles = mBluetoothAdapter.getPreferredAudioProfiles(device); if (preferredAudioProfiles != null && !preferredAudioProfiles.isEmpty() && preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX) != 0) { Log.i(this, "Preferred duplex profile for device=" + address + " is " + preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX)); callProfile = preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX); } if (callProfile == BluetoothProfile.LE_AUDIO) { if (mBluetoothAdapter.setActiveDevice( device, BluetoothAdapter.ACTIVE_DEVICE_ALL)) { /* ACTION_ACTIVE_DEVICE_CHANGED intent will trigger setting communication device. * Only after receiving ACTION_ACTIVE_DEVICE_CHANGED it is known that device that * will be audio switched to is available to be choose as communication device */ if (!switchingBtDevices) { return mFeatureFlags.callAudioCommunicationDeviceRefactor() ? mCommunicationDeviceTracker.setCommunicationDevice( AudioDeviceInfo.TYPE_BLE_HEADSET, device) : setLeAudioCommunicationDevice(); } return true; } return false; } else if (callProfile == BluetoothProfile.HEARING_AID) { if (mBluetoothAdapter.setActiveDevice(device, BluetoothAdapter.ACTIVE_DEVICE_ALL)) { /* ACTION_ACTIVE_DEVICE_CHANGED intent will trigger setting communication device. * Only after receiving ACTION_ACTIVE_DEVICE_CHANGED it is known that device that * will be audio switched to is available to be choose as communication device */ if (!switchingBtDevices) { return mFeatureFlags.callAudioCommunicationDeviceRefactor() ? mCommunicationDeviceTracker.setCommunicationDevice( AudioDeviceInfo.TYPE_HEARING_AID, null) : setHearingAidCommunicationDevice(); } return true; } return false; } else if (callProfile == BluetoothProfile.HEADSET) { boolean success = mBluetoothAdapter.setActiveDevice(device, BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL); if (!success) { Log.w(this, "Couldn't set active device to %s", address); return false; } if (getBluetoothHeadset() != null) { int scoConnectionRequest = mBluetoothHeadset.connectAudio(); return scoConnectionRequest == BluetoothStatusCodes.SUCCESS || scoConnectionRequest == BluetoothStatusCodes.ERROR_AUDIO_DEVICE_ALREADY_CONNECTED; } else { Log.w(this, "Couldn't find bluetooth headset service"); return false; } } else { Log.w(this, "Attempting to turn on audio for a disconnected device"); return false; } } /** * Used by CallAudioRouteController in order to connect the BT device. * @param device {@link BluetoothDevice} to connect to. * @param type {@link AudioRoute.AudioRouteType} associated with the device. * @return {@code true} if device was successfully connected, {@code false} otherwise. */ public boolean connectAudio(BluetoothDevice device, @AudioRoute.AudioRouteType int type) { String address = device.getAddress(); int callProfile = BluetoothProfile.LE_AUDIO; if (type == TYPE_BLUETOOTH_SCO) { callProfile = BluetoothProfile.HEADSET; } else if (type == TYPE_BLUETOOTH_HA) { callProfile = BluetoothProfile.HEARING_AID; } Bundle preferredAudioProfiles = mBluetoothAdapter.getPreferredAudioProfiles(device); if (preferredAudioProfiles != null && !preferredAudioProfiles.isEmpty() && preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX) != 0) { Log.i(this, "Preferred duplex profile for device=" + address + " is " + preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX)); callProfile = preferredAudioProfiles.getInt(BluetoothAdapter.AUDIO_MODE_DUPLEX); } if (callProfile == BluetoothProfile.LE_AUDIO || callProfile == BluetoothProfile.HEARING_AID) { return mBluetoothAdapter.setActiveDevice(device, BluetoothAdapter.ACTIVE_DEVICE_ALL); } else if (callProfile == BluetoothProfile.HEADSET) { boolean success = mBluetoothAdapter.setActiveDevice(device, BluetoothAdapter.ACTIVE_DEVICE_PHONE_CALL); if (!success) { Log.w(this, "Couldn't set active device to %s", address); return false; } if (getBluetoothHeadset() != null) { int scoConnectionRequest = mBluetoothHeadset.connectAudio(); return scoConnectionRequest == BluetoothStatusCodes.SUCCESS || scoConnectionRequest == BluetoothStatusCodes.ERROR_AUDIO_DEVICE_ALREADY_CONNECTED; } else { Log.w(this, "Couldn't find bluetooth headset service"); return false; } } else { Log.w(this, "Attempting to turn on audio for a disconnected device"); return false; } } public void cacheHearingAidDevice() { if (mBluetoothAdapter != null) { for (BluetoothDevice device : mBluetoothAdapter.getActiveDevices( BluetoothProfile.HEARING_AID)) { if (device != null) { mBluetoothHearingAidActiveDeviceCache = device; } } } } public void restoreHearingAidDevice() { if (mBluetoothHearingAidActiveDeviceCache != null) { mBluetoothAdapter.setActiveDevice(mBluetoothHearingAidActiveDeviceCache, BluetoothAdapter.ACTIVE_DEVICE_ALL); mBluetoothHearingAidActiveDeviceCache = null; } } public boolean isInbandRingingEnabled() { // Get the inband ringing enabled status of expected BT device to route call audio instead // of using the address of currently connected device. BluetoothDevice activeDevice = mBluetoothRouteManager.getMostRecentlyReportedActiveDevice(); return isInbandRingEnabled(activeDevice); } public boolean isInbandRingEnabled(BluetoothDevice bluetoothDevice) { Log.i(this, "isInbandRingEnabled: device: " + bluetoothDevice); if (mBluetoothRouteManager.isCachedLeAudioDevice(bluetoothDevice)) { if (mBluetoothLeAudioService == null) { Log.i(this, "isInbandRingingEnabled: no leaudio service available."); return false; } int groupId = mBluetoothLeAudioService.getGroupId(bluetoothDevice); return mBluetoothLeAudioService.isInbandRingtoneEnabled(groupId); } else { if (getBluetoothHeadset() == null) { Log.i(this, "isInbandRingingEnabled: no headset service available."); return false; } return mBluetoothHeadset.isInbandRingingEnabled(); } } public void setCallAudioRouteAdapter(CallAudioRouteAdapter adapter) { mCallAudioRouteAdapter = adapter; } public void dump(IndentingPrintWriter pw) { mLocalLog.dump(pw); } }