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

Commit 1fe5f59c authored by Sungsoo Lim's avatar Sungsoo Lim Committed by Gerrit Code Review
Browse files

Merge "Refactoring: Audio Routing Handler (Step 5)" into main

parents 90810b2f bee68ad5
Loading
Loading
Loading
Loading
+80 −143
Original line number Diff line number Diff line
@@ -118,9 +118,9 @@ public class AudioRoutingManager extends ActiveDeviceManager {
    public void profileConnectionStateChanged(
            int profile, BluetoothDevice device, int fromState, int toState) {
        if (toState == BluetoothProfile.STATE_CONNECTED) {
            mHandler.post(() -> mHandler.handleDeviceConnected(device, profile));
            mHandler.post(() -> mHandler.handleProfileConnected(profile, device));
        } else if (fromState == BluetoothProfile.STATE_CONNECTED) {
            mHandler.post(() -> mHandler.handleDeviceDisconnected(device, profile));
            mHandler.post(() -> mHandler.handleProfileDisconnected(profile, device));
        }
    }

@@ -640,125 +640,6 @@ public class AudioRoutingManager extends ActiveDeviceManager {
        return true;
    }

    /**
     * TODO: This method can return true when a fallback device for an unrelated profile is found.
     * Take disconnected profile as an argument, and find the exact fallback device. Also, split
     * this method to smaller methods for better readability.
     *
     * @return true when the fallback device is activated, false otherwise
     */
    @GuardedBy("mLock")
    private boolean setFallbackDeviceActiveLocked() {
        if (DBG) {
            Log.d(TAG, "setFallbackDeviceActive");
        }
        mDbManager = mAdapterService.getDatabase();
        List<BluetoothDevice> connectedHearingAidDevices = new ArrayList<>();
        if (!mHearingAidConnectedDevices.isEmpty()) {
            connectedHearingAidDevices.addAll(mHearingAidConnectedDevices);
        }
        if (!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);
                    }
                    setHearingAidActiveDevice(null, true);
                    setA2dpActiveDevice(null, true);
                    setHfpActiveDevice(null);
                }
                return true;
            }
        }

        List<BluetoothDevice> hfpFallbackCandidates = removeWatchDevices(mHfpConnectedDevices);
        List<BluetoothDevice> fallbackCandidates = new ArrayList<>();
        fallbackCandidates.addAll(mLeAudioConnectedDevices);

        if (mAudioManager.getMode() == AudioManager.MODE_NORMAL) {
            fallbackCandidates.addAll(mA2dpConnectedDevices);
        } else {
            fallbackCandidates.addAll(hfpFallbackCandidates);
        }
        BluetoothDevice device =
                mDbManager.getMostRecentlyConnectedDevicesInList(fallbackCandidates);
        if (device != null) {
            if (mAudioManager.getMode() == AudioManager.MODE_NORMAL) {
                if (mA2dpConnectedDevices.contains(device)) {
                    if (DBG) {
                        Log.d(TAG, "Found an A2DP fallback device: " + device);
                    }
                    setA2dpActiveDevice(device);
                    if (hfpFallbackCandidates.contains(device)) {
                        setHfpActiveDevice(device);
                    } else {
                        setHfpActiveDevice(null);
                    }
                    /* If dual mode is enabled, LEA will be made active once all supported
                    classic audio profiles are made active for the device. */
                    if (!Utils.isDualModeAudioEnabled()) {
                        setLeAudioActiveDevice(null, true);
                    }
                    setHearingAidActiveDevice(null, true);
                } else {
                    if (DBG) {
                        Log.d(TAG, "Found a LE audio fallback device: " + device);
                    }
                    if (!setLeAudioActiveDevice(device)) {
                        return false;
                    }

                    if (!Utils.isDualModeAudioEnabled()) {
                        setA2dpActiveDevice(null, true);
                        setHfpActiveDevice(null);
                    }
                    setHearingAidActiveDevice(null, true);
                }
            } else {
                if (hfpFallbackCandidates.contains(device)) {
                    if (DBG) {
                        Log.d(TAG, "Found a HFP fallback device: " + device);
                    }
                    setHfpActiveDevice(device);
                    if (mA2dpConnectedDevices.contains(device)) {
                        setA2dpActiveDevice(device);
                    } else {
                        setA2dpActiveDevice(null, true);
                    }
                    if (!Utils.isDualModeAudioEnabled()) {
                        setLeAudioActiveDevice(null, true);
                    }
                    setHearingAidActiveDevice(null, true);
                } else {
                    if (DBG) {
                        Log.d(TAG, "Found a LE audio fallback device: " + device);
                    }
                    setLeAudioActiveDevice(device);
                    if (!Utils.isDualModeAudioEnabled()) {
                        setA2dpActiveDevice(null, true);
                        setHfpActiveDevice(null);
                    }
                    setHearingAidActiveDevice(null, true);
                }
            }
            return true;
        }

        if (DBG) {
            Log.d(TAG, "No fallback devices are found");
        }
        return false;
    }

    private void resetState() {
        synchronized (mLock) {
@@ -894,11 +775,11 @@ public class AudioRoutingManager extends ActiveDeviceManager {
            super(looper);
        }

        public void handleDeviceConnected(BluetoothDevice device, int profile) {
        public void handleProfileConnected(int profile, BluetoothDevice device) {
            if (DBG) {
                Log.d(
                        TAG,
                        "handleDeviceConnected(device="
                        "handleProfileConnected(device="
                                + device
                                + ", profile="
                                + BluetoothProfile.getProfileName(profile)
@@ -930,17 +811,18 @@ public class AudioRoutingManager extends ActiveDeviceManager {
                    Log.d(TAG, "Can not activate now: " + BluetoothProfile.getProfileName(profile));
                }
                mHandler.postDelayed(
                        () -> arDevice.activate(profile), A2DP_HFP_SYNC_CONNECTION_TIMEOUT_MS);
                        () -> arDevice.activateProfile(profile),
                        A2DP_HFP_SYNC_CONNECTION_TIMEOUT_MS);
                return;
            }
            arDevice.activate(profile);
            arDevice.activateProfile(profile);
        }

        public void handleDeviceDisconnected(BluetoothDevice device, int profile) {
        public void handleProfileDisconnected(int profile, BluetoothDevice device) {
            if (DBG) {
                Log.d(
                        TAG,
                        "handleDeviceDisconnected(device="
                        "handleProfileDisconnected(device="
                                + device
                                + ", profile="
                                + BluetoothProfile.getProfileName(profile)
@@ -962,29 +844,56 @@ public class AudioRoutingManager extends ActiveDeviceManager {
            }
            List<BluetoothDevice> activeDevices = mActiveDevices.get(profile);
            if (activeDevices != null && activeDevices.contains(device)) {
                // TODO: move setFallbackDeviceActiveLocked into AudioRoutingHandler
                //  and update mConnectedDevices
                activeDevices.remove(device);
                if (activeDevices.size() == 0) {
                    synchronized (mLock) {
                        if (!setFallbackDeviceActiveLocked()) {
                    if (!setFallbackDeviceActive()) {
                        arDevice.deactivate(profile, false);
                    }
                }
            }
        }

        private boolean setFallbackDeviceActive() {
            if (DBG) {
                Log.d(TAG, "setFallbackDeviceActive");
            }
            List<BluetoothDevice> candidates = new ArrayList<>();
            int audioMode = mAudioManager.getMode();
            for (AudioRoutingDevice arDevice : mConnectedDevices.values()) {
                for (int profile : arDevice.connectedProfiles) {
                    if (audioMode == AudioManager.MODE_NORMAL) {
                        if (profile != BluetoothProfile.HEADSET) {
                            candidates.add(arDevice.device);
                            break;
                        }
                    } else {
                        if (profile != BluetoothProfile.A2DP) {
                            candidates.add(arDevice.device);
                            break;
                        }
                    }
                }
            }
            AudioRoutingDevice deviceToActivate =
                    getAudioRoutingDevice(
                            mDbManager.getMostRecentlyConnectedDevicesInList(candidates));
            if (deviceToActivate != null) {
                return deviceToActivate.activateDevice();
            }
            return false;
        }

        // TODO: make getAudioRoutingDevice private
        // TODO: handle the connection policy change events.
        public AudioRoutingDevice getAudioRoutingDevice(BluetoothDevice device) {
            AudioRoutingDevice arDevice = mConnectedDevices.get(device);
            if (arDevice == null) {
            if (arDevice != null) {
                return arDevice;
            }
            arDevice = new AudioRoutingDevice();
            arDevice.device = device;
            arDevice.supportedProfiles = new HashSet<>();
            arDevice.connectedProfiles = new HashSet<>();
                mConnectedDevices.put(device, arDevice);
            }
            if (mDbManager.getProfileConnectionPolicy(device, BluetoothProfile.HEADSET)
                    == BluetoothProfile.CONNECTION_POLICY_ALLOWED) {
                arDevice.supportedProfiles.add(BluetoothProfile.HEADSET);
@@ -1009,6 +918,7 @@ public class AudioRoutingManager extends ActiveDeviceManager {
            } else {
                arDevice.supportedProfiles.remove(BluetoothProfile.LE_AUDIO);
            }
            mConnectedDevices.put(device, arDevice);
            return arDevice;
        }

@@ -1033,6 +943,33 @@ public class AudioRoutingManager extends ActiveDeviceManager {
                };
            }

            /**
             * Activate the device. If supported, this will activate hearing aid and LE audio first,
             * then A2DP and HFP.
             *
             * @return true if any profile was activated.
             */
            public boolean activateDevice() {
                if (DBG) {
                    Log.d(TAG, "activateDevice: device=" + device);
                }

                // Try to activate hearing aid and LE audio first
                if (connectedProfiles.contains(BluetoothProfile.HEARING_AID)) {
                    return activateProfile(BluetoothProfile.HEARING_AID);
                } else if (connectedProfiles.contains(BluetoothProfile.LE_AUDIO)) {
                    return activateProfile(BluetoothProfile.LE_AUDIO);
                } else if (connectedProfiles.contains(BluetoothProfile.A2DP)) {
                    return activateProfile(BluetoothProfile.A2DP);
                } else if (connectedProfiles.contains(BluetoothProfile.HEADSET)) {
                    return activateProfile(BluetoothProfile.HEADSET);
                }
                Log.w(
                        TAG,
                        "Fail to activate the device: " + device + ", no connected audio profiles");
                return false;
            }

            /**
             * Activate the given profile and related profiles if possible. A2DP and HFP would be
             * activated together if possible. If there are any activated profiles that can't be
@@ -1042,7 +979,7 @@ public class AudioRoutingManager extends ActiveDeviceManager {
             * @return true if any profile was activated or the given profile was already active.
             */
            @SuppressLint("MissingPermission")
            public boolean activate(int profile) {
            public boolean activateProfile(int profile) {
                List<BluetoothDevice> activeDevices = mActiveDevices.get(profile);
                if (activeDevices != null && activeDevices.contains(device)) {
                    return true;
@@ -1102,14 +1039,13 @@ public class AudioRoutingManager extends ActiveDeviceManager {
                boolean isAnyProfileActivated = false;
                for (Integer p : profilesToActivate) {
                    if (DBG) {
                        Log.d(TAG, "Activate profile: " + p);
                        Log.d(TAG, "Activate profile: " + BluetoothProfile.getProfileName(p));
                    }
                    boolean activated = switch (p) {
                        case BluetoothProfile.A2DP -> setA2dpActiveDevice(device);
                        case BluetoothProfile.HEADSET -> setHfpActiveDevice(device);
                        case BluetoothProfile.LE_AUDIO -> setLeAudioActiveDevice(device);
                        case BluetoothProfile.HEARING_AID -> setHearingAidActiveDevice(
                                device);
                        case BluetoothProfile.HEARING_AID -> setHearingAidActiveDevice(device);
                        default -> false;
                    };
                    if (activated) {
@@ -1129,7 +1065,8 @@ public class AudioRoutingManager extends ActiveDeviceManager {
                // Do not deactivate profiles if no profiles were activated.
                if (!isAnyProfileActivated) return false;
                for (Integer p : profilesToDeactivate) {
                    Log.d(TAG, "Deactivate profile: " + p);
                    Log.d(TAG, "Deactivate profile: " + BluetoothProfile.getProfileName(p));
                    mActiveDevices.remove(p);
                    switch (p) {
                        case BluetoothProfile.A2DP -> setA2dpActiveDevice(null, true);
                        case BluetoothProfile.HEADSET -> setHfpActiveDevice(null);
+12 −71
Original line number Diff line number Diff line
@@ -81,7 +81,6 @@ public class AudioRoutingManagerTest {
    private BluetoothDevice mHearingAidDevice;
    private BluetoothDevice mLeAudioDevice;
    private BluetoothDevice mLeAudioDevice2;
    private BluetoothDevice mLeHearingAidDevice;
    private BluetoothDevice mSecondaryAudioDevice;
    private BluetoothDevice mDualModeAudioDevice;
    private ArrayList<BluetoothDevice> mDeviceConnectionStack;
@@ -140,10 +139,9 @@ public class AudioRoutingManagerTest {
        mA2dpHeadsetDevice = TestUtils.getTestDevice(mAdapter, 2);
        mHearingAidDevice = TestUtils.getTestDevice(mAdapter, 3);
        mLeAudioDevice = TestUtils.getTestDevice(mAdapter, 4);
        mLeHearingAidDevice = TestUtils.getTestDevice(mAdapter, 5);
        mSecondaryAudioDevice = TestUtils.getTestDevice(mAdapter, 6);
        mDualModeAudioDevice = TestUtils.getTestDevice(mAdapter, 7);
        mLeAudioDevice2 = TestUtils.getTestDevice(mAdapter, 8);
        mSecondaryAudioDevice = TestUtils.getTestDevice(mAdapter, 5);
        mDualModeAudioDevice = TestUtils.getTestDevice(mAdapter, 6);
        mLeAudioDevice2 = TestUtils.getTestDevice(mAdapter, 7);
        mDeviceConnectionStack = new ArrayList<>();
        mMostRecentDevice = null;
        mOriginalDualModeAudioState = Utils.isDualModeAudioEnabled();
@@ -898,35 +896,12 @@ public class AudioRoutingManagerTest {
        verify(mHearingAidService, timeout(TIMEOUT_MS)).setActiveDevice(mHearingAidDevice);
    }

    /** One LE Hearing Aid is connected. */
    @Test
    public void onlyLeHearingAidConnected_setLeAudioActive() {
        leHearingAidConnected(mLeHearingAidDevice);
        TestUtils.waitForLooperToFinishScheduledTask(mAudioRoutingManager.getHandlerLooper());
        verify(mLeAudioService, never()).setActiveDevice(mLeHearingAidDevice);

        leAudioConnected(mLeHearingAidDevice);
        verify(mLeAudioService, timeout(TIMEOUT_MS)).setActiveDevice(mLeHearingAidDevice);
    }

    /** LE audio is connected after LE Hearing Aid device. LE audio active. */
    @Test
    public void leAudioConnectedAfterLeHearingAid_callsSetLeAudioActive() {
        leHearingAidConnected(mLeHearingAidDevice);
        leAudioConnected(mLeHearingAidDevice);
        verify(mLeAudioService, timeout(TIMEOUT_MS)).setActiveDevice(mLeHearingAidDevice);

        leAudioConnected(mLeAudioDevice);
        TestUtils.waitForLooperToFinishScheduledTask(mAudioRoutingManager.getHandlerLooper());
        verify(mLeAudioService).setActiveDevice(mLeAudioDevice);
    }

    /**
     * Test connect/disconnect of devices. Hearing Aid, A2DP connected, LE Hearing Aid, then LE
     * hearing Aid and hearing aid disconnected.
     * Test connect/disconnect of devices. Hearing Aid, A2DP connected, LE audio, then LE audio
     * disconnected.
     */
    @Test
    public void activeDeviceChange_withHearingAidLeHearingAidAndA2dpDevices() {
    public void activeDeviceChange_withHearingAidLeAudioAndA2dpDevices() {
        when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_NORMAL);
        when(mHearingAidService.removeActiveDevice(anyBoolean())).thenReturn(true);

@@ -938,15 +913,13 @@ public class AudioRoutingManagerTest {
        verify(mHearingAidService, timeout(TIMEOUT_MS)).removeActiveDevice(false);

        Mockito.clearInvocations(mHearingAidService, mA2dpService, mLeAudioService);
        leHearingAidConnected(mLeHearingAidDevice);
        leAudioConnected(mLeHearingAidDevice);
        verify(mLeAudioService, timeout(TIMEOUT_MS)).setActiveDevice(mLeHearingAidDevice);
        leAudioConnected(mLeAudioDevice);
        verify(mLeAudioService, timeout(TIMEOUT_MS)).setActiveDevice(mLeAudioDevice);
        verify(mA2dpService, timeout(TIMEOUT_MS)).removeActiveDevice(false);

        Mockito.clearInvocations(mHearingAidService, mA2dpService, mLeAudioService);
        leHearingAidDisconnected(mLeHearingAidDevice);
        leAudioDisconnected(mLeHearingAidDevice);
        verify(mHearingAidService, timeout(TIMEOUT_MS)).setActiveDevice(mHearingAidDevice);
        leAudioDisconnected(mLeAudioDevice);
        verify(mA2dpService, timeout(TIMEOUT_MS)).setActiveDevice(mA2dpDevice);
        verify(mLeAudioService, timeout(TIMEOUT_MS)).removeActiveDevice(true);
    }

@@ -1065,10 +1038,9 @@ public class AudioRoutingManagerTest {
        verify(mA2dpService).removeActiveDevice(anyBoolean());
        verify(mHeadsetService).setActiveDevice(null);

        leAudioConnected(mLeHearingAidDevice);
        leHearingAidConnected(mLeHearingAidDevice);
        leAudioConnected(mLeAudioDevice);
        TestUtils.waitForLooperToFinishScheduledTask(mAudioRoutingManager.getHandlerLooper());
        verify(mLeAudioService).setActiveDevice(mLeHearingAidDevice);
        verify(mLeAudioService).setActiveDevice(mLeAudioDevice);
        verify(mA2dpService).removeActiveDevice(anyBoolean());
        verify(mHeadsetService).setActiveDevice(null);
        verify(mHearingAidService).removeActiveDevice(anyBoolean());
@@ -1245,33 +1217,6 @@ public class AudioRoutingManagerTest {
        mAudioRoutingManager.profileActiveDeviceChanged(BluetoothProfile.LE_AUDIO, device);
    }

    /** Helper to indicate LE Hearing Aid connected for a device. */
    private void leHearingAidConnected(BluetoothDevice device) {
        mDeviceConnectionStack.add(device);
        mMostRecentDevice = device;

        mAudioRoutingManager.profileConnectionStateChanged(
                BluetoothProfile.HAP_CLIENT,
                device,
                BluetoothProfile.STATE_DISCONNECTED,
                BluetoothProfile.STATE_CONNECTED);
    }

    /** Helper to indicate LE Hearing Aid disconnected for a device. */
    private void leHearingAidDisconnected(BluetoothDevice device) {
        mDeviceConnectionStack.remove(device);
        mMostRecentDevice =
                (mDeviceConnectionStack.size() > 0)
                        ? mDeviceConnectionStack.get(mDeviceConnectionStack.size() - 1)
                        : null;

        mAudioRoutingManager.profileConnectionStateChanged(
                BluetoothProfile.HAP_CLIENT,
                device,
                BluetoothProfile.STATE_CONNECTED,
                BluetoothProfile.STATE_DISCONNECTED);
    }

    private class TestDatabaseManager extends DatabaseManager {
        ArrayMap<BluetoothDevice, SparseIntArray> mProfileConnectionPolicy;

@@ -1285,10 +1230,6 @@ public class AudioRoutingManagerTest {
                List<BluetoothDevice> devices) {
            if (devices == null || devices.size() == 0) {
                return null;
            } else if (devices.contains(mLeHearingAidDevice)) {
                return mLeHearingAidDevice;
            } else if (devices.contains(mHearingAidDevice)) {
                return mHearingAidDevice;
            } else if (mMostRecentDevice != null && devices.contains(mMostRecentDevice)) {
                return mMostRecentDevice;
            }