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

Commit aecb9a3d authored by Pranav Madapurmath's avatar Pranav Madapurmath
Browse files

Respect active BT device for routing and resolve flaky BT timing issues

This CL addresses three issues:

(1) The active BT device not being respected in the base routing.
Currently, the device ordering is determined by when the device is added
(paired). Depending on which device is actually active
(BT_ACTIVE_DEVICE_PRESENT), we should attempt to prioritize the active
device over others.

(2) BT not connecting via voice dial: We were seeing issues with BT
stack informing us of SCO audio disconnected once the search assistant
wasn't being used. The timing of when this occured was after active
focus for the call is received, which causes audio to be routed out of
BT. Instead, we should ignore switching the baseline route in the case
that BT is in the middle of connecting (or is already connected).

(3) There was an issue reported with transactional calling where if
there's a BT device connected and the user requests to switch the
callendpoint to speaker in the pre-call audio flow, then the request
isn't honored. This happens because the device is still in the middle of
connecting when the request comes in so Telecom ends up waiting for the
SCO audio connected message to be signaled in from the BT stack. But,
the switch will cause SCO to be disconnected before we set the
communication device for speaker, so the audio route controller stays
stuck waiting on the SCO audio connected pending msg to be received.
This CL ensures that when the original routing is being cleaned up that
we also clean up the associated SCO audio connected msg for it as well
if it exists.

Bug: 372029371
Flag: com.android.server.telecom.flags.resolve_active_bt_routing_and_bt_timing_issue
Test: Manual for voice dial and active device issue
Test: atest CallAudioRouteControllerTest (added unit tests for all
cases)

Change-Id: I16ee9c2afe3d73ae438d27660cf53744273399b2
parent d17d306c
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -17,6 +17,14 @@ flag {
  bug: "306395598"
}

# OWNER=pmadapurmath TARGET=25Q1
flag {
  name: "resolve_active_bt_routing_and_bt_timing_issue"
  namespace: "telecom"
  description: "Resolve the active BT device routing and flaky timing issues noted in BT routing."
  bug: "372029371"
}

# OWNER=tgunn TARGET=24Q3
flag {
  name: "ensure_audio_mode_updates_on_foreground_call_change"
+23 −5
Original line number Diff line number Diff line
@@ -320,13 +320,13 @@ public class AudioRoute {
            AudioManager audioManager, BluetoothRouteManager bluetoothRouteManager) {
        Log.i(this, "onOrigRouteAsPendingRoute: active (%b), type (%d)", active, mAudioRouteType);
        if (active) {
            if (mAudioRouteType == TYPE_SPEAKER) {
                pendingAudioRoute.addMessage(SPEAKER_OFF, null);
            }
            int result = clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager,
                    audioManager);
            if (mAudioRouteType == TYPE_SPEAKER) {
                pendingAudioRoute.addMessage(SPEAKER_OFF, null);
            } else if (mAudioRouteType == TYPE_BLUETOOTH_SCO
                    && result == BluetoothStatusCodes.SUCCESS) {
                // Only send BT_AUDIO_DISCONNECTED for SCO if disconnect was successful.
            if (mAudioRouteType == TYPE_BLUETOOTH_SCO && result == BluetoothStatusCodes.SUCCESS) {
                pendingAudioRoute.addMessage(BT_AUDIO_DISCONNECTED, mBluetoothAddress);
            }
        }
@@ -407,8 +407,26 @@ public class AudioRoute {
        }

        if (result == BluetoothStatusCodes.SUCCESS) {
            if (pendingAudioRoute.getFeatureFlags().resolveActiveBtRoutingAndBtTimingIssue()) {
                maybeClearConnectedPendingMessages(pendingAudioRoute);
            }
            pendingAudioRoute.setCommunicationDeviceType(AudioRoute.TYPE_INVALID);
        }
        return result;
    }

    private void maybeClearConnectedPendingMessages(PendingAudioRoute pendingAudioRoute) {
        // If we're still waiting on BT_AUDIO_CONNECTED/SPEAKER_ON but have routed out of it
        // since and disconnected the device, then remove that message so we aren't waiting for
        // it in the message queue.
        if (mAudioRouteType == TYPE_BLUETOOTH_SCO) {
            Log.i(this, "clearCommunicationDevice: Clearing pending "
                    + "BT_AUDIO_CONNECTED messages.");
            pendingAudioRoute.clearPendingMessage(
                    new Pair<>(BT_AUDIO_CONNECTED, mBluetoothAddress));
        } else if (mAudioRouteType == TYPE_SPEAKER) {
            Log.i(this, "clearCommunicationDevice: Clearing pending SPEAKER_ON messages.");
            pendingAudioRoute.clearPendingMessage(new Pair<>(SPEAKER_ON, null));
        }
    }
}
+39 −4
Original line number Diff line number Diff line
@@ -522,7 +522,8 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
                            + "%s(active=%b)",
                    mPendingAudioRoute.getDestRoute(), mIsActive, destRoute, active);
            // Ensure we don't keep waiting for SPEAKER_ON if dest route gets overridden.
            if (active && mPendingAudioRoute.getDestRoute().getType() == TYPE_SPEAKER) {
            if (!mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue() && active
                    && mPendingAudioRoute.getDestRoute().getType() == TYPE_SPEAKER) {
                mPendingAudioRoute.clearPendingMessage(new Pair<>(SPEAKER_ON, null));
            }
            // override pending route while keep waiting for still pending messages for the
@@ -930,8 +931,26 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
    }

    private void handleSwitchBaselineRoute(boolean includeBluetooth, String btAddressToExclude) {
        Log.i(this, "handleSwitchBaselineRoute: includeBluetooth: %b, "
                + "btAddressToExclude: %s", includeBluetooth, btAddressToExclude);
        boolean areExcludedBtAndDestBtSame = btAddressToExclude != null
                && Objects.equals(btAddressToExclude, mPendingAudioRoute.getDestRoute()
                .getBluetoothAddress());
        Pair<Integer, String> btDevicePendingMsg =
                new Pair<>(BT_AUDIO_CONNECTED, btAddressToExclude);

        // If SCO is once again connected or there's a pending message for BT_AUDIO_CONNECTED, then
        // we know that the device has reconnected or is in the middle of connecting. Ignore routing
        // out of this BT device.
        if (mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue() && areExcludedBtAndDestBtSame
                && (mIsScoAudioConnected || mPendingAudioRoute.getPendingMessages()
                .contains(btDevicePendingMsg))) {
            Log.i(this, "BT device with address (%s) is currently connecting/connected. "
                    + "Ignore route switch.");
        } else {
            routeTo(mIsActive, calculateBaselineRoute(includeBluetooth, btAddressToExclude));
        }
    }

    private void handleSpeakerOn() {
        if (isPending()) {
@@ -1322,7 +1341,7 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
            return getMostRecentlyActiveBtRoute(btAddressToExclude);
        }

        List<AudioRoute> bluetoothRoutes = mBluetoothRoutes.keySet().stream().toList();
        List<AudioRoute> bluetoothRoutes = getAvailableBluetoothDevicesForRouting();
        // Traverse the routes from the most recently active recorded devices first.
        AudioRoute nonWatchDeviceRoute = null;
        for (int i = bluetoothRoutes.size() - 1; i >= 0; i--) {
@@ -1341,7 +1360,7 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
                return bluetoothRoutes.get(0);
            }
            // Record the first occurrence of a non-watch device route if found.
            if (!mBluetoothRouteManager.isWatch(device) && nonWatchDeviceRoute == null) {
            if (!mBluetoothRouteManager.isWatch(device)) {
                nonWatchDeviceRoute = route;
                break;
            }
@@ -1351,6 +1370,22 @@ public class CallAudioRouteController implements CallAudioRouteAdapter {
        return nonWatchDeviceRoute;
    }

    private List<AudioRoute> getAvailableBluetoothDevicesForRouting() {
        List<AudioRoute> bluetoothRoutes = new ArrayList<>(mBluetoothRoutes.keySet());
        if (!mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()) {
            return bluetoothRoutes;
        }
        // Consider the active device (BT_ACTIVE_DEVICE_PRESENT) if it exists first.
        AudioRoute activeDeviceRoute = getArbitraryBluetoothDevice();
        if (activeDeviceRoute != null && (bluetoothRoutes.isEmpty()
                || !bluetoothRoutes.get(bluetoothRoutes.size() - 1).equals(activeDeviceRoute))) {
            Log.i(this, "getActiveWatchOrNonWatchDeviceRoute: active BT device (%s) present."
                    + "Considering this device for selection first.", activeDeviceRoute);
            bluetoothRoutes.add(activeDeviceRoute);
        }
        return bluetoothRoutes;
    }

    /**
     * Returns the most actively reported bluetooth route excluding the passed in route.
     */
+8 −0
Original line number Diff line number Diff line
@@ -130,6 +130,10 @@ public class PendingAudioRoute {
        mPendingMessages.remove(message);
    }

    public Set<Pair<Integer, String>> getPendingMessages() {
        return mPendingMessages;
    }

    public boolean isActive() {
        return mActive;
    }
@@ -146,4 +150,8 @@ public class PendingAudioRoute {
    public void overrideDestRoute(AudioRoute route) {
        mDestRoute = route;
    }

    public FeatureFlags getFeatureFlags() {
        return mFeatureFlags;
    }
}
+123 −0
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import static com.android.server.telecom.CallAudioRouteAdapter.ACTIVE_FOCUS;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_ACTIVE_DEVICE_GONE;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_ACTIVE_DEVICE_PRESENT;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_CONNECTED;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_AUDIO_DISCONNECTED;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_ADDED;
import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_REMOVED;
import static com.android.server.telecom.CallAudioRouteAdapter.CONNECT_DOCK;
@@ -40,6 +41,8 @@ import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_BLUET
import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_EARPIECE;
import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_HEADSET;
import static com.android.server.telecom.CallAudioRouteAdapter.USER_SWITCH_SPEAKER;
import static com.android.server.telecom.CallAudioRouteController.INCLUDE_BLUETOOTH_IN_BASELINE;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -189,6 +192,7 @@ public class CallAudioRouteControllerTest extends TelecomTestCase {
        when(mCall.getSupportedAudioRoutes()).thenReturn(CallAudioState.ROUTE_ALL);
        when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(false);
        when(mFeatureFlags.useRefactoredAudioRouteSwitching()).thenReturn(true);
        when(mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()).thenReturn(false);
    }

    @After
@@ -908,6 +912,125 @@ public class CallAudioRouteControllerTest extends TelecomTestCase {

    }

    @SmallTest
    @Test
    public void testMimicVoiceDialWithBt() {
        when(mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()).thenReturn(true);
        mController.initialize();
        mController.setActive(true);

        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
                BLUETOOTH_DEVICE_1);
        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
                        | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
                any(CallAudioState.class), eq(expectedState));

        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS, 0);
        // Mimic behavior of controller processing BT_AUDIO_DISCONNECTED
        mController.sendMessageWithSessionInfo(SWITCH_BASELINE_ROUTE,
                INCLUDE_BLUETOOTH_IN_BASELINE, BLUETOOTH_DEVICE_1.getAddress());
        // Process BT_AUDIO_CONNECTED from connecting to BT device in active focus request.
        mController.setIsScoAudioConnected(true);
        mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, BLUETOOTH_DEVICE_1);
        // Verify SCO not disconnected and route stays on connected BT device.
        verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT).times(0)).disconnectSco();
        expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
                any(CallAudioState.class), eq(expectedState));
    }

    @SmallTest
    @Test
    public void testTransactionalCallBtConnectingAndSwitchCallEndpoint() {
        when(mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()).thenReturn(true);
        mController.initialize();
        mController.setActive(true);

        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
                BLUETOOTH_DEVICE_1);
        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
                        | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
                any(CallAudioState.class), eq(expectedState));

        mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
                AudioRoute.TYPE_BLUETOOTH_SCO, BT_ADDRESS_1);
        // Omit sending BT_AUDIO_CONNECTED to mimic scenario where BT is still connecting and user
        // switches to speaker.
        mController.sendMessageWithSessionInfo(USER_SWITCH_SPEAKER);
        mController.sendMessageWithSessionInfo(SPEAKER_ON);
        mController.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0,
                BLUETOOTH_DEVICE_1);

        // Verify SCO disconnected
        verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT)).disconnectSco();
        // Verify audio properly routes into speaker.
        expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
                        | CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
                any(CallAudioState.class), eq(expectedState));
    }

    @Test
    @SmallTest
    public void testBluetoothRouteToActiveDevice() {
        when(mFeatureFlags.resolveActiveBtRoutingAndBtTimingIssue()).thenReturn(true);
        // Connect first BT device.
        verifyConnectBluetoothDevice(AudioRoute.TYPE_BLUETOOTH_SCO);
        // Connect another BT device.
        String scoDeviceAddress = "00:00:00:00:00:03";
        BluetoothDevice scoDevice =
                BluetoothRouteManagerTest.makeBluetoothDevice(scoDeviceAddress);
        BLUETOOTH_DEVICES.add(scoDevice);
        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
                scoDevice);
        mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
                AudioRoute.TYPE_BLUETOOTH_SCO, scoDeviceAddress);
        mController.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0,
                BLUETOOTH_DEVICE_1);
        mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0,
                scoDevice);
        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
                        | CallAudioState.ROUTE_SPEAKER, scoDevice, BLUETOOTH_DEVICES);
        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
                any(CallAudioState.class), eq(expectedState));

        // Mimic behavior when inactive headset is used to answer the call (i.e. tap headset). In
        // this case, the inactive BT device will become the active device (reported to us from BT
        // stack to controller via BT_ACTIVE_DEVICE_PRESENT).
        mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
                AudioRoute.TYPE_BLUETOOTH_SCO, BLUETOOTH_DEVICE_1.getAddress());
        mController.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0,
                scoDevice);
        mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0,
                BLUETOOTH_DEVICE_1);
        // Verify audio routed to BLUETOOTH_DEVICE_1
        expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
                any(CallAudioState.class), eq(expectedState));

        // Now switch call to active focus so that base route can be recalculated.
        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS, 0);
        expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
        // Verify that audio is still routed into BLUETOOTH_DEVICE_1 and not the 2nd BT device.
        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
                any(CallAudioState.class), eq(expectedState));

        // Clean up BLUETOOTH_DEVICES for subsequent tests.
        BLUETOOTH_DEVICES.remove(scoDevice);
    }

    private void verifyConnectBluetoothDevice(int audioType) {
        mController.initialize();
        mController.setActive(true);