Resolve update system audio route NPE and foreground updates

With the recent addition of handling UPDATE_SYSTEM_AUDIO_ROUTE in the
new call audio routing path, there was a report of a NPE found due to
the CS being unbound during the system audio update. As a result, it
caused a NPE on IConnectionService.onCallEndpointChanged further down
the stack. We should ensure that these updates are done under the
telecom lock.

Following that, also ensure that the audio routing is adjusted to the
foreground call. Currently, CallAudioRouteController considers all
potential audio routes that the device supports but for individual
calls, this is determined via Call#getSupportedAudioRoutes(). We should
ensure that the ICS/CS are notified which routes are available and
adjust the current route based on what the foreground call supports.
When we're not in a call, we can fall back to using the device supported
routes.

Bug: 351977307
Test: atest CallAudioRouteControllerTest
Flag: com.android.server.telecom.flags.use_refactored_audio_route_switching

Change-Id: I5fdd630fb82469783f7d298e25ba3f73910fa2c7
diff --git a/src/com/android/server/telecom/CallAudioRouteController.java b/src/com/android/server/telecom/CallAudioRouteController.java
index 5fb4a3a..6235ec9 100644
--- a/src/com/android/server/telecom/CallAudioRouteController.java
+++ b/src/com/android/server/telecom/CallAudioRouteController.java
@@ -89,6 +89,7 @@
     private final Handler mHandler;
     private final WiredHeadsetManager mWiredHeadsetManager;
     private Set<AudioRoute> mAvailableRoutes;
+    private Set<AudioRoute> mCallSupportedRoutes;
     private AudioRoute mCurrentRoute;
     private AudioRoute mEarpieceWiredRoute;
     private AudioRoute mSpeakerDockRoute;
@@ -104,6 +105,7 @@
     private StatusBarNotifier mStatusBarNotifier;
     private FeatureFlags mFeatureFlags;
     private int mFocusType;
+    private int mCallSupportedRouteMask = -1;
     private boolean mIsScoAudioConnected;
     private final Object mLock = new Object();
     private final TelecomSystem.SyncRoot mTelecomLock;
@@ -314,6 +316,9 @@
                             handleExitPendingRoute();
                             break;
                         case UPDATE_SYSTEM_AUDIO_ROUTE:
+                            // Based on the available routes for foreground call, adjust routing.
+                            updateRouteForForeground();
+                            // Force update to notify all ICS/CS.
                             updateCallAudioState(new CallAudioState(mIsMute,
                                     mCallAudioState.getRoute(),
                                     mCallAudioState.getSupportedRouteMask(),
@@ -330,6 +335,7 @@
     @Override
     public void initialize() {
         mAvailableRoutes = new HashSet<>();
+        mCallSupportedRoutes = new HashSet<>();
         mBluetoothRoutes = new LinkedHashMap<>();
         mActiveDeviceCache = new HashMap<>();
         mActiveDeviceCache.put(AudioRoute.TYPE_BLUETOOTH_SCO, null);
@@ -485,7 +491,8 @@
     }
 
     private void routeTo(boolean active, AudioRoute destRoute) {
-        if (!destRoute.equals(mStreamingRoute) && !getAvailableRoutes().contains(destRoute)) {
+        if (destRoute == null || (!destRoute.equals(mStreamingRoute)
+                && !getCallSupportedRoutes().contains(destRoute))) {
             Log.i(this, "Ignore routing to unavailable route: %s", destRoute);
             return;
         }
@@ -510,7 +517,7 @@
             Log.i(this, "Enter pending route, orig%s(active=%b), dest%s(active=%b)", mCurrentRoute,
                     mIsActive, destRoute, active);
             // route to pending route
-            if (getAvailableRoutes().contains(mCurrentRoute)) {
+            if (getCallSupportedRoutes().contains(mCurrentRoute)) {
                 mPendingAudioRoute.setOrigRoute(mIsActive, mCurrentRoute);
             } else {
                 // Avoid waiting for pending messages for an unavailable route
@@ -841,7 +848,7 @@
 
     public void handleSwitchEarpiece() {
         AudioRoute earpieceRoute = mTypeRoutes.get(AudioRoute.TYPE_EARPIECE);
-        if (earpieceRoute != null && getAvailableRoutes().contains(earpieceRoute)) {
+        if (earpieceRoute != null && getCallSupportedRoutes().contains(earpieceRoute)) {
             routeTo(mIsActive, earpieceRoute);
         } else {
             Log.i(this, "ignore switch earpiece request");
@@ -856,7 +863,7 @@
             bluetoothRoute = getArbitraryBluetoothDevice();
             bluetoothDevice = mBluetoothRoutes.get(bluetoothRoute);
         } else {
-            for (AudioRoute route : getAvailableRoutes()) {
+            for (AudioRoute route : getCallSupportedRoutes()) {
                 if (Objects.equals(address, route.getBluetoothAddress())) {
                     bluetoothRoute = route;
                     bluetoothDevice = mBluetoothRoutes.get(route);
@@ -894,7 +901,7 @@
 
     private void handleSwitchHeadset() {
         AudioRoute headsetRoute = mTypeRoutes.get(AudioRoute.TYPE_WIRED);
-        if (headsetRoute != null && getAvailableRoutes().contains(headsetRoute)) {
+        if (headsetRoute != null && getCallSupportedRoutes().contains(headsetRoute)) {
             routeTo(mIsActive, headsetRoute);
         } else {
             Log.i(this, "ignore switch headset request");
@@ -902,7 +909,7 @@
     }
 
     private void handleSwitchSpeaker() {
-        if (mSpeakerDockRoute != null && getAvailableRoutes().contains(mSpeakerDockRoute)) {
+        if (mSpeakerDockRoute != null && getCallSupportedRoutes().contains(mSpeakerDockRoute)) {
             routeTo(mIsActive, mSpeakerDockRoute);
         } else {
             Log.i(this, "ignore switch speaker request");
@@ -920,7 +927,8 @@
             // Update status bar notification if we are in a call.
             mStatusBarNotifier.notifySpeakerphone(mCallsManager.hasAnyCalls());
         } else {
-            if (mSpeakerDockRoute != null && getAvailableRoutes().contains(mSpeakerDockRoute)) {
+            if (mSpeakerDockRoute != null && getCallSupportedRoutes()
+                    .contains(mSpeakerDockRoute)) {
                 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
@@ -984,7 +992,7 @@
         synchronized (mLock) {
             int routeMask = 0;
             Set<BluetoothDevice> availableBluetoothDevices = new HashSet<>();
-            for (AudioRoute route : getAvailableRoutes()) {
+            for (AudioRoute route : getCallSupportedRoutes()) {
                 routeMask |= ROUTE_MAP.get(route.getType());
                 if (BT_AUDIO_ROUTE_TYPES.contains(route.getType())) {
                     BluetoothDevice deviceToAdd = mBluetoothRoutes.get(route);
@@ -1004,6 +1012,7 @@
                     }
                 }
             }
+
             updateCallAudioState(new CallAudioState(mIsMute, mCallAudioState.getRoute(), routeMask,
                     mCallAudioState.getActiveBluetoothDevice(), availableBluetoothDevices));
         }
@@ -1015,14 +1024,58 @@
                 mCallAudioState.getSupportedBluetoothDevices()));
     }
 
+    /**
+     * Retrieves the current call's supported audio route and adjusts the audio routing if the
+     * current route isn't supported.
+     */
+    private void updateRouteForForeground() {
+        boolean updatedRouteForCall = updateCallSupportedAudioRoutes();
+        // Ensure that current call audio state has updated routes for current call.
+        if (updatedRouteForCall) {
+            mCallAudioState = new CallAudioState(mIsMute, mCallAudioState.getRoute(),
+                    mCallSupportedRouteMask, mCallAudioState.getActiveBluetoothDevice(),
+                    mCallAudioState.getSupportedBluetoothDevices());
+            // Update audio route if foreground call doesn't support the current route.
+            if ((mCallSupportedRouteMask & mCallAudioState.getRoute()) == 0) {
+                routeTo(mIsActive, getBaseRoute(true, null));
+            }
+        }
+    }
+
+    /**
+     * Update supported audio routes for the foreground call if present.
+     */
+    private boolean updateCallSupportedAudioRoutes() {
+        int availableRouteMask = 0;
+        Call foregroundCall = mCallsManager.getForegroundCall();
+        if (foregroundCall != null) {
+            int foregroundCallSupportedRouteMask = foregroundCall.getSupportedAudioRoutes();
+            for (AudioRoute route : getAvailableRoutes()) {
+                int routeType = ROUTE_MAP.get(route.getType());
+                availableRouteMask |= routeType;
+                if ((routeType & foregroundCallSupportedRouteMask) == routeType) {
+                    mCallSupportedRoutes.add(route);
+                }
+            }
+            mCallSupportedRouteMask = availableRouteMask & foregroundCallSupportedRouteMask;
+            return true;
+        } else {
+            mCallSupportedRoutes.clear();
+            mCallSupportedRouteMask = -1;
+            return false;
+        }
+    }
+
     private void updateCallAudioState(CallAudioState newCallAudioState) {
-        Log.i(this, "updateCallAudioState: updating call audio state to %s", newCallAudioState);
-        CallAudioState oldState = mCallAudioState;
-        mCallAudioState = newCallAudioState;
-        // Update status bar notification
-        mStatusBarNotifier.notifyMute(newCallAudioState.isMuted());
-        mCallsManager.onCallAudioStateChanged(oldState, mCallAudioState);
-        updateAudioStateForTrackedCalls(mCallAudioState);
+        synchronized (mTelecomLock) {
+            Log.i(this, "updateCallAudioState: updating call audio state to %s", newCallAudioState);
+            CallAudioState oldState = mCallAudioState;
+            mCallAudioState = newCallAudioState;
+            // Update status bar notification
+            mStatusBarNotifier.notifyMute(newCallAudioState.isMuted());
+            mCallsManager.onCallAudioStateChanged(oldState, mCallAudioState);
+            updateAudioStateForTrackedCalls(mCallAudioState);
+        }
     }
 
     private void updateAudioStateForTrackedCalls(CallAudioState newCallAudioState) {
@@ -1080,11 +1133,17 @@
         // are only wearables available.
         AudioRoute activeWatchOrNonWatchDeviceRoute =
                 getActiveWatchOrNonWatchDeviceRoute(btAddressToExclude);
-        if (mBluetoothRoutes.isEmpty() || !includeBluetooth
-                || activeWatchOrNonWatchDeviceRoute == null) {
+        if ((!mCallSupportedRoutes.isEmpty() && (mCallSupportedRouteMask
+                & CallAudioState.ROUTE_BLUETOOTH) == 0) || mBluetoothRoutes.isEmpty()
+                || !includeBluetooth || activeWatchOrNonWatchDeviceRoute == null) {
             Log.i(this, "getPreferredAudioRouteFromDefault: Audio routing defaulting to "
                     + "available non-BT route.");
-            AudioRoute defaultRoute = mEarpieceWiredRoute != null
+            boolean callSupportsEarpieceWiredRoute = mCallSupportedRoutes.isEmpty()
+                    || mCallSupportedRoutes.contains(mEarpieceWiredRoute);
+            // If call supported route doesn't contain earpiece/wired/BT, it should have speaker
+            // enabled. Otherwise, no routes would be supported for the call which should never be
+            // the case.
+            AudioRoute defaultRoute = mEarpieceWiredRoute != null && callSupportsEarpieceWiredRoute
                     ? mEarpieceWiredRoute
                     : mSpeakerDockRoute;
             // Ensure that we default to speaker route if we're in a video call, but disregard it if
@@ -1136,6 +1195,14 @@
         }
     }
 
+    public Set<AudioRoute> getCallSupportedRoutes() {
+        if (mCurrentRoute.equals(mStreamingRoute)) {
+            return mStreamingRoutes;
+        } else {
+            return mCallSupportedRoutes.isEmpty() ? mAvailableRoutes : mCallSupportedRoutes;
+        }
+    }
+
     public AudioRoute getCurrentRoute() {
         return mCurrentRoute;
     }
@@ -1155,7 +1222,7 @@
         if (destRoute == null || (destRoute.getBluetoothAddress() != null && !includeBluetooth)) {
             destRoute = getPreferredAudioRouteFromDefault(includeBluetooth, btAddressToExclude);
         }
-        if (destRoute != null && !getAvailableRoutes().contains(destRoute)) {
+        if (destRoute != null && !getCallSupportedRoutes().contains(destRoute)) {
             destRoute = null;
         }
         Log.i(this, "getBaseRoute - audio routing to %s", destRoute);
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
index f8cc857..942eb18 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteControllerTest.java
@@ -72,6 +72,7 @@
 import com.android.server.telecom.Call;
 import com.android.server.telecom.CallAudioManager;
 import com.android.server.telecom.CallAudioRouteController;
+import com.android.server.telecom.CallAudioRouteStateMachine;
 import com.android.server.telecom.CallsManager;
 import com.android.server.telecom.PendingAudioRoute;
 import com.android.server.telecom.StatusBarNotifier;
@@ -152,6 +153,7 @@
         when(mCallsManager.getCurrentUserHandle()).thenReturn(
                 new UserHandle(UserHandle.USER_SYSTEM));
         when(mCallsManager.getLock()).thenReturn(mLock);
+        when(mCallsManager.getForegroundCall()).thenReturn(mCall);
         when(mBluetoothRouteManager.getDeviceManager()).thenReturn(mBluetoothDeviceManager);
         when(mBluetoothDeviceManager.connectAudio(any(BluetoothDevice.class), anyInt()))
                 .thenReturn(true);
@@ -172,6 +174,7 @@
         mController.setCallAudioManager(mCallAudioManager);
         when(mCallAudioManager.getForegroundCall()).thenReturn(mCall);
         when(mCall.getVideoState()).thenReturn(VideoProfile.STATE_AUDIO_ONLY);
+        when(mCall.getSupportedAudioRoutes()).thenReturn(CallAudioState.ROUTE_ALL);
         when(mFeatureFlags.ignoreAutoRouteToWatchDevice()).thenReturn(false);
         when(mFeatureFlags.useRefactoredAudioRouteSwitching()).thenReturn(true);
     }
@@ -799,6 +802,35 @@
             any(CallAudioState.class), eq(expectedState));
     }
 
+    @SmallTest
+    @Test
+    public void testUpdateRouteForForeground() {
+        mController.initialize();
+        mController.sendMessageWithSessionInfo(BT_DEVICE_ADDED, AudioRoute.TYPE_BLUETOOTH_SCO,
+                BLUETOOTH_DEVICE_1);
+
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH
+                        | CallAudioState.ROUTE_SPEAKER, BLUETOOTH_DEVICE_1, BLUETOOTH_DEVICES);
+        mController.sendMessageWithSessionInfo(BT_ACTIVE_DEVICE_PRESENT,
+                AudioRoute.TYPE_BLUETOOTH_SCO, BT_ADDRESS_1);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+
+        // Ensure that supported routes is updated along with the current route to reflect the
+        // foreground call's supported audio routes.
+        when(mCall.getSupportedAudioRoutes()).thenReturn(CallAudioState.ROUTE_SPEAKER);
+        mController.sendMessageWithSessionInfo(
+                CallAudioRouteStateMachine.UPDATE_SYSTEM_AUDIO_ROUTE);
+        mController.sendMessageWithSessionInfo(SPEAKER_ON);
+        expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+                CallAudioState.ROUTE_SPEAKER, null, BLUETOOTH_DEVICES);
+        verify(mCallsManager, timeout(TEST_TIMEOUT)).onCallAudioStateChanged(
+                any(CallAudioState.class), eq(expectedState));
+        assertEquals(3, mController.getAvailableRoutes().size());
+        assertEquals(1, mController.getCallSupportedRoutes().size());
+    }
+
     private void verifyConnectBluetoothDevice(int audioType) {
         mController.initialize();
         mController.setActive(true);