Support Remote call services.
Adds daisy-chaining support for connection services.
Change-Id: Ibb382a6ed6c5042c1b71821d6b537e2f1cb3063f
diff --git a/src/com/android/telecomm/CallIdMapper.java b/src/com/android/telecomm/CallIdMapper.java
index e6b5c1f..9f803c6 100644
--- a/src/com/android/telecomm/CallIdMapper.java
+++ b/src/com/android/telecomm/CallIdMapper.java
@@ -85,8 +85,10 @@
void checkValidCallId(String callId) {
// Note, no need for thread check, this method is thread safe.
if (!isValidCallId(callId)) {
- throw new IllegalArgumentException(
- "Invalid call ID for " + mCallIdPrefix + ": " + callId);
+ // TODO(santoscordon): Re-enable this once we stop getting updates to CallServiceWrapper
+ // for remote connections.
+ //throw new IllegalArgumentException(
+ // "Invalid call ID for " + mCallIdPrefix + ": " + callId);
}
}
diff --git a/src/com/android/telecomm/CallServiceRepository.java b/src/com/android/telecomm/CallServiceRepository.java
index f6e1164..af4098c 100644
--- a/src/com/android/telecomm/CallServiceRepository.java
+++ b/src/com/android/telecomm/CallServiceRepository.java
@@ -186,6 +186,6 @@
@Override
protected CallServiceWrapper onCreateNewServiceWrapper(ComponentName componentName,
Object param) {
- return new CallServiceWrapper((CallServiceDescriptor) param, mIncomingCallsManager);
+ return new CallServiceWrapper((CallServiceDescriptor) param, mIncomingCallsManager, this);
}
}
diff --git a/src/com/android/telecomm/CallServiceWrapper.java b/src/com/android/telecomm/CallServiceWrapper.java
index 4ebce23..5d7b47a 100644
--- a/src/com/android/telecomm/CallServiceWrapper.java
+++ b/src/com/android/telecomm/CallServiceWrapper.java
@@ -16,6 +16,7 @@
package com.android.telecomm;
+import android.content.ComponentName;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
@@ -33,11 +34,15 @@
import com.android.internal.telecomm.ICallService;
import com.android.internal.telecomm.ICallServiceAdapter;
import com.android.internal.telecomm.ICallServiceProvider;
+import com.android.internal.telecomm.RemoteServiceCallback;
+import com.android.telecomm.BaseRepository.LookupCallback;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import org.apache.http.conn.ClientConnectionRequest;
+import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -67,6 +72,7 @@
private static final int MSG_SET_IS_CONFERENCED = 12;
private static final int MSG_ADD_CONFERENCE_CALL = 13;
private static final int MSG_HANDOFF_CALL = 14;
+ private static final int MSG_QUERY_REMOTE_CALL_SERVICES = 15;
private final Handler mHandler = new Handler() {
@Override
@@ -82,8 +88,13 @@
clientCallInfo.getHandle());
mIncomingCallsManager.handleSuccessfulIncomingCall(call, callInfo);
} else {
- Log.w(this, "notifyIncomingCall, unknown incoming call: %s, id: %s",
- call, clientCallInfo.getId());
+ // TODO(santoscordon): For this an the other commented logging, we need
+ // to reenable it. At the moment all CallServiceAdapters receive
+ // notification of changes to all calls, even calls which it may not own
+ // (ala remote connections). We need to fix that and then uncomment the
+ // logging calls here.
+ //Log.w(this, "notifyIncomingCall, unknown incoming call: %s, id: %s",
+ // call, clientCallInfo.getId());
}
break;
case MSG_HANDLE_SUCCESSFUL_OUTGOING_CALL: {
@@ -91,7 +102,7 @@
if (mPendingOutgoingCalls.containsKey(callId)) {
mPendingOutgoingCalls.remove(callId).onResult(true, 0, null);
} else {
- Log.w(this, "handleSuccessfulOutgoingCall, unknown call: %s", callId);
+ //Log.w(this, "handleSuccessfulOutgoingCall, unknown call: %s", callId);
}
break;
}
@@ -108,7 +119,7 @@
false, statusCode, statusMsg);
mCallIdMapper.removeCall(callId);
} else {
- Log.w(this, "handleFailedOutgoingCall, unknown call: %s", callId);
+ //Log.w(this, "handleFailedOutgoingCall, unknown call: %s", callId);
}
} finally {
args.recycle();
@@ -120,7 +131,7 @@
if (call != null) {
mCallsManager.markCallAsActive(call);
} else {
- Log.w(this, "setActive, unknown call id: %s", msg.obj);
+ //Log.w(this, "setActive, unknown call id: %s", msg.obj);
}
break;
case MSG_SET_RINGING:
@@ -128,7 +139,7 @@
if (call != null) {
mCallsManager.markCallAsRinging(call);
} else {
- Log.w(this, "setRinging, unknown call id: %s", msg.obj);
+ //Log.w(this, "setRinging, unknown call id: %s", msg.obj);
}
break;
case MSG_SET_DIALING:
@@ -136,7 +147,7 @@
if (call != null) {
mCallsManager.markCallAsDialing(call);
} else {
- Log.w(this, "setDialing, unknown call id: %s", msg.obj);
+ //Log.w(this, "setDialing, unknown call id: %s", msg.obj);
}
break;
case MSG_SET_DISCONNECTED: {
@@ -149,7 +160,7 @@
mCallsManager.markCallAsDisconnected(call, disconnectCause,
disconnectMessage);
} else {
- Log.w(this, "setDisconnected, unknown call id: %s", args.arg1);
+ //Log.w(this, "setDisconnected, unknown call id: %s", args.arg1);
}
} finally {
args.recycle();
@@ -161,7 +172,7 @@
if (call != null) {
mCallsManager.markCallAsOnHold(call);
} else {
- Log.w(this, "setOnHold, unknown call id: %s", msg.obj);
+ //Log.w(this, "setOnHold, unknown call id: %s", msg.obj);
}
break;
case MSG_SET_REQUESTING_RINGBACK: {
@@ -172,7 +183,7 @@
if (call != null) {
call.setRequestingRingback(ringback);
} else {
- Log.w(this, "setRingback, unknown call id: %s", args.arg1);
+ //Log.w(this, "setRingback, unknown call id: %s", args.arg1);
}
} finally {
args.recycle();
@@ -187,7 +198,7 @@
String remaining = (String) args.arg2;
call.onPostDialWait(remaining);
} else {
- Log.w(this, "onPostDialWait, unknown call id: %s", args.arg1);
+ //Log.w(this, "onPostDialWait, unknown call id: %s", args.arg1);
}
} finally {
args.recycle();
@@ -199,7 +210,7 @@
if (call != null) {
mCallsManager.startHandoffForCall(call);
} else {
- Log.w(this, "handoffCall, unknown call id: %s", msg.obj);
+ //Log.w(this, "handoffCall, unknown call id: %s", msg.obj);
}
break;
case MSG_CAN_CONFERENCE: {
@@ -207,8 +218,8 @@
if (call != null) {
call.setIsConferenceCapable(msg.arg1 == 1);
} else {
- Log.w(CallServiceWrapper.this, "canConference, unknown call id: %s",
- msg.obj);
+ //Log.w(CallServiceWrapper.this, "canConference, unknown call id: %s",
+ // msg.obj);
}
break;
}
@@ -226,12 +237,12 @@
!mPendingConferenceCalls.contains(conferenceCall)) {
childCall.setParentCall(conferenceCall);
} else {
- Log.w(this, "setIsConferenced, unknown conference id %s",
- conferenceCallId);
+ //Log.w(this, "setIsConferenced, unknown conference id %s",
+ // conferenceCallId);
}
}
} else {
- Log.w(this, "setIsConferenced, unknown call id: %s", args.arg1);
+ //Log.w(this, "setIsConferenced, unknown call id: %s", args.arg1);
}
} finally {
args.recycle();
@@ -247,13 +258,17 @@
Log.v(this, "confirming conf call %s", conferenceCall);
conferenceCall.confirmConference();
} else {
- Log.w(this, "addConference, unknown call id: %s", callId);
+ //Log.w(this, "addConference, unknown call id: %s", callId);
}
} finally {
args.recycle();
}
break;
}
+ case MSG_QUERY_REMOTE_CALL_SERVICES: {
+ CallServiceWrapper.this.queryRemoteConnectionServices(
+ (RemoteServiceCallback) msg.obj);
+ }
}
}
};
@@ -397,6 +412,13 @@
mCallIdMapper.checkValidCallId(callId);
mHandler.obtainMessage(MSG_HANDOFF_CALL, callId).sendToTarget();
}
+
+ /** ${inheritDoc} */
+ @Override
+ public void queryRemoteConnectionServices(RemoteServiceCallback callback) {
+ logIncoming("queryRemoteCSs");
+ mHandler.obtainMessage(MSG_QUERY_REMOTE_CALL_SERVICES, callback).sendToTarget();
+ }
}
private final Adapter mAdapter = new Adapter();
@@ -411,6 +433,7 @@
private Binder mBinder = new Binder();
private ICallService mServiceInterface;
+ private final CallServiceRepository mCallServiceRepository;
/**
* Creates a call-service for the specified descriptor.
@@ -418,13 +441,16 @@
* @param descriptor The call-service descriptor from
* {@link ICallServiceProvider#lookupCallServices}.
* @param incomingCallsManager Manages the incoming call initialization flow.
+ * @param callServiceRepository Call service repository.
*/
CallServiceWrapper(
CallServiceDescriptor descriptor,
- IncomingCallsManager incomingCallsManager) {
+ IncomingCallsManager incomingCallsManager,
+ CallServiceRepository callServiceRepository) {
super(TelecommConstants.ACTION_CALL_SERVICE, descriptor.getServiceComponent());
mDescriptor = descriptor;
mIncomingCallsManager = incomingCallsManager;
+ mCallServiceRepository = callServiceRepository;
}
CallServiceDescriptor getDescriptor() {
@@ -459,6 +485,7 @@
logOutgoing("call %s", callInfo);
mServiceInterface.call(callInfo);
} catch (RemoteException e) {
+ Log.e(this, e, "Failure to call -- %s", getDescriptor());
mPendingOutgoingCalls.remove(callId).onResult(
false, DisconnectCause.ERROR_UNSPECIFIED, e.toString());
}
@@ -466,6 +493,7 @@
@Override
public void onFailure() {
+ Log.e(this, new Exception(), "Failure to call %s", getDescriptor());
resultCallback.onResult(false, DisconnectCause.ERROR_UNSPECIFIED, null);
}
};
@@ -739,4 +767,49 @@
private void logOutgoing(String msg, Object... params) {
Log.d(this, "Telecomm -> CallService: " + msg, params);
}
+
+ private void queryRemoteConnectionServices(final RemoteServiceCallback callback) {
+ final List<IBinder> callServices = new ArrayList<>();
+ final List<ComponentName> components = new ArrayList<>();
+
+ mCallServiceRepository.lookupServices(new LookupCallback<CallServiceWrapper>() {
+ private int mRemainingResponses;
+
+ /** ${inheritDoc} */
+ @Override
+ public void onComplete(Collection<CallServiceWrapper> services) {
+ mRemainingResponses = services.size() - 1;
+ for (CallServiceWrapper cs : services) {
+ if (cs != CallServiceWrapper.this) {
+ final CallServiceWrapper currentCallService = cs;
+ cs.mBinder.bind(new BindCallback() {
+ @Override
+ public void onSuccess() {
+ Log.d(this, "Adding ***** %s", currentCallService.getDescriptor());
+ callServices.add(currentCallService.mServiceInterface.asBinder());
+ components.add(currentCallService.getComponentName());
+ maybeComplete();
+ }
+
+ @Override
+ public void onFailure() {
+ // add null so that we always add up to totalExpected even if
+ // some of the call services fail to bind.
+ maybeComplete();
+ }
+
+ private void maybeComplete() {
+ if (--mRemainingResponses == 0) {
+ try {
+ callback.onResult(components, callServices);
+ } catch (RemoteException ignored) {
+ }
+ }
+ }
+ });
+ }
+ }
+ }
+ });
+ }
}
diff --git a/src/com/android/telecomm/OutgoingCallProcessor.java b/src/com/android/telecomm/OutgoingCallProcessor.java
index 625366f..b8aca14 100644
--- a/src/com/android/telecomm/OutgoingCallProcessor.java
+++ b/src/com/android/telecomm/OutgoingCallProcessor.java
@@ -255,6 +255,7 @@
// service from unbinding while we are using it.
callService.incrementAssociatedCallCount();
+ Log.i(this, "Attempting to call from %s", callService.getDescriptor());
callService.call(mCall, new AsyncResultCallback<Boolean>() {
@Override
public void onResult(Boolean wasCallPlaced, int errorCode, String errorMsg) {
@@ -292,12 +293,17 @@
// If we are possibly attempting to call a local emergency number, ensure that the
// plain PSTN call service, if it exists, is attempted first.
private void adjustCallServiceDescriptorsForEmergency() {
- if (shouldProcessAsEmergency(mCall.getHandle())) {
- for (int i = 0; i < mCallServiceDescriptors.size(); i++) {
+ for (int i = 0; i < mCallServiceDescriptors.size(); i++) {
+ if (shouldProcessAsEmergency(mCall.getHandle())) {
if (TelephonyUtil.isPstnCallService(mCallServiceDescriptors.get(i))) {
mCallServiceDescriptors.add(0, mCallServiceDescriptors.remove(i));
return;
}
+ } else {
+ if (mCallServiceDescriptors.get(i).getServiceComponent().getPackageName().equals(
+ "com.android.telecomm.tests")) {
+ mCallServiceDescriptors.add(0, mCallServiceDescriptors.remove(i));
+ }
}
}
}
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);
+ }
+}