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
diff --git a/flags/telecom_callaudioroutestatemachine_flags.aconfig b/flags/telecom_callaudioroutestatemachine_flags.aconfig
index 33bccba..62b8bdb 100644
--- a/flags/telecom_callaudioroutestatemachine_flags.aconfig
+++ b/flags/telecom_callaudioroutestatemachine_flags.aconfig
@@ -17,6 +17,14 @@
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"
diff --git a/src/com/android/server/telecom/AudioRoute.java b/src/com/android/server/telecom/AudioRoute.java
index d469a43..6dc64ba 100644
--- a/src/com/android/server/telecom/AudioRoute.java
+++ b/src/com/android/server/telecom/AudioRoute.java
@@ -320,13 +320,13 @@
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);
- // Only send BT_AUDIO_DISCONNECTED for SCO if disconnect was successful.
- if (mAudioRouteType == TYPE_BLUETOOTH_SCO && result == BluetoothStatusCodes.SUCCESS) {
+ 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.
pendingAudioRoute.addMessage(BT_AUDIO_DISCONNECTED, mBluetoothAddress);
}
}
@@ -407,8 +407,26 @@
}
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));
+ }
+ }
}
diff --git a/src/com/android/server/telecom/CallAudioRouteController.java b/src/com/android/server/telecom/CallAudioRouteController.java
index bc2c0cb..5cae393 100644
--- a/src/com/android/server/telecom/CallAudioRouteController.java
+++ b/src/com/android/server/telecom/CallAudioRouteController.java
@@ -522,7 +522,8 @@
+ "%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,7 +931,25 @@
}
private void handleSwitchBaselineRoute(boolean includeBluetooth, String btAddressToExclude) {
- routeTo(mIsActive, calculateBaselineRoute(includeBluetooth, 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() {
@@ -1322,7 +1341,7 @@
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 @@
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 @@
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.
*/
diff --git a/src/com/android/server/telecom/PendingAudioRoute.java b/src/com/android/server/telecom/PendingAudioRoute.java
index ffde964..d21ac56 100644
--- a/src/com/android/server/telecom/PendingAudioRoute.java
+++ b/src/com/android/server/telecom/PendingAudioRoute.java
@@ -130,6 +130,10 @@
mPendingMessages.remove(message);
}
+ public Set<Pair<Integer, String>> getPendingMessages() {
+ return mPendingMessages;
+ }
+
public boolean isActive() {
return mActive;
}
@@ -146,4 +150,8 @@
public void overrideDestRoute(AudioRoute route) {
mDestRoute = route;
}
+
+ public FeatureFlags getFeatureFlags() {
+ return mFeatureFlags;
+ }
}
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
index ade2a22..c0e9904 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
@@ -20,6 +20,7 @@
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_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 @@
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 @@
}
+ @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);