Loading android/app/src/com/android/bluetooth/btservice/AudioRoutingManager.java +71 −107 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ 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; Loading Loading @@ -128,16 +130,8 @@ public class AudioRoutingManager extends ActiveDeviceManager { switch (profile) { case BluetoothProfile.A2DP: case BluetoothProfile.HEADSET: mHandler.post(() -> mHandler.handleDeviceConnected(device, profile)); break; case BluetoothProfile.LE_AUDIO: mHandler.post( () -> { AudioRoutingHandler.AudioRoutingDevice arDevice = mHandler.getAudioRoutingDevice(device); arDevice.connectedProfiles.add(profile); handleLeAudioConnected(device); }); mHandler.post(() -> mHandler.handleDeviceConnected(device, profile)); break; case BluetoothProfile.HEARING_AID: mHandler.post( Loading @@ -162,16 +156,8 @@ public class AudioRoutingManager extends ActiveDeviceManager { switch (profile) { case BluetoothProfile.A2DP: case BluetoothProfile.HEADSET: mHandler.post(() -> mHandler.handleDeviceDisconnected(device, profile)); break; case BluetoothProfile.LE_AUDIO: mHandler.post( () -> { AudioRoutingHandler.AudioRoutingDevice arDevice = mHandler.getAudioRoutingDevice(device); arDevice.connectedProfiles.remove(profile); handleLeAudioDisconnected(device); }); mHandler.post(() -> mHandler.handleDeviceDisconnected(device, profile)); break; case BluetoothProfile.HEARING_AID: mHandler.post( Loading Loading @@ -281,45 +267,6 @@ public class AudioRoutingManager extends ActiveDeviceManager { } } 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) { Loading Loading @@ -366,36 +313,6 @@ public class AudioRoutingManager extends ActiveDeviceManager { } } 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) { Loading Loading @@ -1176,6 +1093,7 @@ public class AudioRoutingManager extends ActiveDeviceManager { switch (profile) { case BluetoothProfile.HEADSET -> mHfpConnectedDevices.add(device); case BluetoothProfile.A2DP -> mA2dpConnectedDevices.add(device); case BluetoothProfile.LE_AUDIO -> mLeAudioConnectedDevices.add(device); } } if (isWatch(device)) { Loading Loading @@ -1213,12 +1131,15 @@ public class AudioRoutingManager extends ActiveDeviceManager { switch (profile) { case BluetoothProfile.HEADSET -> mHfpConnectedDevices.remove(device); case BluetoothProfile.A2DP -> mA2dpConnectedDevices.remove(device); case BluetoothProfile.LE_AUDIO -> mLeAudioConnectedDevices.remove(device); } } List<BluetoothDevice> activeDevices = mActiveDevices.get(profile); if (activeDevices != null && Objects.equals(device, activeDevices.get(0))) { 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); Loading @@ -1226,6 +1147,7 @@ public class AudioRoutingManager extends ActiveDeviceManager { } } } } // TODO: make getAudioRoutingDevice private public AudioRoutingDevice getAudioRoutingDevice(BluetoothDevice device) { Loading Loading @@ -1281,11 +1203,11 @@ public class AudioRoutingManager extends ActiveDeviceManager { // 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) case BluetoothProfile.HEADSET -> !supportedProfiles.contains( BluetoothProfile.A2DP) || connectedProfiles.contains(BluetoothProfile.A2DP); case BluetoothProfile.A2DP -> !supportedProfiles.contains(BluetoothProfile.HEADSET) case BluetoothProfile.A2DP -> !supportedProfiles.contains( BluetoothProfile.HEADSET) || connectedProfiles.contains(BluetoothProfile.HEADSET); default -> true; }; Loading @@ -1302,7 +1224,7 @@ public class AudioRoutingManager extends ActiveDeviceManager { @SuppressLint("MissingPermission") public boolean activate(int profile) { List<BluetoothDevice> activeDevices = mActiveDevices.get(profile); if (activeDevices != null && Objects.equals(device, activeDevices.get(0))) { if (activeDevices != null && activeDevices.contains(device)) { return true; } HashSet<Integer> profilesToActivate = new HashSet<>(); Loading @@ -1319,15 +1241,13 @@ public class AudioRoutingManager extends ActiveDeviceManager { profilesToDeactivate.remove(BluetoothProfile.HEADSET); if (connectedProfiles.contains(BluetoothProfile.HEADSET)) { activeDevices = mActiveDevices.get(BluetoothProfile.HEADSET); if (activeDevices == null || !Objects.equals(device, activeDevices.get(0))) { if (activeDevices == null || !activeDevices.contains(device)) { profilesToActivate.add(BluetoothProfile.HEADSET); } } if (Utils.isDualModeAudioEnabled()) { activeDevices = mActiveDevices.get(BluetoothProfile.LE_AUDIO); if (activeDevices != null && Objects.equals(device, activeDevices.get(0))) { if (activeDevices != null && activeDevices.contains(device)) { profilesToDeactivate.remove(BluetoothProfile.LE_AUDIO); } } Loading @@ -1336,19 +1256,28 @@ public class AudioRoutingManager extends ActiveDeviceManager { profilesToDeactivate.remove(BluetoothProfile.A2DP); if (connectedProfiles.contains(BluetoothProfile.A2DP)) { activeDevices = mActiveDevices.get(BluetoothProfile.A2DP); if (activeDevices == null || !Objects.equals(device, activeDevices.get(0))) { if (activeDevices == null || !activeDevices.contains(device)) { profilesToActivate.add(BluetoothProfile.A2DP); } } if (Utils.isDualModeAudioEnabled()) { activeDevices = mActiveDevices.get(BluetoothProfile.LE_AUDIO); if (activeDevices != null && Objects.equals(device, activeDevices.get(0))) { 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) { Loading @@ -1365,7 +1294,18 @@ public class AudioRoutingManager extends ActiveDeviceManager { device); default -> false; }; if (activated) mActiveDevices.put(p, Arrays.asList(device)); 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. Loading @@ -1389,9 +1329,33 @@ public class AudioRoutingManager extends ActiveDeviceManager { 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 (group == null || group.isEmpty()) { return false; } switch (profile) { // TODO: handle HAP_CLIENT and HEARING_AID case BluetoothProfile.LE_AUDIO: final LeAudioService leAudioService = mFactory.getLeAudioService(); if (leAudioService == null || device == null) { return false; } int groupId = leAudioService.getGroupId(device); if (groupId != LE_AUDIO_GROUP_ID_INVALID && groupId == leAudioService.getGroupId(group.get(0))) { return true; } } return false; } } } } android/app/src/com/android/bluetooth/le_audio/LeAudioService.java +10 −3 Original line number Diff line number Diff line Loading @@ -74,6 +74,8 @@ import com.android.bluetooth.btservice.ProfileService; import com.android.bluetooth.btservice.ServiceFactory; import com.android.bluetooth.btservice.storage.DatabaseManager; import com.android.bluetooth.csip.CsipSetCoordinatorService; import com.android.bluetooth.flags.FeatureFlags; import com.android.bluetooth.flags.FeatureFlagsImpl; import com.android.bluetooth.hap.HapClientService; import com.android.bluetooth.hfp.HeadsetService; import com.android.bluetooth.mcp.McpService; Loading Loading @@ -130,6 +132,7 @@ public class LeAudioService extends ProfileService { private BluetoothDevice mExposedActiveDevice; private LeAudioCodecConfig mLeAudioCodecConfig; private final Object mGroupLock = new Object(); private final FeatureFlags mFeatureFlags = new FeatureFlagsImpl(); ServiceFactory mServiceFactory = new ServiceFactory(); LeAudioNativeInterface mLeAudioNativeInterface; Loading @@ -148,7 +151,6 @@ public class LeAudioService extends ProfileService { private final LinkedList<BluetoothLeBroadcastSettings> mCreateBroadcastQueue = new LinkedList<>(); @VisibleForTesting TbsService mTbsService; Loading Loading @@ -2410,8 +2412,13 @@ public class LeAudioService extends ProfileService { Log.d(TAG, "Creating a new state machine for " + device); } sm = LeAudioStateMachine.make(device, this, mLeAudioNativeInterface, mStateMachinesThread.getLooper()); sm = LeAudioStateMachine.make( device, this, mLeAudioNativeInterface, mStateMachinesThread.getLooper(), mFeatureFlags); descriptor.mStateMachine = sm; return sm; } Loading android/app/src/com/android/bluetooth/le_audio/LeAudioStateMachine.java +24 −6 Original line number Diff line number Diff line Loading @@ -53,6 +53,7 @@ import android.os.Message; import android.util.Log; import com.android.bluetooth.btservice.ProfileService; import com.android.bluetooth.flags.FeatureFlags; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.State; import com.android.internal.util.StateMachine; Loading Loading @@ -87,13 +88,19 @@ final class LeAudioStateMachine extends StateMachine { private LeAudioNativeInterface mNativeInterface; private final BluetoothDevice mDevice; LeAudioStateMachine(BluetoothDevice device, LeAudioService svc, LeAudioNativeInterface nativeInterface, Looper looper) { private final FeatureFlags mFeatureFlags; LeAudioStateMachine( BluetoothDevice device, LeAudioService svc, LeAudioNativeInterface nativeInterface, Looper looper, FeatureFlags featureFlags) { super(TAG, looper); mDevice = device; mService = svc; mNativeInterface = nativeInterface; mFeatureFlags = featureFlags; mDisconnected = new Disconnected(); mConnecting = new Connecting(); Loading @@ -108,10 +115,15 @@ final class LeAudioStateMachine extends StateMachine { setInitialState(mDisconnected); } static LeAudioStateMachine make(BluetoothDevice device, LeAudioService svc, LeAudioNativeInterface nativeInterface, Looper looper) { static LeAudioStateMachine make( BluetoothDevice device, LeAudioService svc, LeAudioNativeInterface nativeInterface, Looper looper, FeatureFlags featureFlags) { Log.i(TAG, "make for device"); LeAudioStateMachine LeAudioSm = new LeAudioStateMachine(device, svc, nativeInterface, looper); LeAudioStateMachine LeAudioSm = new LeAudioStateMachine(device, svc, nativeInterface, looper, featureFlags); LeAudioSm.start(); return LeAudioSm; } Loading Loading @@ -139,6 +151,9 @@ final class LeAudioStateMachine extends StateMachine { // Don't broadcast during startup broadcastConnectionState(BluetoothProfile.STATE_DISCONNECTED, mLastConnectionState); if (mFeatureFlags.audioRoutingCentralization()) { mService.deviceDisconnected(mDevice, false); } } } Loading Loading @@ -426,6 +441,9 @@ final class LeAudioStateMachine extends StateMachine { + messageWhatToString(getCurrentMessage().what)); mConnectionState = BluetoothProfile.STATE_CONNECTED; removeDeferredMessages(CONNECT); if (mFeatureFlags.audioRoutingCentralization()) { mService.deviceConnected(mDevice); } broadcastConnectionState(BluetoothProfile.STATE_CONNECTED, mLastConnectionState); } Loading android/app/tests/unit/src/com/android/bluetooth/btservice/AudioRoutingManagerTest.java +6 −6 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package com.android.bluetooth.btservice; import static android.bluetooth.IBluetoothLeAudio.LE_AUDIO_GROUP_ID_INVALID; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.anyBoolean; Loading Loading @@ -150,6 +152,7 @@ public class AudioRoutingManagerTest { when(mHearingAidService.setActiveDevice(any())).thenReturn(true); when(mLeAudioService.setActiveDevice(any())).thenReturn(true); when(mLeAudioService.removeActiveDevice(anyBoolean())).thenReturn(true); when(mLeAudioService.getGroupId(any())).thenReturn(LE_AUDIO_GROUP_ID_INVALID); List<BluetoothDevice> connectedHearingAidDevices = new ArrayList<>(); connectedHearingAidDevices.add(mHearingAidDevice); Loading Loading @@ -782,7 +785,6 @@ public class AudioRoutingManagerTest { verify(mLeAudioService, never()).removeActiveDevice(false); verify(mLeAudioService, never()).setActiveDevice(mLeAudioDevice2); verify(mLeAudioService, timeout(TIMEOUT_MS)).deviceDisconnected(mLeAudioDevice, false); } /** Loading @@ -809,7 +811,6 @@ public class AudioRoutingManagerTest { leAudioDisconnected(mLeAudioDevice2); verify(mLeAudioService, timeout(TIMEOUT_MS)).removeActiveDevice(false); verify(mLeAudioService, timeout(TIMEOUT_MS)).deviceDisconnected(mLeAudioDevice2, false); } /** Loading @@ -832,7 +833,6 @@ public class AudioRoutingManagerTest { leAudioDisconnected(mLeAudioDevice2); verify(mLeAudioService, timeout(TIMEOUT_MS)).setActiveDevice(mLeAudioDevice); verify(mLeAudioService, timeout(TIMEOUT_MS)).deviceDisconnected(mLeAudioDevice2, true); } /** Loading Loading @@ -890,16 +890,16 @@ public class AudioRoutingManagerTest { verify(mLeAudioService, timeout(TIMEOUT_MS)).setActiveDevice(mLeHearingAidDevice); } /** LE audio is connected after LE Hearing Aid device. Keep LE hearing Aid active. */ /** LE audio is connected after LE Hearing Aid device. LE audio active. */ @Test public void leAudioConnectedAfterLeHearingAid_setLeAudioActiveShouldNotBeCalled() { public void leAudioConnectedAfterLeHearingAid_callsSetLeAudioActive() { leHearingAidConnected(mLeHearingAidDevice); leAudioConnected(mLeHearingAidDevice); verify(mLeAudioService, timeout(TIMEOUT_MS)).setActiveDevice(mLeHearingAidDevice); leAudioConnected(mLeAudioDevice); TestUtils.waitForLooperToFinishScheduledTask(mAudioRoutingManager.getHandlerLooper()); verify(mLeAudioService, never()).setActiveDevice(mLeAudioDevice); verify(mLeAudioService).setActiveDevice(mLeAudioDevice); } /** Loading android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioStateMachineTest.java +13 −4 Original line number Diff line number Diff line Loading @@ -42,6 +42,8 @@ import androidx.test.runner.AndroidJUnit4; import com.android.bluetooth.TestUtils; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.flags.FakeFeatureFlagsImpl; import com.android.bluetooth.flags.Flags; import org.junit.After; import org.junit.Before; Loading @@ -58,6 +60,7 @@ public class LeAudioStateMachineTest { private HandlerThread mHandlerThread; private LeAudioStateMachine mLeAudioStateMachine; private BluetoothDevice mTestDevice; private FakeFeatureFlagsImpl mFakeFlagsImpl; private static final int TIMEOUT_MS = 1000; @Mock private AdapterService mAdapterService; Loading @@ -72,6 +75,8 @@ public class LeAudioStateMachineTest { TestUtils.setAdapterService(mAdapterService); mAdapter = BluetoothAdapter.getDefaultAdapter(); mFakeFlagsImpl = new FakeFeatureFlagsImpl(); mFakeFlagsImpl.setFlag(Flags.FLAG_AUDIO_ROUTING_CENTRALIZATION, false); // Get a device for testing mTestDevice = mAdapter.getRemoteDevice("00:01:02:03:04:05"); Loading @@ -79,11 +84,15 @@ public class LeAudioStateMachineTest { // Set up thread and looper mHandlerThread = new HandlerThread("LeAudioStateMachineTestHandlerThread"); mHandlerThread.start(); mLeAudioStateMachine = new LeAudioStateMachine(mTestDevice, mLeAudioService, mLeAudioNativeInterface, mHandlerThread.getLooper()); // Override the timeout value to speed up the test mLeAudioStateMachine.sConnectTimeoutMs = 1000; // 1s mLeAudioStateMachine.start(); LeAudioStateMachine.sConnectTimeoutMs = 1000; // 1s mLeAudioStateMachine = LeAudioStateMachine.make( mTestDevice, mLeAudioService, mLeAudioNativeInterface, mHandlerThread.getLooper(), mFakeFlagsImpl); } @After Loading Loading
android/app/src/com/android/bluetooth/btservice/AudioRoutingManager.java +71 −107 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ 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; Loading Loading @@ -128,16 +130,8 @@ public class AudioRoutingManager extends ActiveDeviceManager { switch (profile) { case BluetoothProfile.A2DP: case BluetoothProfile.HEADSET: mHandler.post(() -> mHandler.handleDeviceConnected(device, profile)); break; case BluetoothProfile.LE_AUDIO: mHandler.post( () -> { AudioRoutingHandler.AudioRoutingDevice arDevice = mHandler.getAudioRoutingDevice(device); arDevice.connectedProfiles.add(profile); handleLeAudioConnected(device); }); mHandler.post(() -> mHandler.handleDeviceConnected(device, profile)); break; case BluetoothProfile.HEARING_AID: mHandler.post( Loading @@ -162,16 +156,8 @@ public class AudioRoutingManager extends ActiveDeviceManager { switch (profile) { case BluetoothProfile.A2DP: case BluetoothProfile.HEADSET: mHandler.post(() -> mHandler.handleDeviceDisconnected(device, profile)); break; case BluetoothProfile.LE_AUDIO: mHandler.post( () -> { AudioRoutingHandler.AudioRoutingDevice arDevice = mHandler.getAudioRoutingDevice(device); arDevice.connectedProfiles.remove(profile); handleLeAudioDisconnected(device); }); mHandler.post(() -> mHandler.handleDeviceDisconnected(device, profile)); break; case BluetoothProfile.HEARING_AID: mHandler.post( Loading Loading @@ -281,45 +267,6 @@ public class AudioRoutingManager extends ActiveDeviceManager { } } 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) { Loading Loading @@ -366,36 +313,6 @@ public class AudioRoutingManager extends ActiveDeviceManager { } } 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) { Loading Loading @@ -1176,6 +1093,7 @@ public class AudioRoutingManager extends ActiveDeviceManager { switch (profile) { case BluetoothProfile.HEADSET -> mHfpConnectedDevices.add(device); case BluetoothProfile.A2DP -> mA2dpConnectedDevices.add(device); case BluetoothProfile.LE_AUDIO -> mLeAudioConnectedDevices.add(device); } } if (isWatch(device)) { Loading Loading @@ -1213,12 +1131,15 @@ public class AudioRoutingManager extends ActiveDeviceManager { switch (profile) { case BluetoothProfile.HEADSET -> mHfpConnectedDevices.remove(device); case BluetoothProfile.A2DP -> mA2dpConnectedDevices.remove(device); case BluetoothProfile.LE_AUDIO -> mLeAudioConnectedDevices.remove(device); } } List<BluetoothDevice> activeDevices = mActiveDevices.get(profile); if (activeDevices != null && Objects.equals(device, activeDevices.get(0))) { 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); Loading @@ -1226,6 +1147,7 @@ public class AudioRoutingManager extends ActiveDeviceManager { } } } } // TODO: make getAudioRoutingDevice private public AudioRoutingDevice getAudioRoutingDevice(BluetoothDevice device) { Loading Loading @@ -1281,11 +1203,11 @@ public class AudioRoutingManager extends ActiveDeviceManager { // 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) case BluetoothProfile.HEADSET -> !supportedProfiles.contains( BluetoothProfile.A2DP) || connectedProfiles.contains(BluetoothProfile.A2DP); case BluetoothProfile.A2DP -> !supportedProfiles.contains(BluetoothProfile.HEADSET) case BluetoothProfile.A2DP -> !supportedProfiles.contains( BluetoothProfile.HEADSET) || connectedProfiles.contains(BluetoothProfile.HEADSET); default -> true; }; Loading @@ -1302,7 +1224,7 @@ public class AudioRoutingManager extends ActiveDeviceManager { @SuppressLint("MissingPermission") public boolean activate(int profile) { List<BluetoothDevice> activeDevices = mActiveDevices.get(profile); if (activeDevices != null && Objects.equals(device, activeDevices.get(0))) { if (activeDevices != null && activeDevices.contains(device)) { return true; } HashSet<Integer> profilesToActivate = new HashSet<>(); Loading @@ -1319,15 +1241,13 @@ public class AudioRoutingManager extends ActiveDeviceManager { profilesToDeactivate.remove(BluetoothProfile.HEADSET); if (connectedProfiles.contains(BluetoothProfile.HEADSET)) { activeDevices = mActiveDevices.get(BluetoothProfile.HEADSET); if (activeDevices == null || !Objects.equals(device, activeDevices.get(0))) { if (activeDevices == null || !activeDevices.contains(device)) { profilesToActivate.add(BluetoothProfile.HEADSET); } } if (Utils.isDualModeAudioEnabled()) { activeDevices = mActiveDevices.get(BluetoothProfile.LE_AUDIO); if (activeDevices != null && Objects.equals(device, activeDevices.get(0))) { if (activeDevices != null && activeDevices.contains(device)) { profilesToDeactivate.remove(BluetoothProfile.LE_AUDIO); } } Loading @@ -1336,19 +1256,28 @@ public class AudioRoutingManager extends ActiveDeviceManager { profilesToDeactivate.remove(BluetoothProfile.A2DP); if (connectedProfiles.contains(BluetoothProfile.A2DP)) { activeDevices = mActiveDevices.get(BluetoothProfile.A2DP); if (activeDevices == null || !Objects.equals(device, activeDevices.get(0))) { if (activeDevices == null || !activeDevices.contains(device)) { profilesToActivate.add(BluetoothProfile.A2DP); } } if (Utils.isDualModeAudioEnabled()) { activeDevices = mActiveDevices.get(BluetoothProfile.LE_AUDIO); if (activeDevices != null && Objects.equals(device, activeDevices.get(0))) { 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) { Loading @@ -1365,7 +1294,18 @@ public class AudioRoutingManager extends ActiveDeviceManager { device); default -> false; }; if (activated) mActiveDevices.put(p, Arrays.asList(device)); 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. Loading @@ -1389,9 +1329,33 @@ public class AudioRoutingManager extends ActiveDeviceManager { 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 (group == null || group.isEmpty()) { return false; } switch (profile) { // TODO: handle HAP_CLIENT and HEARING_AID case BluetoothProfile.LE_AUDIO: final LeAudioService leAudioService = mFactory.getLeAudioService(); if (leAudioService == null || device == null) { return false; } int groupId = leAudioService.getGroupId(device); if (groupId != LE_AUDIO_GROUP_ID_INVALID && groupId == leAudioService.getGroupId(group.get(0))) { return true; } } return false; } } } }
android/app/src/com/android/bluetooth/le_audio/LeAudioService.java +10 −3 Original line number Diff line number Diff line Loading @@ -74,6 +74,8 @@ import com.android.bluetooth.btservice.ProfileService; import com.android.bluetooth.btservice.ServiceFactory; import com.android.bluetooth.btservice.storage.DatabaseManager; import com.android.bluetooth.csip.CsipSetCoordinatorService; import com.android.bluetooth.flags.FeatureFlags; import com.android.bluetooth.flags.FeatureFlagsImpl; import com.android.bluetooth.hap.HapClientService; import com.android.bluetooth.hfp.HeadsetService; import com.android.bluetooth.mcp.McpService; Loading Loading @@ -130,6 +132,7 @@ public class LeAudioService extends ProfileService { private BluetoothDevice mExposedActiveDevice; private LeAudioCodecConfig mLeAudioCodecConfig; private final Object mGroupLock = new Object(); private final FeatureFlags mFeatureFlags = new FeatureFlagsImpl(); ServiceFactory mServiceFactory = new ServiceFactory(); LeAudioNativeInterface mLeAudioNativeInterface; Loading @@ -148,7 +151,6 @@ public class LeAudioService extends ProfileService { private final LinkedList<BluetoothLeBroadcastSettings> mCreateBroadcastQueue = new LinkedList<>(); @VisibleForTesting TbsService mTbsService; Loading Loading @@ -2410,8 +2412,13 @@ public class LeAudioService extends ProfileService { Log.d(TAG, "Creating a new state machine for " + device); } sm = LeAudioStateMachine.make(device, this, mLeAudioNativeInterface, mStateMachinesThread.getLooper()); sm = LeAudioStateMachine.make( device, this, mLeAudioNativeInterface, mStateMachinesThread.getLooper(), mFeatureFlags); descriptor.mStateMachine = sm; return sm; } Loading
android/app/src/com/android/bluetooth/le_audio/LeAudioStateMachine.java +24 −6 Original line number Diff line number Diff line Loading @@ -53,6 +53,7 @@ import android.os.Message; import android.util.Log; import com.android.bluetooth.btservice.ProfileService; import com.android.bluetooth.flags.FeatureFlags; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.State; import com.android.internal.util.StateMachine; Loading Loading @@ -87,13 +88,19 @@ final class LeAudioStateMachine extends StateMachine { private LeAudioNativeInterface mNativeInterface; private final BluetoothDevice mDevice; LeAudioStateMachine(BluetoothDevice device, LeAudioService svc, LeAudioNativeInterface nativeInterface, Looper looper) { private final FeatureFlags mFeatureFlags; LeAudioStateMachine( BluetoothDevice device, LeAudioService svc, LeAudioNativeInterface nativeInterface, Looper looper, FeatureFlags featureFlags) { super(TAG, looper); mDevice = device; mService = svc; mNativeInterface = nativeInterface; mFeatureFlags = featureFlags; mDisconnected = new Disconnected(); mConnecting = new Connecting(); Loading @@ -108,10 +115,15 @@ final class LeAudioStateMachine extends StateMachine { setInitialState(mDisconnected); } static LeAudioStateMachine make(BluetoothDevice device, LeAudioService svc, LeAudioNativeInterface nativeInterface, Looper looper) { static LeAudioStateMachine make( BluetoothDevice device, LeAudioService svc, LeAudioNativeInterface nativeInterface, Looper looper, FeatureFlags featureFlags) { Log.i(TAG, "make for device"); LeAudioStateMachine LeAudioSm = new LeAudioStateMachine(device, svc, nativeInterface, looper); LeAudioStateMachine LeAudioSm = new LeAudioStateMachine(device, svc, nativeInterface, looper, featureFlags); LeAudioSm.start(); return LeAudioSm; } Loading Loading @@ -139,6 +151,9 @@ final class LeAudioStateMachine extends StateMachine { // Don't broadcast during startup broadcastConnectionState(BluetoothProfile.STATE_DISCONNECTED, mLastConnectionState); if (mFeatureFlags.audioRoutingCentralization()) { mService.deviceDisconnected(mDevice, false); } } } Loading Loading @@ -426,6 +441,9 @@ final class LeAudioStateMachine extends StateMachine { + messageWhatToString(getCurrentMessage().what)); mConnectionState = BluetoothProfile.STATE_CONNECTED; removeDeferredMessages(CONNECT); if (mFeatureFlags.audioRoutingCentralization()) { mService.deviceConnected(mDevice); } broadcastConnectionState(BluetoothProfile.STATE_CONNECTED, mLastConnectionState); } Loading
android/app/tests/unit/src/com/android/bluetooth/btservice/AudioRoutingManagerTest.java +6 −6 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package com.android.bluetooth.btservice; import static android.bluetooth.IBluetoothLeAudio.LE_AUDIO_GROUP_ID_INVALID; import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.anyBoolean; Loading Loading @@ -150,6 +152,7 @@ public class AudioRoutingManagerTest { when(mHearingAidService.setActiveDevice(any())).thenReturn(true); when(mLeAudioService.setActiveDevice(any())).thenReturn(true); when(mLeAudioService.removeActiveDevice(anyBoolean())).thenReturn(true); when(mLeAudioService.getGroupId(any())).thenReturn(LE_AUDIO_GROUP_ID_INVALID); List<BluetoothDevice> connectedHearingAidDevices = new ArrayList<>(); connectedHearingAidDevices.add(mHearingAidDevice); Loading Loading @@ -782,7 +785,6 @@ public class AudioRoutingManagerTest { verify(mLeAudioService, never()).removeActiveDevice(false); verify(mLeAudioService, never()).setActiveDevice(mLeAudioDevice2); verify(mLeAudioService, timeout(TIMEOUT_MS)).deviceDisconnected(mLeAudioDevice, false); } /** Loading @@ -809,7 +811,6 @@ public class AudioRoutingManagerTest { leAudioDisconnected(mLeAudioDevice2); verify(mLeAudioService, timeout(TIMEOUT_MS)).removeActiveDevice(false); verify(mLeAudioService, timeout(TIMEOUT_MS)).deviceDisconnected(mLeAudioDevice2, false); } /** Loading @@ -832,7 +833,6 @@ public class AudioRoutingManagerTest { leAudioDisconnected(mLeAudioDevice2); verify(mLeAudioService, timeout(TIMEOUT_MS)).setActiveDevice(mLeAudioDevice); verify(mLeAudioService, timeout(TIMEOUT_MS)).deviceDisconnected(mLeAudioDevice2, true); } /** Loading Loading @@ -890,16 +890,16 @@ public class AudioRoutingManagerTest { verify(mLeAudioService, timeout(TIMEOUT_MS)).setActiveDevice(mLeHearingAidDevice); } /** LE audio is connected after LE Hearing Aid device. Keep LE hearing Aid active. */ /** LE audio is connected after LE Hearing Aid device. LE audio active. */ @Test public void leAudioConnectedAfterLeHearingAid_setLeAudioActiveShouldNotBeCalled() { public void leAudioConnectedAfterLeHearingAid_callsSetLeAudioActive() { leHearingAidConnected(mLeHearingAidDevice); leAudioConnected(mLeHearingAidDevice); verify(mLeAudioService, timeout(TIMEOUT_MS)).setActiveDevice(mLeHearingAidDevice); leAudioConnected(mLeAudioDevice); TestUtils.waitForLooperToFinishScheduledTask(mAudioRoutingManager.getHandlerLooper()); verify(mLeAudioService, never()).setActiveDevice(mLeAudioDevice); verify(mLeAudioService).setActiveDevice(mLeAudioDevice); } /** Loading
android/app/tests/unit/src/com/android/bluetooth/le_audio/LeAudioStateMachineTest.java +13 −4 Original line number Diff line number Diff line Loading @@ -42,6 +42,8 @@ import androidx.test.runner.AndroidJUnit4; import com.android.bluetooth.TestUtils; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.flags.FakeFeatureFlagsImpl; import com.android.bluetooth.flags.Flags; import org.junit.After; import org.junit.Before; Loading @@ -58,6 +60,7 @@ public class LeAudioStateMachineTest { private HandlerThread mHandlerThread; private LeAudioStateMachine mLeAudioStateMachine; private BluetoothDevice mTestDevice; private FakeFeatureFlagsImpl mFakeFlagsImpl; private static final int TIMEOUT_MS = 1000; @Mock private AdapterService mAdapterService; Loading @@ -72,6 +75,8 @@ public class LeAudioStateMachineTest { TestUtils.setAdapterService(mAdapterService); mAdapter = BluetoothAdapter.getDefaultAdapter(); mFakeFlagsImpl = new FakeFeatureFlagsImpl(); mFakeFlagsImpl.setFlag(Flags.FLAG_AUDIO_ROUTING_CENTRALIZATION, false); // Get a device for testing mTestDevice = mAdapter.getRemoteDevice("00:01:02:03:04:05"); Loading @@ -79,11 +84,15 @@ public class LeAudioStateMachineTest { // Set up thread and looper mHandlerThread = new HandlerThread("LeAudioStateMachineTestHandlerThread"); mHandlerThread.start(); mLeAudioStateMachine = new LeAudioStateMachine(mTestDevice, mLeAudioService, mLeAudioNativeInterface, mHandlerThread.getLooper()); // Override the timeout value to speed up the test mLeAudioStateMachine.sConnectTimeoutMs = 1000; // 1s mLeAudioStateMachine.start(); LeAudioStateMachine.sConnectTimeoutMs = 1000; // 1s mLeAudioStateMachine = LeAudioStateMachine.make( mTestDevice, mLeAudioService, mLeAudioNativeInterface, mHandlerThread.getLooper(), mFakeFlagsImpl); } @After Loading