Support TYPE_BUS for call audio routing.

This CL adds support for handling TYPE_BUS (auto case) in the audio
routing. This route is used by auto devices where earpiece and speaker
aren't available. The current implementation resolves the routing to
an INVALID type which causes tests to fail as call endpoints aren't
updated accordingly. For now, we can resolve the route to
CallAudioState.ROUTE_SPEAKER as is done in the legacy audio routing in
order to resolve the test failures.

Moving forward, we can rework this if audio fwk team decides to
whitelist TYPE_BUS as a valid communication device. For now, this audio
device doesn't need to use the set/clear communication device APIs.

Bug: 364562758
Test: atest CallAudioRouteControllerTest
Flag: com.android.server.telecom.flags.use_refactored_audio_route_switching
Change-Id: Idf30cfb6b4e65e4add0c5b402211aeb680cda7c9
diff --git a/src/com/android/server/telecom/AudioRoute.java b/src/com/android/server/telecom/AudioRoute.java
index 7ce9fcc..4f563a1 100644
--- a/src/com/android/server/telecom/AudioRoute.java
+++ b/src/com/android/server/telecom/AudioRoute.java
@@ -117,6 +117,8 @@
     public static final int TYPE_BLUETOOTH_HA = 6;
     public static final int TYPE_BLUETOOTH_LE = 7;
     public static final int TYPE_STREAMING = 8;
+    // Used by auto
+    public static final int TYPE_BUS = 9;
     @IntDef(prefix = "TYPE", value = {
             TYPE_INVALID,
             TYPE_EARPIECE,
@@ -126,7 +128,8 @@
             TYPE_BLUETOOTH_SCO,
             TYPE_BLUETOOTH_HA,
             TYPE_BLUETOOTH_LE,
-            TYPE_STREAMING
+            TYPE_STREAMING,
+            TYPE_BUS
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface AudioRouteType {}
@@ -155,35 +158,38 @@
         DEVICE_TYPE_STRINGS.put(TYPE_WIRED, "TYPE_WIRED_HEADSET");
         DEVICE_TYPE_STRINGS.put(TYPE_SPEAKER, "TYPE_SPEAKER");
         DEVICE_TYPE_STRINGS.put(TYPE_DOCK, "TYPE_DOCK");
+        DEVICE_TYPE_STRINGS.put(TYPE_BUS, "TYPE_BUS");
         DEVICE_TYPE_STRINGS.put(TYPE_BLUETOOTH_SCO, "TYPE_BLUETOOTH_SCO");
         DEVICE_TYPE_STRINGS.put(TYPE_BLUETOOTH_HA, "TYPE_BLUETOOTH_HA");
         DEVICE_TYPE_STRINGS.put(TYPE_BLUETOOTH_LE, "TYPE_BLUETOOTH_LE");
         DEVICE_TYPE_STRINGS.put(TYPE_STREAMING, "TYPE_STREAMING");
     }
 
-    public static final HashMap<Integer, Integer> DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE;
+    public static final HashMap<Integer, Integer> DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE;
     static {
-        DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE = new HashMap<>();
-        DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE,
+        DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE = new HashMap<>();
+        DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BUILTIN_EARPIECE,
                 TYPE_EARPIECE);
-        DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, TYPE_SPEAKER);
-        DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_WIRED_HEADSET, TYPE_WIRED);
-        DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_WIRED_HEADPHONES, TYPE_WIRED);
-        DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
+        DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER,
+                TYPE_SPEAKER);
+        DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_WIRED_HEADSET, TYPE_WIRED);
+        DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_WIRED_HEADPHONES, TYPE_WIRED);
+        DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BLUETOOTH_SCO,
                 TYPE_BLUETOOTH_SCO);
-        DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_USB_DEVICE, TYPE_WIRED);
-        DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_USB_ACCESSORY, TYPE_WIRED);
-        DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_DOCK, TYPE_DOCK);
-        DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_USB_HEADSET, TYPE_WIRED);
-        DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_HEARING_AID,
+        DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_USB_DEVICE, TYPE_WIRED);
+        DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_USB_ACCESSORY, TYPE_WIRED);
+        DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_DOCK, TYPE_DOCK);
+        DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_USB_HEADSET, TYPE_WIRED);
+        DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_HEARING_AID,
                 TYPE_BLUETOOTH_HA);
-        DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BLE_HEADSET,
+        DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BLE_HEADSET,
                 TYPE_BLUETOOTH_LE);
-        DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BLE_SPEAKER,
+        DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BLE_SPEAKER,
                 TYPE_BLUETOOTH_LE);
-        DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BLE_BROADCAST,
+        DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BLE_BROADCAST,
                 TYPE_BLUETOOTH_LE);
-        DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_DOCK_ANALOG, TYPE_DOCK);
+        DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_DOCK_ANALOG, TYPE_DOCK);
+        DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.put(AudioDeviceInfo.TYPE_BUS, TYPE_BUS);
     }
 
     private static final HashMap<Integer, List<Integer>> AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE;
@@ -210,6 +216,10 @@
         dockDeviceInfoTypes.add(AudioDeviceInfo.TYPE_DOCK_ANALOG);
         AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.put(TYPE_DOCK, dockDeviceInfoTypes);
 
+        List<Integer> busDeviceInfoTypes = new ArrayList<>();
+        busDeviceInfoTypes.add(AudioDeviceInfo.TYPE_BUS);
+        AUDIO_ROUTE_TYPE_TO_DEVICE_INFO_TYPE.put(TYPE_BUS, busDeviceInfoTypes);
+
         List<Integer> bluetoothScoDeviceInfoTypes = new ArrayList<>();
         bluetoothScoDeviceInfoTypes.add(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
         bluetoothScoDeviceInfoTypes.add(AudioDeviceInfo.TYPE_BLUETOOTH_SCO);
@@ -241,6 +251,10 @@
             BluetoothRouteManager bluetoothRouteManager, boolean isScoAudioConnected) {
         Log.i(this, "onDestRouteAsPendingRoute: active (%b), type (%s)", active,
                 DEVICE_TYPE_STRINGS.get(mAudioRouteType));
+        if (mAudioRouteType == TYPE_BUS) {
+            Log.i(this, "onDestRouteAsPendingRoute: Ignore processing dest route for TYPE_BUS");
+            return;
+        }
         if (pendingAudioRoute.isActive() && !active) {
             clearCommunicationDevice(pendingAudioRoute, bluetoothRouteManager, audioManager);
         } else if (active) {
@@ -302,6 +316,10 @@
     void onOrigRouteAsPendingRoute(boolean active, PendingAudioRoute pendingAudioRoute,
             AudioManager audioManager, BluetoothRouteManager bluetoothRouteManager) {
         Log.i(this, "onOrigRouteAsPendingRoute: active (%b), type (%d)", active, mAudioRouteType);
+        if (mAudioRouteType == TYPE_BUS) {
+            Log.i(this, "onOrigRouteAsPendingRoute: Ignore processing dest route for TYPE_BUS");
+            return;
+        }
         if (active) {
             if (mAudioRouteType == TYPE_SPEAKER) {
                 pendingAudioRoute.addMessage(SPEAKER_OFF, null);
diff --git a/src/com/android/server/telecom/CallAudioRouteController.java b/src/com/android/server/telecom/CallAudioRouteController.java
index 3dcd58e..0ed4308 100644
--- a/src/com/android/server/telecom/CallAudioRouteController.java
+++ b/src/com/android/server/telecom/CallAudioRouteController.java
@@ -63,7 +63,6 @@
 import java.util.Set;
 
 public class CallAudioRouteController implements CallAudioRouteAdapter {
-    private static final long TIMEOUT_LIMIT = 2000L;
     private static final AudioRoute DUMMY_ROUTE = new AudioRoute(TYPE_INVALID, null, null);
     private static final Map<Integer, Integer> ROUTE_MAP;
     static {
@@ -73,6 +72,7 @@
         ROUTE_MAP.put(AudioRoute.TYPE_WIRED, CallAudioState.ROUTE_WIRED_HEADSET);
         ROUTE_MAP.put(AudioRoute.TYPE_SPEAKER, CallAudioState.ROUTE_SPEAKER);
         ROUTE_MAP.put(AudioRoute.TYPE_DOCK, CallAudioState.ROUTE_SPEAKER);
+        ROUTE_MAP.put(AudioRoute.TYPE_BUS, CallAudioState.ROUTE_SPEAKER);
         ROUTE_MAP.put(AudioRoute.TYPE_BLUETOOTH_SCO, CallAudioState.ROUTE_BLUETOOTH);
         ROUTE_MAP.put(AudioRoute.TYPE_BLUETOOTH_HA, CallAudioState.ROUTE_BLUETOOTH);
         ROUTE_MAP.put(AudioRoute.TYPE_BLUETOOTH_LE, CallAudioState.ROUTE_BLUETOOTH);
@@ -353,14 +353,22 @@
 
         int supportMask = calculateSupportedRouteMaskInit();
         if ((supportMask & CallAudioState.ROUTE_SPEAKER) != 0) {
+            int audioRouteType = AudioRoute.TYPE_SPEAKER;
             // Create speaker routes
             mSpeakerDockRoute = mAudioRouteFactory.create(AudioRoute.TYPE_SPEAKER, null,
                     mAudioManager);
-            if (mSpeakerDockRoute == null) {
-                Log.w(this, "Can't find available audio device info for route TYPE_SPEAKER");
-            } else {
-                mTypeRoutes.put(AudioRoute.TYPE_SPEAKER, mSpeakerDockRoute);
+            // Todo(b/364562758): Represent TYPE_BUS as CallAudioState.ROUTE_SPEAKER (moving
+            // forward, we may rework this if audio fwk team decides to allow list this as a
+            // valid communication device.
+            if (mSpeakerDockRoute != null || hasBusAudioDevice()) {
+                if (mSpeakerDockRoute == null){
+                    mSpeakerDockRoute = new AudioRoute(AudioRoute.TYPE_BUS, null, null);
+                    audioRouteType = AudioRoute.TYPE_BUS;
+                }
+                mTypeRoutes.put(audioRouteType, mSpeakerDockRoute);
                 updateAvailableRoutes(mSpeakerDockRoute, true);
+            } else {
+                Log.w(this, "Can't find available audio device info for route TYPE_SPEAKER");
             }
         }
 
@@ -905,7 +913,8 @@
     }
 
     private void handleSwitchSpeaker() {
-        if (mSpeakerDockRoute != null && getCallSupportedRoutes().contains(mSpeakerDockRoute)) {
+        if (mSpeakerDockRoute != null && getCallSupportedRoutes().contains(mSpeakerDockRoute)
+                && mSpeakerDockRoute.getType() == AudioRoute.TYPE_SPEAKER) {
             routeTo(mIsActive, mSpeakerDockRoute);
         } else {
             Log.i(this, "ignore switch speaker request");
@@ -923,8 +932,8 @@
             // Update status bar notification if we are in a call.
             mStatusBarNotifier.notifySpeakerphone(mCallsManager.hasAnyCalls());
         } else {
-            if (mSpeakerDockRoute != null && getCallSupportedRoutes()
-                    .contains(mSpeakerDockRoute)) {
+            if (mSpeakerDockRoute != null && getCallSupportedRoutes().contains(mSpeakerDockRoute)
+                    && mSpeakerDockRoute.getType() == AudioRoute.TYPE_SPEAKER) {
                 routeTo(mIsActive, mSpeakerDockRoute);
                 // Since the route switching triggered by this message, we need to manually send it
                 // again so that we won't stuck in the pending route
@@ -1084,6 +1093,24 @@
     }
 
     private AudioRoute getPreferredAudioRouteFromStrategy() {
+        // Get preferred device
+        AudioDeviceAttributes deviceAttr = getPreferredDeviceForStrategy();
+        Log.i(this, "getPreferredAudioRouteFromStrategy: preferred device is %s", deviceAttr);
+        if (deviceAttr == null) {
+            return null;
+        }
+
+        // Get corresponding audio route
+        @AudioRoute.AudioRouteType int type = AudioRoute.DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.get(
+                deviceAttr.getType());
+        if (BT_AUDIO_ROUTE_TYPES.contains(type)) {
+            return getBluetoothRoute(type, deviceAttr.getAddress());
+        } else {
+            return mTypeRoutes.get(type);
+        }
+    }
+
+    private AudioDeviceAttributes getPreferredDeviceForStrategy() {
         // Get audio produce strategy
         AudioProductStrategy strategy = null;
         final AudioAttributes attr = new AudioAttributes.Builder()
@@ -1099,21 +1126,26 @@
             return null;
         }
 
-        // Get preferred device
-        AudioDeviceAttributes deviceAttr = mAudioManager.getPreferredDeviceForStrategy(strategy);
-        Log.i(this, "getPreferredAudioRouteFromStrategy: preferred device is %s", deviceAttr);
-        if (deviceAttr == null) {
-            return null;
-        }
+        return mAudioManager.getPreferredDeviceForStrategy(strategy);
+    }
 
-        // Get corresponding audio route
-        @AudioRoute.AudioRouteType int type = AudioRoute.DEVICE_INFO_TYPETO_AUDIO_ROUTE_TYPE.get(
-                deviceAttr.getType());
-        if (BT_AUDIO_ROUTE_TYPES.contains(type)) {
-            return getBluetoothRoute(type, deviceAttr.getAddress());
-        } else {
-            return mTypeRoutes.get(type);
+    /**
+     * For auto, there is no earpiece or speakerphone routes available. The audio is routed to the
+     * bus but because this isn't a valid communication device,
+     * {@link AudioManager#getCommunicationDevice()} will not provide this device even if audio fwk
+     * reports it as the active communication device (refer to
+     * AudioDeviceBroker#getCommunicationDeviceInt()}. Check if the device is the preferred device
+     * for strategy instead.
+     */
+    private boolean hasBusAudioDevice() {
+        AudioDeviceAttributes deviceAttr = getPreferredDeviceForStrategy();
+        if (deviceAttr == null) {
+            return false;
         }
+        // Get corresponding audio route mapping
+        @AudioRoute.AudioRouteType int type = AudioRoute.DEVICE_INFO_TYPE_TO_AUDIO_ROUTE_TYPE.get(
+                deviceAttr.getType());
+        return type == AudioRoute.TYPE_BUS;
     }
 
     private AudioRoute getPreferredAudioRouteFromDefault(boolean includeBluetooth,
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
index 72e2111..1254b8a 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
@@ -43,6 +43,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.nullable;
@@ -63,7 +64,6 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.media.AudioDeviceInfo;
-import android.media.AudioFocusRequest;
 import android.media.AudioManager;
 import android.media.IAudioService;
 import android.media.audiopolicy.AudioProductStrategy;
@@ -86,7 +86,6 @@
 import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
 import com.android.server.telecom.bluetooth.BluetoothRouteManager;
 
-import dalvik.annotation.TestTarget;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -119,6 +118,7 @@
     @Mock private TelecomSystem.SyncRoot mLock;
     private AudioRoute mEarpieceRoute;
     private AudioRoute mSpeakerRoute;
+    private boolean mOverrideSpeakerToBus;
     private static final String BT_ADDRESS_1 = "00:00:00:00:00:01";
     private static final BluetoothDevice BLUETOOTH_DEVICE_1 =
             BluetoothRouteManagerTest.makeBluetoothDevice("00:00:00:00:00:01");
@@ -132,6 +132,9 @@
         @Override
         public AudioRoute create(@AudioRoute.AudioRouteType int type, String bluetoothAddress,
                                  AudioManager audioManager) {
+            if (mOverrideSpeakerToBus && type == AudioRoute.TYPE_SPEAKER) {
+                type = AudioRoute.TYPE_BUS;
+            }
             return new AudioRoute(type, bluetoothAddress, mAudioDeviceInfo);
         }
     };
@@ -177,6 +180,7 @@
         mController.setAudioManager(mAudioManager);
         mEarpieceRoute = new AudioRoute(AudioRoute.TYPE_EARPIECE, null, null);
         mSpeakerRoute = new AudioRoute(AudioRoute.TYPE_SPEAKER, null, null);
+        mOverrideSpeakerToBus = false;
         mController.setCallAudioManager(mCallAudioManager);
         when(mCallAudioManager.getForegroundCall()).thenReturn(mCall);
         when(mCall.getVideoState()).thenReturn(VideoProfile.STATE_AUDIO_ONLY);
@@ -877,6 +881,31 @@
         assertEquals(1, mController.getCallSupportedRoutes().size());
     }
 
+    @SmallTest
+    @Test
+    public void testRouteToBusForAuto() {
+        when(mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS))
+                .thenReturn(new AudioDeviceInfo[0]);
+        mOverrideSpeakerToBus = true;
+        mController.initialize();
+
+        mController.sendMessageWithSessionInfo(SWITCH_FOCUS, ACTIVE_FOCUS, 0);
+        waitForHandlerAction(mController.getAdapterHandler(), TEST_TIMEOUT);
+        PendingAudioRoute pendingRoute = mController.getPendingAudioRoute();
+        assertEquals(AudioRoute.TYPE_BUS, pendingRoute.getDestRoute().getType());
+
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+                CallAudioState.ROUTE_SPEAKER, null, new HashSet<>());
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Ensure that turning speaker phone on doesn't get triggered when speaker isn't available.
+        mController.sendMessageWithSessionInfo(USER_SWITCH_SPEAKER);
+        mController.sendMessageWithSessionInfo(SPEAKER_ON);
+        verify(mockStatusBarNotifier, times(0)).notifySpeakerphone(anyBoolean());
+
+    }
+
     private void verifyConnectBluetoothDevice(int audioType) {
         mController.initialize();
         mController.setActive(true);