| /* |
| * Copyright 2023 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.bluetooth.btservice; |
| |
| import static android.bluetooth.IBluetoothLeAudio.LE_AUDIO_GROUP_ID_INVALID; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.RequiresPermission; |
| import android.annotation.SuppressLint; |
| import android.bluetooth.BluetoothAdapter; |
| import android.bluetooth.BluetoothClass; |
| import android.bluetooth.BluetoothDevice; |
| import android.bluetooth.BluetoothHearingAid; |
| import android.bluetooth.BluetoothProfile; |
| import android.bluetooth.BluetoothSinkAudioPolicy; |
| import android.media.AudioDeviceCallback; |
| import android.media.AudioDeviceInfo; |
| import android.media.AudioManager; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.Looper; |
| import android.util.ArrayMap; |
| import android.util.ArraySet; |
| import android.util.Log; |
| import android.util.SparseArray; |
| |
| import com.android.bluetooth.BluetoothMethodProxy; |
| import com.android.bluetooth.Utils; |
| import com.android.bluetooth.a2dp.A2dpService; |
| import com.android.bluetooth.btservice.storage.DatabaseManager; |
| import com.android.bluetooth.hearingaid.HearingAidService; |
| import com.android.bluetooth.hfp.HeadsetService; |
| import com.android.bluetooth.le_audio.LeAudioService; |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.annotations.VisibleForTesting; |
| |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.Set; |
| |
| public class AudioRoutingManager extends ActiveDeviceManager { |
| private static final String TAG = AudioRoutingManager.class.getSimpleName(); |
| private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); |
| @VisibleForTesting static final int A2DP_HFP_SYNC_CONNECTION_TIMEOUT_MS = 5_000; |
| |
| private final AdapterService mAdapterService; |
| private DatabaseManager mDbManager; |
| private final ServiceFactory mFactory; |
| private HandlerThread mHandlerThread = null; |
| private AudioRoutingHandler mHandler = null; |
| private final AudioManager mAudioManager; |
| private final AudioManagerAudioDeviceCallback mAudioManagerAudioDeviceCallback; |
| |
| private final Object mLock = new Object(); |
| // TODO: remove mA2dpConnectedDevices |
| @GuardedBy("mLock") |
| private final List<BluetoothDevice> mA2dpConnectedDevices = new ArrayList<>(); |
| // TODO: remove mHfpConnectedDevices |
| @GuardedBy("mLock") |
| private final List<BluetoothDevice> mHfpConnectedDevices = new ArrayList<>(); |
| |
| @GuardedBy("mLock") |
| private final List<BluetoothDevice> mHearingAidConnectedDevices = new ArrayList<>(); |
| |
| @GuardedBy("mLock") |
| private final List<BluetoothDevice> mLeAudioConnectedDevices = new ArrayList<>(); |
| |
| @GuardedBy("mLock") |
| private final List<BluetoothDevice> mLeHearingAidConnectedDevices = new ArrayList<>(); |
| |
| @GuardedBy("mLock") |
| private List<BluetoothDevice> mPendingLeHearingAidActiveDevice = new ArrayList<>(); |
| |
| @GuardedBy("mLock") |
| private BluetoothDevice mA2dpActiveDevice = null; |
| |
| @GuardedBy("mLock") |
| private BluetoothDevice mHfpActiveDevice = null; |
| |
| @GuardedBy("mLock") |
| private final Set<BluetoothDevice> mHearingAidActiveDevices = new ArraySet<>(); |
| |
| @GuardedBy("mLock") |
| private BluetoothDevice mLeAudioActiveDevice = null; |
| |
| @GuardedBy("mLock") |
| private BluetoothDevice mLeHearingAidActiveDevice = null; |
| |
| @GuardedBy("mLock") |
| private BluetoothDevice mPendingActiveDevice = null; |
| |
| private BluetoothDevice mClassicDeviceToBeActivated = null; |
| private BluetoothDevice mClassicDeviceNotToBeActivated = null; |
| |
| @Override |
| public void onBluetoothStateChange(int prevState, int newState) { |
| mHandler.post(() -> handleAdapterStateChanged(newState)); |
| } |
| |
| /** |
| * Called when audio profile connection state changed |
| * |
| * @param profile The Bluetooth profile of which connection state changed |
| * @param device The device of which connection state was changed |
| * @param fromState The previous connection state of the device |
| * @param toState The new connection state of the device |
| */ |
| @Override |
| public void profileConnectionStateChanged( |
| int profile, BluetoothDevice device, int fromState, int toState) { |
| if (toState == BluetoothProfile.STATE_CONNECTED) { |
| switch (profile) { |
| case BluetoothProfile.A2DP: |
| case BluetoothProfile.HEADSET: |
| case BluetoothProfile.LE_AUDIO: |
| case BluetoothProfile.HEARING_AID: |
| mHandler.post(() -> mHandler.handleDeviceConnected(device, profile)); |
| break; |
| |
| case BluetoothProfile.HAP_CLIENT: |
| mHandler.post( |
| () -> { |
| AudioRoutingHandler.AudioRoutingDevice arDevice = |
| mHandler.getAudioRoutingDevice(device); |
| arDevice.connectedProfiles.add(profile); |
| handleHapConnected(device); |
| }); |
| break; |
| } |
| } else if (fromState == BluetoothProfile.STATE_CONNECTED) { |
| switch (profile) { |
| case BluetoothProfile.A2DP: |
| case BluetoothProfile.HEADSET: |
| case BluetoothProfile.LE_AUDIO: |
| case BluetoothProfile.HEARING_AID: |
| mHandler.post(() -> mHandler.handleDeviceDisconnected(device, profile)); |
| break; |
| case BluetoothProfile.HAP_CLIENT: |
| mHandler.post( |
| () -> { |
| AudioRoutingHandler.AudioRoutingDevice arDevice = |
| mHandler.getAudioRoutingDevice(device); |
| arDevice.connectedProfiles.remove(profile); |
| handleHapDisconnected(device); |
| }); |
| break; |
| } |
| } |
| } |
| |
| /** |
| * Called when active state of audio profiles changed |
| * |
| * @param profile The Bluetooth profile of which active state changed |
| * @param device The device currently activated. {@code null} if no device is active |
| */ |
| @Override |
| public void profileActiveDeviceChanged(int profile, BluetoothDevice device) { |
| switch (profile) { |
| case BluetoothProfile.A2DP: |
| mHandler.post( |
| () -> { |
| if (device != null) { |
| ArrayList<BluetoothDevice> devices = new ArrayList<>(); |
| devices.add(device); |
| mHandler.mActiveDevices.put(profile, devices); |
| } else { |
| mHandler.mActiveDevices.remove(profile); |
| } |
| handleA2dpActiveDeviceChanged(device); |
| }); |
| break; |
| case BluetoothProfile.HEADSET: |
| mHandler.post( |
| () -> { |
| if (device != null) { |
| ArrayList<BluetoothDevice> devices = new ArrayList<>(); |
| devices.add(device); |
| mHandler.mActiveDevices.put(profile, devices); |
| } else { |
| mHandler.mActiveDevices.remove(profile); |
| } |
| handleHfpActiveDeviceChanged(device); |
| }); |
| break; |
| case BluetoothProfile.LE_AUDIO: |
| mHandler.post( |
| () -> { |
| if (device != null) { |
| ArrayList<BluetoothDevice> devices = new ArrayList<>(); |
| devices.add(device); |
| mHandler.mActiveDevices.put(profile, devices); |
| } else { |
| mHandler.mActiveDevices.remove(profile); |
| } |
| handleLeAudioActiveDeviceChanged(device); |
| }); |
| break; |
| case BluetoothProfile.HEARING_AID: |
| mHandler.post( |
| () -> { |
| if (device != null) { |
| ArrayList<BluetoothDevice> devices = new ArrayList<>(); |
| devices.add(device); |
| mHandler.mActiveDevices.put(profile, devices); |
| } else { |
| mHandler.mActiveDevices.remove(profile); |
| } |
| handleHearingAidActiveDeviceChanged(device); |
| }); |
| break; |
| } |
| } |
| |
| private void handleAdapterStateChanged(int currentState) { |
| if (DBG) { |
| Log.d(TAG, "handleAdapterStateChanged: currentState=" + currentState); |
| } |
| if (currentState == BluetoothAdapter.STATE_ON) { |
| resetState(); |
| } |
| } |
| |
| private void handleHapConnected(BluetoothDevice device) { |
| synchronized (mLock) { |
| if (DBG) { |
| Log.d(TAG, "handleHapConnected: " + device); |
| } |
| if (mLeHearingAidConnectedDevices.contains(device)) { |
| if (DBG) { |
| Log.d(TAG, "This device is already connected: " + device); |
| } |
| return; |
| } |
| mLeHearingAidConnectedDevices.add(device); |
| if (!mLeAudioConnectedDevices.contains(device)) { |
| mPendingLeHearingAidActiveDevice.add(device); |
| } else if (Objects.equals(mLeAudioActiveDevice, device)) { |
| mLeHearingAidActiveDevice = device; |
| } else { |
| // New connected device: select it as active |
| if (setLeHearingAidActiveDevice(device)) { |
| setHearingAidActiveDevice(null, true); |
| setA2dpActiveDevice(null, true); |
| setHfpActiveDevice(null); |
| } |
| } |
| } |
| } |
| |
| private void handleHapDisconnected(BluetoothDevice device) { |
| synchronized (mLock) { |
| if (DBG) { |
| Log.d( |
| TAG, |
| "handleHapDisconnected: " |
| + device |
| + ", mLeHearingAidActiveDevice=" |
| + mLeHearingAidActiveDevice); |
| } |
| mLeHearingAidConnectedDevices.remove(device); |
| mPendingLeHearingAidActiveDevice.remove(device); |
| if (Objects.equals(mLeHearingAidActiveDevice, device)) { |
| mLeHearingAidActiveDevice = null; |
| } |
| } |
| } |
| |
| /** |
| * Handles the active device logic for when the A2DP active device changes. Does the following: |
| * 1. Clear the active hearing aid. 2. If dual mode is enabled and all supported classic audio |
| * profiles are enabled, makes this device active for LE Audio. If not, clear the LE Audio |
| * active device. 3. Make HFP active for this device if it is already connected to HFP. 4. |
| * Stores the new A2DP active device. |
| * |
| * @param device is the device that was connected to A2DP |
| */ |
| private void handleA2dpActiveDeviceChanged(BluetoothDevice device) { |
| synchronized (mLock) { |
| if (DBG) { |
| Log.d( |
| TAG, |
| "handleA2dpActiveDeviceChanged: " |
| + device |
| + ", mA2dpActiveDevice=" |
| + mA2dpActiveDevice); |
| } |
| if (!Objects.equals(mA2dpActiveDevice, device)) { |
| if (device != null) { |
| setHearingAidActiveDevice(null, true); |
| } |
| if (Utils.isDualModeAudioEnabled() |
| && mAdapterService.isAllSupportedClassicAudioProfilesActive(device)) { |
| setLeAudioActiveDevice(device); |
| } else { |
| setLeAudioActiveDevice(null, true); |
| } |
| } |
| // Just assign locally the new value |
| mA2dpActiveDevice = device; |
| |
| // Activate HFP if needed. |
| if (device != null) { |
| if (Objects.equals(mClassicDeviceNotToBeActivated, device)) { |
| mHandler.removeCallbacksAndMessages(mClassicDeviceNotToBeActivated); |
| mClassicDeviceNotToBeActivated = null; |
| return; |
| } |
| if (Objects.equals(mClassicDeviceToBeActivated, device)) { |
| mHandler.removeCallbacksAndMessages(mClassicDeviceToBeActivated); |
| mClassicDeviceToBeActivated = null; |
| } |
| |
| if (mClassicDeviceToBeActivated != null) { |
| mClassicDeviceNotToBeActivated = mClassicDeviceToBeActivated; |
| mHandler.removeCallbacksAndMessages(mClassicDeviceToBeActivated); |
| mHandler.postDelayed( |
| () -> mClassicDeviceNotToBeActivated = null, |
| mClassicDeviceNotToBeActivated, |
| A2DP_HFP_SYNC_CONNECTION_TIMEOUT_MS); |
| mClassicDeviceToBeActivated = null; |
| } |
| if (!Objects.equals(mHfpActiveDevice, device) |
| && mHfpConnectedDevices.contains(device) |
| && mDbManager.getProfileConnectionPolicy(device, BluetoothProfile.HEADSET) |
| == BluetoothProfile.CONNECTION_POLICY_ALLOWED) { |
| mClassicDeviceToBeActivated = device; |
| setHfpActiveDevice(device); |
| mHandler.postDelayed( |
| () -> mClassicDeviceToBeActivated = null, |
| mClassicDeviceToBeActivated, |
| A2DP_HFP_SYNC_CONNECTION_TIMEOUT_MS); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Handles the active device logic for when the HFP active device changes. Does the following: |
| * 1. Clear the active hearing aid. 2. If dual mode is enabled and all supported classic audio |
| * profiles are enabled, makes this device active for LE Audio. If not, clear the LE Audio |
| * active device. 3. Make A2DP active for this device if it is already connected to A2DP. 4. |
| * Stores the new HFP active device. |
| * |
| * @param device is the device that was connected to A2DP |
| */ |
| private void handleHfpActiveDeviceChanged(BluetoothDevice device) { |
| synchronized (mLock) { |
| if (DBG) { |
| Log.d( |
| TAG, |
| "handleHfpActiveDeviceChanged: " |
| + device |
| + ", mHfpActiveDevice=" |
| + mHfpActiveDevice); |
| } |
| if (!Objects.equals(mHfpActiveDevice, device)) { |
| if (device != null) { |
| setHearingAidActiveDevice(null, true); |
| } |
| if (Utils.isDualModeAudioEnabled() |
| && mAdapterService.isAllSupportedClassicAudioProfilesActive(device)) { |
| setLeAudioActiveDevice(device); |
| } else { |
| setLeAudioActiveDevice(null, true); |
| } |
| } |
| // Just assign locally the new value |
| mHfpActiveDevice = device; |
| |
| // Activate A2DP if needed. |
| if (device != null) { |
| if (Objects.equals(mClassicDeviceNotToBeActivated, device)) { |
| mHandler.removeCallbacksAndMessages(mClassicDeviceNotToBeActivated); |
| mClassicDeviceNotToBeActivated = null; |
| return; |
| } |
| if (Objects.equals(mClassicDeviceToBeActivated, device)) { |
| mHandler.removeCallbacksAndMessages(mClassicDeviceToBeActivated); |
| mClassicDeviceToBeActivated = null; |
| } |
| |
| if (mClassicDeviceToBeActivated != null) { |
| mClassicDeviceNotToBeActivated = mClassicDeviceToBeActivated; |
| mHandler.removeCallbacksAndMessages(mClassicDeviceToBeActivated); |
| mHandler.postDelayed( |
| () -> mClassicDeviceNotToBeActivated = null, |
| mClassicDeviceNotToBeActivated, |
| A2DP_HFP_SYNC_CONNECTION_TIMEOUT_MS); |
| mClassicDeviceToBeActivated = null; |
| } |
| if (!Objects.equals(mA2dpActiveDevice, device) |
| && mA2dpConnectedDevices.contains(device) |
| && mDbManager.getProfileConnectionPolicy(device, BluetoothProfile.A2DP) |
| == BluetoothProfile.CONNECTION_POLICY_ALLOWED) { |
| mClassicDeviceToBeActivated = device; |
| setA2dpActiveDevice(device); |
| mHandler.postDelayed( |
| () -> mClassicDeviceToBeActivated = null, |
| mClassicDeviceToBeActivated, |
| A2DP_HFP_SYNC_CONNECTION_TIMEOUT_MS); |
| } |
| } |
| } |
| } |
| |
| private void handleHearingAidActiveDeviceChanged(BluetoothDevice device) { |
| synchronized (mLock) { |
| if (DBG) { |
| Log.d( |
| TAG, |
| "handleHearingAidActiveDeviceChanged: " |
| + device |
| + ", mHearingAidActiveDevices=" |
| + mHearingAidActiveDevices); |
| } |
| // Just assign locally the new value |
| final HearingAidService hearingAidService = mFactory.getHearingAidService(); |
| if (hearingAidService != null) { |
| long hiSyncId = hearingAidService.getHiSyncId(device); |
| if (device != null && getHearingAidActiveHiSyncIdLocked() == hiSyncId) { |
| mHearingAidActiveDevices.add(device); |
| } else { |
| mHearingAidActiveDevices.clear(); |
| mHearingAidActiveDevices.addAll( |
| hearingAidService.getConnectedPeerDevices(hiSyncId)); |
| } |
| } |
| } |
| if (device != null) { |
| setA2dpActiveDevice(null, true); |
| setHfpActiveDevice(null); |
| setLeAudioActiveDevice(null, true); |
| } |
| } |
| |
| private void handleLeAudioActiveDeviceChanged(BluetoothDevice device) { |
| synchronized (mLock) { |
| if (DBG) { |
| Log.d( |
| TAG, |
| "handleLeAudioActiveDeviceChanged: " |
| + device |
| + ", mLeAudioActiveDevice=" |
| + mLeAudioActiveDevice); |
| } |
| if (device != null && !mLeAudioConnectedDevices.contains(device)) { |
| mLeAudioConnectedDevices.add(device); |
| } |
| // Just assign locally the new value |
| if (device != null && !Objects.equals(mLeAudioActiveDevice, device)) { |
| if (!Utils.isDualModeAudioEnabled()) { |
| setA2dpActiveDevice(null, true); |
| setHfpActiveDevice(null); |
| } |
| setHearingAidActiveDevice(null, true); |
| } |
| |
| if (mLeHearingAidConnectedDevices.contains(device)) { |
| mLeHearingAidActiveDevice = device; |
| } |
| |
| mLeAudioActiveDevice = device; |
| } |
| } |
| |
| /** Notifications of audio device connection and disconnection events. */ |
| @SuppressLint("AndroidFrameworkRequiresPermission") |
| private class AudioManagerAudioDeviceCallback extends AudioDeviceCallback { |
| private boolean isWiredAudioHeadset(AudioDeviceInfo deviceInfo) { |
| switch (deviceInfo.getType()) { |
| case AudioDeviceInfo.TYPE_WIRED_HEADSET: |
| case AudioDeviceInfo.TYPE_WIRED_HEADPHONES: |
| case AudioDeviceInfo.TYPE_USB_HEADSET: |
| return true; |
| default: |
| break; |
| } |
| return false; |
| } |
| |
| @Override |
| public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { |
| if (DBG) { |
| Log.d(TAG, "onAudioDevicesAdded"); |
| } |
| boolean hasAddedWiredDevice = false; |
| for (AudioDeviceInfo deviceInfo : addedDevices) { |
| if (DBG) { |
| Log.d( |
| TAG, |
| "Audio device added: " |
| + deviceInfo.getProductName() |
| + " type: " |
| + deviceInfo.getType()); |
| } |
| if (isWiredAudioHeadset(deviceInfo)) { |
| hasAddedWiredDevice = true; |
| break; |
| } |
| } |
| if (hasAddedWiredDevice) { |
| wiredAudioDeviceConnected(); |
| } |
| } |
| |
| @Override |
| public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {} |
| } |
| |
| AudioRoutingManager(AdapterService service, ServiceFactory factory) { |
| super(service, factory); |
| mAdapterService = service; |
| mDbManager = mAdapterService.getDatabase(); |
| mFactory = factory; |
| mAudioManager = service.getSystemService(AudioManager.class); |
| mAudioManagerAudioDeviceCallback = new AudioManagerAudioDeviceCallback(); |
| } |
| |
| @Override |
| void start() { |
| if (DBG) { |
| Log.d(TAG, "start()"); |
| } |
| |
| mHandlerThread = new HandlerThread("BluetoothActiveDeviceManager"); |
| BluetoothMethodProxy mp = BluetoothMethodProxy.getInstance(); |
| mp.threadStart(mHandlerThread); |
| mHandler = new AudioRoutingHandler(mp.handlerThreadGetLooper(mHandlerThread)); |
| |
| mAudioManager.registerAudioDeviceCallback(mAudioManagerAudioDeviceCallback, mHandler); |
| mAdapterService.registerBluetoothStateCallback((command) -> mHandler.post(command), this); |
| } |
| |
| @Override |
| void cleanup() { |
| if (DBG) { |
| Log.d(TAG, "cleanup()"); |
| } |
| |
| mAudioManager.unregisterAudioDeviceCallback(mAudioManagerAudioDeviceCallback); |
| mAdapterService.unregisterBluetoothStateCallback(this); |
| if (mHandlerThread != null) { |
| mHandlerThread.quit(); |
| mHandlerThread = null; |
| } |
| resetState(); |
| } |
| |
| /** |
| * Get the {@link Looper} for the handler thread. This is used in testing and helper objects |
| * |
| * @return {@link Looper} for the handler thread |
| */ |
| @VisibleForTesting |
| @Override |
| public Looper getHandlerLooper() { |
| if (mHandler == null) { |
| return null; |
| } |
| return mHandler.getLooper(); |
| } |
| |
| private boolean setA2dpActiveDevice(@NonNull BluetoothDevice device) { |
| return setA2dpActiveDevice(device, false); |
| } |
| |
| private boolean setA2dpActiveDevice( |
| @Nullable BluetoothDevice device, boolean hasFallbackDevice) { |
| if (DBG) { |
| Log.d( |
| TAG, |
| "setA2dpActiveDevice(" |
| + device |
| + ")" |
| + (device == null ? " hasFallbackDevice=" + hasFallbackDevice : "")); |
| } |
| synchronized (mLock) { |
| if (mPendingActiveDevice != null) { |
| mHandler.removeCallbacksAndMessages(mPendingActiveDevice); |
| mPendingActiveDevice = null; |
| } |
| } |
| |
| final A2dpService a2dpService = mFactory.getA2dpService(); |
| if (a2dpService == null) { |
| return false; |
| } |
| |
| boolean success = false; |
| if (device == null) { |
| success = a2dpService.removeActiveDevice(!hasFallbackDevice); |
| } else { |
| success = a2dpService.setActiveDevice(device); |
| } |
| |
| if (!success) { |
| return false; |
| } |
| |
| synchronized (mLock) { |
| mA2dpActiveDevice = device; |
| } |
| return true; |
| } |
| |
| @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) |
| private boolean setHfpActiveDevice(BluetoothDevice device) { |
| if (DBG) { |
| Log.d(TAG, "setHfpActiveDevice(" + device + ")"); |
| } |
| synchronized (mLock) { |
| if (mPendingActiveDevice != null) { |
| mHandler.removeCallbacksAndMessages(mPendingActiveDevice); |
| mPendingActiveDevice = null; |
| } |
| } |
| final HeadsetService headsetService = mFactory.getHeadsetService(); |
| if (headsetService == null) { |
| return false; |
| } |
| BluetoothSinkAudioPolicy audioPolicy = headsetService.getHfpCallAudioPolicy(device); |
| if (audioPolicy != null |
| && audioPolicy.getActiveDevicePolicyAfterConnection() |
| == BluetoothSinkAudioPolicy.POLICY_NOT_ALLOWED) { |
| return false; |
| } |
| if (!headsetService.setActiveDevice(device)) { |
| return false; |
| } |
| synchronized (mLock) { |
| mHfpActiveDevice = device; |
| } |
| return true; |
| } |
| |
| private boolean setHearingAidActiveDevice(@NonNull BluetoothDevice device) { |
| return setHearingAidActiveDevice(device, false); |
| } |
| |
| private boolean setHearingAidActiveDevice( |
| @Nullable BluetoothDevice device, boolean hasFallbackDevice) { |
| if (DBG) { |
| Log.d( |
| TAG, |
| "setHearingAidActiveDevice(" |
| + device |
| + ")" |
| + (device == null ? " hasFallbackDevice=" + hasFallbackDevice : "")); |
| } |
| |
| final HearingAidService hearingAidService = mFactory.getHearingAidService(); |
| if (hearingAidService == null) { |
| return false; |
| } |
| |
| synchronized (mLock) { |
| if (device == null) { |
| if (!hearingAidService.removeActiveDevice(!hasFallbackDevice)) { |
| return false; |
| } |
| mHearingAidActiveDevices.clear(); |
| return true; |
| } |
| |
| long hiSyncId = hearingAidService.getHiSyncId(device); |
| if (getHearingAidActiveHiSyncIdLocked() == hiSyncId) { |
| mHearingAidActiveDevices.add(device); |
| return true; |
| } |
| |
| if (!hearingAidService.setActiveDevice(device)) { |
| return false; |
| } |
| mHearingAidActiveDevices.clear(); |
| mHearingAidActiveDevices.addAll(hearingAidService.getConnectedPeerDevices(hiSyncId)); |
| } |
| return true; |
| } |
| |
| private boolean setLeAudioActiveDevice(@NonNull BluetoothDevice device) { |
| return setLeAudioActiveDevice(device, false); |
| } |
| |
| private boolean setLeAudioActiveDevice( |
| @Nullable BluetoothDevice device, boolean hasFallbackDevice) { |
| if (DBG) { |
| Log.d( |
| TAG, |
| "setLeAudioActiveDevice(" |
| + device |
| + ")" |
| + (device == null ? " hasFallbackDevice=" + hasFallbackDevice : "")); |
| } |
| final LeAudioService leAudioService = mFactory.getLeAudioService(); |
| if (leAudioService == null) { |
| return false; |
| } |
| boolean success; |
| if (device == null) { |
| success = leAudioService.removeActiveDevice(hasFallbackDevice); |
| } else { |
| success = leAudioService.setActiveDevice(device); |
| } |
| |
| if (!success) { |
| return false; |
| } |
| |
| synchronized (mLock) { |
| mLeAudioActiveDevice = device; |
| if (device == null) { |
| mLeHearingAidActiveDevice = null; |
| mPendingLeHearingAidActiveDevice.remove(device); |
| } |
| } |
| return true; |
| } |
| |
| private boolean setLeHearingAidActiveDevice(BluetoothDevice device) { |
| synchronized (mLock) { |
| if (!Objects.equals(mLeAudioActiveDevice, device)) { |
| if (!setLeAudioActiveDevice(device)) { |
| return false; |
| } |
| } |
| if (Objects.equals(mLeAudioActiveDevice, device)) { |
| // setLeAudioActiveDevice succeed |
| mLeHearingAidActiveDevice = device; |
| mPendingLeHearingAidActiveDevice.remove(device); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * TODO: This method can return true when a fallback device for an unrelated profile is found. |
| * Take disconnected profile as an argument, and find the exact fallback device. Also, split |
| * this method to smaller methods for better readability. |
| * |
| * @return true when the fallback device is activated, false otherwise |
| */ |
| @GuardedBy("mLock") |
| private boolean setFallbackDeviceActiveLocked() { |
| if (DBG) { |
| Log.d(TAG, "setFallbackDeviceActive"); |
| } |
| mDbManager = mAdapterService.getDatabase(); |
| List<BluetoothDevice> connectedHearingAidDevices = new ArrayList<>(); |
| if (!mHearingAidConnectedDevices.isEmpty()) { |
| connectedHearingAidDevices.addAll(mHearingAidConnectedDevices); |
| } |
| if (!mLeHearingAidConnectedDevices.isEmpty()) { |
| connectedHearingAidDevices.addAll(mLeHearingAidConnectedDevices); |
| } |
| if (!connectedHearingAidDevices.isEmpty()) { |
| BluetoothDevice device = |
| mDbManager.getMostRecentlyConnectedDevicesInList(connectedHearingAidDevices); |
| if (device != null) { |
| if (mHearingAidConnectedDevices.contains(device)) { |
| if (DBG) { |
| Log.d(TAG, "Found a hearing aid fallback device: " + device); |
| } |
| setHearingAidActiveDevice(device); |
| setA2dpActiveDevice(null, true); |
| setHfpActiveDevice(null); |
| setLeAudioActiveDevice(null, true); |
| } else { |
| if (DBG) { |
| Log.d(TAG, "Found a LE hearing aid fallback device: " + device); |
| } |
| setLeHearingAidActiveDevice(device); |
| setHearingAidActiveDevice(null, true); |
| setA2dpActiveDevice(null, true); |
| setHfpActiveDevice(null); |
| } |
| return true; |
| } |
| } |
| |
| List<BluetoothDevice> hfpFallbackCandidates = removeWatchDevices(mHfpConnectedDevices); |
| List<BluetoothDevice> fallbackCandidates = new ArrayList<>(); |
| fallbackCandidates.addAll(mLeAudioConnectedDevices); |
| |
| HeadsetService headsetService = mFactory.getHeadsetService(); |
| switch (mAudioManager.getMode()) { |
| case AudioManager.MODE_NORMAL: |
| fallbackCandidates.addAll(mA2dpConnectedDevices); |
| break; |
| case AudioManager.MODE_RINGTONE: |
| if (headsetService.isInbandRingingEnabled()) { |
| fallbackCandidates.addAll(hfpFallbackCandidates); |
| } |
| break; |
| default: |
| fallbackCandidates.addAll(hfpFallbackCandidates); |
| } |
| BluetoothDevice device = |
| mDbManager.getMostRecentlyConnectedDevicesInList(fallbackCandidates); |
| if (device != null) { |
| if (mAudioManager.getMode() == AudioManager.MODE_NORMAL) { |
| if (mA2dpConnectedDevices.contains(device)) { |
| if (DBG) { |
| Log.d(TAG, "Found an A2DP fallback device: " + device); |
| } |
| setA2dpActiveDevice(device); |
| if (hfpFallbackCandidates.contains(device)) { |
| setHfpActiveDevice(device); |
| } else { |
| setHfpActiveDevice(null); |
| } |
| /* If dual mode is enabled, LEA will be made active once all supported |
| classic audio profiles are made active for the device. */ |
| if (!Utils.isDualModeAudioEnabled()) { |
| setLeAudioActiveDevice(null, true); |
| } |
| setHearingAidActiveDevice(null, true); |
| } else { |
| if (DBG) { |
| Log.d(TAG, "Found a LE audio fallback device: " + device); |
| } |
| if (!setLeAudioActiveDevice(device)) { |
| return false; |
| } |
| |
| if (!Utils.isDualModeAudioEnabled()) { |
| setA2dpActiveDevice(null, true); |
| setHfpActiveDevice(null); |
| } |
| setHearingAidActiveDevice(null, true); |
| } |
| } else { |
| if (hfpFallbackCandidates.contains(device)) { |
| if (DBG) { |
| Log.d(TAG, "Found a HFP fallback device: " + device); |
| } |
| setHfpActiveDevice(device); |
| if (mA2dpConnectedDevices.contains(device)) { |
| setA2dpActiveDevice(device); |
| } else { |
| setA2dpActiveDevice(null, true); |
| } |
| if (!Utils.isDualModeAudioEnabled()) { |
| setLeAudioActiveDevice(null, true); |
| } |
| setHearingAidActiveDevice(null, true); |
| } else { |
| if (DBG) { |
| Log.d(TAG, "Found a LE audio fallback device: " + device); |
| } |
| setLeAudioActiveDevice(device); |
| if (!Utils.isDualModeAudioEnabled()) { |
| setA2dpActiveDevice(null, true); |
| setHfpActiveDevice(null); |
| } |
| setHearingAidActiveDevice(null, true); |
| } |
| } |
| return true; |
| } |
| |
| if (DBG) { |
| Log.d(TAG, "No fallback devices are found"); |
| } |
| return false; |
| } |
| |
| private void resetState() { |
| synchronized (mLock) { |
| mA2dpConnectedDevices.clear(); |
| mA2dpActiveDevice = null; |
| |
| mHfpConnectedDevices.clear(); |
| mHfpActiveDevice = null; |
| |
| mHearingAidConnectedDevices.clear(); |
| mHearingAidActiveDevices.clear(); |
| |
| mLeAudioConnectedDevices.clear(); |
| mLeAudioActiveDevice = null; |
| |
| mLeHearingAidConnectedDevices.clear(); |
| mLeHearingAidActiveDevice = null; |
| mPendingLeHearingAidActiveDevice.clear(); |
| } |
| } |
| |
| @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) |
| List<BluetoothDevice> removeWatchDevices(List<BluetoothDevice> devices) { |
| List<BluetoothDevice> fallbackCandidates; |
| synchronized (mLock) { |
| fallbackCandidates = new ArrayList<>(devices); |
| } |
| List<BluetoothDevice> uninterestedCandidates = new ArrayList<>(); |
| for (BluetoothDevice device : fallbackCandidates) { |
| byte[] deviceType = |
| mDbManager.getCustomMeta(device, BluetoothDevice.METADATA_DEVICE_TYPE); |
| BluetoothClass deviceClass = device.getBluetoothClass(); |
| if ((deviceClass != null |
| && deviceClass.getMajorDeviceClass() |
| == BluetoothClass.Device.WEARABLE_WRIST_WATCH) |
| || (deviceType != null |
| && BluetoothDevice.DEVICE_TYPE_WATCH.equals(new String(deviceType)))) { |
| uninterestedCandidates.add(device); |
| } |
| } |
| for (BluetoothDevice device : uninterestedCandidates) { |
| fallbackCandidates.remove(device); |
| } |
| return fallbackCandidates; |
| } |
| |
| @VisibleForTesting |
| BluetoothDevice getA2dpActiveDevice() { |
| synchronized (mLock) { |
| return mA2dpActiveDevice; |
| } |
| } |
| |
| @VisibleForTesting |
| BluetoothDevice getHfpActiveDevice() { |
| synchronized (mLock) { |
| return mHfpActiveDevice; |
| } |
| } |
| |
| @VisibleForTesting |
| Set<BluetoothDevice> getHearingAidActiveDevices() { |
| synchronized (mLock) { |
| return mHearingAidActiveDevices; |
| } |
| } |
| |
| @VisibleForTesting |
| BluetoothDevice getLeAudioActiveDevice() { |
| synchronized (mLock) { |
| return mLeAudioActiveDevice; |
| } |
| } |
| |
| @GuardedBy("mLock") |
| private long getHearingAidActiveHiSyncIdLocked() { |
| final HearingAidService hearingAidService = mFactory.getHearingAidService(); |
| if (hearingAidService != null && !mHearingAidActiveDevices.isEmpty()) { |
| return hearingAidService.getHiSyncId(mHearingAidActiveDevices.iterator().next()); |
| } |
| return BluetoothHearingAid.HI_SYNC_ID_INVALID; |
| } |
| |
| /** |
| * Checks CoD and metadata to determine if the device is a watch |
| * |
| * @param device the remote device |
| * @return {@code true} if it's a watch, {@code false} otherwise |
| */ |
| private boolean isWatch(BluetoothDevice device) { |
| // Check CoD |
| BluetoothClass deviceClass = device.getBluetoothClass(); |
| if (deviceClass != null |
| && deviceClass.getDeviceClass() == BluetoothClass.Device.WEARABLE_WRIST_WATCH) { |
| return true; |
| } |
| |
| // Check metadata |
| byte[] deviceType = mDbManager.getCustomMeta(device, BluetoothDevice.METADATA_DEVICE_TYPE); |
| if (deviceType == null) { |
| return false; |
| } |
| String deviceTypeStr = new String(deviceType); |
| if (deviceTypeStr.equals(BluetoothDevice.DEVICE_TYPE_WATCH)) { |
| return true; |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Called when a wired audio device is connected. It might be called multiple times each time a |
| * wired audio device is connected. |
| */ |
| @VisibleForTesting |
| @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) |
| @Override |
| void wiredAudioDeviceConnected() { |
| if (DBG) { |
| Log.d(TAG, "wiredAudioDeviceConnected"); |
| } |
| setA2dpActiveDevice(null, true); |
| setHfpActiveDevice(null); |
| setHearingAidActiveDevice(null, true); |
| setLeAudioActiveDevice(null, true); |
| } |
| |
| // TODO: make AudioRoutingHandler private |
| class AudioRoutingHandler extends Handler { |
| // TODO: make mConnectedDevices private |
| public final ArrayMap<BluetoothDevice, AudioRoutingDevice> mConnectedDevices = |
| new ArrayMap<>(); |
| // TODO: make mActiveDevices private |
| public final SparseArray<List<BluetoothDevice>> mActiveDevices = new SparseArray<>(); |
| |
| AudioRoutingHandler(Looper looper) { |
| super(looper); |
| } |
| |
| public void handleDeviceConnected(BluetoothDevice device, int profile) { |
| if (DBG) { |
| Log.d( |
| TAG, |
| "handleDeviceConnected(device=" |
| + device |
| + ", profile=" |
| + BluetoothProfile.getProfileName(profile) |
| + ")"); |
| } |
| AudioRoutingDevice arDevice = getAudioRoutingDevice(device); |
| if (arDevice.connectedProfiles.contains(profile)) { |
| if (DBG) { |
| Log.d(TAG, "This device is already connected: " + device); |
| } |
| return; |
| } |
| arDevice.connectedProfiles.add(profile); |
| // TODO: remove the following switch-case statement |
| synchronized (mLock) { |
| switch (profile) { |
| case BluetoothProfile.HEADSET -> mHfpConnectedDevices.add(device); |
| case BluetoothProfile.A2DP -> mA2dpConnectedDevices.add(device); |
| case BluetoothProfile.LE_AUDIO -> mLeAudioConnectedDevices.add(device); |
| case BluetoothProfile.HEARING_AID -> mHearingAidConnectedDevices.add(device); |
| } |
| } |
| if (isWatch(device)) { |
| Log.i(TAG, "Do not set profile active for watch device when connected: " + device); |
| return; |
| } |
| if (!arDevice.canActivateNow(profile)) { |
| if (DBG) { |
| Log.d(TAG, "Can not activate now: " + BluetoothProfile.getProfileName(profile)); |
| } |
| mHandler.postDelayed( |
| () -> arDevice.activate(profile), A2DP_HFP_SYNC_CONNECTION_TIMEOUT_MS); |
| return; |
| } |
| arDevice.activate(profile); |
| } |
| |
| public void handleDeviceDisconnected(BluetoothDevice device, int profile) { |
| if (DBG) { |
| Log.d( |
| TAG, |
| "handleDeviceDisconnected(device=" |
| + device |
| + ", profile=" |
| + BluetoothProfile.getProfileName(profile) |
| + ")"); |
| } |
| AudioRoutingDevice arDevice = getAudioRoutingDevice(device); |
| arDevice.connectedProfiles.remove(profile); |
| if (arDevice.connectedProfiles.isEmpty()) { |
| mConnectedDevices.remove(device); |
| } |
| // TODO: remove the following switch-case statement |
| synchronized (mLock) { |
| switch (profile) { |
| case BluetoothProfile.HEADSET -> mHfpConnectedDevices.remove(device); |
| case BluetoothProfile.A2DP -> mA2dpConnectedDevices.remove(device); |
| case BluetoothProfile.LE_AUDIO -> mLeAudioConnectedDevices.remove(device); |
| case BluetoothProfile.HEARING_AID -> mHearingAidConnectedDevices.remove(device); |
| } |
| } |
| List<BluetoothDevice> activeDevices = mActiveDevices.get(profile); |
| if (activeDevices != null && activeDevices.contains(device)) { |
| // TODO: move setFallbackDeviceActiveLocked into AudioRoutingHandler |
| // and update mConnectedDevices |
| activeDevices.remove(device); |
| if (activeDevices.size() == 0) { |
| synchronized (mLock) { |
| if (!setFallbackDeviceActiveLocked()) { |
| arDevice.deactivate(profile, false); |
| } |
| } |
| } |
| } |
| } |
| |
| // TODO: make getAudioRoutingDevice private |
| public AudioRoutingDevice getAudioRoutingDevice(BluetoothDevice device) { |
| AudioRoutingDevice arDevice = mConnectedDevices.get(device); |
| if (arDevice == null) { |
| arDevice = new AudioRoutingDevice(); |
| arDevice.device = device; |
| arDevice.supportedProfiles = new HashSet<>(); |
| arDevice.connectedProfiles = new HashSet<>(); |
| mConnectedDevices.put(device, arDevice); |
| } |
| if (mDbManager.getProfileConnectionPolicy(device, BluetoothProfile.HEADSET) |
| == BluetoothProfile.CONNECTION_POLICY_ALLOWED) { |
| arDevice.supportedProfiles.add(BluetoothProfile.HEADSET); |
| } else { |
| arDevice.supportedProfiles.remove(BluetoothProfile.HEADSET); |
| } |
| if (mDbManager.getProfileConnectionPolicy(device, BluetoothProfile.A2DP) |
| == BluetoothProfile.CONNECTION_POLICY_ALLOWED) { |
| arDevice.supportedProfiles.add(BluetoothProfile.A2DP); |
| } else { |
| arDevice.supportedProfiles.remove(BluetoothProfile.A2DP); |
| } |
| if (mDbManager.getProfileConnectionPolicy(device, BluetoothProfile.HEARING_AID) |
| == BluetoothProfile.CONNECTION_POLICY_ALLOWED) { |
| arDevice.supportedProfiles.add(BluetoothProfile.HEARING_AID); |
| } else { |
| arDevice.supportedProfiles.remove(BluetoothProfile.HEARING_AID); |
| } |
| if (mDbManager.getProfileConnectionPolicy(device, BluetoothProfile.HAP_CLIENT) |
| == BluetoothProfile.CONNECTION_POLICY_ALLOWED) { |
| arDevice.supportedProfiles.add(BluetoothProfile.HAP_CLIENT); |
| } else { |
| arDevice.supportedProfiles.remove(BluetoothProfile.HAP_CLIENT); |
| } |
| if (mDbManager.getProfileConnectionPolicy(device, BluetoothProfile.LE_AUDIO) |
| == BluetoothProfile.CONNECTION_POLICY_ALLOWED) { |
| arDevice.supportedProfiles.add(BluetoothProfile.LE_AUDIO); |
| } else { |
| arDevice.supportedProfiles.remove(BluetoothProfile.LE_AUDIO); |
| } |
| return arDevice; |
| } |
| |
| // TODO: make AudioRoutingDevice private |
| public class AudioRoutingDevice { |
| public BluetoothDevice device; |
| public Set<Integer> supportedProfiles; |
| public Set<Integer> connectedProfiles; |
| |
| public boolean canActivateNow(int profile) { |
| if (!connectedProfiles.contains(profile)) return false; |
| // TODO: Return false if there are another active remote streaming an audio. |
| // TODO: consider LE audio and HearingAid, HapClient. |
| return switch (profile) { |
| case BluetoothProfile.HEADSET -> !supportedProfiles.contains( |
| BluetoothProfile.A2DP) |
| || connectedProfiles.contains(BluetoothProfile.A2DP); |
| case BluetoothProfile.A2DP -> !supportedProfiles.contains( |
| BluetoothProfile.HEADSET) |
| || connectedProfiles.contains(BluetoothProfile.HEADSET); |
| default -> true; |
| }; |
| } |
| |
| /** |
| * Activate the given profile and related profiles if possible. A2DP and HFP would be |
| * activated together if possible. If there are any activated profiles that can't be |
| * activated together, they will be deactivated. |
| * |
| * @param profile the profile requited to be activated |
| * @return true if any profile was activated or the given profile was already active. |
| */ |
| @SuppressLint("MissingPermission") |
| public boolean activate(int profile) { |
| List<BluetoothDevice> activeDevices = mActiveDevices.get(profile); |
| if (activeDevices != null && activeDevices.contains(device)) { |
| return true; |
| } |
| HashSet<Integer> profilesToActivate = new HashSet<>(); |
| HashSet<Integer> profilesToDeactivate = new HashSet<>(); |
| for (int i = 0; i < mActiveDevices.size(); i++) { |
| profilesToDeactivate.add(mActiveDevices.keyAt(i)); |
| } |
| |
| profilesToActivate.add(profile); |
| profilesToDeactivate.remove(profile); |
| |
| switch (profile) { |
| case BluetoothProfile.A2DP: |
| profilesToDeactivate.remove(BluetoothProfile.HEADSET); |
| if (connectedProfiles.contains(BluetoothProfile.HEADSET)) { |
| activeDevices = mActiveDevices.get(BluetoothProfile.HEADSET); |
| if (activeDevices == null || !activeDevices.contains(device)) { |
| profilesToActivate.add(BluetoothProfile.HEADSET); |
| } |
| } |
| if (Utils.isDualModeAudioEnabled()) { |
| activeDevices = mActiveDevices.get(BluetoothProfile.LE_AUDIO); |
| if (activeDevices != null && activeDevices.contains(device)) { |
| profilesToDeactivate.remove(BluetoothProfile.LE_AUDIO); |
| } |
| } |
| break; |
| case BluetoothProfile.HEADSET: |
| profilesToDeactivate.remove(BluetoothProfile.A2DP); |
| if (connectedProfiles.contains(BluetoothProfile.A2DP)) { |
| activeDevices = mActiveDevices.get(BluetoothProfile.A2DP); |
| if (activeDevices == null || !activeDevices.contains(device)) { |
| profilesToActivate.add(BluetoothProfile.A2DP); |
| } |
| } |
| if (Utils.isDualModeAudioEnabled()) { |
| activeDevices = mActiveDevices.get(BluetoothProfile.LE_AUDIO); |
| if (activeDevices != null && activeDevices.contains(device)) { |
| profilesToDeactivate.remove(BluetoothProfile.LE_AUDIO); |
| } |
| } |
| break; |
| case BluetoothProfile.LE_AUDIO: |
| if (Utils.isDualModeAudioEnabled()) { |
| activeDevices = mActiveDevices.get(BluetoothProfile.HEADSET); |
| if (activeDevices != null && activeDevices.contains(device)) { |
| profilesToDeactivate.remove(BluetoothProfile.HEADSET); |
| } |
| activeDevices = mActiveDevices.get(BluetoothProfile.A2DP); |
| if (activeDevices != null && activeDevices.contains(device)) { |
| profilesToDeactivate.remove(BluetoothProfile.A2DP); |
| } |
| } |
| } |
| boolean isAnyProfileActivated = false; |
| for (Integer p : profilesToActivate) { |
| if (DBG) { |
| Log.d(TAG, "Activate profile: " + p); |
| } |
| boolean activated = switch (p) { |
| case BluetoothProfile.A2DP -> setA2dpActiveDevice(device); |
| case BluetoothProfile.HEADSET -> setHfpActiveDevice(device); |
| case BluetoothProfile.LE_AUDIO -> setLeAudioActiveDevice(device); |
| case BluetoothProfile.HEARING_AID -> setHearingAidActiveDevice( |
| device); |
| case BluetoothProfile.HAP_CLIENT -> setLeHearingAidActiveDevice( |
| device); |
| default -> false; |
| }; |
| if (activated) { |
| // TODO: handle this inside of setXxxActiveDevice() method |
| activeDevices = mActiveDevices.get(p); |
| if (activeDevices == null) { |
| activeDevices = new ArrayList<>(); |
| mActiveDevices.put(p, activeDevices); |
| } |
| if (!canActivateTogether(p, device, activeDevices)) { |
| activeDevices.clear(); |
| } |
| activeDevices.add(device); |
| } |
| isAnyProfileActivated |= activated; |
| } |
| // Do not deactivate profiles if no profiles were activated. |
| if (!isAnyProfileActivated) return false; |
| for (Integer p : profilesToDeactivate) { |
| Log.d(TAG, "Deactivate profile: " + p); |
| switch (p) { |
| case BluetoothProfile.A2DP -> setA2dpActiveDevice(null, true); |
| case BluetoothProfile.HEADSET -> setHfpActiveDevice(null); |
| case BluetoothProfile.LE_AUDIO -> setLeAudioActiveDevice(null, true); |
| case BluetoothProfile.HEARING_AID -> setHearingAidActiveDevice(null, true); |
| case BluetoothProfile.HAP_CLIENT -> setLeHearingAidActiveDevice(null); |
| } |
| } |
| return true; |
| } |
| |
| @SuppressLint("MissingPermission") |
| public void deactivate(int profile, boolean hasFallbackDevice) { |
| if (!mActiveDevices.contains(profile)) return; |
| switch (profile) { |
| case BluetoothProfile.A2DP -> setA2dpActiveDevice(null, hasFallbackDevice); |
| case BluetoothProfile.HEADSET -> setHfpActiveDevice(null); |
| case BluetoothProfile.LE_AUDIO -> setLeAudioActiveDevice(null, false); |
| case BluetoothProfile.HEARING_AID -> setHearingAidActiveDevice(null, false); |
| case BluetoothProfile.HAP_CLIENT -> setLeHearingAidActiveDevice(null); |
| } |
| mActiveDevices.remove(profile); |
| } |
| |
| private boolean canActivateTogether( |
| int profile, BluetoothDevice device, List<BluetoothDevice> group) { |
| if (device == null || group == null || group.isEmpty()) { |
| return false; |
| } |
| switch (profile) { |
| // TODO: handle HAP_CLIENT |
| case BluetoothProfile.LE_AUDIO: { |
| final LeAudioService leAudioService = mFactory.getLeAudioService(); |
| if (leAudioService == null) { |
| return false; |
| } |
| int groupId = leAudioService.getGroupId(device); |
| if (groupId != LE_AUDIO_GROUP_ID_INVALID |
| && groupId == leAudioService.getGroupId(group.get(0))) { |
| return true; |
| } |
| break; |
| } |
| case BluetoothProfile.HEARING_AID: { |
| final HearingAidService hearingAidService = mFactory.getHearingAidService(); |
| if (hearingAidService == null) { |
| return false; |
| } |
| long hiSyncId = hearingAidService.getHiSyncId(device); |
| if (hiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID |
| && hiSyncId == hearingAidService.getHiSyncId(group.get(0))) { |
| return true; |
| } |
| break; |
| } |
| } |
| return false; |
| } |
| } |
| } |
| } |