| /* |
| * Copyright (C) 2018 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 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.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| 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.ArraySet; |
| import android.util.Log; |
| |
| 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.List; |
| import java.util.Objects; |
| import java.util.Set; |
| |
| /** |
| * The active device manager is responsible for keeping track of the |
| * connected A2DP/HFP/AVRCP/HearingAid/LE audio devices and select which device is |
| * active (for each profile). |
| * The active device manager selects a fallback device when the currently active device |
| * is disconnected, and it selects BT devices that are lastly activated one. |
| * |
| * Current policy (subject to change): |
| * 1) If the maximum number of connected devices is one, the manager doesn't |
| * do anything. Each profile is responsible for automatically selecting |
| * the connected device as active. Only if the maximum number of connected |
| * devices is more than one, the rules below will apply. |
| * 2) The selected A2DP active device is the one used for AVRCP as well. |
| * 3) The HFP active device might be different from the A2DP active device. |
| * 4) The Active Device Manager always listens for the change of active devices. |
| * When it changed (e.g., triggered indirectly by user action on the UI), |
| * the new active device is marked as the current active device for that profile. |
| * 5) If there is a HearingAid active device, then A2DP, HFP and LE audio active devices |
| * must be set to null (i.e., A2DP, HFP and LE audio cannot have active devices). |
| * The reason is that A2DP, HFP or LE audio cannot be used together with HearingAid. |
| * 6) If there are no connected devices (e.g., during startup, or after all |
| * devices have been disconnected, the active device per profile |
| * (A2DP/HFP/HearingAid/LE audio) is selected as follows: |
| * 6.1) The last connected HearingAid device is selected as active. |
| * If there is an active A2DP, HFP or LE audio device, those must be set to null. |
| * 6.2) The last connected A2DP, HFP or LE audio device is selected as active. |
| * However, if there is an active HearingAid device, then the |
| * A2DP, HFP, or LE audio active device is not set (must remain null). |
| * 7) If the currently active device (per profile) is disconnected, the |
| * Active Device Manager just marks that the profile has no active device, |
| * and the lastly activated BT device that is still connected would be selected. |
| * 8) If there is already an active device, however, if active device change notified |
| * with a null device, the corresponding profile is marked as having no active device. |
| * 9) If a wired audio device is connected, the audio output is switched |
| * by the Audio Framework itself to that device. We detect this here, |
| * and the active device for each profile (A2DP/HFP/HearingAid/LE audio) is set |
| * to null to reflect the output device state change. However, if the |
| * wired audio device is disconnected, we don't do anything explicit |
| * and apply the default behavior instead: |
| * 9.1) If the wired headset is still the selected output device (i.e. the |
| * active device is set to null), the Phone itself will become the output |
| * device (i.e., the active device will remain null). If music was |
| * playing, it will stop. |
| * 9.2) If one of the Bluetooth devices is the selected active device |
| * (e.g., by the user in the UI), disconnecting the wired audio device |
| * will have no impact. E.g., music will continue streaming over the |
| * active Bluetooth device. |
| */ |
| public class ActiveDeviceManager { |
| private static final String TAG = "ActiveDeviceManager"; |
| private static final boolean DBG = true; |
| @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 Handler mHandler = null; |
| private final AudioManager mAudioManager; |
| private final AudioManagerAudioDeviceCallback mAudioManagerAudioDeviceCallback; |
| |
| private final Object mLock = new Object(); |
| @GuardedBy("mLock") |
| private final List<BluetoothDevice> mA2dpConnectedDevices = new ArrayList<>(); |
| @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; |
| |
| private final BroadcastReceiver mReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| if (DBG) { |
| Log.d(TAG, "Received intent: action=" + intent.getAction() + ", extras=" |
| + intent.getExtras()); |
| } |
| String action = intent.getAction(); |
| if (action == null) { |
| Log.e(TAG, "Received intent with null action"); |
| return; |
| } |
| |
| if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) { |
| int currentState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1); |
| mHandler.post(() -> handleAdapterStateChanged(currentState)); |
| } |
| } |
| }; |
| |
| /** |
| * Called when A2DP connection state changed by A2dpService |
| * |
| * @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 |
| */ |
| public void a2dpConnectionStateChanged(BluetoothDevice device, int fromState, int toState) { |
| if (toState == BluetoothProfile.STATE_CONNECTED) { |
| mHandler.post(() -> handleA2dpConnected(device)); |
| } else if (fromState == BluetoothProfile.STATE_CONNECTED) { |
| mHandler.post(() -> handleA2dpDisconnected(device)); |
| } |
| } |
| |
| /** |
| * Called when A2DP active state changed by A2dpService |
| * |
| * @param device The device currently activated. {@code null} if no A2DP device activated |
| */ |
| public void a2dpActiveStateChanged(BluetoothDevice device) { |
| mHandler.post(() -> handleA2dpActiveDeviceChanged(device)); |
| } |
| |
| /** |
| * Called when HFP connection state changed by HeadsetService |
| * |
| * @param device The device of which connection state was changed |
| * @param prevState The previous connection state of the device |
| * @param newState The new connection state of the device |
| */ |
| public void hfpConnectionStateChanged(BluetoothDevice device, int prevState, int newState) { |
| if (newState == BluetoothProfile.STATE_CONNECTED) { |
| mHandler.post(() -> handleHfpConnected(device)); |
| } else if (prevState == BluetoothProfile.STATE_CONNECTED) { |
| mHandler.post(() -> handleHfpDisconnected(device)); |
| } |
| } |
| |
| /** |
| * Called when HFP active state changed by HeadsetService |
| * |
| * @param device The device currently activated. {@code null} if no HFP device activated |
| */ |
| public void hfpActiveStateChanged(BluetoothDevice device) { |
| mHandler.post(() -> handleHfpActiveDeviceChanged(device)); |
| } |
| |
| /** |
| * Called when LE audio connection state changed by LeAudioService |
| * |
| * @param device The device of which connection state was changed |
| * @param prevState The previous connection state of the device |
| * @param newState The new connection state of the device |
| */ |
| public void leAudioConnectionStateChanged(BluetoothDevice device, int prevState, int newState) { |
| if (newState == BluetoothProfile.STATE_CONNECTED) { |
| mHandler.post(() -> handleLeAudioConnected(device)); |
| } else if (prevState == BluetoothProfile.STATE_CONNECTED) { |
| mHandler.post(() -> handleLeAudioDisconnected(device)); |
| } |
| } |
| |
| /** |
| * Called when LE audio active state changed by LeAudioService |
| * |
| * @param device The device currently activated. {@code null} if no LE audio device activated |
| */ |
| public void leAudioActiveStateChanged(BluetoothDevice device) { |
| mHandler.post(() -> handleLeAudioActiveDeviceChanged(device)); |
| } |
| |
| /** |
| * Called when HearingAid connection state changed by HearingAidService |
| * |
| * @param device The device of which connection state was changed |
| * @param prevState The previous connection state of the device |
| * @param newState The new connection state of the device |
| */ |
| public void hearingAidConnectionStateChanged( |
| BluetoothDevice device, int prevState, int newState) { |
| if (newState == BluetoothProfile.STATE_CONNECTED) { |
| mHandler.post(() -> handleHearingAidConnected(device)); |
| } else if (prevState == BluetoothProfile.STATE_CONNECTED) { |
| mHandler.post(() -> handleHearingAidDisconnected(device)); |
| } |
| } |
| |
| /** |
| * Called when HearingAid active state changed by HearingAidService |
| * |
| * @param device The device currently activated. {@code null} if no HearingAid device activated |
| */ |
| public void hearingAidActiveStateChanged(BluetoothDevice device) { |
| mHandler.post(() -> handleHearingAidActiveDeviceChanged(device)); |
| } |
| |
| /** |
| * Called when HAP connection state changed by HapClientService |
| * |
| * @param device The device of which connection state was changed |
| * @param prevState The previous connection state of the device |
| * @param newState The new connection state of the device |
| */ |
| public void hapConnectionStateChanged(BluetoothDevice device, int prevState, int newState) { |
| if (newState == BluetoothProfile.STATE_CONNECTED) { |
| mHandler.post(() -> handleHapConnected(device)); |
| } else if (prevState == BluetoothProfile.STATE_CONNECTED) { |
| mHandler.post(() -> handleHapDisconnected(device)); |
| } |
| } |
| |
| private void handleAdapterStateChanged(int currentState) { |
| if (DBG) { |
| Log.d(TAG, "handleAdapterStateChanged: currentState=" + currentState); |
| } |
| if (currentState == BluetoothAdapter.STATE_ON) { |
| resetState(); |
| } |
| } |
| |
| /** |
| * Handles the active device logic for when A2DP is connected. Does the following: |
| * 1. Check if a hearing aid device is active. We will always prefer hearing aid devices, so if |
| * one is active, we will not make this A2DP device active. |
| * 2. If there is no hearing aid device active, we will make this A2DP device active. |
| * 3. We will make this device active for HFP if it's already connected to HFP |
| * 4. If dual mode is disabled, we clear the LE Audio active device to ensure mutual exclusion |
| * between classic and LE audio. |
| * |
| * @param device is the device that was connected to A2DP |
| */ |
| private void handleA2dpConnected(BluetoothDevice device) { |
| synchronized (mLock) { |
| if (DBG) { |
| Log.d(TAG, "handleA2dpConnected: " + device); |
| } |
| if (mA2dpConnectedDevices.contains(device)) { |
| if (DBG) { |
| Log.d(TAG, "This device is already connected: " + device); |
| } |
| return; |
| } |
| mA2dpConnectedDevices.add(device); |
| if (mHearingAidActiveDevices.isEmpty() && mLeHearingAidActiveDevice == null) { |
| // New connected device: select it as active |
| // Activate HFP and A2DP at the same time if both profile already connected. |
| if (mHfpConnectedDevices.contains(device)) { |
| boolean a2dpMadeActive = setA2dpActiveDevice(device); |
| boolean hfpMadeActive = setHfpActiveDevice(device); |
| if ((a2dpMadeActive || hfpMadeActive) && !Utils.isDualModeAudioEnabled()) { |
| setLeAudioActiveDevice(null, true); |
| } |
| return; |
| } |
| // Activate A2DP if audio mode is normal or HFP is not supported or enabled. |
| if (mDbManager.getProfileConnectionPolicy(device, BluetoothProfile.HEADSET) |
| != BluetoothProfile.CONNECTION_POLICY_ALLOWED |
| || mAudioManager.getMode() == AudioManager.MODE_NORMAL) { |
| boolean a2dpMadeActive = setA2dpActiveDevice(device); |
| if (a2dpMadeActive && !Utils.isDualModeAudioEnabled()) { |
| setLeAudioActiveDevice(null, true); |
| } |
| } else { |
| if (DBG) { |
| Log.d(TAG, "A2DP activation is suspended until HFP connected: " |
| + device); |
| } |
| |
| mHandler.removeCallbacksAndMessages(mPendingActiveDevice); |
| mPendingActiveDevice = device; |
| // Activate A2DP if HFP is failed to connect. |
| mHandler.postDelayed( |
| () -> { |
| Log.w(TAG, "HFP connection timeout. Activate A2DP for " + device); |
| setA2dpActiveDevice(device); |
| }, |
| mPendingActiveDevice, |
| A2DP_HFP_SYNC_CONNECTION_TIMEOUT_MS); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Handles the active device logic for when HFP is connected. Does the following: |
| * 1. Check if a hearing aid device is active. We will always prefer hearing aid devices, so if |
| * one is active, we will not make this HFP device active. |
| * 2. If there is no hearing aid device active, we will make this HFP device active. |
| * 3. We will make this device active for A2DP if it's already connected to A2DP |
| * 4. If dual mode is disabled, we clear the LE Audio active device to ensure mutual exclusion |
| * between classic and LE audio. |
| * |
| * @param device is the device that was connected to A2DP |
| */ |
| private void handleHfpConnected(BluetoothDevice device) { |
| synchronized (mLock) { |
| if (DBG) { |
| Log.d(TAG, "handleHfpConnected: " + device); |
| } |
| if (mHfpConnectedDevices.contains(device)) { |
| if (DBG) { |
| Log.d(TAG, "This device is already connected: " + device); |
| } |
| return; |
| } |
| mHfpConnectedDevices.add(device); |
| if (mHearingAidActiveDevices.isEmpty() && mLeHearingAidActiveDevice == null) { |
| // New connected device: select it as active |
| // Activate HFP and A2DP at the same time once both profile connected. |
| if (mA2dpConnectedDevices.contains(device)) { |
| boolean a2dpMadeActive = setA2dpActiveDevice(device); |
| boolean hfpMadeActive = setHfpActiveDevice(device); |
| |
| /* Make LEA inactive if device is made active for any classic audio profile |
| and dual mode is disabled */ |
| if ((a2dpMadeActive || hfpMadeActive) && !Utils.isDualModeAudioEnabled()) { |
| setLeAudioActiveDevice(null, true); |
| } |
| return; |
| } |
| // Activate HFP if audio mode is not normal or A2DP is not supported or enabled. |
| if (mDbManager.getProfileConnectionPolicy(device, BluetoothProfile.A2DP) |
| != BluetoothProfile.CONNECTION_POLICY_ALLOWED |
| || mAudioManager.getMode() != AudioManager.MODE_NORMAL) { |
| if (isWatch(device)) { |
| Log.i(TAG, "Do not set hfp active for watch device " + device); |
| return; |
| } |
| // Tries to make the device active for HFP |
| boolean hfpMadeActive = setHfpActiveDevice(device); |
| |
| // Makes LEA inactive if device is made active for HFP & dual mode is disabled |
| if (hfpMadeActive && !Utils.isDualModeAudioEnabled()) { |
| setLeAudioActiveDevice(null, true); |
| } |
| } else { |
| if (DBG) { |
| Log.d(TAG, "HFP activation is suspended until A2DP connected: " |
| + device); |
| } |
| mHandler.removeCallbacksAndMessages(mPendingActiveDevice); |
| mPendingActiveDevice = device; |
| // Activate HFP if A2DP is failed to connect. |
| mHandler.postDelayed( |
| () -> { |
| Log.w(TAG, "A2DP connection timeout. Activate HFP for " + device); |
| setHfpActiveDevice(device); |
| }, |
| mPendingActiveDevice, |
| A2DP_HFP_SYNC_CONNECTION_TIMEOUT_MS); |
| } |
| } |
| } |
| } |
| |
| private void handleHearingAidConnected(BluetoothDevice device) { |
| synchronized (mLock) { |
| if (DBG) { |
| Log.d(TAG, "handleHearingAidConnected: " + device); |
| } |
| if (mHearingAidConnectedDevices.contains(device)) { |
| if (DBG) { |
| Log.d(TAG, "This device is already connected: " + device); |
| } |
| return; |
| } |
| mHearingAidConnectedDevices.add(device); |
| // New connected device: select it as active |
| if (setHearingAidActiveDevice(device)) { |
| setA2dpActiveDevice(null, true); |
| setHfpActiveDevice(null); |
| setLeAudioActiveDevice(null, true); |
| } |
| } |
| } |
| |
| private void handleLeAudioConnected(BluetoothDevice device) { |
| synchronized (mLock) { |
| if (DBG) { |
| Log.d(TAG, "handleLeAudioConnected: " + device); |
| } |
| |
| final LeAudioService leAudioService = mFactory.getLeAudioService(); |
| if (leAudioService == null || device == null) { |
| return; |
| } |
| leAudioService.deviceConnected(device); |
| |
| if (mLeAudioConnectedDevices.contains(device)) { |
| if (DBG) { |
| Log.d(TAG, "This device is already connected: " + device); |
| } |
| return; |
| } |
| |
| mLeAudioConnectedDevices.add(device); |
| if (mHearingAidActiveDevices.isEmpty() |
| && mLeHearingAidActiveDevice == null |
| && mPendingLeHearingAidActiveDevice.isEmpty()) { |
| // New connected device: select it as active |
| boolean leAudioMadeActive = setLeAudioActiveDevice(device); |
| if (leAudioMadeActive && !Utils.isDualModeAudioEnabled()) { |
| setA2dpActiveDevice(null, true); |
| setHfpActiveDevice(null); |
| } |
| } else if (mPendingLeHearingAidActiveDevice.contains(device)) { |
| if (setLeHearingAidActiveDevice(device)) { |
| setHearingAidActiveDevice(null, true); |
| setA2dpActiveDevice(null, true); |
| setHfpActiveDevice(null); |
| } |
| } |
| } |
| } |
| |
| 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 handleA2dpDisconnected(BluetoothDevice device) { |
| synchronized (mLock) { |
| if (DBG) { |
| Log.d(TAG, "handleA2dpDisconnected: " + device |
| + ", mA2dpActiveDevice=" + mA2dpActiveDevice); |
| } |
| mA2dpConnectedDevices.remove(device); |
| if (Objects.equals(mA2dpActiveDevice, device)) { |
| if (!setFallbackDeviceActiveLocked()) { |
| setA2dpActiveDevice(null, false); |
| } |
| } |
| } |
| } |
| |
| private void handleHfpDisconnected(BluetoothDevice device) { |
| synchronized (mLock) { |
| if (DBG) { |
| Log.d(TAG, "handleHfpDisconnected: " + device |
| + ", mHfpActiveDevice=" + mHfpActiveDevice); |
| } |
| mHfpConnectedDevices.remove(device); |
| if (Objects.equals(mHfpActiveDevice, device)) { |
| if (mHfpConnectedDevices.isEmpty()) { |
| setHfpActiveDevice(null); |
| } |
| setFallbackDeviceActiveLocked(); |
| } |
| } |
| } |
| |
| private void handleHearingAidDisconnected(BluetoothDevice device) { |
| synchronized (mLock) { |
| if (DBG) { |
| Log.d(TAG, "handleHearingAidDisconnected: " + device |
| + ", mHearingAidActiveDevices=" + mHearingAidActiveDevices); |
| } |
| mHearingAidConnectedDevices.remove(device); |
| if (mHearingAidActiveDevices.remove(device) && mHearingAidActiveDevices.isEmpty()) { |
| if (!setFallbackDeviceActiveLocked()) { |
| setHearingAidActiveDevice(null, false); |
| } |
| } |
| } |
| } |
| |
| private void handleLeAudioDisconnected(BluetoothDevice device) { |
| synchronized (mLock) { |
| if (DBG) { |
| Log.d(TAG, "handleLeAudioDisconnected: " + device |
| + ", mLeAudioActiveDevice=" + mLeAudioActiveDevice); |
| } |
| |
| final LeAudioService leAudioService = mFactory.getLeAudioService(); |
| if (leAudioService == null || device == null) { |
| return; |
| } |
| |
| mLeAudioConnectedDevices.remove(device); |
| mLeHearingAidConnectedDevices.remove(device); |
| |
| boolean hasFallbackDevice = false; |
| if (Objects.equals(mLeAudioActiveDevice, device)) { |
| hasFallbackDevice = setFallbackDeviceActiveLocked(); |
| if (!hasFallbackDevice) { |
| setLeAudioActiveDevice(null, false); |
| } |
| } |
| leAudioService.deviceDisconnected(device, hasFallbackDevice); |
| } |
| } |
| |
| 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 (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) { |
| } |
| } |
| |
| ActiveDeviceManager(AdapterService service, ServiceFactory factory) { |
| mAdapterService = service; |
| mDbManager = mAdapterService.getDatabase(); |
| mFactory = factory; |
| mAudioManager = service.getSystemService(AudioManager.class); |
| mAudioManagerAudioDeviceCallback = new AudioManagerAudioDeviceCallback(); |
| } |
| |
| void start() { |
| if (DBG) { |
| Log.d(TAG, "start()"); |
| } |
| |
| mHandlerThread = new HandlerThread("BluetoothActiveDeviceManager"); |
| mHandlerThread.start(); |
| mHandler = new Handler(mHandlerThread.getLooper()); |
| |
| IntentFilter filter = new IntentFilter(); |
| filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY); |
| filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); |
| mAdapterService.registerReceiver(mReceiver, filter, Context.RECEIVER_EXPORTED); |
| |
| mAudioManager.registerAudioDeviceCallback(mAudioManagerAudioDeviceCallback, mHandler); |
| } |
| |
| void cleanup() { |
| if (DBG) { |
| Log.d(TAG, "cleanup()"); |
| } |
| |
| mAudioManager.unregisterAudioDeviceCallback(mAudioManagerAudioDeviceCallback); |
| mAdapterService.unregisterReceiver(mReceiver); |
| 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 |
| public Looper getHandlerLooper() { |
| if (mHandlerThread == null) { |
| return null; |
| } |
| return mHandlerThread.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) { |
| synchronized (mLock) { |
| if (DBG) { |
| Log.d(TAG, "setHfpActiveDevice(" + device + ")"); |
| } |
| 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; |
| } |
| 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 : "")); |
| } |
| synchronized (mLock) { |
| final HearingAidService hearingAidService = mFactory.getHearingAidService(); |
| if (hearingAidService == null) { |
| return false; |
| } |
| |
| 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 : "")); |
| } |
| synchronized (mLock) { |
| 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; |
| } |
| |
| 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 |
| */ |
| 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; |
| } |
| } |
| |
| A2dpService a2dpService = mFactory.getA2dpService(); |
| BluetoothDevice a2dpFallbackDevice = null; |
| if (a2dpService != null) { |
| a2dpFallbackDevice = a2dpService.getFallbackDevice(); |
| } |
| |
| HeadsetService headsetService = mFactory.getHeadsetService(); |
| BluetoothDevice headsetFallbackDevice = null; |
| if (headsetService != null) { |
| headsetFallbackDevice = headsetService.getFallbackDevice(); |
| } |
| |
| List<BluetoothDevice> connectedDevices = new ArrayList<>(); |
| connectedDevices.addAll(mLeAudioConnectedDevices); |
| switch (mAudioManager.getMode()) { |
| case AudioManager.MODE_NORMAL: |
| if (a2dpFallbackDevice != null) { |
| connectedDevices.add(a2dpFallbackDevice); |
| } |
| break; |
| case AudioManager.MODE_RINGTONE: |
| if (headsetFallbackDevice != null && headsetService.isInbandRingingEnabled()) { |
| connectedDevices.add(headsetFallbackDevice); |
| } |
| break; |
| default: |
| if (headsetFallbackDevice != null) { |
| connectedDevices.add(headsetFallbackDevice); |
| } |
| } |
| BluetoothDevice device = mDbManager.getMostRecentlyConnectedDevicesInList(connectedDevices); |
| if (device != null) { |
| if (mAudioManager.getMode() == AudioManager.MODE_NORMAL) { |
| if (Objects.equals(a2dpFallbackDevice, device)) { |
| if (DBG) { |
| Log.d(TAG, "Found an A2DP fallback device: " + device); |
| } |
| setA2dpActiveDevice(device); |
| if (Objects.equals(headsetFallbackDevice, 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 (Objects.equals(headsetFallbackDevice, device)) { |
| if (DBG) { |
| Log.d(TAG, "Found a HFP fallback device: " + device); |
| } |
| setHfpActiveDevice(device); |
| if (Objects.equals(a2dpFallbackDevice, device)) { |
| setA2dpActiveDevice(a2dpFallbackDevice); |
| } 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 |
| BroadcastReceiver getBroadcastReceiver() { |
| return mReceiver; |
| } |
| |
| @VisibleForTesting |
| BluetoothDevice getA2dpActiveDevice() { |
| return mA2dpActiveDevice; |
| } |
| |
| @VisibleForTesting |
| BluetoothDevice getHfpActiveDevice() { |
| return mHfpActiveDevice; |
| } |
| |
| @VisibleForTesting |
| Set<BluetoothDevice> getHearingAidActiveDevices() { |
| return mHearingAidActiveDevices; |
| } |
| |
| @VisibleForTesting |
| BluetoothDevice getLeAudioActiveDevice() { |
| return mLeAudioActiveDevice; |
| } |
| |
| 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) |
| void wiredAudioDeviceConnected() { |
| if (DBG) { |
| Log.d(TAG, "wiredAudioDeviceConnected"); |
| } |
| setA2dpActiveDevice(null, true); |
| setHfpActiveDevice(null); |
| setHearingAidActiveDevice(null, true); |
| setLeAudioActiveDevice(null, true); |
| } |
| } |