Loading android/app/src/com/android/bluetooth/a2dp/A2dpService.java +1 −1 Original line number Diff line number Diff line Loading @@ -1258,7 +1258,7 @@ public class A2dpService extends ProfileService { /** * Retrieves the most recently connected device in the A2DP connected devices list. */ private BluetoothDevice getFallbackDevice() { public BluetoothDevice getFallbackDevice() { DatabaseManager dbManager = mAdapterService.getDatabase(); return dbManager != null ? dbManager .getMostRecentlyConnectedDevicesInList(getConnectedDevices()) Loading android/app/src/com/android/bluetooth/btservice/ActiveDeviceManager.java +111 −21 Original line number Diff line number Diff line Loading @@ -40,6 +40,7 @@ import android.os.Message; import android.util.Log; 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; Loading @@ -51,8 +52,10 @@ import java.util.Objects; /** * The active device manager is responsible for keeping track of the * connected A2DP/HFP/AVRCP/HearingAid devices and select which device is * 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 Loading @@ -66,24 +69,24 @@ import java.util.Objects; * - BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED for A2DP * - BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED for HFP * - BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED for HearingAid * - BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED for LE audio * If such broadcast is received (e.g., triggered indirectly by user * action on the UI), the device in the received broacast is marked * action on the UI), the device in the received broadcast is marked * as the current active device for that profile. * 5) If there is a HearingAid active device, then A2DP and HFP active devices * must be set to null (i.e., A2DP and HFP cannot have active devices). * The reason is because A2DP or HFP cannot be used together with HearingAid. * 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) is selected as follows: * (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 or HFP device, those must be set to null. * 6.2) The last connected A2DP or HFP 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 or HFP active device is not set (must remain null). * 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, * but does not attempt to select a new one. Currently, the expectation is * that the user will explicitly select the new active device. * and the lastly activated BT device that is still connected would be selected. * 8) If there is already an active device, and the corresponding * ACTION_ACTIVE_DEVICE_CHANGED broadcast is received, the device * contained in the broadcast is marked as active. However, if Loading @@ -91,7 +94,7 @@ import java.util.Objects; * 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) is set * 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: Loading Loading @@ -252,10 +255,12 @@ class ActiveDeviceManager { + "device " + device + " disconnected"); } mA2dpConnectedDevices.remove(device); if (mA2dpConnectedDevices.isEmpty() && Objects.equals(mA2dpActiveDevice, device)) { if (Objects.equals(mA2dpActiveDevice, device)) { if (mA2dpConnectedDevices.isEmpty()) { setA2dpActiveDevice(null); } setFallbackDeviceActive(); } } } break; Loading Loading @@ -314,10 +319,12 @@ class ActiveDeviceManager { + "device " + device + " disconnected"); } mHfpConnectedDevices.remove(device); if (mHfpConnectedDevices.isEmpty() && Objects.equals(mHfpActiveDevice, device)) { if (Objects.equals(mHfpActiveDevice, device)) { if (mHfpConnectedDevices.isEmpty()) { setHfpActiveDevice(null); } setFallbackDeviceActive(); } } } break; Loading Loading @@ -392,10 +399,12 @@ class ActiveDeviceManager { + "_CHANGED): device " + device + " disconnected"); } mLeAudioConnectedDevices.remove(device); if (mLeAudioConnectedDevices.isEmpty() && Objects.equals(mLeAudioActiveDevice, device)) { if (Objects.equals(mLeAudioActiveDevice, device)) { if (mLeAudioConnectedDevices.isEmpty()) { setLeAudioActiveDevice(null); } setFallbackDeviceActive(); } } } break; Loading Loading @@ -658,6 +667,87 @@ class ActiveDeviceManager { } } private void setFallbackDeviceActive() { if (DBG) { Log.d(TAG, "setFallbackDeviceActive"); } DatabaseManager dbManager = mAdapterService.getDatabase(); if (dbManager == null) { return; } 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 LinkedList<>(); 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 = dbManager.getMostRecentlyConnectedDevicesInList(connectedDevices); if (device != null) { if (mAudioManager.getMode() == AudioManager.MODE_NORMAL) { if (Objects.equals(a2dpFallbackDevice, device)) { if (DBG) { Log.d(TAG, "set A2DP device active: " + device); } setA2dpActiveDevice(device); if (headsetFallbackDevice != null) { setHfpActiveDevice(device); setLeAudioActiveDevice(null); } } else { if (DBG) { Log.d(TAG, "set LE audio device active: " + device); } setLeAudioActiveDevice(device); setA2dpActiveDevice(null); setHfpActiveDevice(null); } } else { if (Objects.equals(headsetFallbackDevice, device)) { if (DBG) { Log.d(TAG, "set HFP device active: " + device); } setHfpActiveDevice(device); if (a2dpFallbackDevice != null) { setA2dpActiveDevice(a2dpFallbackDevice); setLeAudioActiveDevice(null); } } else { if (DBG) { Log.d(TAG, "set LE audio device active: " + device); } setLeAudioActiveDevice(device); setA2dpActiveDevice(null); setHfpActiveDevice(null); } } } } private void resetState() { mA2dpConnectedDevices.clear(); mA2dpActiveDevice = null; Loading android/app/src/com/android/bluetooth/hfp/HeadsetService.java +7 −2 Original line number Diff line number Diff line Loading @@ -1877,7 +1877,12 @@ public class HeadsetService extends ProfileService { return true; } boolean isInbandRingingEnabled() { /** * Checks if headset devices are able to get inband ringing. * * @return True if inband ringing is enabled. */ public boolean isInbandRingingEnabled() { boolean isInbandRingingSupported = getResources().getBoolean( com.android.bluetooth.R.bool.config_bluetooth_hfp_inband_ringing_support); return isInbandRingingSupported && !SystemProperties.getBoolean( Loading Loading @@ -2125,7 +2130,7 @@ public class HeadsetService extends ProfileService { /** * Retrieves the most recently connected device in the A2DP connected devices list. */ private BluetoothDevice getFallbackDevice() { public BluetoothDevice getFallbackDevice() { DatabaseManager dbManager = mAdapterService.getDatabase(); return dbManager != null ? dbManager .getMostRecentlyConnectedDevicesInList(getConnectedDevices()) Loading android/app/tests/unit/src/com/android/bluetooth/btservice/ActiveDeviceManagerTest.java +52 −0 Original line number Diff line number Diff line Loading @@ -50,6 +50,8 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.List; @MediumTest @RunWith(AndroidJUnit4.class) public class ActiveDeviceManagerTest { Loading Loading @@ -473,6 +475,56 @@ public class ActiveDeviceManagerTest { Assert.assertEquals(null, mActiveDeviceManager.getLeAudioActiveDevice()); } /** * An LE Audio connected. An A2DP connected. The A2DP disconnected. * Then the LE Audio should be the active one. */ @Test public void leAudioAndA2dpConnectedThenA2dpDisconnected_fallbackToLeAudio() { leAudioConnected(mLeAudioDevice); verify(mLeAudioService, timeout(TIMEOUT_MS)).setActiveDevice(mLeAudioDevice); a2dpConnected(mA2dpDevice); verify(mA2dpService, timeout(TIMEOUT_MS)).setActiveDevice(mA2dpDevice); when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_NORMAL); when(mDatabaseManager.getMostRecentlyConnectedDevicesInList(any())).thenAnswer( invocation -> { List<BluetoothDevice> devices = invocation.getArgument(0); return (devices != null && devices.contains(mLeAudioDevice)) ? mLeAudioDevice : null; } ); a2dpDisconnected(mA2dpDevice); verify(mA2dpService, timeout(TIMEOUT_MS).atLeast(1)).setActiveDevice(isNull()); verify(mLeAudioService, timeout(TIMEOUT_MS).times(2)).setActiveDevice(mLeAudioDevice); } /** * An A2DP connected. An LE Audio connected. The LE Audio disconnected. * Then the A2DP should be the active one. */ @Test public void a2dpAndLeAudioConnectedThenLeAudioDisconnected_fallbackToA2dp() { a2dpConnected(mA2dpDevice); verify(mA2dpService, timeout(TIMEOUT_MS)).setActiveDevice(mA2dpDevice); leAudioConnected(mLeAudioDevice); verify(mLeAudioService, timeout(TIMEOUT_MS)).setActiveDevice(mLeAudioDevice); when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_NORMAL); when(mA2dpService.getFallbackDevice()).thenReturn(mA2dpDevice); when(mDatabaseManager.getMostRecentlyConnectedDevicesInList(any())).thenAnswer( invocation -> { List<BluetoothDevice> devices = invocation.getArgument(0); return (devices != null && devices.contains(mA2dpDevice)) ? mA2dpDevice : null; } ); leAudioDisconnected(mLeAudioDevice); verify(mLeAudioService, timeout(TIMEOUT_MS).atLeast(1)).setActiveDevice(isNull()); verify(mA2dpService, timeout(TIMEOUT_MS).times(2)).setActiveDevice(mA2dpDevice); } /** * One LE Hearing Aid is connected. */ Loading Loading
android/app/src/com/android/bluetooth/a2dp/A2dpService.java +1 −1 Original line number Diff line number Diff line Loading @@ -1258,7 +1258,7 @@ public class A2dpService extends ProfileService { /** * Retrieves the most recently connected device in the A2DP connected devices list. */ private BluetoothDevice getFallbackDevice() { public BluetoothDevice getFallbackDevice() { DatabaseManager dbManager = mAdapterService.getDatabase(); return dbManager != null ? dbManager .getMostRecentlyConnectedDevicesInList(getConnectedDevices()) Loading
android/app/src/com/android/bluetooth/btservice/ActiveDeviceManager.java +111 −21 Original line number Diff line number Diff line Loading @@ -40,6 +40,7 @@ import android.os.Message; import android.util.Log; 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; Loading @@ -51,8 +52,10 @@ import java.util.Objects; /** * The active device manager is responsible for keeping track of the * connected A2DP/HFP/AVRCP/HearingAid devices and select which device is * 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 Loading @@ -66,24 +69,24 @@ import java.util.Objects; * - BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED for A2DP * - BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED for HFP * - BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED for HearingAid * - BluetoothLeAudio.ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED for LE audio * If such broadcast is received (e.g., triggered indirectly by user * action on the UI), the device in the received broacast is marked * action on the UI), the device in the received broadcast is marked * as the current active device for that profile. * 5) If there is a HearingAid active device, then A2DP and HFP active devices * must be set to null (i.e., A2DP and HFP cannot have active devices). * The reason is because A2DP or HFP cannot be used together with HearingAid. * 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) is selected as follows: * (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 or HFP device, those must be set to null. * 6.2) The last connected A2DP or HFP 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 or HFP active device is not set (must remain null). * 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, * but does not attempt to select a new one. Currently, the expectation is * that the user will explicitly select the new active device. * and the lastly activated BT device that is still connected would be selected. * 8) If there is already an active device, and the corresponding * ACTION_ACTIVE_DEVICE_CHANGED broadcast is received, the device * contained in the broadcast is marked as active. However, if Loading @@ -91,7 +94,7 @@ import java.util.Objects; * 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) is set * 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: Loading Loading @@ -252,10 +255,12 @@ class ActiveDeviceManager { + "device " + device + " disconnected"); } mA2dpConnectedDevices.remove(device); if (mA2dpConnectedDevices.isEmpty() && Objects.equals(mA2dpActiveDevice, device)) { if (Objects.equals(mA2dpActiveDevice, device)) { if (mA2dpConnectedDevices.isEmpty()) { setA2dpActiveDevice(null); } setFallbackDeviceActive(); } } } break; Loading Loading @@ -314,10 +319,12 @@ class ActiveDeviceManager { + "device " + device + " disconnected"); } mHfpConnectedDevices.remove(device); if (mHfpConnectedDevices.isEmpty() && Objects.equals(mHfpActiveDevice, device)) { if (Objects.equals(mHfpActiveDevice, device)) { if (mHfpConnectedDevices.isEmpty()) { setHfpActiveDevice(null); } setFallbackDeviceActive(); } } } break; Loading Loading @@ -392,10 +399,12 @@ class ActiveDeviceManager { + "_CHANGED): device " + device + " disconnected"); } mLeAudioConnectedDevices.remove(device); if (mLeAudioConnectedDevices.isEmpty() && Objects.equals(mLeAudioActiveDevice, device)) { if (Objects.equals(mLeAudioActiveDevice, device)) { if (mLeAudioConnectedDevices.isEmpty()) { setLeAudioActiveDevice(null); } setFallbackDeviceActive(); } } } break; Loading Loading @@ -658,6 +667,87 @@ class ActiveDeviceManager { } } private void setFallbackDeviceActive() { if (DBG) { Log.d(TAG, "setFallbackDeviceActive"); } DatabaseManager dbManager = mAdapterService.getDatabase(); if (dbManager == null) { return; } 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 LinkedList<>(); 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 = dbManager.getMostRecentlyConnectedDevicesInList(connectedDevices); if (device != null) { if (mAudioManager.getMode() == AudioManager.MODE_NORMAL) { if (Objects.equals(a2dpFallbackDevice, device)) { if (DBG) { Log.d(TAG, "set A2DP device active: " + device); } setA2dpActiveDevice(device); if (headsetFallbackDevice != null) { setHfpActiveDevice(device); setLeAudioActiveDevice(null); } } else { if (DBG) { Log.d(TAG, "set LE audio device active: " + device); } setLeAudioActiveDevice(device); setA2dpActiveDevice(null); setHfpActiveDevice(null); } } else { if (Objects.equals(headsetFallbackDevice, device)) { if (DBG) { Log.d(TAG, "set HFP device active: " + device); } setHfpActiveDevice(device); if (a2dpFallbackDevice != null) { setA2dpActiveDevice(a2dpFallbackDevice); setLeAudioActiveDevice(null); } } else { if (DBG) { Log.d(TAG, "set LE audio device active: " + device); } setLeAudioActiveDevice(device); setA2dpActiveDevice(null); setHfpActiveDevice(null); } } } } private void resetState() { mA2dpConnectedDevices.clear(); mA2dpActiveDevice = null; Loading
android/app/src/com/android/bluetooth/hfp/HeadsetService.java +7 −2 Original line number Diff line number Diff line Loading @@ -1877,7 +1877,12 @@ public class HeadsetService extends ProfileService { return true; } boolean isInbandRingingEnabled() { /** * Checks if headset devices are able to get inband ringing. * * @return True if inband ringing is enabled. */ public boolean isInbandRingingEnabled() { boolean isInbandRingingSupported = getResources().getBoolean( com.android.bluetooth.R.bool.config_bluetooth_hfp_inband_ringing_support); return isInbandRingingSupported && !SystemProperties.getBoolean( Loading Loading @@ -2125,7 +2130,7 @@ public class HeadsetService extends ProfileService { /** * Retrieves the most recently connected device in the A2DP connected devices list. */ private BluetoothDevice getFallbackDevice() { public BluetoothDevice getFallbackDevice() { DatabaseManager dbManager = mAdapterService.getDatabase(); return dbManager != null ? dbManager .getMostRecentlyConnectedDevicesInList(getConnectedDevices()) Loading
android/app/tests/unit/src/com/android/bluetooth/btservice/ActiveDeviceManagerTest.java +52 −0 Original line number Diff line number Diff line Loading @@ -50,6 +50,8 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.List; @MediumTest @RunWith(AndroidJUnit4.class) public class ActiveDeviceManagerTest { Loading Loading @@ -473,6 +475,56 @@ public class ActiveDeviceManagerTest { Assert.assertEquals(null, mActiveDeviceManager.getLeAudioActiveDevice()); } /** * An LE Audio connected. An A2DP connected. The A2DP disconnected. * Then the LE Audio should be the active one. */ @Test public void leAudioAndA2dpConnectedThenA2dpDisconnected_fallbackToLeAudio() { leAudioConnected(mLeAudioDevice); verify(mLeAudioService, timeout(TIMEOUT_MS)).setActiveDevice(mLeAudioDevice); a2dpConnected(mA2dpDevice); verify(mA2dpService, timeout(TIMEOUT_MS)).setActiveDevice(mA2dpDevice); when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_NORMAL); when(mDatabaseManager.getMostRecentlyConnectedDevicesInList(any())).thenAnswer( invocation -> { List<BluetoothDevice> devices = invocation.getArgument(0); return (devices != null && devices.contains(mLeAudioDevice)) ? mLeAudioDevice : null; } ); a2dpDisconnected(mA2dpDevice); verify(mA2dpService, timeout(TIMEOUT_MS).atLeast(1)).setActiveDevice(isNull()); verify(mLeAudioService, timeout(TIMEOUT_MS).times(2)).setActiveDevice(mLeAudioDevice); } /** * An A2DP connected. An LE Audio connected. The LE Audio disconnected. * Then the A2DP should be the active one. */ @Test public void a2dpAndLeAudioConnectedThenLeAudioDisconnected_fallbackToA2dp() { a2dpConnected(mA2dpDevice); verify(mA2dpService, timeout(TIMEOUT_MS)).setActiveDevice(mA2dpDevice); leAudioConnected(mLeAudioDevice); verify(mLeAudioService, timeout(TIMEOUT_MS)).setActiveDevice(mLeAudioDevice); when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_NORMAL); when(mA2dpService.getFallbackDevice()).thenReturn(mA2dpDevice); when(mDatabaseManager.getMostRecentlyConnectedDevicesInList(any())).thenAnswer( invocation -> { List<BluetoothDevice> devices = invocation.getArgument(0); return (devices != null && devices.contains(mA2dpDevice)) ? mA2dpDevice : null; } ); leAudioDisconnected(mLeAudioDevice); verify(mLeAudioService, timeout(TIMEOUT_MS).atLeast(1)).setActiveDevice(isNull()); verify(mA2dpService, timeout(TIMEOUT_MS).times(2)).setActiveDevice(mA2dpDevice); } /** * One LE Hearing Aid is connected. */ Loading