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