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

Commit df413280 authored by Sungsoo Lim's avatar Sungsoo Lim Committed by Automerger Merge Worker
Browse files

Merge "Revisit audio routing policy when disconnected" into main am: 762205fd am: 074b774a

parents 86d78c94 074b774a
Loading
Loading
Loading
Loading
+61 −43
Original line number Diff line number Diff line
@@ -51,10 +51,13 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.SynchronousResultReceiver;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

public class AudioRoutingManager extends ActiveDeviceManager {
@@ -309,8 +312,8 @@ public class AudioRoutingManager extends ActiveDeviceManager {
            List<BluetoothDevice> activeDevices = mActiveDevices.get(profile);
            if (activeDevices != null && activeDevices.contains(device)) {
                activeDevices.remove(device);
                if (activeDevices.size() == 0) {
                    if (!setFallbackDeviceActive()) {
                if (activeDevices.isEmpty()) {
                    if (!setFallbackDeviceActive(profile)) {
                        removeActiveDevice(profile, false);
                    }
                }
@@ -329,54 +332,70 @@ public class AudioRoutingManager extends ActiveDeviceManager {
                            + device);
        }

        private boolean setFallbackDeviceActive() {
            if (DBG) {
                Log.d(TAG, "setFallbackDeviceActive");
            }
            List<BluetoothDevice> candidates = new ArrayList<>();
            int audioMode = mAudioManager.getMode();
            for (AudioRoutingDevice routingDevice : mConnectedDevices.values()) {
                for (int profile : routingDevice.connectedProfiles) {
                    if (audioMode == AudioManager.MODE_NORMAL) {
                        if (profile != BluetoothProfile.HEADSET) {
                            candidates.add(routingDevice.device);
        private Optional<BluetoothDevice> getFallbackDevice(
                Collection<AudioRoutingDevice> candidates) {
            List<BluetoothDevice> activatableDevices = new ArrayList<>();
            for (AudioRoutingDevice d : candidates) {
                if (d.isA2dpOnly() || d.isHfpOnly()) continue;
                boolean canActivate = true;
                for (int p : d.connectedProfiles) {
                    if (!d.canActivateNow(p)) {
                        canActivate = false;
                        break;
                    } else if (p != BluetoothProfile.A2DP && p != BluetoothProfile.HEADSET) {
                        break;
                    }
                }
                if (canActivate) {
                    activatableDevices.add(d.device);
                }
            }
            return Optional.ofNullable(
                    mDbManager.getMostRecentlyConnectedDevicesInList(activatableDevices));
        }

        private boolean setFallbackDeviceActive(int profile) {
            if (DBG) {
                Log.d(TAG, "setFallbackDeviceActive: " + BluetoothProfile.getProfileName(profile));
            }
            // 1. Activate the lastly activated device among currently activated devices.
            Set<AudioRoutingDevice> candidates = new HashSet<>();
            for (int i = 0; i < mActiveDevices.size(); ++i) {
                for (BluetoothDevice d : mActiveDevices.valueAt(i)) {
                    candidates.add(getAudioRoutingDevice(d));
                }
            }
            try {
                // 2. Activate the lastly activated device for the profile
                Optional<BluetoothDevice> fallbackDevice =
                        getFallbackDevice(candidates)
                                .or(() -> getFallbackDevice(mConnectedDevices.values()));
                AudioRoutingDevice fallbackRoutingDevice =
                        getAudioRoutingDevice(fallbackDevice.get());
                int profileToActivate = profile;
                if (!fallbackRoutingDevice.canActivateNow(profile)) {
                    // if it can't activate the given profile, try LE_AUDIO
                    if (fallbackRoutingDevice.canActivateNow(BluetoothProfile.LE_AUDIO)) {
                        profileToActivate = BluetoothProfile.LE_AUDIO;
                    } else {
                        if (profile != BluetoothProfile.A2DP) {
                            candidates.add(routingDevice.device);
                        // if it can't activate both the given profile and LE_AUDIO, select any
                        for (int p : fallbackRoutingDevice.connectedProfiles) {
                            if (fallbackRoutingDevice.canActivateNow(p)) {
                                profileToActivate = p;
                                break;
                            }
                        }
                    }
                }
            AudioRoutingDevice deviceToActivate = null;
            BluetoothDevice device = mDbManager.getMostRecentlyConnectedDevicesInList(candidates);
            if (device != null) {
                deviceToActivate = getAudioRoutingDevice(device);
            }
            if (deviceToActivate != null) {
                return activateDeviceProfile(fallbackRoutingDevice, profileToActivate);
            } catch (NoSuchElementException e) {
                // Thrown when no available fallback devices found
                if (DBG) {
                    Log.d(TAG, "activateDevice: device=" + deviceToActivate.device);
                }
                // Try to activate hearing aid and LE audio first
                if (deviceToActivate.connectedProfiles.contains(BluetoothProfile.HEARING_AID)) {
                    return activateDeviceProfile(deviceToActivate, BluetoothProfile.HEARING_AID);
                } else if (deviceToActivate.connectedProfiles.contains(BluetoothProfile.LE_AUDIO)) {
                    return activateDeviceProfile(deviceToActivate, BluetoothProfile.LE_AUDIO);
                } else if (deviceToActivate.connectedProfiles.contains(BluetoothProfile.A2DP)) {
                    return activateDeviceProfile(deviceToActivate, BluetoothProfile.A2DP);
                } else if (deviceToActivate.connectedProfiles.contains(BluetoothProfile.HEADSET)) {
                    return activateDeviceProfile(deviceToActivate, BluetoothProfile.HEADSET);
                }
                Log.w(
                        TAG,
                        "Fail to activate the device: "
                                + deviceToActivate.device
                                + ", no connected audio profiles");
                    Log.d(TAG, "Found no available BT fallback devices.");
                }
                return false;
            }
        }

        // TODO: handle the connection policy change events.
        private AudioRoutingDevice getAudioRoutingDevice(@NonNull BluetoothDevice device) {
@@ -660,7 +679,7 @@ public class AudioRoutingManager extends ActiveDeviceManager {
            for (int p : connectedDevice.supportedProfiles) {
                if (!getActiveDevices(p).isEmpty()) {
                    BluetoothMethodProxy mp = BluetoothMethodProxy.getInstance();
                    if (mp.mediaSessionManagerGetActiveSessions(mSessionManager).size() > 0
                    if (!mp.mediaSessionManagerGetActiveSessions(mSessionManager).isEmpty()
                            || mAudioManager.getMode() == AudioManager.MODE_IN_CALL) {
                        Log.i(
                                TAG,
@@ -733,7 +752,6 @@ public class AudioRoutingManager extends ActiveDeviceManager {

            public boolean canActivateNow(int profile) {
                if (!connectedProfiles.contains(profile)) return false;
                // TODO: Return false if there are another active remote streaming an audio.
                return switch (profile) {
                    case BluetoothProfile.HEADSET -> !supportedProfiles.contains(
                                    BluetoothProfile.A2DP)
+55 −27
Original line number Diff line number Diff line
@@ -250,25 +250,26 @@ public class AudioRoutingManagerTest {
    }

    /**
     * Two A2DP devices are connected and the current active is then disconnected. Should then set
     * active device to fallback device.
     * A2DP Headset and A2DP only devices are connected and the current activated A2DP only is then
     * disconnected. Should then set active device to fallback device.
     */
    @Test
    public void a2dpSecondDeviceDisconnected_fallbackDeviceActive() {
    public void a2dpDeviceDisconnected_fallbackA2dpHeadset() {
        a2dpConnected(mA2dpHeadsetDevice, true);
        headsetConnected(mA2dpHeadsetDevice, true);
        mTestLooper.dispatchAll();
        verify(mA2dpService).setActiveDevice(mA2dpHeadsetDevice);
        verify(mHeadsetService).setActiveDevice(mA2dpHeadsetDevice);

        a2dpConnected(mA2dpDevice, false);
        switchA2dpActiveDevice(mA2dpDevice);
        mTestLooper.dispatchAll();
        verify(mA2dpService).setActiveDevice(mA2dpDevice);

        a2dpConnected(mSecondaryAudioDevice, false);
        switchA2dpActiveDevice(mSecondaryAudioDevice);
        mTestLooper.dispatchAll();
        verify(mA2dpService).setActiveDevice(mSecondaryAudioDevice);

        Mockito.clearInvocations(mA2dpService);
        a2dpDisconnected(mSecondaryAudioDevice);
        Mockito.clearInvocations(mA2dpService, mHeadsetService);
        a2dpDisconnected(mA2dpDevice);
        mTestLooper.dispatchAll();
        verify(mA2dpService).setActiveDevice(mA2dpDevice);
        verify(mA2dpService).setActiveDevice(mA2dpHeadsetDevice);
    }

    /** One Headset is connected. */
@@ -323,11 +324,11 @@ public class AudioRoutingManagerTest {
    }

    /**
     * Two Headsets are connected and the current active is then disconnected. Should then set
     * active device to fallback device.
     * Two Headset only devices are connected and the current active is then disconnected. Then it
     * should be fallback to phone.
     */
    @Test
    public void headsetSecondDeviceDisconnected_fallbackDeviceActive() {
    public void headsetSecondDeviceDisconnected_fallbackToPhone() {
        when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_IN_CALL);

        headsetConnected(mHeadsetDevice, false);
@@ -343,27 +344,28 @@ public class AudioRoutingManagerTest {
        Mockito.clearInvocations(mHeadsetService);
        headsetDisconnected(mSecondaryAudioDevice);
        mTestLooper.dispatchAll();
        verify(mHeadsetService).setActiveDevice(mHeadsetDevice);
        verify(mHeadsetService, never()).setActiveDevice(mHeadsetDevice);
    }

    @Test
    public void headsetSecondDeviceDisconnected_fallbackDeviceActiveWhileRinging() {
        when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_RINGTONE);

        headsetConnected(mA2dpHeadsetDevice, true);
        a2dpConnected(mA2dpHeadsetDevice, true);
        mTestLooper.dispatchAll();
        verify(mHeadsetService).setActiveDevice(mA2dpHeadsetDevice);
        verify(mA2dpService).setActiveDevice(mA2dpHeadsetDevice);

        headsetConnected(mHeadsetDevice, false);
        switchHeadsetActiveDevice(mHeadsetDevice);
        mTestLooper.dispatchAll();
        verify(mHeadsetService).setActiveDevice(mHeadsetDevice);

        headsetConnected(mSecondaryAudioDevice, false);
        switchHeadsetActiveDevice(mSecondaryAudioDevice);
        mTestLooper.dispatchAll();
        verify(mHeadsetService).setActiveDevice(mSecondaryAudioDevice);

        Mockito.clearInvocations(mHeadsetService);
        headsetDisconnected(mSecondaryAudioDevice);
        headsetDisconnected(mHeadsetDevice);
        mTestLooper.dispatchAll();
        verify(mHeadsetService).setActiveDevice(mHeadsetDevice);
        verify(mHeadsetService).setActiveDevice(mA2dpHeadsetDevice);
    }

    @Test
@@ -842,11 +844,11 @@ public class AudioRoutingManagerTest {
    }

    /**
     * An A2DP connected. An LE Audio connected. The LE Audio disconnected. Then the A2DP should be
     * the active one.
     * An A2DP only device connected. An LE Audio connected. The LE Audio disconnected. Then it
     * should be fallback to phone instead of the A2DP only device.
     */
    @Test
    public void a2dpAndLeAudioConnectedThenLeAudioDisconnected_fallbackToA2dp() {
    public void a2dpAndLeAudioConnectedThenLeAudioDisconnected_fallbackToPhone() {
        when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_NORMAL);

        a2dpConnected(mA2dpDevice, false);
@@ -861,8 +863,34 @@ public class AudioRoutingManagerTest {
        Mockito.clearInvocations(mA2dpService);
        leAudioDisconnected(mLeAudioDevice);
        mTestLooper.dispatchAll();
        verify(mLeAudioService).removeActiveDevice(false);
        verify(mA2dpService, never()).setActiveDevice(mA2dpDevice);
    }

    /**
     * An A2DP headset connected. An LE Audio connected. The LE Audio disconnected. Then the A2DP
     * headset should be the active one.
     */
    @Test
    public void a2dpHeadsetAndLeAudioConnectedThenLeAudioDisconnected_fallbackToA2dpHeadset() {
        when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_NORMAL);

        a2dpConnected(mHeadsetDevice, true);
        headsetConnected(mHeadsetDevice, true);
        mTestLooper.dispatchAll();
        verify(mA2dpService).setActiveDevice(mHeadsetDevice);
        verify(mHeadsetService).setActiveDevice(mHeadsetDevice);

        leAudioConnected(mLeAudioDevice);
        mTestLooper.dispatchAll();
        verify(mLeAudioService).setActiveDevice(mLeAudioDevice);

        Mockito.clearInvocations(mA2dpService, mHeadsetService);
        leAudioDisconnected(mLeAudioDevice);
        mTestLooper.dispatchAll();
        verify(mLeAudioService).removeActiveDevice(true);
        verify(mA2dpService).setActiveDevice(mA2dpDevice);
        verify(mA2dpService).setActiveDevice(mHeadsetDevice);
        verify(mHeadsetService).setActiveDevice(mHeadsetDevice);
    }

    /**
@@ -920,7 +948,7 @@ public class AudioRoutingManagerTest {
        Mockito.clearInvocations(mHearingAidService, mA2dpService, mLeAudioService);
        leAudioDisconnected(mLeAudioDevice);
        mTestLooper.dispatchAll();
        verify(mA2dpService).setActiveDevice(mA2dpDevice);
        verify(mHearingAidService).setActiveDevice(mHearingAidDevice);
        verify(mLeAudioService).removeActiveDevice(true);
    }