Clear pending BT messages in call audio routing

Clear the pending BT messages when receiving BT_AUDIO_CONNECTED and
BT_AUDIO_DISCONNECTED signals from the BT stack. We already do this as
part of routing when moving from the original route to the destination
route but we should also cover this explicitly as part of the BT stack
broadcasts that are sent to Telecom. This has been causing issues with
improper routing in some scenarios like the aforementioned bug.

In addition, we should remove pending BT_AUDIO_DISCONNECTED messages
from SPEAKER_ON. We should also clear SPEAKER_OFF in this case as well
and for SPEAKER_OFF, we should clear the SPEAKER_ON message similar to
the BT cases.

Bug: 404473378
Flag: EXEMPT bugfix
Test: atest CallAudioRouteControllerTest

Change-Id: Ibfafcf0680351c2b5c3f78cdfda8963a1a1d0c96
diff --git a/src/com/android/server/telecom/CallAudioRouteController.java b/src/com/android/server/telecom/CallAudioRouteController.java
index 094f89b..a46bbd5 100644
--- a/src/com/android/server/telecom/CallAudioRouteController.java
+++ b/src/com/android/server/telecom/CallAudioRouteController.java
@@ -761,6 +761,14 @@
     private void handleBtAudioActive(BluetoothDevice bluetoothDevice) {
         if (mIsPending && bluetoothDevice != null) {
             Log.i(this, "handleBtAudioActive: is pending path");
+            // Ensure we aren't keeping track of pending speaker off and SCO audio disconnected
+            // messages  for this device if BT stack indicates that SCO audio is connected.
+            mPendingAudioRoute.clearPendingMessage(
+                    new Pair<>(BT_AUDIO_DISCONNECTED, bluetoothDevice.getAddress()));
+            mPendingAudioRoute.clearPendingMessage(new Pair<>(SPEAKER_OFF, null));
+            // Maybe turn off speaker from notification bar. This will be a no-op if the enabled
+            // status is already off.
+            mStatusBarNotifier.notifySpeakerphone(false);
             if (Objects.equals(mPendingAudioRoute.getDestRoute().getBluetoothAddress(),
                     bluetoothDevice.getAddress())) {
                 mPendingAudioRoute.onMessageReceived(new Pair<>(BT_AUDIO_CONNECTED,
@@ -780,6 +788,10 @@
     private void handleBtAudioInactive(BluetoothDevice bluetoothDevice) {
         if (mIsPending && bluetoothDevice != null) {
             Log.i(this, "handleBtAudioInactive: is pending path");
+            // Ensure we aren't keeping track of pending s SCO audio connected messages for this
+            // device if the BT stack has indicated that SCO audio has disconnected.
+            mPendingAudioRoute.clearPendingMessage(
+                    new Pair<>(BT_AUDIO_CONNECTED, bluetoothDevice.getAddress()));
             if (Objects.equals(mPendingAudioRoute.getOrigRoute().getBluetoothAddress(),
                     bluetoothDevice.getAddress())) {
                 mPendingAudioRoute.onMessageReceived(new Pair<>(BT_AUDIO_DISCONNECTED,
@@ -1107,6 +1119,15 @@
     private void handleSpeakerOn() {
         if (isPending()) {
             Log.i(this, "handleSpeakerOn: sending SPEAKER_ON to pending audio route");
+            // Clear any pending speaker off message as the speaker has been explicitly turned on as
+            // indicated by the audio fwk.
+            mPendingAudioRoute.clearPendingMessage(new Pair<>(SPEAKER_OFF, null));
+            // Clear any pending BT_AUDIO_DISCONNECTED messages for connected BT devices if speaker
+            // has explicitly been turned on.
+            for (BluetoothDevice device: mBluetoothRoutes.values()) {
+                mPendingAudioRoute.clearPendingMessage(new Pair<>(BT_AUDIO_DISCONNECTED,
+                        device.getAddress()));
+            }
             mPendingAudioRoute.onMessageReceived(new Pair<>(SPEAKER_ON, null), null);
             // Update status bar notification if we are in a call.
             mStatusBarNotifier.notifySpeakerphone(mCallsManager.hasAnyCalls());
@@ -1127,6 +1148,9 @@
     private void handleSpeakerOff() {
         if (isPending()) {
             Log.i(this, "handleSpeakerOff - sending SPEAKER_OFF to pending audio route");
+            // Clear any pending speaker on message as the speaker has been explicitly turned off as
+            // indicated by the audio fwk.
+            mPendingAudioRoute.clearPendingMessage(new Pair<>(SPEAKER_ON, null));
             mPendingAudioRoute.onMessageReceived(new Pair<>(SPEAKER_OFF, null), null);
             // Update status bar notification
             mStatusBarNotifier.notifySpeakerphone(false);
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
index 738ab29..85430be 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
@@ -1458,6 +1458,84 @@
         verify(mBluetoothDeviceManager, timeout(TEST_TIMEOUT).times(0)).disconnectSco();
     }
 
+    @Test
+    @SmallTest
+    public void testClearPendingMessages() {
+        mController.initialize();
+
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS, 0);
+        waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT);
+        assertTrue(mController.isActive());
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER, null,
+                new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Mock testing for pending audio route. This will initialize the pending audio route with
+        // initialized orig + dest routes.
+        BluetoothDevice scoDevice =
+                BluetoothRouteManagerTest.makeBluetoothDevice("00:00:00:00:00:03");
+        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+                BLUETOOTH_DEVICE_1);
+        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+                scoDevice);
+        BLUETOOTH_DEVICES.add(scoDevice);
+        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));
+
+        // Add pending BT_AUDIO_DISCONNECTED msg and verify it's removed when we get
+        // BT_AUDIO_CONNECTED.
+        mController.getPendingAudioRoute().addMessage(BT_AUDIO_DISCONNECTED, BT_ADDRESS_1);
+        mController.getPendingAudioRoute().addMessage(SPEAKER_OFF, null);
+        mController.sendMessageWithSessionInfo(BT_AUDIO_CONNECTED, 0, BLUETOOTH_DEVICE_1);
+        mController.overrideIsPending(true);
+        waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT);
+        assertFalse(mController.getPendingAudioRoute().getPendingMessages().contains(
+                new Pair<>(BT_AUDIO_DISCONNECTED, BT_ADDRESS_1)));
+        // Verify the speaker off message was cleared as well and the status bar notifier was
+        // invoked.
+        assertFalse(mController.getPendingAudioRoute().getPendingMessages().contains(
+                new Pair<>(SPEAKER_OFF, null)));
+        verify(mockStatusBarNotifier, timeout(TEST_TIMEOUT)).notifySpeakerphone(anyBoolean());
+
+        // Add pending BT_AUDIO_CONNECTED msg and verify it's removed when we get
+        // BT_AUDIO_DISCONNECTED.
+        mController.getPendingAudioRoute().addMessage(BT_AUDIO_CONNECTED, BT_ADDRESS_1);
+        mController.sendMessageWithSessionInfo(BT_AUDIO_DISCONNECTED, 0, BLUETOOTH_DEVICE_1);
+        waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT);
+        assertFalse(mController.getPendingAudioRoute().getPendingMessages().contains(
+                new Pair<>(BT_AUDIO_CONNECTED, BT_ADDRESS_1)));
+
+        // Verify the same for SPEAKER_ON that SPEAKER_OFF and BT_AUDIO_DISCONNECTED messages are
+        // cleared
+        mController.getPendingAudioRoute().addMessage(BT_AUDIO_DISCONNECTED, BT_ADDRESS_1);
+        mController.getPendingAudioRoute().addMessage(BT_AUDIO_DISCONNECTED,
+                scoDevice.getAddress());
+        mController.getPendingAudioRoute().addMessage(SPEAKER_OFF, null);
+        mController.sendMessageWithSessionInfo(SPEAKER_ON);
+        waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT);
+        assertFalse(mController.getPendingAudioRoute().getPendingMessages().contains(
+                new Pair<>(BT_AUDIO_DISCONNECTED, BT_ADDRESS_1)));
+        assertFalse(mController.getPendingAudioRoute().getPendingMessages().contains(
+                new Pair<>(BT_AUDIO_DISCONNECTED, scoDevice.getAddress())));
+        // Verify the speaker off message was cleared as well and the status bar notifier was
+        // invoked.
+        assertFalse(mController.getPendingAudioRoute().getPendingMessages().contains(
+                new Pair<>(SPEAKER_OFF, null)));
+
+        // Verify that for SPEAKER_OFF, we clear the SPEAKER_ON pending message
+        mController.getPendingAudioRoute().addMessage(SPEAKER_ON, null);
+        mController.sendMessageWithSessionInfo(SPEAKER_OFF);
+        waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT);
+        assertFalse(mController.getPendingAudioRoute().getPendingMessages().contains(
+                new Pair<>(SPEAKER_ON, null)));
+        BLUETOOTH_DEVICES.remove(scoDevice);
+    }
+
     private void verifyConnectBluetoothDevice(int audioType) {
         mController.initialize();
         mController.setActive(true);