Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit df90cbdb authored by Sungsoo Lim's avatar Sungsoo Lim
Browse files

Refactoring: Audio Routing Handler (Step 2)

This CL refactors connection and activation events to be handled in
the AudioRoutingHandler. This will remove the mLock and migrate the
variables guarded by the mLock into the handler.

It will also remove the priority of hearing aid devices so that they
are activated at the same level as other BT devices.

This CL refactors the connection events of LE audio, and more CLs
will follow.

Bug: 299023147
Test: atest BluetoothInstrumentationTests
Change-Id: Ibe9e894bd95920887dd8395e661b6d3b187c0a82
parent 0456d4ea
Loading
Loading
Loading
Loading
+71 −107
Original line number Diff line number Diff line
@@ -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;
@@ -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(
@@ -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(
@@ -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) {
@@ -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) {
@@ -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)) {
@@ -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);
@@ -1226,6 +1147,7 @@ public class AudioRoutingManager extends ActiveDeviceManager {
                    }
                }
            }
        }

        // TODO: make getAudioRoutingDevice private
        public AudioRoutingDevice getAudioRoutingDevice(BluetoothDevice device) {
@@ -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;
                };
@@ -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<>();
@@ -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);
                            }
                        }
@@ -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) {
@@ -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.
@@ -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;
            }
        }
    }
}
+10 −3
Original line number Diff line number Diff line
@@ -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;
@@ -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;
@@ -148,7 +151,6 @@ public class LeAudioService extends ProfileService {
    private final LinkedList<BluetoothLeBroadcastSettings> mCreateBroadcastQueue =
            new LinkedList<>();


    @VisibleForTesting
    TbsService mTbsService;

@@ -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;
    }
+24 −6
Original line number Diff line number Diff line
@@ -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;
@@ -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();
@@ -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;
    }
@@ -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);
                }
            }
        }

@@ -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);
        }

+6 −6
Original line number Diff line number Diff line
@@ -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;
@@ -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);
@@ -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);
    }

    /**
@@ -809,7 +811,6 @@ public class AudioRoutingManagerTest {
        leAudioDisconnected(mLeAudioDevice2);

        verify(mLeAudioService, timeout(TIMEOUT_MS)).removeActiveDevice(false);
        verify(mLeAudioService, timeout(TIMEOUT_MS)).deviceDisconnected(mLeAudioDevice2, false);
    }

    /**
@@ -832,7 +833,6 @@ public class AudioRoutingManagerTest {
        leAudioDisconnected(mLeAudioDevice2);

        verify(mLeAudioService, timeout(TIMEOUT_MS)).setActiveDevice(mLeAudioDevice);
        verify(mLeAudioService, timeout(TIMEOUT_MS)).deviceDisconnected(mLeAudioDevice2, true);
    }

    /**
@@ -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);
    }

    /**
+13 −4
Original line number Diff line number Diff line
@@ -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;
@@ -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;
@@ -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");
@@ -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