Filter external calls when InCallService doesn't support them.

Use TelecomManager#METADATA_INCLUDE_EXTERNAL_CALLS metadata to check
whether an InCallService supports external calls.  Will only send
external calls to an InCallService which has correct Metadata to indicate
that it wants to be informed of external calls.

This required some changes to how InCallController keeps track of the
incallservices.  I have added an InCallServiceInfo class which tracks
the component name of an ICS as well as whether it supports external calls.

The change to CallsManager#getFirstCallWithState and getNumCallsWithSate
ensures that when TelecomServiceImpl checks to see if there are ongoing
calls it doesn't consider external calls when checking if there is an
ongoing call, and also when determining if room needs to be made for a
call.

Bug: 27458894
Change-Id: I69652877332be8527fb16a38c692b6f51a92e469
diff --git a/src/com/android/server/telecom/BluetoothPhoneServiceImpl.java b/src/com/android/server/telecom/BluetoothPhoneServiceImpl.java
index 23a10f8..cd3abe0 100644
--- a/src/com/android/server/telecom/BluetoothPhoneServiceImpl.java
+++ b/src/com/android/server/telecom/BluetoothPhoneServiceImpl.java
@@ -291,11 +291,17 @@
     public CallsManagerListener mCallsManagerListener = new CallsManagerListenerBase() {
         @Override
         public void onCallAdded(Call call) {
+            if (call.isExternalCall()) {
+                return;
+            }
             updateHeadsetWithCallState(false /* force */);
         }
 
         @Override
         public void onCallRemoved(Call call) {
+            if (call.isExternalCall()) {
+                return;
+            }
             mClccIndexMap.remove(call);
             updateHeadsetWithCallState(false /* force */);
         }
@@ -318,6 +324,9 @@
 
         @Override
         public void onCallStateChanged(Call call, int oldState, int newState) {
+            if (call.isExternalCall()) {
+                return;
+            }
             // If a call is being put on hold because of a new connecting call, ignore the
             // CONNECTING since the BT state update needs to send out the numHeld = 1 + dialing
             // state atomically.
@@ -343,6 +352,9 @@
 
         @Override
         public void onIsConferencedChanged(Call call) {
+            if (call.isExternalCall()) {
+                return;
+            }
             /*
              * Filter certain onIsConferencedChanged callbacks. Unfortunately this needs to be done
              * because conference change events are not atomic and multiple callbacks get fired
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 4f65322..2fa6efb 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -1535,6 +1535,10 @@
                     continue;
                 }
 
+                if (call.isExternalCall()) {
+                    continue;
+                }
+
                 if (currentState == call.getState()) {
                     return call;
                 }
@@ -1784,7 +1788,9 @@
         int count = 0;
         for (int state : states) {
             for (Call call : mCalls) {
-                if (call.getParentCall() == null && call.getState() == state) {
+                if (call.getParentCall() == null && call.getState() == state &&
+                        !call.isExternalCall()) {
+
                     count++;
                 }
             }
diff --git a/src/com/android/server/telecom/HeadsetMediaButton.java b/src/com/android/server/telecom/HeadsetMediaButton.java
index a8bd354..af0ce13 100644
--- a/src/com/android/server/telecom/HeadsetMediaButton.java
+++ b/src/com/android/server/telecom/HeadsetMediaButton.java
@@ -135,6 +135,9 @@
     /** ${inheritDoc} */
     @Override
     public void onCallRemoved(Call call) {
+        if (call.isExternalCall()) {
+            return;
+        }
         if (!mCallsManager.hasAnyCalls()) {
             mMediaSessionHandler.obtainMessage(MSG_MEDIA_SESSION_SET_ACTIVE, 0, 0).sendToTarget();
         }
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index e5a9bac..cd913aa 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -78,6 +78,52 @@
         public void dump(IndentingPrintWriter pw) {}
     }
 
+    private class InCallServiceInfo {
+        private ComponentName mComponentName;
+        private boolean mIsExternalCallsSupported;
+
+        public InCallServiceInfo(ComponentName componentName, boolean isExternalCallsSupported) {
+            mComponentName = componentName;
+            mIsExternalCallsSupported = isExternalCallsSupported;
+        }
+
+        public ComponentName getComponentName() {
+            return mComponentName;
+        }
+
+        public boolean isExternalCallsSupported() {
+            return mIsExternalCallsSupported;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+
+            InCallServiceInfo that = (InCallServiceInfo) o;
+
+            if (mIsExternalCallsSupported != that.mIsExternalCallsSupported) {
+                return false;
+            }
+            return mComponentName.equals(that.mComponentName);
+
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mComponentName, mIsExternalCallsSupported);
+        }
+
+        @Override
+        public String toString() {
+            return "[" + mComponentName + " supportsExternal? " + mIsExternalCallsSupported + "]";
+        }
+    }
+
     private class InCallServiceBindingConnection extends InCallServiceConnection {
 
         private final ServiceConnection mServiceConnection = new ServiceConnection() {
@@ -109,12 +155,12 @@
             }
         };
 
-        private final ComponentName mComponentName;
+        private final InCallServiceInfo mInCallServiceInfo;
         private boolean mIsConnected = false;
         private boolean mIsBound = false;
 
-        public InCallServiceBindingConnection(ComponentName componentName) {
-            mComponentName = componentName;
+        public InCallServiceBindingConnection(InCallServiceInfo info) {
+            mInCallServiceInfo = info;
         }
 
         @Override
@@ -125,7 +171,7 @@
             }
 
             Intent intent = new Intent(InCallService.SERVICE_INTERFACE);
-            intent.setComponent(mComponentName);
+            intent.setComponent(mInCallServiceInfo.getComponentName());
             if (call != null && !call.isIncoming() && !call.isExternalCall()){
                 intent.putExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS,
                         call.getIntentExtras());
@@ -133,7 +179,7 @@
                         call.getTargetPhoneAccount());
             }
 
-            Log.i(this, "Attempting to bind to InCall %s, with %s", mComponentName, intent);
+            Log.i(this, "Attempting to bind to InCall %s, with %s", mInCallServiceInfo, intent);
             mIsConnected = true;
             if (!mContext.bindServiceAsUser(intent, mServiceConnection,
                         Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
@@ -164,7 +210,7 @@
 
         protected void onConnected(IBinder service) {
             boolean shouldRemainConnected =
-                    InCallController.this.onConnected(mComponentName, service);
+                    InCallController.this.onConnected(mInCallServiceInfo, service);
             if (!shouldRemainConnected) {
                 // Sometimes we can opt to disconnect for certain reasons, like if the
                 // InCallService rejected our intialization step, or the calls went away
@@ -175,7 +221,7 @@
         }
 
         protected void onDisconnected() {
-            InCallController.this.onDisconnected(mComponentName);
+            InCallController.this.onDisconnected(mInCallServiceInfo.getComponentName());
             disconnect();  // Unbind explicitly if we get disconnected.
             if (mListener != null) {
                 mListener.onDisconnect(InCallServiceBindingConnection.this);
@@ -210,8 +256,9 @@
         };
 
         public EmergencyInCallServiceConnection(
-                ComponentName componentName, InCallServiceConnection subConnection) {
-            super(componentName);
+                InCallServiceInfo info, InCallServiceConnection subConnection) {
+
+            super(info);
             mSubConnection = subConnection;
             if (mSubConnection != null) {
                 mSubConnection.setListener(mSubListener);
@@ -531,7 +578,7 @@
     private static final int IN_CALL_SERVICE_TYPE_NON_UI = 4;
 
     /** The in-call app implementations, see {@link IInCallService}. */
-    private final Map<ComponentName, IInCallService> mInCallServices = new ArrayMap<>();
+    private final Map<InCallServiceInfo, IInCallService> mInCallServices = new ArrayMap<>();
 
     /**
      * The {@link ComponentName} of the bound In-Call UI Service.
@@ -579,9 +626,15 @@
             // Track the call if we don't already know about it.
             addCall(call);
 
-            for (Map.Entry<ComponentName, IInCallService> entry : mInCallServices.entrySet()) {
-                ComponentName componentName = entry.getKey();
+            for (Map.Entry<InCallServiceInfo, IInCallService> entry : mInCallServices.entrySet()) {
+                InCallServiceInfo info = entry.getKey();
+
+                if (call.isExternalCall() && !info.isExternalCallsSupported()) {
+                    continue;
+                }
+
                 IInCallService inCallService = entry.getValue();
+
                 ParcelableCall parcelableCall = ParcelableCallUtils.toParcelableCall(call,
                         true /* includeVideoProvider */, mCallsManager.getPhoneAccountRegistrar());
                 try {
@@ -739,23 +792,25 @@
     @VisibleForTesting
     public void bindToServices(Call call) {
         InCallServiceConnection dialerInCall = null;
-        ComponentName defaultDialerComponent = getDefaultDialerComponent();
-        Log.i(this, "defaultDialer: " + defaultDialerComponent);
-        if (defaultDialerComponent != null &&
-                !defaultDialerComponent.equals(mSystemInCallComponentName)) {
-            dialerInCall = new InCallServiceBindingConnection(defaultDialerComponent);
+        InCallServiceInfo defaultDialerComponentInfo = getDefaultDialerComponent();
+        Log.i(this, "defaultDialer: " + defaultDialerComponentInfo);
+        if (defaultDialerComponentInfo != null &&
+                !defaultDialerComponentInfo.getComponentName().equals(mSystemInCallComponentName)) {
+            dialerInCall = new InCallServiceBindingConnection(defaultDialerComponentInfo);
         }
         Log.i(this, "defaultDialer: " + dialerInCall);
 
+        InCallServiceInfo systemInCallInfo =  getInCallServiceComponent(mSystemInCallComponentName,
+                IN_CALL_SERVICE_TYPE_SYSTEM_UI);
         EmergencyInCallServiceConnection systemInCall =
-                new EmergencyInCallServiceConnection(mSystemInCallComponentName, dialerInCall);
+                new EmergencyInCallServiceConnection(systemInCallInfo, dialerInCall);
         systemInCall.setHasEmergency(mCallsManager.hasEmergencyCall());
 
         InCallServiceConnection carModeInCall = null;
-        ComponentName carModeComponent = getCarModeComponent();
-        if (carModeComponent != null &&
-                !carModeComponent.equals(mSystemInCallComponentName)) {
-            carModeInCall = new InCallServiceBindingConnection(carModeComponent);
+        InCallServiceInfo carModeComponentInfo = getCarModeComponent();
+        if (carModeComponentInfo != null &&
+                !carModeComponentInfo.getComponentName().equals(mSystemInCallComponentName)) {
+            carModeInCall = new InCallServiceBindingConnection(carModeComponentInfo);
         }
 
         mInCallServiceConnection =
@@ -763,18 +818,17 @@
         mInCallServiceConnection.setCarMode(shouldUseCarModeUI());
         mInCallServiceConnection.connect(call);
 
-
-        List<ComponentName> nonUIInCallComponents =
-                getInCallServiceComponents(null, IN_CALL_SERVICE_TYPE_NON_UI);
+        List<InCallServiceInfo> nonUIInCallComponents =
+                getInCallServiceComponents(IN_CALL_SERVICE_TYPE_NON_UI);
         List<InCallServiceBindingConnection> nonUIInCalls = new LinkedList<>();
-        for (ComponentName componentName : nonUIInCallComponents) {
-            nonUIInCalls.add(new InCallServiceBindingConnection(componentName));
+        for (InCallServiceInfo serviceInfo : nonUIInCallComponents) {
+            nonUIInCalls.add(new InCallServiceBindingConnection(serviceInfo));
         }
         mNonUIInCallServiceConnections = new NonUIInCallServiceConnectionCollection(nonUIInCalls);
         mNonUIInCallServiceConnections.connect(call);
     }
 
-    private ComponentName getDefaultDialerComponent() {
+    private InCallServiceInfo getDefaultDialerComponent() {
         String packageName = mDefaultDialerAdapter.getDefaultDialerApplication(
                 mContext, mCallsManager.getCurrentUserHandle().getIdentifier());
         Log.d(this, "Default Dialer package: " + packageName);
@@ -782,25 +836,53 @@
         return getInCallServiceComponent(packageName, IN_CALL_SERVICE_TYPE_DIALER_UI);
     }
 
-    private ComponentName getCarModeComponent() {
-        return getInCallServiceComponent(null, IN_CALL_SERVICE_TYPE_CAR_MODE_UI);
+    private InCallServiceInfo getCarModeComponent() {
+        // Seems strange to cast a String to null, but the signatures of getInCallServiceComponent
+        // differ in the types of the first parameter, and passing in null is inherently ambiguous.
+        return getInCallServiceComponent((String) null, IN_CALL_SERVICE_TYPE_CAR_MODE_UI);
     }
 
-    private ComponentName getInCallServiceComponent(String packageName, int type) {
-        List<ComponentName> list = getInCallServiceComponents(packageName, type);
+    private InCallServiceInfo getInCallServiceComponent(ComponentName componentName, int type) {
+        List<InCallServiceInfo> list = getInCallServiceComponents(componentName, type);
         if (list != null && !list.isEmpty()) {
             return list.get(0);
         }
         return null;
     }
 
-    private List<ComponentName> getInCallServiceComponents(String packageName, int type) {
-        List<ComponentName> retval = new LinkedList<>();
+    private InCallServiceInfo getInCallServiceComponent(String packageName, int type) {
+        List<InCallServiceInfo> list = getInCallServiceComponents(packageName, type);
+        if (list != null && !list.isEmpty()) {
+            return list.get(0);
+        }
+        return null;
+    }
+
+    private List<InCallServiceInfo> getInCallServiceComponents(int type) {
+        return getInCallServiceComponents(null, null, type);
+    }
+
+    private List<InCallServiceInfo> getInCallServiceComponents(String packageName, int type) {
+        return getInCallServiceComponents(packageName, null, type);
+    }
+
+    private List<InCallServiceInfo> getInCallServiceComponents(ComponentName componentName,
+            int type) {
+        return getInCallServiceComponents(null, componentName, type);
+    }
+
+    private List<InCallServiceInfo> getInCallServiceComponents(String packageName,
+            ComponentName componentName, int requestedType) {
+
+        List<InCallServiceInfo> retval = new LinkedList<>();
 
         Intent serviceIntent = new Intent(InCallService.SERVICE_INTERFACE);
         if (packageName != null) {
             serviceIntent.setPackage(packageName);
         }
+        if (componentName != null) {
+            serviceIntent.setComponent(componentName);
+        }
 
         PackageManager packageManager = mContext.getPackageManager();
         for (ResolveInfo entry : packageManager.queryIntentServicesAsUser(
@@ -810,8 +892,15 @@
             ServiceInfo serviceInfo = entry.serviceInfo;
 
             if (serviceInfo != null) {
-                if (type == 0 || type == getInCallServiceType(entry.serviceInfo, packageManager)) {
-                    retval.add(new ComponentName(serviceInfo.packageName, serviceInfo.name));
+                boolean isExternalCallsSupported = serviceInfo.metaData != null &&
+                        serviceInfo.metaData.getBoolean(
+                                TelecomManager.METADATA_INCLUDE_EXTERNAL_CALLS, false);
+                if (requestedType == 0 || requestedType == getInCallServiceType(entry.serviceInfo,
+                        packageManager)) {
+
+                    retval.add(new InCallServiceInfo(
+                            new ComponentName(serviceInfo.packageName, serviceInfo.name),
+                            isExternalCallsSupported));
                 }
             }
         }
@@ -857,7 +946,6 @@
             return IN_CALL_SERVICE_TYPE_CAR_MODE_UI;
         }
 
-
         // Check to see that it is the default dialer package
         boolean isDefaultDialerPackage = Objects.equals(serviceInfo.packageName,
                 mDefaultDialerAdapter.getDefaultDialerApplication(
@@ -895,16 +983,16 @@
      * this class and in-call app by sending the first update to in-call app. This method is
      * called after a successful binding connection is established.
      *
-     * @param componentName The service {@link ComponentName}.
+     * @param info Info about the service, including its {@link ComponentName}.
      * @param service The {@link IInCallService} implementation.
      * @return True if we successfully connected.
      */
-    private boolean onConnected(ComponentName componentName, IBinder service) {
-        Trace.beginSection("onConnected: " + componentName);
-        Log.i(this, "onConnected to %s", componentName);
+    private boolean onConnected(InCallServiceInfo info, IBinder service) {
+        Trace.beginSection("onConnected: " + info.getComponentName());
+        Log.i(this, "onConnected to %s", info.getComponentName());
 
         IInCallService inCallService = IInCallService.Stub.asInterface(service);
-        mInCallServices.put(componentName, inCallService);
+        mInCallServices.put(info, inCallService);
 
         try {
             inCallService.setInCallAdapter(
@@ -912,7 +1000,7 @@
                             mCallsManager,
                             mCallIdMapper,
                             mLock,
-                            componentName.getPackageName()));
+                            info.getComponentName().getPackageName()));
         } catch (RemoteException e) {
             Log.e(this, e, "Failed to set the in-call adapter.");
             Trace.endSection();
@@ -923,11 +1011,16 @@
         List<Call> calls = orderCallsWithChildrenFirst(mCallsManager.getCalls());
         if (!calls.isEmpty()) {
             Log.i(this, "Adding %s calls to InCallService after onConnected: %s", calls.size(),
-                    componentName);
+                    info.getComponentName());
             for (Call call : calls) {
                 try {
+                    if (call.isExternalCall() && !info.isExternalCallsSupported()) {
+                        continue;
+                    }
+
                     // Track the call if we don't already know about it.
                     addCall(call);
+
                     inCallService.addCall(ParcelableCallUtils.toParcelableCall(
                             call,
                             true /* includeVideoProvider */,
@@ -982,10 +1075,16 @@
                     mCallsManager.getPhoneAccountRegistrar());
             Log.i(this, "Sending updateCall %s ==> %s", call, parcelableCall);
             List<ComponentName> componentsUpdated = new ArrayList<>();
-            for (Map.Entry<ComponentName, IInCallService> entry : mInCallServices.entrySet()) {
-                ComponentName componentName = entry.getKey();
+            for (Map.Entry<InCallServiceInfo, IInCallService> entry : mInCallServices.entrySet()) {
+                InCallServiceInfo info = entry.getKey();
+                if (call.isExternalCall() && !info.isExternalCallsSupported()) {
+                    continue;
+                }
+
+                ComponentName componentName = info.getComponentName();
                 IInCallService inCallService = entry.getValue();
                 componentsUpdated.add(componentName);
+
                 try {
                     inCallService.updateCall(parcelableCall);
                 } catch (RemoteException ignored) {
@@ -1018,8 +1117,8 @@
     public void dump(IndentingPrintWriter pw) {
         pw.println("mInCallServices (InCalls registered):");
         pw.increaseIndent();
-        for (ComponentName componentName : mInCallServices.keySet()) {
-            pw.println(componentName);
+        for (InCallServiceInfo info : mInCallServices.keySet()) {
+            pw.println(info);
         }
         pw.decreaseIndent();
 
diff --git a/src/com/android/server/telecom/InCallWakeLockController.java b/src/com/android/server/telecom/InCallWakeLockController.java
index ffa6a3f..0de8123 100644
--- a/src/com/android/server/telecom/InCallWakeLockController.java
+++ b/src/com/android/server/telecom/InCallWakeLockController.java
@@ -37,16 +37,25 @@
 
     @Override
     public void onCallAdded(Call call) {
+        if (call.isExternalCall()) {
+            return;
+        }
         handleWakeLock();
     }
 
     @Override
     public void onCallRemoved(Call call) {
+        if (call.isExternalCall()) {
+            return;
+        }
         handleWakeLock();
     }
 
     @Override
     public void onCallStateChanged(Call call, int oldState, int newState) {
+        if (call.isExternalCall()) {
+            return;
+        }
         handleWakeLock();
     }
 
diff --git a/src/com/android/server/telecom/PhoneStateBroadcaster.java b/src/com/android/server/telecom/PhoneStateBroadcaster.java
index 413f06c..b1f7e40 100644
--- a/src/com/android/server/telecom/PhoneStateBroadcaster.java
+++ b/src/com/android/server/telecom/PhoneStateBroadcaster.java
@@ -43,6 +43,9 @@
 
     @Override
     public void onCallStateChanged(Call call, int oldState, int newState) {
+        if (call.isExternalCall()) {
+            return;
+        }
         updateStates(call);
     }
 
diff --git a/src/com/android/server/telecom/ProximitySensorManager.java b/src/com/android/server/telecom/ProximitySensorManager.java
index dd336c4..e53b1d5 100644
--- a/src/com/android/server/telecom/ProximitySensorManager.java
+++ b/src/com/android/server/telecom/ProximitySensorManager.java
@@ -38,6 +38,9 @@
 
     @Override
     public void onCallRemoved(Call call) {
+        if (call.isExternalCall()) {
+            return;
+        }
         if (mCallsManager.getCalls().isEmpty()) {
             Log.i(this, "All calls removed, resetting proximity sensor to default state");
             turnOff(true);
diff --git a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
index 2468774..7081aed 100644
--- a/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
+++ b/tests/src/com/android/server/telecom/tests/InCallControllerTests.java
@@ -28,12 +28,12 @@
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.UserHandle;
-import android.telecom.ConnectionService;
 import android.telecom.InCallService;
 import android.telecom.PhoneAccountHandle;
 import android.telecom.TelecomManager;
 import android.test.mock.MockContext;
 import android.test.suitebuilder.annotation.MediumTest;
+import android.text.TextUtils;
 
 import com.android.internal.telecom.IInCallAdapter;
 import com.android.internal.telecom.IInCallService;
@@ -50,18 +50,18 @@
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
 
 import java.util.LinkedList;
 
 import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyBoolean;
-import static org.mockito.Matchers.anyChar;
 import static org.mockito.Matchers.anyInt;
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Matchers.eq;
 import static org.mockito.Matchers.isNull;
 import static org.mockito.Matchers.isNull;
-import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -114,11 +114,10 @@
         when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
         when(mMockCallsManager.hasEmergencyCall()).thenReturn(false);
         when(mMockCall.isIncoming()).thenReturn(true);
+        when(mMockCall.isExternalCall()).thenReturn(false);
 
         Intent queryIntent = new Intent(InCallService.SERVICE_INTERFACE);
-        when(mMockPackageManager.queryIntentServicesAsUser(
-                queryIntent, PackageManager.GET_META_DATA, CURRENT_USER_ID))
-            .thenReturn(new LinkedList<ResolveInfo>());
+        setupMockPackageManager(false /* default */, true /* system */, false /* external calls */);
         mInCallController.bindToServices(mMockCall);
 
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -146,11 +145,10 @@
         when(mMockCall.isIncoming()).thenReturn(false);
         when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
         when(mMockCall.getIntentExtras()).thenReturn(callExtras);
+        when(mMockCall.isExternalCall()).thenReturn(false);
 
         Intent queryIntent = new Intent(InCallService.SERVICE_INTERFACE);
-        when(mMockPackageManager.queryIntentServicesAsUser(
-                queryIntent, PackageManager.GET_META_DATA, CURRENT_USER_ID))
-            .thenReturn(new LinkedList<ResolveInfo>());
+        setupMockPackageManager(false /* default */, true /* system */, false /* external calls */);
         mInCallController.bindToServices(mMockCall);
 
         ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
@@ -181,36 +179,18 @@
         when(mMockCall.isIncoming()).thenReturn(false);
         when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
         when(mMockCall.getIntentExtras()).thenReturn(callExtras);
+        when(mMockCall.isExternalCall()).thenReturn(false);
         when(mMockDefaultDialerAdapter.getDefaultDialerApplication(mMockContext, CURRENT_USER_ID))
                 .thenReturn(DEF_PKG);
         when(mMockContext.bindServiceAsUser(any(Intent.class), any(ServiceConnection.class),
                 anyInt(), eq(UserHandle.CURRENT))).thenReturn(true);
 
-        Intent queryIntent = new Intent(InCallService.SERVICE_INTERFACE);
-        when(mMockPackageManager.queryIntentServicesAsUser(
-                any(Intent.class), eq(PackageManager.GET_META_DATA), eq(CURRENT_USER_ID)))
-            .thenReturn(new LinkedList<ResolveInfo>() {{
-                add(new ResolveInfo() {{
-                    serviceInfo = new ServiceInfo();
-                    serviceInfo.packageName = DEF_PKG;
-                    serviceInfo.name = DEF_CLASS;
-                    serviceInfo.permission = Manifest.permission.BIND_INCALL_SERVICE;
-                    serviceInfo.metaData = new Bundle();
-                    serviceInfo.metaData.putBoolean(
-                            TelecomManager.METADATA_IN_CALL_SERVICE_UI, true);
-                }});
-                add(new ResolveInfo() {{
-                    serviceInfo = new ServiceInfo();
-                    serviceInfo.packageName = SYS_PKG;
-                    serviceInfo.name = SYS_CLASS;
-                    serviceInfo.permission = Manifest.permission.BIND_INCALL_SERVICE;
-                }});
-            }});
+        setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
         mInCallController.bindToServices(mMockCall);
 
         // Query for the different InCallServices
         ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
-        verify(mMockPackageManager, times(3)).queryIntentServicesAsUser(
+        verify(mMockPackageManager, times(4)).queryIntentServicesAsUser(
                 queryIntentCaptor.capture(),
                 eq(PackageManager.GET_META_DATA), eq(CURRENT_USER_ID));
 
@@ -249,37 +229,20 @@
         when(mMockCall.isIncoming()).thenReturn(false);
         when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
         when(mMockCall.getIntentExtras()).thenReturn(callExtras);
+        when(mMockCall.isExternalCall()).thenReturn(false);
         when(mMockDefaultDialerAdapter.getDefaultDialerApplication(mMockContext, CURRENT_USER_ID))
                 .thenReturn(DEF_PKG);
         when(mMockContext.bindServiceAsUser(any(Intent.class), any(ServiceConnection.class),
                 eq(Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE),
                 eq(UserHandle.CURRENT))).thenReturn(true);
 
-        Intent queryIntent = new Intent(InCallService.SERVICE_INTERFACE);
-        when(mMockPackageManager.queryIntentServicesAsUser(
-                any(Intent.class), eq(PackageManager.GET_META_DATA), eq(CURRENT_USER_ID)))
-            .thenReturn(new LinkedList<ResolveInfo>() {{
-                add(new ResolveInfo() {{
-                    serviceInfo = new ServiceInfo();
-                    serviceInfo.packageName = DEF_PKG;
-                    serviceInfo.name = DEF_CLASS;
-                    serviceInfo.permission = Manifest.permission.BIND_INCALL_SERVICE;
-                    serviceInfo.metaData = new Bundle();
-                    serviceInfo.metaData.putBoolean(
-                            TelecomManager.METADATA_IN_CALL_SERVICE_UI, true);
-                }});
-                add(new ResolveInfo() {{
-                    serviceInfo = new ServiceInfo();
-                    serviceInfo.packageName = SYS_PKG;
-                    serviceInfo.name = SYS_CLASS;
-                    serviceInfo.permission = Manifest.permission.BIND_INCALL_SERVICE;
-                }});
-            }});
+        setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
+
         mInCallController.bindToServices(mMockCall);
 
         // Query for the different InCallServices
         ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
-        verify(mMockPackageManager, times(3)).queryIntentServicesAsUser(
+        verify(mMockPackageManager, times(4)).queryIntentServicesAsUser(
                 queryIntentCaptor.capture(),
                 eq(PackageManager.GET_META_DATA), eq(CURRENT_USER_ID));
 
@@ -318,37 +281,19 @@
         when(mMockCall.isIncoming()).thenReturn(false);
         when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
         when(mMockCall.getIntentExtras()).thenReturn(callExtras);
+        when(mMockCall.isExternalCall()).thenReturn(false);
         when(mMockDefaultDialerAdapter.getDefaultDialerApplication(mMockContext, CURRENT_USER_ID))
                 .thenReturn(DEF_PKG);
         when(mMockContext.bindServiceAsUser(
                 any(Intent.class), any(ServiceConnection.class), anyInt(), any(UserHandle.class)))
                 .thenReturn(true);
 
-        Intent queryIntent = new Intent(InCallService.SERVICE_INTERFACE);
-        when(mMockPackageManager.queryIntentServicesAsUser(
-                any(Intent.class), eq(PackageManager.GET_META_DATA), eq(CURRENT_USER_ID)))
-            .thenReturn(new LinkedList<ResolveInfo>() {{
-                add(new ResolveInfo() {{
-                    serviceInfo = new ServiceInfo();
-                    serviceInfo.packageName = DEF_PKG;
-                    serviceInfo.name = DEF_CLASS;
-                    serviceInfo.permission = Manifest.permission.BIND_INCALL_SERVICE;
-                    serviceInfo.metaData = new Bundle();
-                    serviceInfo.metaData.putBoolean(
-                            TelecomManager.METADATA_IN_CALL_SERVICE_UI, true);
-                }});
-                add(new ResolveInfo() {{
-                    serviceInfo = new ServiceInfo();
-                    serviceInfo.packageName = SYS_PKG;
-                    serviceInfo.name = SYS_CLASS;
-                    serviceInfo.permission = Manifest.permission.BIND_INCALL_SERVICE;
-                }});
-            }});
+        setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
         mInCallController.bindToServices(mMockCall);
 
         // Query for the different InCallServices
         ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
-        verify(mMockPackageManager, times(3)).queryIntentServicesAsUser(
+        verify(mMockPackageManager, times(4)).queryIntentServicesAsUser(
                 queryIntentCaptor.capture(),
                 eq(PackageManager.GET_META_DATA), eq(CURRENT_USER_ID));
 
@@ -404,4 +349,139 @@
         assertEquals(SYS_PKG, bindIntent.getComponent().getPackageName());
         assertEquals(SYS_CLASS, bindIntent.getComponent().getClassName());
     }
+
+    /**
+     * Ensures that the {@link InCallController} will bind to an {@link InCallService} which
+     * supports external calls.
+     */
+    @MediumTest
+    public void testBindToService_IncludeExternal() throws Exception {
+        setupMocks(true /* isExternalCall */);
+        setupMockPackageManager(true /* default */, true /* system */, true /* external calls */);
+        mInCallController.bindToServices(mMockCall);
+
+        // Query for the different InCallServices
+        ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
+        verify(mMockPackageManager, times(4)).queryIntentServicesAsUser(
+                queryIntentCaptor.capture(),
+                eq(PackageManager.GET_META_DATA), eq(CURRENT_USER_ID));
+
+        // Verify call for default dialer InCallService
+        assertEquals(DEF_PKG, queryIntentCaptor.getAllValues().get(0).getPackage());
+        // Verify call for car-mode InCallService
+        assertEquals(null, queryIntentCaptor.getAllValues().get(1).getPackage());
+        // Verify call for non-UI InCallServices
+        assertEquals(null, queryIntentCaptor.getAllValues().get(2).getPackage());
+
+        ArgumentCaptor<Intent> bindIntentCaptor = ArgumentCaptor.forClass(Intent.class);
+        verify(mMockContext, times(1)).bindServiceAsUser(
+                bindIntentCaptor.capture(),
+                any(ServiceConnection.class),
+                eq(Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE),
+                eq(UserHandle.CURRENT));
+
+        Intent bindIntent = bindIntentCaptor.getValue();
+        assertEquals(InCallService.SERVICE_INTERFACE, bindIntent.getAction());
+        assertEquals(DEF_PKG, bindIntent.getComponent().getPackageName());
+        assertEquals(DEF_CLASS, bindIntent.getComponent().getClassName());
+    }
+
+    /**
+     * Ensures that the {@link InCallController} will not bind to an incall service for an external
+     * call if the incall service does not support it.
+     */
+    @MediumTest
+    public void testBindToService_ExcludeExternal() throws Exception {
+        setupMocks(true /* isExternalCall */);
+        setupMockPackageManager(true /* default */, true /* system */, false /* external calls */);
+        mInCallController.bindToServices(mMockCall);
+
+        // Query for the different InCallServices
+        ArgumentCaptor<Intent> queryIntentCaptor = ArgumentCaptor.forClass(Intent.class);
+        verify(mMockPackageManager, times(4)).queryIntentServicesAsUser(
+                queryIntentCaptor.capture(),
+                eq(PackageManager.GET_META_DATA), eq(CURRENT_USER_ID));
+
+        // Verify call for default dialer InCallService
+        assertEquals(DEF_PKG, queryIntentCaptor.getAllValues().get(0).getPackage());
+        // Verify call for car-mode InCallService
+        assertEquals(null, queryIntentCaptor.getAllValues().get(1).getPackage());
+        // Verify call for non-UI InCallServices
+        assertEquals(null, queryIntentCaptor.getAllValues().get(2).getPackage());
+
+        verify(mMockContext, never()).bindServiceAsUser(
+                any(Intent.class),
+                any(ServiceConnection.class),
+                eq(Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE),
+                eq(UserHandle.CURRENT));
+    }
+
+    private void setupMocks(boolean isExternalCall) {
+        when(mMockCallsManager.getCurrentUserHandle()).thenReturn(mUserHandle);
+        when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager);
+        when(mMockCallsManager.hasEmergencyCall()).thenReturn(false);
+        when(mMockCall.isIncoming()).thenReturn(false);
+        when(mMockCall.getTargetPhoneAccount()).thenReturn(PA_HANDLE);
+        when(mMockDefaultDialerAdapter.getDefaultDialerApplication(mMockContext, CURRENT_USER_ID))
+                .thenReturn(DEF_PKG);
+        when(mMockContext.bindServiceAsUser(any(Intent.class), any(ServiceConnection.class),
+                anyInt(), eq(UserHandle.CURRENT))).thenReturn(true);
+        when(mMockCall.isExternalCall()).thenReturn(isExternalCall);
+    }
+
+    private ResolveInfo getDefResolveInfo(final boolean includeExternalCalls) {
+        return new ResolveInfo() {{
+            serviceInfo = new ServiceInfo();
+            serviceInfo.packageName = DEF_PKG;
+            serviceInfo.name = DEF_CLASS;
+            serviceInfo.permission = Manifest.permission.BIND_INCALL_SERVICE;
+            serviceInfo.metaData = new Bundle();
+            serviceInfo.metaData.putBoolean(
+                    TelecomManager.METADATA_IN_CALL_SERVICE_UI, true);
+            if (includeExternalCalls) {
+                serviceInfo.metaData.putBoolean(
+                        TelecomManager.METADATA_INCLUDE_EXTERNAL_CALLS, true);
+            }
+        }};
+    }
+
+    private ResolveInfo getSysResolveinfo() {
+        return new ResolveInfo() {{
+            serviceInfo = new ServiceInfo();
+            serviceInfo.packageName = SYS_PKG;
+            serviceInfo.name = SYS_CLASS;
+            serviceInfo.permission = Manifest.permission.BIND_INCALL_SERVICE;
+        }};
+    }
+
+    private void setupMockPackageManager(final boolean useDefaultDialer,
+            final boolean useSystemDialer, final boolean includeExternalCalls) {
+
+        doAnswer(new Answer() {
+            @Override
+            public Object answer(InvocationOnMock invocation) throws Throwable {
+                Object[] args = invocation.getArguments();
+                Intent intent = (Intent) args[0];
+                String packageName = intent.getPackage();
+                ComponentName componentName = intent.getComponent();
+                if (componentName != null) {
+                    packageName = componentName.getPackageName();
+                }
+                LinkedList<ResolveInfo> resolveInfo = new LinkedList<ResolveInfo>();
+                if (!TextUtils.isEmpty(packageName)) {
+                    if ((TextUtils.isEmpty(packageName) || packageName.equals(DEF_PKG)) &&
+                            useDefaultDialer) {
+                        resolveInfo.add(getDefResolveInfo(includeExternalCalls));
+                    }
+
+                    if ((TextUtils.isEmpty(packageName) || packageName.equals(SYS_PKG)) &&
+                           useSystemDialer) {
+                        resolveInfo.add(getSysResolveinfo());
+                    }
+                }
+                return resolveInfo;
+            }
+        }).when(mMockPackageManager).queryIntentServicesAsUser(
+                any(Intent.class), eq(PackageManager.GET_META_DATA), eq(CURRENT_USER_ID));
+    }
 }
diff --git a/tests/src/com/android/server/telecom/tests/VideoCallTests.java b/tests/src/com/android/server/telecom/tests/VideoCallTests.java
index c079857..ca56247 100644
--- a/tests/src/com/android/server/telecom/tests/VideoCallTests.java
+++ b/tests/src/com/android/server/telecom/tests/VideoCallTests.java
@@ -20,6 +20,7 @@
 
 import android.telecom.CallAudioState;
 import android.telecom.VideoProfile;
+import android.test.suitebuilder.annotation.MediumTest;
 
 import com.android.server.telecom.CallAudioRouteStateMachine;
 
@@ -39,6 +40,7 @@
      * Tests to ensure an incoming video-call is automatically routed to the speakerphone when
      * the call is answered and neither a wired headset nor bluetooth headset are connected.
      */
+    @MediumTest
     public void testAutoSpeakerphoneIncomingBidirectional() throws Exception {
         // Start an incoming video call.
         IdPair ids = startAndMakeActiveIncomingCall("650-555-1212",
@@ -54,6 +56,7 @@
      * always answer incoming video calls as bi-directional.  It is, however, possible for a third
      * party dialer to answer an incoming video call a a one-way video call.
      */
+    @MediumTest
     public void testAutoSpeakerphoneIncomingReceiveOnly() throws Exception {
         // Start an incoming video call.
         IdPair ids = startAndMakeActiveIncomingCall("650-555-1212",
@@ -67,6 +70,7 @@
      * Tests audio routing for an outgoing video call made with bidirectional video.  Expect to be
      * in speaker mode.
      */
+    @MediumTest
     public void testAutoSpeakerphoneOutgoingBidirectional() throws Exception {
         // Start an incoming video call.
         IdPair ids = startAndMakeActiveOutgoingCall("650-555-1212",
@@ -81,6 +85,7 @@
      * in speaker mode.  Note: The default UI does not support making one-way video calls, but the
      * APIs do and a third party incall UI could choose to support that.
      */
+    @MediumTest
     public void testAutoSpeakerphoneOutgoingTransmitOnly() throws Exception {
         // Start an incoming video call.
         IdPair ids = startAndMakeActiveOutgoingCall("650-555-1212",
@@ -95,6 +100,7 @@
      * in speaker mode.  Note: The default UI does not support making one-way video calls, but the
      * APIs do and a third party incall UI could choose to support that.
      */
+    @MediumTest
     public void testNoAutoSpeakerphoneOnOutgoing() throws Exception {
         // Start an incoming video call.
         IdPair ids = startAndMakeActiveOutgoingCall("650-555-1212",
@@ -107,6 +113,7 @@
     /**
      * Tests to ensure an incoming audio-only call is routed to the earpiece.
      */
+    @MediumTest
     public void testNoAutoSpeakerphoneOnIncoming() throws Exception {
 
         // Start an incoming video call.
diff --git a/tests/src/com/android/server/telecom/tests/VideoProviderTest.java b/tests/src/com/android/server/telecom/tests/VideoProviderTest.java
index bc7f9e1..92f876b 100644
--- a/tests/src/com/android/server/telecom/tests/VideoProviderTest.java
+++ b/tests/src/com/android/server/telecom/tests/VideoProviderTest.java
@@ -43,6 +43,7 @@
 import android.telecom.VideoCallImpl;
 import android.telecom.VideoProfile;
 import android.telecom.VideoProfile.CameraCapabilities;
+import android.test.suitebuilder.annotation.MediumTest;
 import android.view.Surface;
 
 import com.google.common.base.Predicate;
@@ -124,6 +125,7 @@
      * and {@link VideoCall.Callback#onCameraCapabilitiesChanged(CameraCapabilities)}
      * APIS.
      */
+    @MediumTest
     public void testCameraChange() throws Exception {
         // Wait until the callback has been received before performing verification.
         doAnswer(mVerification).when(mVideoCallCallback)
@@ -157,6 +159,7 @@
      * Tests the {@link VideoCall#setPreviewSurface(Surface)} and
      * {@link VideoProvider#onSetPreviewSurface(Surface)} APIs.
      */
+    @MediumTest
     public void testSetPreviewSurface() throws Exception {
         final Surface surface = new Surface(new SurfaceTexture(1));
         mVideoCall.setPreviewSurface(surface);
@@ -182,6 +185,7 @@
      * Tests the {@link VideoCall#setDisplaySurface(Surface)} and
      * {@link VideoProvider#onSetDisplaySurface(Surface)} APIs.
      */
+    @MediumTest
     public void testSetDisplaySurface() throws Exception {
         final Surface surface = new Surface(new SurfaceTexture(1));
         mVideoCall.setDisplaySurface(surface);
@@ -207,6 +211,7 @@
      * Tests the {@link VideoCall#setDeviceOrientation(int)} and
      * {@link VideoProvider#onSetDeviceOrientation(int)} APIs.
      */
+    @MediumTest
     public void testSetDeviceOrientation() throws Exception {
         mVideoCall.setDeviceOrientation(ORIENTATION_0);
 
@@ -230,6 +235,7 @@
     /**
      * Tests the {@link VideoCall#setZoom(float)} and {@link VideoProvider#onSetZoom(float)} APIs.
      */
+    @MediumTest
     public void testSetZoom() throws Exception {
         mVideoCall.setZoom(ZOOM_LEVEL);
 
@@ -251,6 +257,7 @@
      * Emulates a scenario where an InCallService sends a request to upgrade to video, which the
      * peer accepts as-is.
      */
+    @MediumTest
     public void testSessionModifyRequest() throws Exception {
         VideoProfile requestProfile = new VideoProfile(VideoProfile.STATE_BIDIRECTIONAL);
 
@@ -288,6 +295,7 @@
      * Tests the {@link VideoCall#sendSessionModifyResponse(VideoProfile)},
      * and {@link VideoProvider#onSendSessionModifyResponse(VideoProfile)} APIs.
      */
+    @MediumTest
     public void testSessionModifyResponse() throws Exception {
         VideoProfile sessionModifyResponse = new VideoProfile(VideoProfile.STATE_TX_ENABLED);
 
@@ -308,6 +316,7 @@
      * {@link VideoProvider#onRequestCameraCapabilities()} ()}, and
      * {@link VideoCall.Callback#onCameraCapabilitiesChanged(CameraCapabilities)} APIs.
      */
+    @MediumTest
     public void testRequestCameraCapabilities() throws Exception {
         // Wait until the callback has been received before performing verification.
         doAnswer(mVerification).when(mVideoCallCallback)
@@ -325,6 +334,7 @@
      * Tests the {@link VideoCall#setPauseImage(Uri)}, and
      * {@link VideoProvider#onSetPauseImage(Uri)} APIs.
      */
+    @MediumTest
     public void testSetPauseImage() throws Exception {
         final Uri testUri = Uri.fromParts("file", "test.jpg", null);
         mVideoCall.setPauseImage(testUri);
@@ -343,6 +353,7 @@
      * {@link VideoProvider#onRequestConnectionDataUsage()}, and
      * {@link VideoCall.Callback#onCallDataUsageChanged(long)} APIs.
      */
+    @MediumTest
     public void testRequestDataUsage() throws Exception {
         // Wait until the callback has been received before performing verification.
         doAnswer(mVerification).when(mVideoCallCallback)
@@ -360,6 +371,7 @@
      * Tests the {@link VideoProvider#receiveSessionModifyRequest(VideoProfile)},
      * {@link VideoCall.Callback#onSessionModifyRequestReceived(VideoProfile)} APIs.
      */
+    @MediumTest
     public void testReceiveSessionModifyRequest() throws Exception {
         // Wait until the callback has been received before performing verification.
         doAnswer(mVerification).when(mVideoCallCallback)
@@ -382,6 +394,7 @@
      * Tests the {@link VideoProvider#handleCallSessionEvent(int)}, and
      * {@link VideoCall.Callback#onCallSessionEvent(int)} APIs.
      */
+    @MediumTest
     public void testSessionEvent() throws Exception {
         // Wait until the callback has been received before performing verification.
         doAnswer(mVerification).when(mVideoCallCallback)
@@ -400,6 +413,7 @@
      * Tests the {@link VideoProvider#changePeerDimensions(int, int)} and
      * {@link VideoCall.Callback#onPeerDimensionsChanged(int, int)} APIs.
      */
+    @MediumTest
     public void testPeerDimensionChange() throws Exception {
         // Wait until the callback has been received before performing verification.
         doAnswer(mVerification).when(mVideoCallCallback)
@@ -419,6 +433,7 @@
      * Tests the {@link VideoProvider#changeVideoQuality(int)} and
      * {@link VideoCall.Callback#onVideoQualityChanged(int)} APIs.
      */
+    @MediumTest
     public void testVideoQualityChange() throws Exception {
         // Wait until the callback has been received before performing verification.
         doAnswer(mVerification).when(mVideoCallCallback)