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;
}