Cleanup default on speaker on wired headset disconnect

Upon investigation, there was a different issue with call audio routing
to speaker when a wired headset was connected and the user turned on/off
speaker. After the headset was disconnected, audio was routing to
speaker because originally we were looking at the original audio route
from the pending audio route. When speaker is turned off, the original
route is still speaker while the destination route is wired headset so
that solution didn't properly address the case when the speaker was
turned off while a headset was plugged in.

This CL modifies this solution further by looking at the user switches
and using that information to determine whether we should default to
speaker when a wired headset is unplugged.

This CL also cleans up the existing flag which has already rolled out
into 25Q2 and is going in as an unflagged change this time.

Bug: 404681140
Flag: EXEMPT bugfix
Test: atest CallAudioRouteControllerTest
Test: Manual test to ensure that when a wired headset is plugged in and
speaker is toggled on/off that we don't route back to speaker when the
headset is unpluffed.
Test: Manual test to ensure that existing cases of speaker turning on
when wired headset is unplugged and speaker was the previous route still
works.
(cherry picked from https://googleplex-android-review.googlesource.com/q/commit:a27b84876fd6a9bdcd569a99c33f7c85d6a4c43f)
Merged-In: I8ec06592da09db4b94068adaa96f20c5d22fb275
Change-Id: I8ec06592da09db4b94068adaa96f20c5d22fb275
diff --git a/flags/telecom_callaudioroutestatemachine_flags.aconfig b/flags/telecom_callaudioroutestatemachine_flags.aconfig
index 96a5e38..76a7f74 100644
--- a/flags/telecom_callaudioroutestatemachine_flags.aconfig
+++ b/flags/telecom_callaudioroutestatemachine_flags.aconfig
@@ -163,17 +163,6 @@
   }
 }
 
-# OWNER=pmadapurmath TARGET=25Q2
-flag {
-  name: "default_speaker_on_wired_headset_disconnect"
-  namespace: "telecom"
-  description: "Maybe route back into speaker (if it was the previous route) when a wired headset disconnects"
-  bug: "376364368"
-  metadata {
-    purpose: PURPOSE_BUGFIX
-  }
-}
-
 # OWNER=tgunn TARGET=25Q2
 flag {
   name: "bus_device_is_a_speaker"
diff --git a/src/com/android/server/telecom/CallAudioRouteController.java b/src/com/android/server/telecom/CallAudioRouteController.java
index ce06d55..2de894b 100644
--- a/src/com/android/server/telecom/CallAudioRouteController.java
+++ b/src/com/android/server/telecom/CallAudioRouteController.java
@@ -188,6 +188,7 @@
     private boolean mIsMute;
     private boolean mIsPending;
     private boolean mIsActive;
+    private boolean mWasOnSpeaker;
     private final TelecomMetricsController mMetricsController;
 
     public CallAudioRouteController(
@@ -210,6 +211,7 @@
         mFocusType = NO_FOCUS;
         mIsScoAudioConnected = false;
         mUsePreferredDeviceStrategy = true;
+        mWasOnSpeaker = false;
         setCurrentCommunicationDevice(null);
 
         mTelecomLock = callsManager.getLock();
@@ -306,16 +308,16 @@
                             break;
                         case SWITCH_EARPIECE:
                         case USER_SWITCH_EARPIECE:
-                            handleSwitchEarpiece();
+                            handleSwitchEarpiece(msg.what == USER_SWITCH_EARPIECE);
                             break;
                         case SWITCH_BLUETOOTH:
                         case USER_SWITCH_BLUETOOTH:
                             address = (String) ((SomeArgs) msg.obj).arg2;
-                            handleSwitchBluetooth(address);
+                            handleSwitchBluetooth(address, msg.what == USER_SWITCH_BLUETOOTH);
                             break;
                         case SWITCH_HEADSET:
                         case USER_SWITCH_HEADSET:
-                            handleSwitchHeadset();
+                            handleSwitchHeadset(msg.what == USER_SWITCH_HEADSET);
                             break;
                         case SWITCH_SPEAKER:
                         case USER_SWITCH_SPEAKER:
@@ -678,10 +680,7 @@
             // Preserve speaker routing if it was the last audio routing path when the wired headset
             // disconnects. Ignore this special cased routing when the route isn't active
             // (in other words, when we're not in a call).
-            AudioRoute route = mFeatureFlags.defaultSpeakerOnWiredHeadsetDisconnect()
-                    && mIsActive && mPendingAudioRoute.getOrigRoute() != null
-                    && mPendingAudioRoute.getOrigRoute().getType() == TYPE_SPEAKER
-                    && mSpeakerDockRoute != null
+            AudioRoute route = mWasOnSpeaker && mIsActive && mSpeakerDockRoute != null
                     && mSpeakerDockRoute.getType() == AudioRoute.TYPE_SPEAKER
                     ? mSpeakerDockRoute : getBaseRoute(true, null);
             routeTo(mIsActive, route);
@@ -894,6 +893,7 @@
             }
         }
         if ((mIsPending && pendingRouteNeedsUpdate) || (!mIsPending && currentRouteNeedsUpdate)) {
+            maybeDisableWasOnSpeaker(true);
             // Fallback to an available route excluding the previously active device.
             routeTo(mIsActive, getBaseRoute(true, previouslyActiveDeviceAddress));
         }
@@ -928,6 +928,7 @@
         mFocusType = focus;
         switch (focus) {
             case NO_FOCUS -> {
+                mWasOnSpeaker = false;
                 // Notify the CallAudioModeStateMachine that audio operations are complete so
                 // that we can relinquish audio focus.
                 mCallAudioManager.notifyAudioOperationsComplete();
@@ -993,16 +994,17 @@
         }
     }
 
-    public void handleSwitchEarpiece() {
+    public void handleSwitchEarpiece(boolean isUserRequest) {
         AudioRoute earpieceRoute = mTypeRoutes.get(AudioRoute.TYPE_EARPIECE);
         if (earpieceRoute != null && getCallSupportedRoutes().contains(earpieceRoute)) {
+            maybeDisableWasOnSpeaker(isUserRequest);
             routeTo(mIsActive, earpieceRoute);
         } else {
             Log.i(this, "ignore switch earpiece request");
         }
     }
 
-    private void handleSwitchBluetooth(String address) {
+    private void handleSwitchBluetooth(String address, boolean isUserRequest) {
         Log.i(this, "handle switch to bluetooth with address %s", address);
         AudioRoute bluetoothRoute = null;
         BluetoothDevice bluetoothDevice = null;
@@ -1020,6 +1022,7 @@
         }
 
         if (bluetoothRoute != null && bluetoothDevice != null) {
+            maybeDisableWasOnSpeaker(isUserRequest);
             if (mFocusType == RINGING_FOCUS) {
                 routeTo(mBluetoothRouteManager
                                 .isInbandRingEnabled(bluetoothRoute.getType(), bluetoothDevice)
@@ -1051,9 +1054,10 @@
         }
     }
 
-    private void handleSwitchHeadset() {
+    private void handleSwitchHeadset(boolean isUserRequest) {
         AudioRoute headsetRoute = mTypeRoutes.get(AudioRoute.TYPE_WIRED);
         if (headsetRoute != null && getCallSupportedRoutes().contains(headsetRoute)) {
+            maybeDisableWasOnSpeaker(isUserRequest);
             routeTo(mIsActive, headsetRoute);
         } else {
             Log.i(this, "ignore switch headset request");
@@ -1103,6 +1107,7 @@
                 return;
             }
         }
+        maybeDisableWasOnSpeaker(isExplicitUserRequest);
         routeTo(mIsActive, calculateBaselineRoute(isExplicitUserRequest, includeBluetooth,
                 btAddressToExclude));
     }
@@ -1157,6 +1162,11 @@
             mPendingAudioRoute.clearPendingMessages();
             onCurrentRouteChanged();
             if (mIsActive) {
+                // Only set mWasOnSpeaker if the routing was active. We don't want to consider this
+                // selection outside of a call.
+                if (mCurrentRoute.getType() == TYPE_SPEAKER) {
+                    mWasOnSpeaker = true;
+                }
                 // Reinitialize the audio ops complete latch since the routing went active. We
                 // should always expect operations to complete after this point.
                 if (mAudioOperationsCompleteLatch.getCount() == 0) {
@@ -1757,4 +1767,10 @@
             return mCurrentCommunicationDevice;
         }
     }
+
+    private void maybeDisableWasOnSpeaker(boolean isUserRequest) {
+        if (isUserRequest) {
+            mWasOnSpeaker = false;
+        }
+    }
 }
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
index 1b1ca56..b31143a 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
@@ -530,23 +530,35 @@
     @SmallTest
     @Test
     public void testDefaultSpeakerOnWiredHeadsetDisconnect() {
-        when(mFeatureFlags.defaultSpeakerOnWiredHeadsetDisconnect()).thenReturn(true);
         mController.initialize();
         mController.setActive(true);
         verifyMaybeDefaultSpeakerOnDisconnectWiredHeadset(
-                CallAudioState.ROUTE_SPEAKER /* expectedAudioType */);
+                CallAudioState.ROUTE_SPEAKER /* expectedAudioType */,
+                false /* includeUserSwitch */);
     }
 
     @SmallTest
     @Test
     public void testIgnoreDefaultSpeakerOnWiredHeadsetDisconnect() {
-        when(mFeatureFlags.defaultSpeakerOnWiredHeadsetDisconnect()).thenReturn(true);
         // Note here that the routing isn't active to represent that we're not in a call. If a wired
         // headset is disconnected and the last route was speaker, we shouldn't switch back to
         // speaker when we're not in a call.
         mController.initialize();
         verifyMaybeDefaultSpeakerOnDisconnectWiredHeadset(
-                CallAudioState.ROUTE_EARPIECE /* expectedAudioType */);
+                CallAudioState.ROUTE_EARPIECE /* expectedAudioType */,
+                false /* includeUserSwitch */);
+    }
+
+    @SmallTest
+    @Test
+    public void testIgnoreDefaultSpeakerOnWiredHeadsetDisconnect_UserSwitchesOutOfSpeaker() {
+        mController.initialize();
+        mController.setActive(true);
+        // Verify that when we turn speaker on/off when a wired headset is plugged in and after the
+        // headset is disconnected that we don't default audio routing back to speaker.
+        verifyMaybeDefaultSpeakerOnDisconnectWiredHeadset(
+                CallAudioState.ROUTE_EARPIECE /* expectedAudioType */,
+                true /* includeUserSwitch */);
     }
 
     @SmallTest
@@ -1473,7 +1485,7 @@
                 any(CallAudioState.class), eq(expectedState));
     }
 
-    private void verifyMaybeDefaultSpeakerOnDisconnectWiredHeadset(int expectedAudioType) {
+    private void verifyMaybeDefaultSpeakerOnDisconnectWiredHeadset(int expectedAudioType, boolean includeUserSwitch) {
         // Ensure audio is routed to speaker initially
         mController.sendMessageWithSessionInfo(SPEAKER_ON);
         CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
@@ -1491,6 +1503,30 @@
         verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
                 any(CallAudioState.class), eq(expectedState));
 
+        // Emulate scenario with user turning on/off speaker. This is to verify that when the user
+        // switches off speaker that we don't auto route back to speaker when the wired headset
+        // disconnects.
+        if (includeUserSwitch) {
+            // Verify speaker turned on from USER_SWITCH_SPEAKER
+            mController.sendMessageWithSessionInfo(USER_SWITCH_SPEAKER);
+            mController.sendMessageWithSessionInfo(SPEAKER_ON);
+            expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+                    CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+                    new HashSet<>());
+            verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                    any(CallAudioState.class), eq(expectedState));
+
+            // Verify speaker turned off from turning off speaker
+            mController.sendMessageWithSessionInfo(USER_SWITCH_BASELINE_ROUTE,
+                    INCLUDE_BLUETOOTH_IN_BASELINE);
+            mController.sendMessageWithSessionInfo(SPEAKER_OFF);
+            expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+                    CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER, null,
+                    new HashSet<>());
+            verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                    any(CallAudioState.class), eq(expectedState));
+        }
+
         // Verify that we route back into speaker once the wired headset disconnects
         mController.sendMessageWithSessionInfo(DISCONNECT_WIRED_HEADSET);
         expectedState = new CallAudioState(false, expectedAudioType,