Support for new multi-endpoint APIs.

- piping through "pullExternalCall" API from incall layer to connection
service.
- mapping of new capabilities/properties between connections/calls.
- basic unit tests for new pullExternalCall API (there will be some
follow-up work on these in the future).
- plumbing through of Connection and Call events.

Bug: 27458894
Change-Id: I421ebab28fada224bddca54ed4d3d9dff6f33bcf
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 2a56422..3402cc4 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -105,6 +105,7 @@
         void onConferenceableCallsChanged(Call call);
         boolean onCanceledViaNewOutgoingCallBroadcast(Call call);
         void onHoldToneRequested(Call call);
+        void onConnectionEvent(Call call, String event, Bundle extras);
     }
 
     public abstract static class ListenerBase implements Listener {
@@ -165,6 +166,8 @@
 
         @Override
         public void onHoldToneRequested(Call call) {}
+        @Override
+        public void onConnectionEvent(Call call, String event, Bundle extras) {}
     }
 
     private final OnQueryCompleteListener mCallerInfoQueryListener =
@@ -1424,6 +1427,55 @@
         }
     }
 
+    /**
+     * Initiates a request to the connection service to pull this call.
+     * <p>
+     * This method can only be used for calls that have the
+     * {@link android.telecom.Connection#CAPABILITY_CAN_PULL_CALL} and
+     * {@link android.telecom.Connection#CAPABILITY_IS_EXTERNAL_CALL} capabilities set.
+     * <p>
+     * An external call is a representation of a call which is taking place on another device
+     * associated with a PhoneAccount on this device.  Issuing a request to pull the external call 
+     * tells the {@link android.telecom.ConnectionService} that it should move the call from the
+     * other device to this one.  An example of this is the IMS multi-endpoint functionality.  A
+     * user may have two phones with the same phone number.  If the user is engaged in an active
+     * call on their first device, the network will inform the second device of that ongoing call in
+     * the form of an external call.  The user may wish to continue their conversation on the second
+     * device, so will issue a request to pull the call to the second device.
+     * <p>
+     * Requests to pull a call which is not external, or a call which is not pullable are ignored.
+     */
+    public void pullExternalCall() {
+        if (mConnectionService == null) {
+            Log.w(this, "pulling a call without a connection service.");
+        }
+
+        if (!can(Connection.CAPABILITY_IS_EXTERNAL_CALL)) {
+            Log.w(this, "pullExternalCall - call %s is not an external call.", mId);
+            return;
+        }
+
+        if (!can(Connection.CAPABILITY_CAN_PULL_CALL)) {
+            Log.w(this, "pullExternalCall - call %s is external but cannot be pulled.", mId);
+            return;
+        }
+
+        Log.event(this, Log.Events.PULL);
+        mConnectionService.pullExternalCall(this);
+    }
+
+    /**
+     * Sends a call event to the {@link ConnectionService} for this call.
+     *
+     * See {@link Call#sendCallEvent(String, Bundle)}.
+     *
+     * @param event The call event.
+     * @param extras Associated extras.
+     */
+    public void sendCallEvent(String event, Bundle extras) {
+        mConnectionService.sendCallEvent(this, event, extras);
+    }
+
     void setParentCall(Call parentCall) {
         if (parentCall == this) {
             Log.e(this, new Exception(), "setting the parent to self");
@@ -1948,8 +2000,9 @@
      * Handles Connection events received from a {@link ConnectionService}.
      *
      * @param event The event.
+     * @param extras The extras.
      */
-    public void onConnectionEvent(String event) {
+    public void onConnectionEvent(String event, Bundle extras) {
         if (Connection.EVENT_ON_HOLD_TONE_START.equals(event)) {
             mIsRemotelyHeld = true;
             Log.event(this, Log.Events.REMOTELY_HELD);
@@ -1964,6 +2017,10 @@
             for (Listener l : mListeners) {
                 l.onHoldToneRequested(this);
             }
+        } else {
+            for (Listener l : mListeners) {
+                l.onConnectionEvent(this, event, extras);
+            }
         }
     }
 }
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index 1feb356..b05c1b8 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -589,14 +589,14 @@
         }
 
         @Override
-        public void onConnectionEvent(String callId, String event) {
+        public void onConnectionEvent(String callId, String event, Bundle extras) {
             Log.startSession("CSW.oCE");
             long token = Binder.clearCallingIdentity();
             try {
                 synchronized (mLock) {
                     Call call = mCallIdMapper.getCall(callId);
                     if (call != null) {
-                        call.onConnectionEvent(event);
+                        call.onConnectionEvent(event, extras);
                     }
                 }
             } finally {
@@ -938,6 +938,28 @@
         }
     }
 
+    void pullExternalCall(Call call) {
+        final String callId = mCallIdMapper.getCallId(call);
+        if (callId != null && isServiceValid("pullExternalCall")) {
+            try {
+                logOutgoing("pullExternalCall %s", callId);
+                mServiceInterface.pullExternalCall(callId);
+            } catch (RemoteException ignored) {
+            }
+        }
+    }
+
+    void sendCallEvent(Call call, String event, Bundle extras) {
+        final String callId = mCallIdMapper.getCallId(call);
+        if (callId != null && isServiceValid("sendCallEvent")) {
+            try {
+                logOutgoing("sendCallEvent %s %s", callId, event);
+                mServiceInterface.sendCallEvent(callId, event, extras);
+            } catch (RemoteException ignored) {
+            }
+        }
+    }
+
     /** {@inheritDoc} */
     @Override
     protected void setServiceInterface(IBinder binder) {
diff --git a/src/com/android/server/telecom/InCallAdapter.java b/src/com/android/server/telecom/InCallAdapter.java
index bee03a2..4dd06d2 100644
--- a/src/com/android/server/telecom/InCallAdapter.java
+++ b/src/com/android/server/telecom/InCallAdapter.java
@@ -17,6 +17,7 @@
 package com.android.server.telecom;
 
 import android.os.Binder;
+import android.os.Bundle;
 import android.telecom.PhoneAccountHandle;
 
 import com.android.internal.telecom.IInCallAdapter;
@@ -370,6 +371,50 @@
     }
 
     @Override
+    public void pullExternalCall(String callId) {
+        try {
+            Log.startSession("ICA.pEC", mOwnerComponentName);
+            long token = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    Call call = mCallIdMapper.getCall(callId);
+                    if (call != null) {
+                        call.pullExternalCall();
+                    } else {
+                        Log.w(this, "pullExternalCall, unknown call id: %s", callId);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        } finally {
+            Log.endSession();
+        }
+    }
+
+    @Override
+    public void sendCallEvent(String callId, String event, Bundle extras) {
+        try {
+            Log.startSession("ICA.sCE", mOwnerComponentName);
+            long token = Binder.clearCallingIdentity();
+            try {
+                synchronized (mLock) {
+                    Call call = mCallIdMapper.getCall(callId);
+                    if (call != null) {
+                        call.sendCallEvent(event, extras);
+                    } else {
+                        Log.w(this, "sendCallEvent, unknown call id: %s", callId);
+                    }
+                }
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        } finally {
+            Log.endSession();
+        }
+    }
+
+    @Override
     public void turnOnProximitySensor() {
         try {
             Log.startSession("ICA.tOnPS", mOwnerComponentName);
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index 150879d..93589ed 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -25,6 +25,7 @@
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
 import android.content.res.Resources;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.Looper;
@@ -132,6 +133,11 @@
         public void onConferenceableCallsChanged(Call call) {
             updateCall(call);
         }
+
+        @Override
+        public void onConnectionEvent(Call call, String event, Bundle extras) {
+            notifyConnectionEvent(call, event, extras);
+        }
     };
 
     private final SystemStateListener mSystemStateListener = new SystemStateListener() {
@@ -321,6 +327,17 @@
         }
     }
 
+    private void notifyConnectionEvent(Call call, String event, Bundle extras) {
+        if (!mInCallServices.isEmpty()) {
+            for (IInCallService inCallService : mInCallServices.values()) {
+                try {
+                    inCallService.onConnectionEvent(mCallIdMapper.getCallId(call), event, extras);
+                } catch (RemoteException ignored) {
+                }
+            }
+        }
+    }
+
     /**
      * Unbinds an existing bound connection to the in-call app.
      */
diff --git a/src/com/android/server/telecom/Log.java b/src/com/android/server/telecom/Log.java
index 8daf6c1..8f027ec 100644
--- a/src/com/android/server/telecom/Log.java
+++ b/src/com/android/server/telecom/Log.java
@@ -103,6 +103,7 @@
         public static final String BLOCK_CHECK_FINISHED = "BLOCK_CHECK_FINISHED";
         public static final String REMOTELY_HELD = "REMOTELY_HELD";
         public static final String REMOTELY_UNHELD = "REMOTELY_UNHELD";
+        public static final String PULL = "PULL";
 
         /**
          * Maps from a request to a response.  The same event could be listed as the
diff --git a/src/com/android/server/telecom/ParcelableCallUtils.java b/src/com/android/server/telecom/ParcelableCallUtils.java
index ae8e425..3f5b804 100644
--- a/src/com/android/server/telecom/ParcelableCallUtils.java
+++ b/src/com/android/server/telecom/ParcelableCallUtils.java
@@ -231,7 +231,10 @@
         android.telecom.Call.Details.CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION,
 
         Connection.CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO,
-        android.telecom.Call.Details.CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO
+        android.telecom.Call.Details.CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO,
+
+        Connection.CAPABILITY_CAN_PULL_CALL,
+        android.telecom.Call.Details.CAPABILITY_CAN_PULL_CALL
     };
 
     private static int convertConnectionToCallCapabilities(int connectionCapabilities) {
@@ -257,7 +260,10 @@
         android.telecom.Call.Details.PROPERTY_GENERIC_CONFERENCE,
 
         Connection.CAPABILITY_SHOW_CALLBACK_NUMBER,
-        android.telecom.Call.Details.PROPERTY_EMERGENCY_CALLBACK_MODE
+        android.telecom.Call.Details.PROPERTY_EMERGENCY_CALLBACK_MODE,
+
+        Connection.CAPABILITY_IS_EXTERNAL_CALL,
+        android.telecom.Call.Details.PROPERTY_IS_EXTERNAL_CALL
     };
 
     private static int convertConnectionToCallProperties(int connectionCapabilities) {
diff --git a/tests/src/com/android/server/telecom/tests/BasicCallTests.java b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
index 49c47a1..b1c4e03 100644
--- a/tests/src/com/android/server/telecom/tests/BasicCallTests.java
+++ b/tests/src/com/android/server/telecom/tests/BasicCallTests.java
@@ -68,10 +68,22 @@
 import java.util.concurrent.CyclicBarrier;
 import java.util.concurrent.TimeUnit;
 
+import org.mockito.ArgumentCaptor;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
 /**
  * Performs various basic call tests in Telecom.
  */
 public class BasicCallTests extends TelecomSystemTest {
+    private static final String TEST_BUNDLE_KEY = "android.telecom.extra.TEST";
+    private static final String TEST_EVENT = "android.telecom.event.TEST";
+
     @LargeTest
     public void testSingleOutgoingCallLocalDisconnect() throws Exception {
         IdPair ids = startAndMakeActiveOutgoingCall("650-555-1212",
@@ -568,6 +580,101 @@
         return conferenceCall;
     }
 
+    /**
+     * Tests the {@link Call#pullExternalCall()} API.  Verifies that if a call is not an external
+     * call, no pull call request is made to the connection service.
+     *
+     * @throws Exception
+     */
+    @MediumTest
+    public void testPullNonExternalCall() throws Exception {
+        // TODO: Revisit this unit test once telecom support for filtering external calls from
+        // InCall services is implemented.
+        IdPair ids = startAndMakeActiveIncomingCall("650-555-1212",
+                mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+        assertEquals(Call.STATE_ACTIVE, mInCallServiceFixtureX.getCall(ids.mCallId).getState());
+
+        // Attempt to pull the call and verify the API call makes it through
+        mInCallServiceFixtureX.mInCallAdapter.pullExternalCall(ids.mCallId);
+        verify(mConnectionServiceFixtureA.getTestDouble(), timeout(TEST_TIMEOUT).never())
+                .pullExternalCall(ids.mCallId);
+    }
+
+    /**
+     * Tests the {@link Connection#sendConnectionEvent(String)} API.
+     *
+     * @throws Exception
+     */
+    @MediumTest
+    public void testSendConnectionEventNull() throws Exception {
+        IdPair ids = startAndMakeActiveIncomingCall("650-555-1212",
+                mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+        assertEquals(Call.STATE_ACTIVE, mInCallServiceFixtureX.getCall(ids.mCallId).getState());
+        mConnectionServiceFixtureA.sendConnectionEvent(ids.mConnectionId, TEST_EVENT, null);
+        verify(mInCallServiceFixtureX.getTestDouble(), timeout(TEST_TIMEOUT))
+                .onConnectionEvent(ids.mCallId, TEST_EVENT, null);
+    }
+
+    /**
+     * Tests the {@link Connection#sendConnectionEvent(String)} API.
+     *
+     * @throws Exception
+     */
+    @MediumTest
+    public void testSendConnectionEventNotNull() throws Exception {
+        IdPair ids = startAndMakeActiveIncomingCall("650-555-1212",
+                mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+        assertEquals(Call.STATE_ACTIVE, mInCallServiceFixtureX.getCall(ids.mCallId).getState());
+
+        Bundle testBundle = new Bundle();
+        testBundle.putString(TEST_BUNDLE_KEY, "TEST");
+
+        ArgumentCaptor<Bundle> bundleArgumentCaptor = ArgumentCaptor.forClass(Bundle.class);
+        mConnectionServiceFixtureA.sendConnectionEvent(ids.mConnectionId, TEST_EVENT, testBundle);
+        verify(mInCallServiceFixtureX.getTestDouble(), timeout(TEST_TIMEOUT))
+                .onConnectionEvent(eq(ids.mCallId), eq(TEST_EVENT), bundleArgumentCaptor.capture());
+        assert (bundleArgumentCaptor.getValue().containsKey(TEST_BUNDLE_KEY));
+    }
+
+    /**
+     * Tests the {@link Call#sendCallEvent(String, Bundle)} API.
+     *
+     * @throws Exception
+     */
+    @MediumTest
+    public void testSendCallEventNull() throws Exception {
+        IdPair ids = startAndMakeActiveIncomingCall("650-555-1212",
+                mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+        assertEquals(Call.STATE_ACTIVE, mInCallServiceFixtureX.getCall(ids.mCallId).getState());
+
+        mInCallServiceFixtureX.mInCallAdapter.sendCallEvent(ids.mCallId, TEST_EVENT, null);
+        verify(mConnectionServiceFixtureA.getTestDouble(), timeout(TEST_TIMEOUT))
+                .sendCallEvent(ids.mCallId, TEST_EVENT, null);
+    }
+
+    /**
+     * Tests the {@link Call#sendCallEvent(String, Bundle)} API.
+     *
+     * @throws Exception
+     */
+    @MediumTest
+    public void testSendCallEventNonNull() throws Exception {
+        IdPair ids = startAndMakeActiveIncomingCall("650-555-1212",
+                mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+        assertEquals(Call.STATE_ACTIVE, mInCallServiceFixtureX.getCall(ids.mCallId).getState());
+
+        Bundle testBundle = new Bundle();
+        testBundle.putString(TEST_BUNDLE_KEY, "TEST");
+
+        ArgumentCaptor<Bundle> bundleArgumentCaptor = ArgumentCaptor.forClass(Bundle.class);
+        mInCallServiceFixtureX.mInCallAdapter.sendCallEvent(ids.mCallId, TEST_EVENT,
+                testBundle);
+        verify(mConnectionServiceFixtureA.getTestDouble(), timeout(TEST_TIMEOUT))
+                .sendCallEvent(eq(ids.mCallId), eq(TEST_EVENT),
+                        bundleArgumentCaptor.capture());
+        assert (bundleArgumentCaptor.getValue().containsKey(TEST_BUNDLE_KEY));
+    }
+
     @MediumTest
     public void testAnalyticsSingleCall() throws Exception {
         IdPair testCall = startAndMakeActiveIncomingCall(
@@ -725,4 +832,51 @@
         assertEquals(Call.STATE_DISCONNECTED, mInCallServiceFixtureX.getCall(callId).getState());
         assertEquals(Call.STATE_DISCONNECTED, mInCallServiceFixtureY.getCall(callId).getState());
     }
+
+    /**
+     * Tests the {@link Call#pullExternalCall()} API.  Ensures that an external call which is
+     * pullable can be pulled.
+     *
+     * @throws Exception
+     */
+    @LargeTest
+    public void testPullExternalCall() throws Exception {
+        // TODO: Revisit this unit test once telecom support for filtering external calls from
+        // InCall services is implemented.
+        mConnectionServiceFixtureA.mConnectionServiceDelegate.mCapabilities =
+                Connection.CAPABILITY_IS_EXTERNAL_CALL | Connection.CAPABILITY_CAN_PULL_CALL;
+
+        IdPair ids = startAndMakeActiveIncomingCall("650-555-1212",
+                mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+        assertEquals(Call.STATE_ACTIVE, mInCallServiceFixtureX.getCall(ids.mCallId).getState());
+
+        // Attempt to pull the call and verify the API call makes it through
+        mInCallServiceFixtureX.mInCallAdapter.pullExternalCall(ids.mCallId);
+        verify(mConnectionServiceFixtureA.getTestDouble(), timeout(TEST_TIMEOUT))
+                .pullExternalCall(ids.mCallId);
+    }
+
+    /**
+     * Tests the {@link Call#pullExternalCall()} API.  Verifies that if an external call is not
+     * marked as pullable that the connection service does not get an API call to pull the external
+     * call.
+     *
+     * @throws Exception
+     */
+    @LargeTest
+    public void testPullNonPullableExternalCall() throws Exception {
+        // TODO: Revisit this unit test once telecom support for filtering external calls from
+        // InCall services is implemented.
+        mConnectionServiceFixtureA.mConnectionServiceDelegate.mCapabilities =
+                Connection.CAPABILITY_IS_EXTERNAL_CALL;
+
+        IdPair ids = startAndMakeActiveIncomingCall("650-555-1212",
+                mPhoneAccountA0.getAccountHandle(), mConnectionServiceFixtureA);
+        assertEquals(Call.STATE_ACTIVE, mInCallServiceFixtureX.getCall(ids.mCallId).getState());
+
+        // Attempt to pull the call and verify the API call makes it through
+        mInCallServiceFixtureX.mInCallAdapter.pullExternalCall(ids.mCallId);
+        verify(mConnectionServiceFixtureA.getTestDouble(), timeout(TEST_TIMEOUT).never())
+                .pullExternalCall(ids.mCallId);
+    }
 }
diff --git a/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java b/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java
index 30cb1cd..27f169d 100644
--- a/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java
+++ b/tests/src/com/android/server/telecom/tests/ConnectionServiceFixture.java
@@ -60,6 +60,7 @@
  */
 public class ConnectionServiceFixture implements TestFixture<IConnectionService> {
     static int INVALID_VIDEO_STATE = -1;
+    static int CAPABILITIES_NOT_SPECIFIED = 0;
 
     /**
      * Implementation of ConnectionService that performs no-ops for tasks normally meant for
@@ -67,6 +68,7 @@
      */
     public class FakeConnectionServiceDelegate extends ConnectionService {
         int mVideoState = INVALID_VIDEO_STATE;
+        int mCapabilities = CAPABILITIES_NOT_SPECIFIED;
 
         @Override
         public Connection onCreateUnknownConnection(
@@ -77,9 +79,14 @@
         @Override
         public Connection onCreateIncomingConnection(
                 PhoneAccountHandle connectionManagerPhoneAccount, ConnectionRequest request) {
-            return new FakeConnection(
+            FakeConnection fakeConnection =  new FakeConnection(
                     mVideoState == INVALID_VIDEO_STATE ? request.getVideoState() : mVideoState,
                     request.getAddress());
+            if (mCapabilities != CAPABILITIES_NOT_SPECIFIED) {
+                fakeConnection.setConnectionCapabilities(mCapabilities);
+            }
+
+            return fakeConnection;
         }
 
         @Override
@@ -235,6 +242,13 @@
         public void onPostDialContinue(String callId, boolean proceed) throws RemoteException { }
 
         @Override
+        public void pullExternalCall(String callId) throws RemoteException { }
+
+        @Override
+        public void sendCallEvent(String callId, String event, Bundle extras) throws RemoteException
+        {}
+
+        @Override
         public IBinder asBinder() {
             return this;
         }
@@ -471,6 +485,12 @@
         }
     }
 
+    public void sendConnectionEvent(String id, String event, Bundle extras) throws Exception {
+        for (IConnectionServiceAdapter a : mConnectionServiceAdapters) {
+            a.onConnectionEvent(id, event, extras);
+        }
+    }
+
     private ParcelableConference parcelable(ConferenceInfo c) {
         return new ParcelableConference(
                 c.phoneAccount,
diff --git a/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java b/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java
index 53e58af..eb007a3 100644
--- a/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java
+++ b/tests/src/com/android/server/telecom/tests/InCallServiceFixture.java
@@ -21,6 +21,7 @@
 
 import org.mockito.Mockito;
 
+import android.os.Bundle;
 import android.os.IBinder;
 import android.os.IInterface;
 import android.os.RemoteException;
@@ -111,6 +112,11 @@
         }
 
         @Override
+        public void onConnectionEvent(String callId, String event, Bundle extras)
+                throws RemoteException {
+        }
+
+        @Override
         public IBinder asBinder() {
             return this;
         }