Resolve WhatsApp audio route switching

This CL addresses two issues with audio routing with self-managed
calling (namely, with WhatsApp).

The first issue is that when WhatsApp
requests a switch to BT (USER_SWITCH_BLUETOOTH), it never specifies a BT
address to connect to. This has always been the existing behavior and in
the legacy audio routing path, Telecom would arbitrarily connect to an
available BT device. This CL ensures that an available device is
connected to if there's no address specified in the client's request.

The second issue deals with when we receive a client request to switch
the baseline route. This occurs when the user is on speaker or on BT and
selects the button again via the UI (in which case, the baseline route
is requested to be routed into). For normal calls, this would usually
mean we route into earpiece and for video calls, we route into speaker.
This CL fixes a bug with how (USER)_SWITCH_BASLINE_ROUTE is handled to
ensure we take into account whether or not we should include BT in the
route selection or not and also ensures that we ignore selecting BT if
it's the preferred audio route and Telecom is requested to ignore it.

Bug: 339684821
Test: Manual with WhatsApp (video) calls.
Test: atest CallAudioRouteControllerTest
Change-Id: I83ae49a8d231e67a041c082388988f36241d56f0
diff --git a/src/com/android/server/telecom/CallAudioRouteController.java b/src/com/android/server/telecom/CallAudioRouteController.java
index 820219d..76555c3 100644
--- a/src/com/android/server/telecom/CallAudioRouteController.java
+++ b/src/com/android/server/telecom/CallAudioRouteController.java
@@ -53,6 +53,7 @@
 import com.android.server.telecom.bluetooth.BluetoothRouteManager;
 import com.android.server.telecom.flags.FeatureFlags;
 
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -76,6 +77,9 @@
         ROUTE_MAP.put(AudioRoute.TYPE_STREAMING, CallAudioState.ROUTE_STREAMING);
     }
 
+    /** Valid values for the first argument for SWITCH_BASELINE_ROUTE */
+    public static final int INCLUDE_BLUETOOTH_IN_BASELINE = 1;
+
     private final CallsManager mCallsManager;
     private final Context mContext;
     private AudioManager mAudioManager;
@@ -91,6 +95,8 @@
     private AudioRoute mStreamingRoute;
     private Set<AudioRoute> mStreamingRoutes;
     private Map<AudioRoute, BluetoothDevice> mBluetoothRoutes;
+    private Pair<Integer, String> mActiveBluetoothDevice;
+    private Map<Integer, String> mActiveDeviceCache;
     private Map<Integer, AudioRoute> mTypeRoutes;
     private PendingAudioRoute mPendingAudioRoute;
     private AudioRoute.Factory mAudioRouteFactory;
@@ -260,9 +266,13 @@
                             handleSwitchSpeaker();
                             break;
                         case SWITCH_BASELINE_ROUTE:
-                        case USER_SWITCH_BASELINE_ROUTE:
                             address = (String) ((SomeArgs) msg.obj).arg2;
-                            handleSwitchBaselineRoute(address);
+                            handleSwitchBaselineRoute(msg.arg1 == INCLUDE_BLUETOOTH_IN_BASELINE,
+                                    address);
+                            break;
+                        case USER_SWITCH_BASELINE_ROUTE:
+                            handleSwitchBaselineRoute(msg.arg1 == INCLUDE_BLUETOOTH_IN_BASELINE,
+                                    null);
                             break;
                         case SPEAKER_ON:
                             handleSpeakerOn();
@@ -312,6 +322,11 @@
     public void initialize() {
         mAvailableRoutes = new HashSet<>();
         mBluetoothRoutes = new LinkedHashMap<>();
+        mActiveDeviceCache = new HashMap<>();
+        mActiveDeviceCache.put(AudioRoute.TYPE_BLUETOOTH_SCO, null);
+        mActiveDeviceCache.put(AudioRoute.TYPE_BLUETOOTH_HA, null);
+        mActiveDeviceCache.put(AudioRoute.TYPE_BLUETOOTH_LE, null);
+        mActiveBluetoothDevice = null;
         mTypeRoutes = new ArrayMap<>();
         mStreamingRoutes = new HashSet<>();
         mPendingAudioRoute = new PendingAudioRoute(this, mAudioManager, mBluetoothRouteManager);
@@ -805,11 +820,16 @@
         Log.i(this, "handle switch to bluetooth with address %s", address);
         AudioRoute bluetoothRoute = null;
         BluetoothDevice bluetoothDevice = null;
-        for (AudioRoute route : getAvailableRoutes()) {
-            if (Objects.equals(address, route.getBluetoothAddress())) {
-                bluetoothRoute = route;
-                bluetoothDevice = mBluetoothRoutes.get(route);
-                break;
+        if (address == null) {
+            bluetoothRoute = getArbitraryBluetoothDevice();
+            bluetoothDevice = mBluetoothRoutes.get(bluetoothRoute);
+        } else {
+            for (AudioRoute route : getAvailableRoutes()) {
+                if (Objects.equals(address, route.getBluetoothAddress())) {
+                    bluetoothRoute = route;
+                    bluetoothDevice = mBluetoothRoutes.get(route);
+                    break;
+                }
             }
         }
 
@@ -825,6 +845,20 @@
         }
     }
 
+    /**
+     * Retrieve the active BT device, if available, otherwise return the most recently tracked
+     * active device, or null if none are available.
+     * @return {@link AudioRoute} of the BT device.
+     */
+    private AudioRoute getArbitraryBluetoothDevice() {
+        if (mActiveBluetoothDevice != null) {
+            return getBluetoothRoute(mActiveBluetoothDevice.first, mActiveBluetoothDevice.second);
+        } else if (!mBluetoothRoutes.isEmpty()) {
+            return mBluetoothRoutes.keySet().stream().toList().get(mBluetoothRoutes.size() - 1);
+        }
+        return null;
+    }
+
     private void handleSwitchHeadset() {
         AudioRoute headsetRoute = mTypeRoutes.get(AudioRoute.TYPE_WIRED);
         if (headsetRoute != null && getAvailableRoutes().contains(headsetRoute)) {
@@ -842,8 +876,8 @@
         }
     }
 
-    private void handleSwitchBaselineRoute(String btAddressToExclude) {
-        routeTo(mIsActive, getBaseRoute(true, btAddressToExclude));
+    private void handleSwitchBaselineRoute(boolean includeBluetooth, String btAddressToExclude) {
+        routeTo(mIsActive, getBaseRoute(includeBluetooth, btAddressToExclude));
     }
 
     private void handleSpeakerOn() {
@@ -1085,7 +1119,7 @@
 
     public AudioRoute getBaseRoute(boolean includeBluetooth, String btAddressToExclude) {
         AudioRoute destRoute = getPreferredAudioRouteFromStrategy();
-        if (destRoute == null) {
+        if (destRoute == null || (destRoute.getBluetoothAddress() != null && !includeBluetooth)) {
             destRoute = getPreferredAudioRouteFromDefault(includeBluetooth, btAddressToExclude);
         }
         if (destRoute != null && !getAvailableRoutes().contains(destRoute)) {
@@ -1243,6 +1277,39 @@
         mIsScoAudioConnected = value;
     }
 
+    /**
+     * Update the active bluetooth device being tracked (as well as for individual profiles).
+     * We need to keep track of active devices for individual profiles because of potential
+     * inconsistencies found in BluetoothStateReceiver#handleActiveDeviceChanged. When multiple
+     * profiles are paired, we could have a scenario where an active device A is replaced
+     * with an active device B (from a different profile), which is then removed as an active
+     * device shortly after, causing device A to be reactive. It's possible that the active device
+     * changed intent is never received again for device A so an active device cache is necessary
+     * to track these devices at a profile level.
+     * @param device {@link Pair} containing the BT audio route type (i.e. SCO/HA/LE) and the
+     *                           address of the device.
+     */
+    public void updateActiveBluetoothDevice(Pair<Integer, String> device) {
+        mActiveDeviceCache.put(device.first, device.second);
+        // Update most recently active device if address isn't null (meaning some device is active).
+        if (device.second != null) {
+            mActiveBluetoothDevice = device;
+        } else {
+            // If a device was removed, check to ensure that no other device is still considered
+            // active.
+            boolean hasActiveDevice = false;
+            for (String address : mActiveDeviceCache.values()) {
+                if (address != null) {
+                    hasActiveDevice = true;
+                    break;
+                }
+            }
+            if (!hasActiveDevice) {
+                mActiveBluetoothDevice = null;
+            }
+        }
+    }
+
     @VisibleForTesting
     public void setActive(boolean active) {
         if (active) {
diff --git a/src/com/android/server/telecom/PendingAudioRoute.java b/src/com/android/server/telecom/PendingAudioRoute.java
index f9cdc35..396aca0 100644
--- a/src/com/android/server/telecom/PendingAudioRoute.java
+++ b/src/com/android/server/telecom/PendingAudioRoute.java
@@ -18,6 +18,7 @@
 
 import static com.android.server.telecom.CallAudioRouteAdapter.PENDING_ROUTE_FAILED;
 import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BASELINE_ROUTE;
+import static com.android.server.telecom.CallAudioRouteController.INCLUDE_BLUETOOTH_IN_BASELINE;
 
 import android.bluetooth.BluetoothDevice;
 import android.media.AudioManager;
@@ -96,7 +97,7 @@
         if (message.first == PENDING_ROUTE_FAILED) {
             // Fallback to base route
             mCallAudioRouteController.sendMessageWithSessionInfo(
-                    SWITCH_BASELINE_ROUTE, 0, btAddressToExclude);
+                    SWITCH_BASELINE_ROUTE, INCLUDE_BLUETOOTH_IN_BASELINE, btAddressToExclude);
             return;
         }
 
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
index 0a8ce5a..3c97d41 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothDeviceManager.java
@@ -20,6 +20,7 @@
 import static com.android.server.telecom.AudioRoute.TYPE_BLUETOOTH_SCO;
 import static com.android.server.telecom.CallAudioRouteAdapter.BT_DEVICE_REMOVED;
 import static com.android.server.telecom.CallAudioRouteAdapter.SWITCH_BASELINE_ROUTE;
+import static com.android.server.telecom.CallAudioRouteController.INCLUDE_BLUETOOTH_IN_BASELINE;
 
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
@@ -257,7 +258,7 @@
                     BT_DEVICE_REMOVED, route.getType(), device);
         }
         mCallAudioRouteAdapter.sendMessageWithSessionInfo(
-                SWITCH_BASELINE_ROUTE, 0, (String) null);
+                SWITCH_BASELINE_ROUTE, INCLUDE_BLUETOOTH_IN_BASELINE, (String) null);
     }
 
     private final LinkedHashMap<String, BluetoothDevice> mHfpDevicesByAddress =
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
index 9168388..f76391c 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothStateReceiver.java
@@ -236,10 +236,15 @@
                 BluetoothDeviceManager.getDeviceTypeString(deviceType));
 
         if (Flags.useRefactoredAudioRouteSwitching()) {
+            CallAudioRouteController audioRouteController = (CallAudioRouteController)
+                    mCallAudioRouteAdapter;
             if (device == null) {
+                audioRouteController.updateActiveBluetoothDevice(new Pair(audioRouteType, null));
                 mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_GONE,
                         audioRouteType);
             } else {
+                audioRouteController.updateActiveBluetoothDevice(
+                        new Pair(audioRouteType, device.getAddress()));
                 mCallAudioRouteAdapter.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
                         audioRouteType, device.getAddress());
                 if (deviceType == BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID