Support Remote call services.
Adds daisy-chaining support for connection services.
Change-Id: Ibb382a6ed6c5042c1b71821d6b537e2f1cb3063f
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index a075c49..2bcb756 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -29,7 +29,7 @@
</intent-filter>
</service>
- <service android:name="com.android.telecomm.testapps.TestCallService">
+ <service android:name="com.android.telecomm.testapps.TestConnectionService">
<intent-filter>
<action android:name="android.telecomm.CallService" />
</intent-filter>
diff --git a/tests/src/com/android/telecomm/testapps/CallServiceNotifier.java b/tests/src/com/android/telecomm/testapps/CallServiceNotifier.java
index 336ce56..6364377 100644
--- a/tests/src/com/android/telecomm/testapps/CallServiceNotifier.java
+++ b/tests/src/com/android/telecomm/testapps/CallServiceNotifier.java
@@ -27,7 +27,7 @@
/**
* Class used to create, update and cancel the notification used to display and update call state
- * for {@link TestCallService}.
+ * for {@link TestConnectionService}.
*/
public class CallServiceNotifier {
private static final CallServiceNotifier INSTANCE = new CallServiceNotifier();
@@ -86,7 +86,7 @@
builder.setSmallIcon(android.R.drawable.stat_sys_phone_call);
builder.setContentText("Test calls via CallService API");
- builder.setContentTitle("TestCallService");
+ builder.setContentTitle("TestConnectionService");
addAddCallAction(builder, context);
addExitAction(builder, context);
@@ -109,9 +109,9 @@
*/
private PendingIntent createIncomingCallIntent(Context context) {
log("Creating incoming call pending intent.");
- // Build descriptor for TestCallService.
+ // Build descriptor for TestConnectionService.
CallServiceDescriptor.Builder descriptorBuilder = CallServiceDescriptor.newBuilder(context);
- descriptorBuilder.setCallService(TestCallService.class);
+ descriptorBuilder.setCallService(TestConnectionService.class);
descriptorBuilder.setNetworkType(CallServiceDescriptor.FLAG_WIFI);
// Create intent for adding an incoming call.
diff --git a/tests/src/com/android/telecomm/testapps/TestCallService.java b/tests/src/com/android/telecomm/testapps/TestCallService.java
deleted file mode 100644
index b795060..0000000
--- a/tests/src/com/android/telecomm/testapps/TestCallService.java
+++ /dev/null
@@ -1,213 +0,0 @@
-/*
- * Copyright (C) 2013 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- Ca* See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.telecomm.testapps;
-
-import android.content.Intent;
-import android.media.MediaPlayer;
-import android.net.Uri;
-import android.os.Bundle;
-import android.os.Handler;
-import android.telecomm.CallAudioState;
-import android.telecomm.CallInfo;
-import android.telecomm.CallService;
-import android.telecomm.CallServiceAdapter;
-import android.telecomm.CallState;
-import android.telephony.DisconnectCause;
-import android.util.Log;
-
-import com.android.telecomm.tests.R;
-import com.google.common.base.Preconditions;
-import com.google.common.base.Strings;
-import com.google.common.collect.Maps;
-
-import java.util.List;
-import java.util.Map;
-
-/**
- * Service which provides fake calls to test the ICallService interface.
- * TODO(santoscordon): Rename all classes in the directory to Dummy* (e.g., DummyCallService).
- */
-public class TestCallService extends CallService {
- private static final String SCHEME_TEL = "tel";
-
- private final Map<String, CallInfo> mCalls = Maps.newHashMap();
- private final Handler mHandler = new Handler();
-
- /** Used to play an audio tone during a call. */
- private MediaPlayer mMediaPlayer;
-
- /** {@inheritDoc} */
- @Override
- public void onAdapterAttached(CallServiceAdapter callServiceAdapter) {
- log("onAdapterAttached");
- mMediaPlayer = createMediaPlayer();
- }
-
- /**
- * Starts a call by calling into the adapter.
- *
- * {@inheritDoc}
- */
- @Override
- public void call(final CallInfo callInfo) {
- String number = callInfo.getHandle().getSchemeSpecificPart();
- log("call, number: " + number);
-
- // Crash on 555-DEAD to test call service crashing.
- if ("5550340".equals(number)) {
- throw new RuntimeException("Goodbye, cruel world.");
- }
-
- mCalls.put(callInfo.getId(), callInfo);
- getAdapter().handleSuccessfulOutgoingCall(callInfo.getId());
- mHandler.postDelayed(new Runnable() {
- @Override
- public void run() {
- activateCall(callInfo.getId());
- }
- }, 4000);
- }
-
- /** {@inheritDoc} */
- @Override
- public void abort(String callId) {
- log("abort, callId: " + callId);
- destroyCall(callId);
- }
-
- /** {@inheritDoc} */
- @Override
- public void setIncomingCallId(String callId, Bundle extras) {
- log("setIncomingCallId, callId: " + callId + " extras: " + extras);
-
- // Use dummy number for testing incoming calls.
- Uri handle = Uri.fromParts(SCHEME_TEL, "5551234", null);
-
- CallInfo callInfo = new CallInfo(callId, CallState.RINGING, handle);
- mCalls.put(callInfo.getId(), callInfo);
- getAdapter().notifyIncomingCall(callInfo);
- }
-
- /** {@inheritDoc} */
- @Override
- public void answer(String callId) {
- log("answer, callId: " + callId);
- activateCall(callId);
- }
-
- /** {@inheritDoc} */
- @Override
- public void reject(String callId) {
- log("reject, callId: " + callId);
- getAdapter().setDisconnected(callId, DisconnectCause.INCOMING_REJECTED, null);
- }
-
- /** {@inheritDoc} */
- @Override
- public void disconnect(String callId) {
- log("disconnect, callId: " + callId);
-
- destroyCall(callId);
- getAdapter().setDisconnected(callId, DisconnectCause.LOCAL, null);
- }
-
- /** {@inheritDoc} */
- @Override
- public void hold(String callId) {
- log("hold, callId: " + callId);
- getAdapter().setOnHold(callId);
- }
-
- /** {@inheritDoc} */
- @Override
- public void unhold(String callId) {
- log("unhold, callId: " + callId);
- getAdapter().setActive(callId);
- }
-
- /** {@inheritDoc} */
- @Override
- public void playDtmfTone(String callId, char digit) {
- log("playDtmfTone, callId: " + callId + " digit: " + digit);
- }
-
- /** {@inheritDoc} */
- @Override
- public void stopDtmfTone(String callId) {
- log("stopDtmfTone, callId: " + callId);
- }
-
- /** {@inheritDoc} */
- @Override
- public void onAudioStateChanged(String callId, CallAudioState audioState) {
- log("onAudioStateChanged, callId: " + callId + " audioState: " + audioState);
- }
-
- /** {@inheritDoc} */
- @Override
- public boolean onUnbind(Intent intent) {
- log("onUnbind");
- mMediaPlayer = null;
- return super.onUnbind(intent);
- }
-
- /** ${inheritDoc} */
- @Override
- public void conference(String conferenceCallId, String callId) {
- }
-
- /** ${inheritDoc} */
- @Override
- public void splitFromConference(String callId) {
- }
-
- private void activateCall(String callId) {
- getAdapter().setActive(callId);
- if (mMediaPlayer != null && !mMediaPlayer.isPlaying()) {
- mMediaPlayer.start();
- }
- }
-
- /**
- * Removes the specified call ID from the set of live call IDs and stops playing audio if
- * there exist no more live calls.
- *
- * @param callId The identifier of the call to destroy.
- */
- private void destroyCall(String callId) {
- Preconditions.checkState(!Strings.isNullOrEmpty(callId));
- mCalls.remove(callId);
-
- // Stops audio if there are no more calls.
- if (mCalls.isEmpty() && mMediaPlayer.isPlaying()) {
- mMediaPlayer.stop();
- mMediaPlayer.release();
- mMediaPlayer = createMediaPlayer();
- }
- }
-
- private MediaPlayer createMediaPlayer() {
- // Prepare the media player to play a tone when there is a call.
- MediaPlayer mediaPlayer = MediaPlayer.create(getApplicationContext(), R.raw.beep_boop);
- mediaPlayer.setLooping(true);
- return mediaPlayer;
- }
-
- private static void log(String msg) {
- Log.w("testcallservice", "[TestCallService] " + msg);
- }
-}
diff --git a/tests/src/com/android/telecomm/testapps/TestCallServiceProvider.java b/tests/src/com/android/telecomm/testapps/TestCallServiceProvider.java
index a8ccaa1..b1d32a3 100644
--- a/tests/src/com/android/telecomm/testapps/TestCallServiceProvider.java
+++ b/tests/src/com/android/telecomm/testapps/TestCallServiceProvider.java
@@ -34,7 +34,7 @@
log("lookupCallServices");
CallServiceDescriptor.Builder builder = CallServiceDescriptor.newBuilder(this);
- builder.setCallService(TestCallService.class);
+ builder.setCallService(TestConnectionService.class);
builder.setNetworkType(CallServiceDescriptor.FLAG_WIFI);
response.setCallServiceDescriptors(Lists.newArrayList(builder.build()));
diff --git a/tests/src/com/android/telecomm/testapps/TestConnectionService.java b/tests/src/com/android/telecomm/testapps/TestConnectionService.java
new file mode 100644
index 0000000..1e23916
--- /dev/null
+++ b/tests/src/com/android/telecomm/testapps/TestConnectionService.java
@@ -0,0 +1,366 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ Ca* See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.telecomm.testapps;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.telecomm.CallAudioState;
+import android.telecomm.CallInfo;
+import android.telecomm.CallServiceAdapter;
+import android.telecomm.CallState;
+import android.telecomm.Connection;
+import android.telecomm.ConnectionRequest;
+import android.telecomm.ConnectionService;
+import android.telecomm.RemoteConnection;
+import android.telecomm.RemoteConnectionService;
+import android.telecomm.Response;
+
+import android.telecomm.SimpleResponse;
+import android.telecomm.Subscription;
+import android.telephony.DisconnectCause;
+import android.util.Log;
+
+import com.android.telecomm.tests.R;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.collect.Maps;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Service which provides fake calls to test the ICallService interface. TODO(santoscordon): Rename
+ * all classes in the directory to Dummy* (e.g., DummyCallService).
+ */
+public class TestConnectionService extends ConnectionService {
+ private final class TestConnection extends Connection {
+ private final RemoteConnection.Listener mProxyListener = new RemoteConnection.Listener() {
+ @Override
+ public void onStateChanged(RemoteConnection connection, int state) {
+ setState(state);
+ }
+
+ @Override
+ public void onAudioStateChanged(RemoteConnection connection, CallAudioState state) {
+ setAudioState(state);
+ }
+
+ @Override
+ public void onDisconnected(RemoteConnection connection, int cause, String message) {
+ setDisconnected(cause, message);
+ destroyCall(TestConnection.this);
+ setDestroyed();
+ }
+
+ @Override
+ public void onRequestingRingback(RemoteConnection connection, boolean ringback) {
+ setRequestingRingback(ringback);
+ }
+
+ @Override
+ public void onPostDialWait(RemoteConnection connection, String remainingDigits) {
+ // TODO(santoscordon): Method needs to be exposed on Connection.java
+ }
+
+ @Override
+ public void onDestroyed(RemoteConnection connection) {
+ setDestroyed();
+ }
+ };
+
+ private final RemoteConnection mRemoteConnection;
+
+ TestConnection(RemoteConnection remoteConnection, int initialState) {
+ mRemoteConnection = remoteConnection;
+ if (mRemoteConnection != null) {
+ mRemoteConnection.addListener(mProxyListener);
+ } else {
+ setState(initialState);
+ }
+ }
+
+ void startOutgoing() {
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ TestConnection.this.setActive();
+ }
+ }, 4000);
+ }
+
+ boolean isProxy() {
+ return mRemoteConnection != null;
+ }
+
+ /** ${inheritDoc} */
+ @Override
+ protected void onAbort() {
+ if (mRemoteConnection != null) {
+ mRemoteConnection.disconnect();
+ mRemoteConnection.removeListener(mProxyListener);
+ } else {
+ destroyCall(this);
+ setDestroyed();
+ }
+ }
+
+ /** ${inheritDoc} */
+ @Override
+ protected void onAnswer() {
+ if (mRemoteConnection != null) {
+ mRemoteConnection.answer();
+ } else {
+ activateCall(this);
+ setActive();
+ }
+ }
+
+ /** ${inheritDoc} */
+ @Override
+ protected void onDisconnect() {
+ if (mRemoteConnection != null) {
+ mRemoteConnection.disconnect();
+ } else {
+ setDisconnected(DisconnectCause.LOCAL, null);
+ destroyCall(this);
+ setDestroyed();
+ }
+ }
+
+ /** ${inheritDoc} */
+ @Override
+ protected void onHold() {
+ if (mRemoteConnection != null) {
+ mRemoteConnection.hold();
+ } else {
+ setOnHold();
+ }
+ }
+
+ /** ${inheritDoc} */
+ @Override
+ protected void onReject() {
+ if (mRemoteConnection != null) {
+ mRemoteConnection.reject();
+ } else {
+ setDisconnected(DisconnectCause.INCOMING_REJECTED, null);
+ destroyCall(this);
+ setDestroyed();
+ }
+ }
+
+ /** ${inheritDoc} */
+ @Override
+ protected void onUnhold() {
+ if (mRemoteConnection != null) {
+ mRemoteConnection.hold();
+ } else {
+ setActive();
+ }
+ }
+
+ private void setState(int state) {
+ switch (state) {
+ case Connection.State.ACTIVE:
+ setActive();
+ break;
+ case Connection.State.HOLDING:
+ setOnHold();
+ break;
+ case Connection.State.DIALING:
+ setDialing();
+ break;
+ case Connection.State.RINGING:
+ setRinging();
+ break;
+ }
+ }
+ }
+
+ private class CallAttempter implements SimpleResponse<ConnectionRequest, RemoteConnection> {
+ private final Iterator<Subscription> mSubscriptionIterator;
+ private final Response<ConnectionRequest, Connection> mCallback;
+ private final ConnectionRequest mOriginalRequest;
+
+ CallAttempter(
+ Iterator<Subscription> iterator,
+ Response<ConnectionRequest, Connection> callback,
+ ConnectionRequest originalRequest) {
+ mSubscriptionIterator = iterator;
+ mCallback = callback;
+ mOriginalRequest = originalRequest;
+ }
+
+ @Override
+ public void onResult(
+ ConnectionRequest request, RemoteConnection remoteConnection) {
+
+ if (remoteConnection != null) {
+ TestConnection connection = new TestConnection(
+ remoteConnection, Connection.State.DIALING);
+ mCalls.add(connection);
+ mCallback.onResult(mOriginalRequest, connection);
+ } else {
+ tryNextSubscription();
+ }
+ }
+
+ @Override
+ public void onError(ConnectionRequest request) {
+ tryNextSubscription();
+ }
+
+ public void tryNextSubscription() {
+ if (mSubscriptionIterator.hasNext()) {
+ ConnectionRequest connectionRequest = new ConnectionRequest(
+ mSubscriptionIterator.next(),
+ mOriginalRequest.getCallId(),
+ mOriginalRequest.getHandle(),
+ null);
+ createRemoteOutgoingConnection(connectionRequest, this);
+ } else {
+ mCallback.onError(mOriginalRequest, 0, null);
+ }
+ }
+ }
+
+ private static final String SCHEME_TEL = "tel";
+
+ private final List<TestConnection> mCalls = new ArrayList<>();
+ private final Handler mHandler = new Handler();
+
+ /** Used to play an audio tone during a call. */
+ private MediaPlayer mMediaPlayer;
+
+ /** {@inheritDoc} */
+ @Override
+ public void onAdapterAttached(CallServiceAdapter callServiceAdapter) {
+ log("onAdapterAttached");
+ mMediaPlayer = createMediaPlayer();
+ super.onAdapterAttached(callServiceAdapter);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean onUnbind(Intent intent) {
+ log("onUnbind");
+ mMediaPlayer = null;
+ return super.onUnbind(intent);
+ }
+
+ private void activateCall(TestConnection connection) {
+ if (!connection.isProxy()) {
+ if (mMediaPlayer != null && !mMediaPlayer.isPlaying()) {
+ mMediaPlayer.start();
+ }
+ }
+ }
+
+ private void destroyCall(TestConnection connection) {
+ mCalls.remove(connection);
+
+ // Stops audio if there are no more calls.
+ if (mCalls.isEmpty() && mMediaPlayer.isPlaying()) {
+ mMediaPlayer.stop();
+ mMediaPlayer.release();
+ mMediaPlayer = createMediaPlayer();
+ }
+ }
+
+ private MediaPlayer createMediaPlayer() {
+ // Prepare the media player to play a tone when there is a call.
+ MediaPlayer mediaPlayer = MediaPlayer.create(getApplicationContext(), R.raw.beep_boop);
+ mediaPlayer.setLooping(true);
+ return mediaPlayer;
+ }
+
+ private static void log(String msg) {
+ Log.w("telecomtestcallservice", "[TestCallService] " + msg);
+ }
+
+ /** ${inheritDoc} */
+ @Override
+ public void onCreateConnections(
+ final ConnectionRequest originalRequest,
+ final Response<ConnectionRequest, Connection> callback) {
+
+ final Uri handle = originalRequest.getHandle();
+ String number = originalRequest.getHandle().getSchemeSpecificPart();
+ log("call, number: " + number);
+
+ // Crash on 555-DEAD to test call service crashing.
+ if ("5550340".equals(number)) {
+ throw new RuntimeException("Goodbye, cruel world.");
+ }
+
+ // Normally we would use the original request as is, but for testing purposes, we are adding
+ // ".." to the end of the number to follow its path more easily through the logs.
+ final ConnectionRequest request = new ConnectionRequest(
+ originalRequest.getCallId(),
+ Uri.fromParts(handle.getScheme(), handle.getSchemeSpecificPart() + "..", ""),
+ originalRequest.getExtras());
+
+ // If the number starts with 555, then we handle it ourselves. If not, then we
+ // use a remote connection service.
+ // TODO(santoscordon): Have a special phone number to test the subscription-picker dialog
+ // flow.
+ if (number.startsWith("555")) {
+ TestConnection connection = new TestConnection(null, Connection.State.DIALING);
+ mCalls.add(connection);
+ callback.onResult(request, connection);
+ connection.startOutgoing();
+ } else {
+ log("looking up subscriptions");
+ lookupRemoteSubscriptions(handle, new SimpleResponse<Uri, List<Subscription>>() {
+ @Override
+ public void onResult(Uri handle, final List<Subscription> subscriptions) {
+ log("starting the call attempter with subscriptions: " + subscriptions);
+ new CallAttempter(subscriptions.iterator(), callback, request)
+ .tryNextSubscription();
+ }
+
+ @Override
+ public void onError(Uri handle) {
+ log("remote subscription lookup failed.");
+ callback.onError(request, 0, null);
+ }
+ });
+ }
+ }
+
+ /** ${inheritDoc} */
+ @Override
+ public void onCreateIncomingConnection(
+ ConnectionRequest request, Response<ConnectionRequest, Connection> callback) {
+
+ // Use dummy number for testing incoming calls.
+ Uri handle = Uri.fromParts(SCHEME_TEL, "5551234", null);
+
+ TestConnection connection = new TestConnection(null, Connection.State.DIALING);
+ mCalls.add(connection);
+ callback.onResult(
+ new ConnectionRequest(handle, request.getExtras()),
+ connection);
+ }
+}