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

Commit 074b774a 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

parents 59f3f1ee 762205fd
Loading
Loading
Loading
Loading
+61 −43
Original line number Original line Diff line number Diff line
@@ -51,10 +51,13 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.SynchronousResultReceiver;
import com.android.modules.utils.SynchronousResultReceiver;


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


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


        private boolean setFallbackDeviceActive() {
        private Optional<BluetoothDevice> getFallbackDevice(
            if (DBG) {
                Collection<AudioRoutingDevice> candidates) {
                Log.d(TAG, "setFallbackDeviceActive");
            List<BluetoothDevice> activatableDevices = new ArrayList<>();
            }
            for (AudioRoutingDevice d : candidates) {
            List<BluetoothDevice> candidates = new ArrayList<>();
                if (d.isA2dpOnly() || d.isHfpOnly()) continue;
            int audioMode = mAudioManager.getMode();
                boolean canActivate = true;
            for (AudioRoutingDevice routingDevice : mConnectedDevices.values()) {
                for (int p : d.connectedProfiles) {
                for (int profile : routingDevice.connectedProfiles) {
                    if (!d.canActivateNow(p)) {
                    if (audioMode == AudioManager.MODE_NORMAL) {
                        canActivate = false;
                        if (profile != BluetoothProfile.HEADSET) {
                        break;
                            candidates.add(routingDevice.device);
                    } else if (p != BluetoothProfile.A2DP && p != BluetoothProfile.HEADSET) {
                        break;
                        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 {
                    } else {
                        if (profile != BluetoothProfile.A2DP) {
                        // if it can't activate both the given profile and LE_AUDIO, select any
                            candidates.add(routingDevice.device);
                        for (int p : fallbackRoutingDevice.connectedProfiles) {
                            if (fallbackRoutingDevice.canActivateNow(p)) {
                                profileToActivate = p;
                                break;
                                break;
                            }
                            }
                        }
                        }
                    }
                    }
                }
                }
            AudioRoutingDevice deviceToActivate = null;
                return activateDeviceProfile(fallbackRoutingDevice, profileToActivate);
            BluetoothDevice device = mDbManager.getMostRecentlyConnectedDevicesInList(candidates);
            } catch (NoSuchElementException e) {
            if (device != null) {
                // Thrown when no available fallback devices found
                deviceToActivate = getAudioRoutingDevice(device);
            }
            if (deviceToActivate != null) {
                if (DBG) {
                if (DBG) {
                    Log.d(TAG, "activateDevice: device=" + deviceToActivate.device);
                    Log.d(TAG, "Found no available BT fallback devices.");
                }
                // 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");
                }
                }
                return false;
                return false;
            }
            }
        }


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


            public boolean canActivateNow(int profile) {
            public boolean canActivateNow(int profile) {
                if (!connectedProfiles.contains(profile)) return false;
                if (!connectedProfiles.contains(profile)) return false;
                // TODO: Return false if there are another active remote streaming an audio.
                return switch (profile) {
                return switch (profile) {
                    case BluetoothProfile.HEADSET -> !supportedProfiles.contains(
                    case BluetoothProfile.HEADSET -> !supportedProfiles.contains(
                                    BluetoothProfile.A2DP)
                                    BluetoothProfile.A2DP)
+55 −27
Original line number Original line 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
     * A2DP Headset and A2DP only devices are connected and the current activated A2DP only is then
     * active device to fallback device.
     * disconnected. Should then set active device to fallback device.
     */
     */
    @Test
    @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);
        a2dpConnected(mA2dpDevice, false);
        switchA2dpActiveDevice(mA2dpDevice);
        switchA2dpActiveDevice(mA2dpDevice);
        mTestLooper.dispatchAll();
        mTestLooper.dispatchAll();
        verify(mA2dpService).setActiveDevice(mA2dpDevice);
        verify(mA2dpService).setActiveDevice(mA2dpDevice);


        a2dpConnected(mSecondaryAudioDevice, false);
        Mockito.clearInvocations(mA2dpService, mHeadsetService);
        switchA2dpActiveDevice(mSecondaryAudioDevice);
        a2dpDisconnected(mA2dpDevice);
        mTestLooper.dispatchAll();
        verify(mA2dpService).setActiveDevice(mSecondaryAudioDevice);

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


    /** One Headset is connected. */
    /** 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
     * Two Headset only devices are connected and the current active is then disconnected. Then it
     * active device to fallback device.
     * should be fallback to phone.
     */
     */
    @Test
    @Test
    public void headsetSecondDeviceDisconnected_fallbackDeviceActive() {
    public void headsetSecondDeviceDisconnected_fallbackToPhone() {
        when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_IN_CALL);
        when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_IN_CALL);


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


    @Test
    @Test
    public void headsetSecondDeviceDisconnected_fallbackDeviceActiveWhileRinging() {
    public void headsetSecondDeviceDisconnected_fallbackDeviceActiveWhileRinging() {
        when(mAudioManager.getMode()).thenReturn(AudioManager.MODE_RINGTONE);
        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);
        headsetConnected(mHeadsetDevice, false);
        switchHeadsetActiveDevice(mHeadsetDevice);
        switchHeadsetActiveDevice(mHeadsetDevice);
        mTestLooper.dispatchAll();
        mTestLooper.dispatchAll();
        verify(mHeadsetService).setActiveDevice(mHeadsetDevice);
        verify(mHeadsetService).setActiveDevice(mHeadsetDevice);


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

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


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


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


        a2dpConnected(mA2dpDevice, false);
        a2dpConnected(mA2dpDevice, false);
@@ -861,8 +863,34 @@ public class AudioRoutingManagerTest {
        Mockito.clearInvocations(mA2dpService);
        Mockito.clearInvocations(mA2dpService);
        leAudioDisconnected(mLeAudioDevice);
        leAudioDisconnected(mLeAudioDevice);
        mTestLooper.dispatchAll();
        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(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);
        Mockito.clearInvocations(mHearingAidService, mA2dpService, mLeAudioService);
        leAudioDisconnected(mLeAudioDevice);
        leAudioDisconnected(mLeAudioDevice);
        mTestLooper.dispatchAll();
        mTestLooper.dispatchAll();
        verify(mA2dpService).setActiveDevice(mA2dpDevice);
        verify(mHearingAidService).setActiveDevice(mHearingAidDevice);
        verify(mLeAudioService).removeActiveDevice(true);
        verify(mLeAudioService).removeActiveDevice(true);
    }
    }