Handle audio route restrictions set on calls

When a call restricts audio routes, it will move the audio route to a
supported route if available. The supported routes are determined initially
and re-evaluated when the call becomes the foreground call.

This is a cherry-pick of abandoned ag/1520954

Bug: 32958838
Change-Id: Ie30847c995839516507df0c70178a591c39f6cdd
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index b9b2fda..2e77654 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -26,6 +26,7 @@
 import android.os.RemoteException;
 import android.os.Trace;
 import android.provider.ContactsContract.Contacts;
+import android.telecom.CallAudioState;
 import android.telecom.DisconnectCause;
 import android.telecom.Connection;
 import android.telecom.GatewayInfo;
@@ -322,6 +323,8 @@
 
     private int mConnectionProperties;
 
+    private int mSupportedAudioRoutes = CallAudioState.ROUTE_ALL;
+
     private boolean mIsConference = false;
 
     private final boolean mShouldAttachToExistingConnection;
@@ -1080,6 +1083,16 @@
         }
     }
 
+    int getSupportedAudioRoutes() {
+        return mSupportedAudioRoutes;
+    }
+
+    void setSupportedAudioRoutes(int audioRoutes) {
+        if (mSupportedAudioRoutes != audioRoutes) {
+            mSupportedAudioRoutes = audioRoutes;
+        }
+    }
+
     @VisibleForTesting
     public Call getParentCall() {
         return mParentCall;
@@ -1176,6 +1189,7 @@
 
         setConnectionCapabilities(connection.getConnectionCapabilities());
         setConnectionProperties(connection.getConnectionProperties());
+        setSupportedAudioRoutes(connection.getSupportedAudioRoutes());
         setVideoProvider(connection.getVideoProvider());
         setVideoState(connection.getVideoState());
         setRingbackRequested(connection.isRingbackRequested());
diff --git a/src/com/android/server/telecom/CallAudioRouteStateMachine.java b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
index 28b3ec5..9014dbe 100644
--- a/src/com/android/server/telecom/CallAudioRouteStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
@@ -207,6 +207,16 @@
         Log.endSession();
     }
 
+    private int getCurrentCallSupportedRoutes() {
+        int supportedRoutes = CallAudioState.ROUTE_ALL;
+
+        if (mCallsManager.getForegroundCall() != null) {
+            supportedRoutes &= mCallsManager.getForegroundCall().getSupportedAudioRoutes();
+        }
+
+        return supportedRoutes;
+    }
+
     abstract class AudioState extends State {
         @Override
         public void enter() {
@@ -224,31 +234,34 @@
 
         @Override
         public boolean processMessage(Message msg) {
+            int addedRoutes = 0;
+            int removedRoutes = 0;
+
             switch (msg.what) {
                 case CONNECT_WIRED_HEADSET:
                     Log.addEvent(mCallsManager.getForegroundCall(), LogUtils.Events.AUDIO_ROUTE,
                             "Wired headset connected");
-                    mAvailableRoutes &= ~ROUTE_EARPIECE;
-                    mAvailableRoutes |= ROUTE_WIRED_HEADSET;
-                    return NOT_HANDLED;
+                    removedRoutes |= ROUTE_EARPIECE;
+                    addedRoutes |= ROUTE_WIRED_HEADSET;
+                    break;
                 case CONNECT_BLUETOOTH:
                     Log.addEvent(mCallsManager.getForegroundCall(), LogUtils.Events.AUDIO_ROUTE,
                             "Bluetooth connected");
-                    mAvailableRoutes |= ROUTE_BLUETOOTH;
-                    return NOT_HANDLED;
+                    addedRoutes |= ROUTE_BLUETOOTH;
+                    break;
                 case DISCONNECT_WIRED_HEADSET:
                     Log.addEvent(mCallsManager.getForegroundCall(), LogUtils.Events.AUDIO_ROUTE,
                             "Wired headset disconnected");
-                    mAvailableRoutes &= ~ROUTE_WIRED_HEADSET;
+                    removedRoutes |= ROUTE_WIRED_HEADSET;
                     if (mDoesDeviceSupportEarpieceRoute) {
-                        mAvailableRoutes |= ROUTE_EARPIECE;
+                        addedRoutes |= ROUTE_EARPIECE;
                     }
-                    return NOT_HANDLED;
+                    break;
                 case DISCONNECT_BLUETOOTH:
                     Log.addEvent(mCallsManager.getForegroundCall(), LogUtils.Events.AUDIO_ROUTE,
                             "Bluetooth disconnected");
-                    mAvailableRoutes &= ~ROUTE_BLUETOOTH;
-                    return NOT_HANDLED;
+                    removedRoutes |= ROUTE_BLUETOOTH;
+                    break;
                 case SWITCH_BASELINE_ROUTE:
                     sendInternalMessage(calculateBaselineRouteMessage(false));
                     return HANDLED;
@@ -261,6 +274,14 @@
                 default:
                     return NOT_HANDLED;
             }
+
+            if (addedRoutes != 0 || removedRoutes != 0) {
+                mAvailableRoutes = modifyRoutes(mAvailableRoutes, removedRoutes, addedRoutes, true);
+                mDeviceSupportedRoutes = modifyRoutes(mDeviceSupportedRoutes, removedRoutes,
+                        addedRoutes, false);
+            }
+
+            return NOT_HANDLED;
         }
 
         // Behavior will depend on whether the state is an active one or a quiescent one.
@@ -268,6 +289,18 @@
         abstract public boolean isActive();
     }
 
+    private int modifyRoutes(int base, int remove, int add, boolean considerCurrentCall) {
+        base &= ~remove;
+
+        if (considerCurrentCall) {
+            add &= getCurrentCallSupportedRoutes();
+        }
+
+        base |= add;
+
+        return base;
+    }
+
     class ActiveEarpieceRoute extends EarpieceRoute {
         @Override
         public String getName() {
@@ -1114,6 +1147,7 @@
      * A few pieces of hidden state. Used to avoid exponential explosion of number of explicit
      * states
      */
+    private int mDeviceSupportedRoutes;
     private int mAvailableRoutes;
     private int mAudioFocusType;
     private boolean mWasOnSpeaker;
@@ -1203,9 +1237,15 @@
     }
 
     public void initialize(CallAudioState initState) {
+        if ((initState.getRoute() & getCurrentCallSupportedRoutes()) == 0) {
+            Log.e(this, new IllegalArgumentException(), "Route " + initState.getRoute()
+                    + "specified when supported call routes are:" + getCurrentCallSupportedRoutes());
+        }
+
         mCurrentCallAudioState = initState;
         mLastKnownCallAudioState = initState;
-        mAvailableRoutes = initState.getSupportedRouteMask();
+        mDeviceSupportedRoutes = initState.getSupportedRouteMask();
+        mAvailableRoutes = mDeviceSupportedRoutes;
         mIsMuted = initState.isMuted();
         mWasOnSpeaker = false;
 
@@ -1263,6 +1303,7 @@
                 }
                 return;
             case UPDATE_SYSTEM_AUDIO_ROUTE:
+                updateRouteForForegroundCall();
                 resendSystemAudioState();
                 return;
             case RUN_RUNNABLE:
@@ -1460,7 +1501,7 @@
     }
 
     private CallAudioState getInitialAudioState() {
-        int supportedRouteMask = calculateSupportedRoutes();
+        int supportedRouteMask = calculateSupportedRoutes() & getCurrentCallSupportedRoutes();
         final int route;
 
         if ((supportedRouteMask & ROUTE_BLUETOOTH) != 0) {
@@ -1525,7 +1566,8 @@
 
     private void reinitialize() {
         CallAudioState initState = getInitialAudioState();
-        mAvailableRoutes = initState.getSupportedRouteMask();
+        mDeviceSupportedRoutes = initState.getSupportedRouteMask();
+        mAvailableRoutes = mDeviceSupportedRoutes & getCurrentCallSupportedRoutes();
         mIsMuted = initState.isMuted();
         setMuteOn(mIsMuted);
         mWasOnSpeaker = false;
@@ -1533,4 +1575,15 @@
         mLastKnownCallAudioState = initState;
         transitionTo(mRouteCodeToQuiescentState.get(initState.getRoute()));
     }
+
+    private void updateRouteForForegroundCall() {
+        mAvailableRoutes = mDeviceSupportedRoutes & getCurrentCallSupportedRoutes();
+
+        CallAudioState currentState = getCurrentCallAudioState();
+
+        // Move to baseline route in the case the current route is no longer available.
+        if ((mAvailableRoutes & currentState.getRoute()) == 0) {
+            sendInternalMessage(calculateBaselineRouteMessage(false));
+        }
+    }
 }
diff --git a/src/com/android/server/telecom/ParcelableCallUtils.java b/src/com/android/server/telecom/ParcelableCallUtils.java
index 9cc61b3..c4ea9cf 100644
--- a/src/com/android/server/telecom/ParcelableCallUtils.java
+++ b/src/com/android/server/telecom/ParcelableCallUtils.java
@@ -88,6 +88,8 @@
         }
         int capabilities = convertConnectionToCallCapabilities(call.getConnectionCapabilities());
         int properties = convertConnectionToCallProperties(call.getConnectionProperties());
+        int supportedAudioRoutes = call.getSupportedAudioRoutes();
+
         if (call.isConference()) {
             properties |= android.telecom.Call.Details.PROPERTY_CONFERENCE;
         }
@@ -157,6 +159,7 @@
                 call.getCannedSmsResponses(),
                 capabilities,
                 properties,
+                supportedAudioRoutes,
                 connectTimeMillis,
                 handle,
                 call.getHandlePresentation(),
diff --git a/src/com/android/server/telecom/PhoneAccountRegistrar.java b/src/com/android/server/telecom/PhoneAccountRegistrar.java
index 143ea53..29f6253 100644
--- a/src/com/android/server/telecom/PhoneAccountRegistrar.java
+++ b/src/com/android/server/telecom/PhoneAccountRegistrar.java
@@ -35,6 +35,7 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
+import android.telecom.CallAudioState;
 import android.telecom.ConnectionService;
 import android.telecom.DefaultDialerManager;
 import android.telecom.Log;
@@ -1508,6 +1509,7 @@
         private static final String ADDRESS = "handle";
         private static final String SUBSCRIPTION_ADDRESS = "subscription_number";
         private static final String CAPABILITIES = "capabilities";
+        private static final String SUPPORTED_AUDIO_ROUTES = "supported_audio_routes";
         private static final String ICON_RES_ID = "icon_res_id";
         private static final String ICON_PACKAGE_NAME = "icon_package_name";
         private static final String ICON_BITMAP = "icon_bitmap";
@@ -1543,6 +1545,8 @@
                 writeStringList(SUPPORTED_URI_SCHEMES, o.getSupportedUriSchemes(), serializer);
                 writeBundle(EXTRAS, o.getExtras(), serializer);
                 writeTextIfNonNull(ENABLED, o.isEnabled() ? "true" : "false" , serializer);
+                writeTextIfNonNull(SUPPORTED_AUDIO_ROUTES, Integer.toString(
+                        o.getSupportedAudioRoutes()), serializer);
 
                 serializer.endTag(null, CLASS_PHONE_ACCOUNT);
             }
@@ -1556,6 +1560,7 @@
                 Uri address = null;
                 Uri subscriptionAddress = null;
                 int capabilities = 0;
+                int supportedAudioRoutes = 0;
                 int iconResId = PhoneAccount.NO_RESOURCE_ID;
                 String iconPackageName = null;
                 Bitmap iconBitmap = null;
@@ -1614,6 +1619,9 @@
                         enabled = "true".equalsIgnoreCase(parser.getText());
                     } else if (parser.getName().equals(EXTRAS)) {
                         extras = readBundle(parser);
+                    } else if (parser.getName().equals(SUPPORTED_AUDIO_ROUTES)) {
+                        parser.next();
+                        supportedAudioRoutes = Integer.parseInt(parser.getText());
                     }
                 }
 
@@ -1672,10 +1680,16 @@
                     }
                 }
 
+                if (version < 9) {
+                    // Set supported audio routes to all by default
+                    supportedAudioRoutes = CallAudioState.ROUTE_ALL;
+                }
+
                 PhoneAccount.Builder builder = PhoneAccount.builder(accountHandle, label)
                         .setAddress(address)
                         .setSubscriptionAddress(subscriptionAddress)
                         .setCapabilities(capabilities)
+                        .setSupportedAudioRoutes(supportedAudioRoutes)
                         .setShortDescription(shortDescription)
                         .setSupportedUriSchemes(supportedUriSchemes)
                         .setHighlightColor(highlightColor)