Further refactor Telecom and add tests

Complete refactoring Telecom to remove singletons from the core code.

Introduce a unit test that simulates a full outgoing phone call from
start to hangup.

Change-Id: I8b09cac6eb9b6aeeb0aeba5d8ae032b4c303c08d
diff --git a/Android.mk b/Android.mk
index 0917793..d3c3316 100644
--- a/Android.mk
+++ b/Android.mk
@@ -12,7 +12,7 @@
 LOCAL_CERTIFICATE := platform
 LOCAL_PRIVILEGED_MODULE := true
 
-LOCAL_PROGUARD_FLAGS := $(proguard.flags)
+LOCAL_PROGUARD_FLAG_FILES := proguard.flags
 
 # Workaround for "local variable type mismatch" error.
 LOCAL_DX_FLAGS += --no-locals
diff --git a/proguard.flags b/proguard.flags
index e52ac20..357336b 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -1,4 +1,8 @@
 -verbose
-
-# Keep @VisibleForTesting elements
 -keep @com.android.internal.annotations.VisibleForTesting class *
+-keep class com.android.server.telecom.TelecomSystem {
+  *;
+}
+-keep class com.android.server.telecom.Log {
+  *;
+}
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 464975f..45072e4 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -94,7 +94,7 @@
         boolean onCanceledViaNewOutgoingCallBroadcast(Call call);
     }
 
-    abstract static class ListenerBase implements Listener {
+    public abstract static class ListenerBase implements Listener {
         @Override
         public void onSuccessfulOutgoingCall(Call call, int callState) {}
         @Override
@@ -327,7 +327,7 @@
      *         one that was registered with the {@link PhoneAccount#CAPABILITY_CALL_PROVIDER} flag.
      * @param isIncoming True if this is an incoming call.
      */
-    Call(
+    public Call(
             Context context,
             CallsManager callsManager,
             ConnectionServiceRepository repository,
@@ -384,11 +384,11 @@
         mConnectTimeMillis = connectTimeMillis;
     }
 
-    void addListener(Listener listener) {
+    public void addListener(Listener listener) {
         mListeners.add(listener);
     }
 
-    void removeListener(Listener listener) {
+    public void removeListener(Listener listener) {
         if (listener != null) {
             mListeners.remove(listener);
         }
@@ -445,7 +445,7 @@
      * misbehave and they do this very often. The result is that we do not enforce state transitions
      * and instead keep the code resilient to unexpected state changes.
      */
-    void setState(int newState) {
+    public void setState(int newState) {
         if (mState != newState) {
             Log.v(this, "setState %s -> %s", mState, newState);
 
@@ -491,7 +491,7 @@
         return mIsConference;
     }
 
-    Uri getHandle() {
+    public Uri getHandle() {
         return mHandle;
     }
 
@@ -504,7 +504,7 @@
         setHandle(handle, TelecomManager.PRESENTATION_ALLOWED);
     }
 
-    void setHandle(Uri handle, int presentation) {
+    public void setHandle(Uri handle, int presentation) {
         if (!Objects.equals(handle, mHandle) || presentation != mHandlePresentation) {
             mHandlePresentation = presentation;
             if (mHandlePresentation == TelecomManager.PRESENTATION_RESTRICTED ||
@@ -548,15 +548,15 @@
         }
     }
 
-    String getName() {
+    public String getName() {
         return mCallerInfo == null ? null : mCallerInfo.name;
     }
 
-    Bitmap getPhotoIcon() {
+    public Bitmap getPhotoIcon() {
         return mCallerInfo == null ? null : mCallerInfo.cachedPhotoIcon;
     }
 
-    Drawable getPhoto() {
+    public Drawable getPhoto() {
         return mCallerInfo == null ? null : mCallerInfo.cachedPhoto;
     }
 
@@ -564,13 +564,13 @@
      * @param disconnectCause The reason for the disconnection, represented by
      *         {@link android.telecom.DisconnectCause}.
      */
-    void setDisconnectCause(DisconnectCause disconnectCause) {
+    public void setDisconnectCause(DisconnectCause disconnectCause) {
         // TODO: Consider combining this method with a setDisconnected() method that is totally
         // separate from setState.
         mDisconnectCause = disconnectCause;
     }
 
-    DisconnectCause getDisconnectCause() {
+    public DisconnectCause getDisconnectCause() {
         return mDisconnectCause;
     }
 
@@ -655,11 +655,11 @@
      * @return The time when this call object was created and added to the set of pending outgoing
      *     calls.
      */
-    long getCreationTimeMillis() {
+    public long getCreationTimeMillis() {
         return mCreationTimeMillis;
     }
 
-    void setCreationTimeMillis(long time) {
+    public void setCreationTimeMillis(long time) {
         mCreationTimeMillis = time;
     }
 
@@ -1321,7 +1321,7 @@
         if (mIsIncoming && isRespondViaSmsCapable() && !mCannedSmsResponsesLoadingStarted) {
             Log.d(this, "maybeLoadCannedSmsResponses: starting task to load messages");
             mCannedSmsResponsesLoadingStarted = true;
-            TelecomSystem.getInstance().getRespondViaSmsManager().loadCannedTextMessages(
+            mCallsManager.getRespondViaSmsManager().loadCannedTextMessages(
                     new Response<Void, List<String>>() {
                         @Override
                         public void onResult(Void request, List<String>... result) {
diff --git a/src/com/android/server/telecom/CallAudioManager.java b/src/com/android/server/telecom/CallAudioManager.java
index 53b6261..3b090a5 100644
--- a/src/com/android/server/telecom/CallAudioManager.java
+++ b/src/com/android/server/telecom/CallAudioManager.java
@@ -37,6 +37,7 @@
     private final AudioManager mAudioManager;
     private final BluetoothManager mBluetoothManager;
     private final WiredHeadsetManager mWiredHeadsetManager;
+    private final CallsManager mCallsManager;
 
     private AudioState mAudioState;
     private int mAudioFocusStreamType;
@@ -45,12 +46,17 @@
     private boolean mWasSpeakerOn;
     private int mMostRecentlyUsedMode = AudioManager.MODE_IN_CALL;
 
-    CallAudioManager(Context context, StatusBarNotifier statusBarNotifier,
-            WiredHeadsetManager wiredHeadsetManager) {
+    CallAudioManager(
+            Context context,
+            StatusBarNotifier statusBarNotifier,
+            WiredHeadsetManager wiredHeadsetManager,
+            CallsManager callsManager) {
         mStatusBarNotifier = statusBarNotifier;
         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
         mBluetoothManager = new BluetoothManager(context, this);
         mWiredHeadsetManager = wiredHeadsetManager;
+        mCallsManager = callsManager;
+
         mWiredHeadsetManager.addListener(this);
 
         saveAudioState(getInitialAudioState(null));
@@ -78,7 +84,7 @@
     public void onCallRemoved(Call call) {
         // If we didn't already have focus, there's nothing to do.
         if (hasFocus()) {
-            if (TelecomSystem.getInstance().getCallsManager().getCalls().isEmpty()) {
+            if (mCallsManager.getCalls().isEmpty()) {
                 Log.v(this, "all calls removed, reseting system audio to default state");
                 setInitialAudioState(null, false /* force */);
                 mWasSpeakerOn = false;
@@ -99,7 +105,7 @@
         // We do two things:
         // (1) If this is the first call, then we can to turn on bluetooth if available.
         // (2) Unmute the audio for the new incoming call.
-        boolean isOnlyCall = TelecomSystem.getInstance().getCallsManager().getCalls().size() == 1;
+        boolean isOnlyCall = mCallsManager.getCalls().size() == 1;
         if (isOnlyCall && mBluetoothManager.isBluetoothAvailable()) {
             mBluetoothManager.connectBluetoothAudio();
             route = AudioState.ROUTE_BLUETOOTH;
@@ -175,7 +181,7 @@
         Log.v(this, "mute, shouldMute: %b", shouldMute);
 
         // Don't mute if there are any emergency calls.
-        if (TelecomSystem.getInstance().getCallsManager().hasEmergencyCall()) {
+        if (mCallsManager.hasEmergencyCall()) {
             shouldMute = false;
             Log.v(this, "ignoring mute for emergency call");
         }
@@ -325,7 +331,7 @@
         }
 
         if (!oldAudioState.equals(mAudioState)) {
-            TelecomSystem.getInstance().getCallsManager()
+            mCallsManager
                     .onAudioStateChanged(oldAudioState, mAudioState);
             updateAudioForForegroundCall();
         }
@@ -360,7 +366,7 @@
             requestAudioFocusAndSetMode(AudioManager.STREAM_RING, AudioManager.MODE_RINGTONE);
         } else {
             Call foregroundCall = getForegroundCall();
-            Call waitingForAccountSelectionCall = TelecomSystem.getInstance().getCallsManager()
+            Call waitingForAccountSelectionCall = mCallsManager
                     .getFirstCallWithState(CallState.PRE_DIAL_WAIT);
             if (foregroundCall != null && waitingForAccountSelectionCall == null) {
                 // In the case where there is a call that is waiting for account selection,
@@ -504,7 +510,7 @@
     }
 
     private void updateAudioForForegroundCall() {
-        Call call = TelecomSystem.getInstance().getCallsManager().getForegroundCall();
+        Call call = mCallsManager.getForegroundCall();
         if (call != null && call.getConnectionService() != null) {
             call.getConnectionService().onAudioStateChanged(call, mAudioState);
         }
@@ -514,7 +520,7 @@
      * Returns the current foreground call in order to properly set the audio mode.
      */
     private Call getForegroundCall() {
-        Call call = TelecomSystem.getInstance().getCallsManager().getForegroundCall();
+        Call call = mCallsManager.getForegroundCall();
 
         // We ignore any foreground call that is in the ringing state because we deal with ringing
         // calls exclusively through the mIsRinging variable set by {@link Ringer}.
@@ -526,7 +532,7 @@
     }
 
     private boolean hasRingingForegroundCall() {
-        Call call = TelecomSystem.getInstance().getCallsManager().getForegroundCall();
+        Call call = mCallsManager.getForegroundCall();
         return call != null && call.getState() == CallState.RINGING;
     }
 
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 2c6109f..e6443c8 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -106,6 +106,7 @@
     private final DtmfLocalTonePlayer mDtmfLocalTonePlayer;
     private final InCallController mInCallController;
     private final CallAudioManager mCallAudioManager;
+    private RespondViaSmsManager mRespondViaSmsManager;
     private final Ringer mRinger;
     private final InCallWakeLockController mInCallWakeLockController;
     // For this set initial table size to 16 because we add 13 listeners in
@@ -139,27 +140,32 @@
     /**
      * Initializes the required Telecom components.
      */
-     CallsManager(Context context, MissedCallNotifier missedCallNotifier,
+     CallsManager(
+             Context context,
+             MissedCallNotifier missedCallNotifier,
              PhoneAccountRegistrar phoneAccountRegistrar,
-             RespondViaSmsManager respondViaSmsManager) {
+             HeadsetMediaButtonFactory headsetMediaButtonFactory,
+             ProximitySensorManagerFactory proximitySensorManagerFactory,
+             InCallWakeLockControllerFactory inCallWakeLockControllerFactory) {
         mContext = context;
         mPhoneAccountRegistrar = phoneAccountRegistrar;
         mMissedCallNotifier = missedCallNotifier;
         StatusBarNotifier statusBarNotifier = new StatusBarNotifier(context, this);
         mWiredHeadsetManager = new WiredHeadsetManager(context);
-        mCallAudioManager = new CallAudioManager(context, statusBarNotifier, mWiredHeadsetManager);
+        mCallAudioManager = new CallAudioManager(
+                context, statusBarNotifier, mWiredHeadsetManager, this);
         InCallTonePlayer.Factory playerFactory = new InCallTonePlayer.Factory(mCallAudioManager);
         mRinger = new Ringer(mCallAudioManager, this, playerFactory, context);
-        mHeadsetMediaButton = new HeadsetMediaButton(context, this);
+        mHeadsetMediaButton = headsetMediaButtonFactory.create(context, this);
         mTtyManager = new TtyManager(context, mWiredHeadsetManager);
-        mProximitySensorManager = new ProximitySensorManager(context);
-        mPhoneStateBroadcaster = new PhoneStateBroadcaster();
+        mProximitySensorManager = proximitySensorManagerFactory.create(context, this);
+        mPhoneStateBroadcaster = new PhoneStateBroadcaster(this);
         mCallLogManager = new CallLogManager(context);
-        mInCallController = new InCallController(context);
+        mInCallController = new InCallController(context, this);
         mDtmfLocalTonePlayer = new DtmfLocalTonePlayer(context);
-        mConnectionServiceRepository = new ConnectionServiceRepository(mPhoneAccountRegistrar,
-                context);
-        mInCallWakeLockController = new InCallWakeLockController(context, this);
+        mConnectionServiceRepository =
+                new ConnectionServiceRepository(mPhoneAccountRegistrar, mContext, this);
+        mInCallWakeLockController = inCallWakeLockControllerFactory.create(context, this);
 
         mListeners.add(statusBarNotifier);
         mListeners.add(mCallLogManager);
@@ -172,10 +178,21 @@
         mListeners.add(missedCallNotifier);
         mListeners.add(mDtmfLocalTonePlayer);
         mListeners.add(mHeadsetMediaButton);
-        mListeners.add(respondViaSmsManager);
         mListeners.add(mProximitySensorManager);
     }
 
+    public void setRespondViaSmsManager(RespondViaSmsManager respondViaSmsManager) {
+        if (mRespondViaSmsManager != null) {
+            mListeners.remove(mRespondViaSmsManager);
+        }
+        mRespondViaSmsManager = respondViaSmsManager;
+        mListeners.add(respondViaSmsManager);
+    }
+
+    public RespondViaSmsManager getRespondViaSmsManager() {
+        return mRespondViaSmsManager;
+    }
+
     @Override
     public void onSuccessfulOutgoingCall(Call call, int callState) {
         Log.v(this, "onSuccessfulOutgoingCall, %s", call);
diff --git a/src/com/android/server/telecom/CallsManagerListenerBase.java b/src/com/android/server/telecom/CallsManagerListenerBase.java
index ffc5947..6b54709 100644
--- a/src/com/android/server/telecom/CallsManagerListenerBase.java
+++ b/src/com/android/server/telecom/CallsManagerListenerBase.java
@@ -21,7 +21,7 @@
 /**
  * Provides a default implementation for listeners of CallsManager.
  */
-class CallsManagerListenerBase implements CallsManager.CallsManagerListener {
+public class CallsManagerListenerBase implements CallsManager.CallsManagerListener {
     @Override
     public void onCallAdded(Call call) {
     }
diff --git a/src/com/android/server/telecom/ConnectionServiceRepository.java b/src/com/android/server/telecom/ConnectionServiceRepository.java
index 5490c2d..d18d051 100644
--- a/src/com/android/server/telecom/ConnectionServiceRepository.java
+++ b/src/com/android/server/telecom/ConnectionServiceRepository.java
@@ -33,6 +33,7 @@
             new HashMap<>();
     private final PhoneAccountRegistrar mPhoneAccountRegistrar;
     private final Context mContext;
+    private final CallsManager mCallsManager;
 
     private final ServiceBinder.Listener<ConnectionServiceWrapper> mUnbindListener =
             new ServiceBinder.Listener<ConnectionServiceWrapper>() {
@@ -42,9 +43,13 @@
                 }
             };
 
-    ConnectionServiceRepository(PhoneAccountRegistrar phoneAccountRegistrar, Context context) {
+    ConnectionServiceRepository(
+            PhoneAccountRegistrar phoneAccountRegistrar,
+            Context context,
+            CallsManager callsManager) {
         mPhoneAccountRegistrar = phoneAccountRegistrar;
         mContext = context;
+        mCallsManager = callsManager;
     }
 
     ConnectionServiceWrapper getService(ComponentName componentName, UserHandle userHandle) {
@@ -55,6 +60,7 @@
                     componentName,
                     this,
                     mPhoneAccountRegistrar,
+                    mCallsManager,
                     mContext,
                     userHandle);
             service.addListener(mUnbindListener);
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index 2f125cf..8255303 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -104,7 +104,7 @@
                 case MSG_SET_ACTIVE:
                     call = mCallIdMapper.getCall(msg.obj);
                     if (call != null) {
-                        TelecomSystem.getInstance().getCallsManager().markCallAsActive(call);
+                        mCallsManager.markCallAsActive(call);
                     } else {
                         //Log.w(this, "setActive, unknown call id: %s", msg.obj);
                     }
@@ -112,7 +112,7 @@
                 case MSG_SET_RINGING:
                     call = mCallIdMapper.getCall(msg.obj);
                     if (call != null) {
-                        TelecomSystem.getInstance().getCallsManager().markCallAsRinging(call);
+                        mCallsManager.markCallAsRinging(call);
                     } else {
                         //Log.w(this, "setRinging, unknown call id: %s", msg.obj);
                     }
@@ -120,7 +120,7 @@
                 case MSG_SET_DIALING:
                     call = mCallIdMapper.getCall(msg.obj);
                     if (call != null) {
-                        TelecomSystem.getInstance().getCallsManager().markCallAsDialing(call);
+                        mCallsManager.markCallAsDialing(call);
                     } else {
                         //Log.w(this, "setDialing, unknown call id: %s", msg.obj);
                     }
@@ -132,7 +132,7 @@
                         DisconnectCause disconnectCause = (DisconnectCause) args.arg2;
                         Log.d(this, "disconnect call %s %s", disconnectCause, call);
                         if (call != null) {
-                            TelecomSystem.getInstance().getCallsManager()
+                            mCallsManager
                                     .markCallAsDisconnected(call, disconnectCause);
                         } else {
                             //Log.w(this, "setDisconnected, unknown call id: %s", args.arg1);
@@ -145,7 +145,7 @@
                 case MSG_SET_ON_HOLD:
                     call = mCallIdMapper.getCall(msg.obj);
                     if (call != null) {
-                        TelecomSystem.getInstance().getCallsManager().markCallAsOnHold(call);
+                        mCallsManager.markCallAsOnHold(call);
                     } else {
                         //Log.w(this, "setOnHold, unknown call id: %s", msg.obj);
                     }
@@ -225,7 +225,7 @@
                                 parcelableConference.getPhoneAccount() != null) {
                             phAcc = parcelableConference.getPhoneAccount();
                         }
-                        Call conferenceCall = TelecomSystem.getInstance().getCallsManager().createConferenceCall(
+                        Call conferenceCall = mCallsManager.createConferenceCall(
                                 phAcc, parcelableConference);
                         mCallIdMapper.addCall(conferenceCall, id);
                         conferenceCall.setConnectionService(ConnectionServiceWrapper.this);
@@ -248,10 +248,10 @@
                     call = mCallIdMapper.getCall(msg.obj);
                     if (call != null) {
                         if (call.isAlive()) {
-                            TelecomSystem.getInstance().getCallsManager().markCallAsDisconnected(
+                            mCallsManager.markCallAsDisconnected(
                                     call, new DisconnectCause(DisconnectCause.REMOTE));
                         } else {
-                            TelecomSystem.getInstance().getCallsManager().markCallAsRemoved(call);
+                            mCallsManager.markCallAsRemoved(call);
                         }
                     }
                     break;
@@ -381,7 +381,7 @@
                     try {
                         String callId = (String)args.arg1;
                         ParcelableConnection connection = (ParcelableConnection)args.arg2;
-                        Call existingCall = TelecomSystem.getInstance().getCallsManager()
+                        Call existingCall = mCallsManager
                                 .createCallForExistingConnection(callId,
                                 connection);
                         mCallIdMapper.addCall(existingCall, callId);
@@ -401,7 +401,7 @@
                 String callId,
                 ConnectionRequest request,
                 ParcelableConnection connection) {
-            logIncoming("handleCreateConnectionComplete %s", request);
+            logIncoming("handleCreateConnectionComplete %s", callId);
             if (mCallIdMapper.isValidCallId(callId)) {
                 SomeArgs args = SomeArgs.obtain();
                 args.arg1 = callId;
@@ -627,6 +627,7 @@
     private IConnectionService mServiceInterface;
     private final ConnectionServiceRepository mConnectionServiceRepository;
     private final PhoneAccountRegistrar mPhoneAccountRegistrar;
+    private final CallsManager mCallsManager;
 
     /**
      * Creates a connection service.
@@ -634,6 +635,7 @@
      * @param componentName The component name of the service with which to bind.
      * @param connectionServiceRepository Connection service repository.
      * @param phoneAccountRegistrar Phone account registrar
+     * @param callsManager Calls manager
      * @param context The context.
      * @param userHandle The {@link UserHandle} to use when binding.
      */
@@ -641,6 +643,7 @@
             ComponentName componentName,
             ConnectionServiceRepository connectionServiceRepository,
             PhoneAccountRegistrar phoneAccountRegistrar,
+            CallsManager callsManager,
             Context context,
             UserHandle userHandle) {
         super(ConnectionService.SERVICE_INTERFACE, componentName, context, userHandle);
@@ -650,6 +653,7 @@
             // To do this, we must proxy remote ConnectionService objects
         });
         mPhoneAccountRegistrar = phoneAccountRegistrar;
+        mCallsManager = callsManager;
     }
 
     /** See {@link IConnectionService#addConnectionServiceAdapter}. */
@@ -715,7 +719,7 @@
         mBinder.bind(callback);
     }
 
-    /** @see ConnectionService#abort(String) */
+    /** @see IConnectionService#abort(String) */
     void abort(Call call) {
         // Clear out any pending outgoing call data
         final String callId = mCallIdMapper.getCallId(call);
@@ -732,7 +736,7 @@
         removeCall(call, new DisconnectCause(DisconnectCause.LOCAL));
     }
 
-    /** @see ConnectionService#hold(String) */
+    /** @see IConnectionService#hold(String) */
     void hold(Call call) {
         final String callId = mCallIdMapper.getCallId(call);
         if (callId != null && isServiceValid("hold")) {
@@ -744,7 +748,7 @@
         }
     }
 
-    /** @see ConnectionService#unhold(String) */
+    /** @see IConnectionService#unhold(String) */
     void unhold(Call call) {
         final String callId = mCallIdMapper.getCallId(call);
         if (callId != null && isServiceValid("unhold")) {
@@ -756,7 +760,7 @@
         }
     }
 
-    /** @see ConnectionService#onAudioStateChanged(String,AudioState) */
+    /** @see IConnectionService#onAudioStateChanged(String,AudioState) */
     void onAudioStateChanged(Call activeCall, AudioState audioState) {
         final String callId = mCallIdMapper.getCallId(activeCall);
         if (callId != null && isServiceValid("onAudioStateChanged")) {
@@ -768,7 +772,7 @@
         }
     }
 
-    /** @see ConnectionService#disconnect(String) */
+    /** @see IConnectionService#disconnect(String) */
     void disconnect(Call call) {
         final String callId = mCallIdMapper.getCallId(call);
         if (callId != null && isServiceValid("disconnect")) {
@@ -780,7 +784,7 @@
         }
     }
 
-    /** @see ConnectionService#answer(String,int) */
+    /** @see IConnectionService#answer(String) */
     void answer(Call call, int videoState) {
         final String callId = mCallIdMapper.getCallId(call);
         if (callId != null && isServiceValid("answer")) {
@@ -796,7 +800,7 @@
         }
     }
 
-    /** @see ConnectionService#reject(String) */
+    /** @see IConnectionService#reject(String) */
     void reject(Call call) {
         final String callId = mCallIdMapper.getCallId(call);
         if (callId != null && isServiceValid("reject")) {
@@ -808,7 +812,7 @@
         }
     }
 
-    /** @see ConnectionService#playDtmfTone(String,char) */
+    /** @see IConnectionService#playDtmfTone(String,char) */
     void playDtmfTone(Call call, char digit) {
         final String callId = mCallIdMapper.getCallId(call);
         if (callId != null && isServiceValid("playDtmfTone")) {
@@ -820,7 +824,7 @@
         }
     }
 
-    /** @see ConnectionService#stopDtmfTone(String) */
+    /** @see IConnectionService#stopDtmfTone(String) */
     void stopDtmfTone(Call call) {
         final String callId = mCallIdMapper.getCallId(call);
         if (callId != null && isServiceValid("stopDtmfTone")) {
@@ -933,7 +937,7 @@
             // outgoing calls to try the next service. This needs to happen before CallsManager
             // tries to clean up any calls still associated with this service.
             handleConnectionServiceDeath();
-            TelecomSystem.getInstance().getCallsManager().handleConnectionServiceDeath(this);
+            mCallsManager.handleConnectionServiceDeath(this);
             mServiceInterface = null;
         } else {
             mServiceInterface = IConnectionService.Stub.asInterface(binder);
@@ -1063,10 +1067,6 @@
     }
 
     private void noRemoteServices(RemoteServiceCallback callback) {
-        try {
-            callback.onResult(Collections.EMPTY_LIST, Collections.EMPTY_LIST);
-        } catch (RemoteException e) {
-            Log.e(this, e, "Contacting ConnectionService %s", this.getComponentName());
-        }
+        setRemoteServices(callback, Collections.EMPTY_LIST, Collections.EMPTY_LIST);
     }
 }
diff --git a/src/com/android/server/telecom/CreateConnectionProcessor.java b/src/com/android/server/telecom/CreateConnectionProcessor.java
index 31114df..1d8ef28 100644
--- a/src/com/android/server/telecom/CreateConnectionProcessor.java
+++ b/src/com/android/server/telecom/CreateConnectionProcessor.java
@@ -102,6 +102,7 @@
     CreateConnectionProcessor(
             Call call, ConnectionServiceRepository repository, CreateConnectionResponse response,
             PhoneAccountRegistrar phoneAccountRegistrar, Context context) {
+        Log.v(this, "CreateConnectionProcessor created for Call = %s", call);
         mCall = call;
         mRepository = repository;
         mResponse = response;
diff --git a/src/com/android/server/telecom/HeadsetMediaButton.java b/src/com/android/server/telecom/HeadsetMediaButton.java
index f0ea1e9..8c1488c 100644
--- a/src/com/android/server/telecom/HeadsetMediaButton.java
+++ b/src/com/android/server/telecom/HeadsetMediaButton.java
@@ -25,7 +25,7 @@
 /**
  * Static class to handle listening to the headset media buttons.
  */
-final class HeadsetMediaButton extends CallsManagerListenerBase {
+public class HeadsetMediaButton extends CallsManagerListenerBase {
 
     // Types of media button presses
     static final int SHORT_PRESS = 1;
@@ -54,7 +54,7 @@
 
     private final MediaSession mSession;
 
-    HeadsetMediaButton(Context context, CallsManager callsManager) {
+    public HeadsetMediaButton(Context context, CallsManager callsManager) {
         mCallsManager = callsManager;
 
         // Create a MediaSession but don't enable it yet. This is a
diff --git a/src/com/android/server/telecom/HeadsetMediaButtonFactory.java b/src/com/android/server/telecom/HeadsetMediaButtonFactory.java
new file mode 100644
index 0000000..becabbf
--- /dev/null
+++ b/src/com/android/server/telecom/HeadsetMediaButtonFactory.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2015, 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.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.content.Context;
+
+/**
+ * This is a TEMPORARY fix to make the {@link HeadsetMediaButton} object injectable for testing.
+ * Class {@link HeadsetMediaButton} itself is not testable because it grabs lots of special stuff
+ * from its {@code Context} that cannot be conveniently mocked.
+ *
+ * TODO: Replace with a better design.
+ */
+public interface HeadsetMediaButtonFactory {
+
+    HeadsetMediaButton create(Context context, CallsManager callsManager);
+}
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
index b846243..9c2efd9 100644
--- a/src/com/android/server/telecom/InCallController.java
+++ b/src/com/android/server/telecom/InCallController.java
@@ -139,9 +139,11 @@
     private final ComponentName mInCallComponentName;
 
     private final Context mContext;
+    private final CallsManager mCallsManager;
 
-    public InCallController(Context context) {
+    public InCallController(Context context, CallsManager callsManager) {
         mContext = context;
+        mCallsManager = callsManager;
         Resources resources = mContext.getResources();
 
         mInCallComponentName = new ComponentName(
@@ -175,7 +177,7 @@
     @Override
     public void onCallRemoved(Call call) {
         Log.i(this, "onCallRemoved: %s", call);
-        if (TelecomSystem.getInstance().getCallsManager().getCalls().isEmpty()) {
+        if (mCallsManager.getCalls().isEmpty()) {
             // TODO: Wait for all messages to be delivered to the service before unbinding.
             unbind();
         }
@@ -357,7 +359,7 @@
         try {
             inCallService.setInCallAdapter(
                     new InCallAdapter(
-                            TelecomSystem.getInstance().getCallsManager(),
+                            mCallsManager,
                             mCallIdMapper));
             mInCallServices.put(componentName, inCallService);
         } catch (RemoteException e) {
@@ -367,25 +369,24 @@
         }
 
         // Upon successful connection, send the state of the world to the service.
-        Collection<Call> calls = TelecomSystem.getInstance().getCallsManager().getCalls();
+        Collection<Call> calls = mCallsManager.getCalls();
         if (!calls.isEmpty()) {
             Log.i(this, "Adding %s calls to InCallService after onConnected: %s", calls.size(),
                     componentName);
             for (Call call : calls) {
                 try {
                     // Track the call if we don't already know about it.
-                    Log.i(this, "addCall after binding: %s", call);
                     addCall(call);
-
-                    inCallService.addCall(toParcelableCall(call,
+                    inCallService.addCall(toParcelableCall(
+                            call,
                             componentName.equals(mInCallComponentName) /* includeVideoProvider */));
                 } catch (RemoteException ignored) {
                 }
             }
             onAudioStateChanged(
                     null,
-                    TelecomSystem.getInstance().getCallsManager().getAudioState());
-            onCanAddCallChanged(TelecomSystem.getInstance().getCallsManager().canAddCall());
+                    mCallsManager.getAudioState());
+            onCanAddCallChanged(mCallsManager.canAddCall());
         } else {
             unbind();
         }
@@ -411,7 +412,7 @@
             // implementations.
             if (disconnectedComponent.equals(mInCallComponentName)) {
                 Log.i(this, "In-call UI %s disconnected.", disconnectedComponent);
-                TelecomSystem.getInstance().getCallsManager().disconnectAllCalls();
+                mCallsManager.disconnectAllCalls();
                 unbind();
             } else {
                 Log.i(this, "In-Call Service %s suddenly disconnected", disconnectedComponent);
@@ -468,7 +469,7 @@
 
         // If this is a single-SIM device, the "default SIM" will always be the only SIM.
         boolean isDefaultSmsAccount =
-                TelecomSystem.getInstance().getCallsManager().getPhoneAccountRegistrar()
+                mCallsManager.getPhoneAccountRegistrar()
                         .isUserSelectedSmsPhoneAccount(call.getTargetPhoneAccount());
         if (call.isRespondViaSmsCapable() && isDefaultSmsAccount) {
             capabilities |= android.telecom.Call.Details.CAPABILITY_RESPOND_VIA_TEXT;
diff --git a/src/com/android/server/telecom/InCallWakeLockControllerFactory.java b/src/com/android/server/telecom/InCallWakeLockControllerFactory.java
new file mode 100644
index 0000000..86335ba
--- /dev/null
+++ b/src/com/android/server/telecom/InCallWakeLockControllerFactory.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2015, 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.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.content.Context;
+
+/**
+ * This is a TEMPORARY fix to make the {@link InCallWakeLockController} object injectable for
+ * testing. Class {@link InCallWakeLockController} itself is not testable because it grabs lots of
+ * special stuff from its {@code Context} that cannot be conveniently mocked.
+ *
+ * TODO: Replace with a better design.
+ */
+public interface InCallWakeLockControllerFactory {
+
+    InCallWakeLockController create(Context context, CallsManager callsManager);
+}
diff --git a/src/com/android/server/telecom/Log.java b/src/com/android/server/telecom/Log.java
index 3ec8267..451e86d 100644
--- a/src/com/android/server/telecom/Log.java
+++ b/src/com/android/server/telecom/Log.java
@@ -25,13 +25,16 @@
 import java.util.IllegalFormatException;
 import java.util.Locale;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 /**
  * Manages logging for the entire module.
  */
+@VisibleForTesting
 public class Log {
 
     // Generic tag for all In Call logging
-    private static final String TAG = "Telecom";
+    private static String TAG = "Telecom";
 
     public static final boolean FORCE_LOGGING = false; /* STOP SHIP if true */
     public static final boolean SYSTRACE_DEBUG = false; /* STOP SHIP if true */
@@ -43,6 +46,11 @@
 
     private Log() {}
 
+    @VisibleForTesting
+    public static void setTag(String tag) {
+        TAG = tag;
+    }
+
     public static boolean isLoggable(int level) {
         return FORCE_LOGGING || android.util.Log.isLoggable(TAG, level);
     }
diff --git a/src/com/android/server/telecom/MissedCallNotifier.java b/src/com/android/server/telecom/MissedCallNotifier.java
index dbbc49e..1dec123 100644
--- a/src/com/android/server/telecom/MissedCallNotifier.java
+++ b/src/com/android/server/telecom/MissedCallNotifier.java
@@ -16,339 +16,14 @@
 
 package com.android.server.telecom;
 
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.app.TaskStackBuilder;
-import android.content.AsyncQueryHandler;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.Intent;
-import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.UserHandle;
-import android.provider.CallLog;
-import android.provider.CallLog.Calls;
-import android.telecom.CallState;
-import android.telecom.DisconnectCause;
-import android.telecom.PhoneAccount;
-import android.telephony.PhoneNumberUtils;
-import android.text.BidiFormatter;
-import android.text.TextDirectionHeuristics;
-import android.text.TextUtils;
-
-// TODO: Needed for move to system service: import com.android.internal.R;
-
 /**
  * Creates a notification for calls that the user missed (neither answered nor rejected).
- * TODO: Make TelephonyManager.clearMissedCalls call into this class.
  */
-class MissedCallNotifier extends CallsManagerListenerBase {
+public interface MissedCallNotifier extends CallsManager.CallsManagerListener {
 
-    private static final String[] CALL_LOG_PROJECTION = new String[] {
-        Calls._ID,
-        Calls.NUMBER,
-        Calls.NUMBER_PRESENTATION,
-        Calls.DATE,
-        Calls.DURATION,
-        Calls.TYPE,
-    };
+    void setCallsManager(CallsManager callsManager);
 
-    private static final int CALL_LOG_COLUMN_ID = 0;
-    private static final int CALL_LOG_COLUMN_NUMBER = 1;
-    private static final int CALL_LOG_COLUMN_NUMBER_PRESENTATION = 2;
-    private static final int CALL_LOG_COLUMN_DATE = 3;
-    private static final int CALL_LOG_COLUMN_DURATION = 4;
-    private static final int CALL_LOG_COLUMN_TYPE = 5;
+    void clearMissedCalls();
 
-    private static final int MISSED_CALL_NOTIFICATION_ID = 1;
-
-    private final Context mContext;
-    private CallsManager mCallsManager;
-    private final NotificationManager mNotificationManager;
-
-    // Used to track the number of missed calls.
-    private int mMissedCallCount = 0;
-
-    MissedCallNotifier(Context context) {
-        mContext = context;
-        mNotificationManager =
-                (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
-
-        updateOnStartup();
-    }
-
-    void setCallsManager(CallsManager callsManager) {
-        this.mCallsManager = callsManager;
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public void onCallStateChanged(Call call, int oldState, int newState) {
-        if (oldState == CallState.RINGING && newState == CallState.DISCONNECTED &&
-                call.getDisconnectCause().getCode() == DisconnectCause.MISSED) {
-            showMissedCallNotification(call);
-        }
-    }
-
-    /** Clears missed call notification and marks the call log's missed calls as read. */
-    void clearMissedCalls() {
-        AsyncTask.execute(new Runnable() {
-            @Override
-            public void run() {
-                // Clear the list of new missed calls from the call log.
-                ContentValues values = new ContentValues();
-                values.put(Calls.NEW, 0);
-                values.put(Calls.IS_READ, 1);
-                StringBuilder where = new StringBuilder();
-                where.append(Calls.NEW);
-                where.append(" = 1 AND ");
-                where.append(Calls.TYPE);
-                where.append(" = ?");
-                mContext.getContentResolver().update(Calls.CONTENT_URI, values, where.toString(),
-                        new String[]{ Integer.toString(Calls.MISSED_TYPE) });
-            }
-        });
-        cancelMissedCallNotification();
-    }
-
-    /**
-     * Create a system notification for the missed call.
-     *
-     * @param call The missed call.
-     */
-    void showMissedCallNotification(Call call) {
-        mMissedCallCount++;
-
-        final int titleResId;
-        final String expandedText;  // The text in the notification's line 1 and 2.
-
-        // Display the first line of the notification:
-        // 1 missed call: <caller name || handle>
-        // More than 1 missed call: <number of calls> + "missed calls"
-        if (mMissedCallCount == 1) {
-            titleResId = R.string.notification_missedCallTitle;
-            expandedText = getNameForCall(call);
-        } else {
-            titleResId = R.string.notification_missedCallsTitle;
-            expandedText =
-                    mContext.getString(R.string.notification_missedCallsMsg, mMissedCallCount);
-        }
-
-        // Create the notification.
-        Notification.Builder builder = new Notification.Builder(mContext);
-        builder.setSmallIcon(android.R.drawable.stat_notify_missed_call)
-                .setColor(mContext.getResources().getColor(R.color.theme_color))
-                .setWhen(call.getCreationTimeMillis())
-                .setContentTitle(mContext.getText(titleResId))
-                .setContentText(expandedText)
-                .setContentIntent(createCallLogPendingIntent())
-                .setAutoCancel(true)
-                .setDeleteIntent(createClearMissedCallsPendingIntent());
-
-        Uri handleUri = call.getHandle();
-        String handle = handleUri == null ? null : handleUri.getSchemeSpecificPart();
-
-        // Add additional actions when there is only 1 missed call, like call-back and SMS.
-        if (mMissedCallCount == 1) {
-            Log.d(this, "Add actions with number %s.", Log.piiHandle(handle));
-
-            if (!TextUtils.isEmpty(handle)
-                    && !TextUtils.equals(handle, mContext.getString(R.string.handle_restricted))) {
-                builder.addAction(R.drawable.stat_sys_phone_call,
-                        mContext.getString(R.string.notification_missedCall_call_back),
-                        createCallBackPendingIntent(handleUri));
-
-                builder.addAction(R.drawable.ic_text_holo_dark,
-                        mContext.getString(R.string.notification_missedCall_message),
-                        createSendSmsFromNotificationPendingIntent(handleUri));
-            }
-
-            Bitmap photoIcon = call.getPhotoIcon();
-            if (photoIcon != null) {
-                builder.setLargeIcon(photoIcon);
-            } else {
-                Drawable photo = call.getPhoto();
-                if (photo != null && photo instanceof BitmapDrawable) {
-                    builder.setLargeIcon(((BitmapDrawable) photo).getBitmap());
-                }
-            }
-        } else {
-            Log.d(this, "Suppress actions. handle: %s, missedCalls: %d.", Log.piiHandle(handle),
-                    mMissedCallCount);
-        }
-
-        Notification notification = builder.build();
-        configureLedOnNotification(notification);
-
-        Log.i(this, "Adding missed call notification for %s.", call);
-        mNotificationManager.notifyAsUser(
-                null /* tag */ , MISSED_CALL_NOTIFICATION_ID, notification, UserHandle.CURRENT);
-    }
-
-    /** Cancels the "missed call" notification. */
-    private void cancelMissedCallNotification() {
-        // Reset the number of missed calls to 0.
-        mMissedCallCount = 0;
-        mNotificationManager.cancel(MISSED_CALL_NOTIFICATION_ID);
-    }
-
-    /**
-     * Returns the name to use in the missed call notification.
-     */
-    private String getNameForCall(Call call) {
-        String handle = call.getHandle() == null ? null : call.getHandle().getSchemeSpecificPart();
-        String name = call.getName();
-
-        if (!TextUtils.isEmpty(name) && TextUtils.isGraphic(name)) {
-            return name;
-        } else if (!TextUtils.isEmpty(handle)) {
-            // A handle should always be displayed LTR using {@link BidiFormatter} regardless of the
-            // content of the rest of the notification.
-            // TODO: Does this apply to SIP addresses?
-            BidiFormatter bidiFormatter = BidiFormatter.getInstance();
-            return bidiFormatter.unicodeWrap(handle, TextDirectionHeuristics.LTR);
-        } else {
-            // Use "unknown" if the call is unidentifiable.
-            return mContext.getString(R.string.unknown);
-        }
-    }
-
-    /**
-     * Creates a new pending intent that sends the user to the call log.
-     *
-     * @return The pending intent.
-     */
-    private PendingIntent createCallLogPendingIntent() {
-        Intent intent = new Intent(Intent.ACTION_VIEW, null);
-        intent.setType(CallLog.Calls.CONTENT_TYPE);
-
-        TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(mContext);
-        taskStackBuilder.addNextIntent(intent);
-
-        return taskStackBuilder.getPendingIntent(0, 0);
-    }
-
-    /**
-     * Creates an intent to be invoked when the missed call notification is cleared.
-     */
-    private PendingIntent createClearMissedCallsPendingIntent() {
-        return createTelecomPendingIntent(
-                TelecomBroadcastIntentProcessor.ACTION_CLEAR_MISSED_CALLS, null);
-    }
-
-    /**
-     * Creates an intent to be invoked when the user opts to "call back" from the missed call
-     * notification.
-     *
-     * @param handle The handle to call back.
-     */
-    private PendingIntent createCallBackPendingIntent(Uri handle) {
-        return createTelecomPendingIntent(
-                TelecomBroadcastIntentProcessor.ACTION_CALL_BACK_FROM_NOTIFICATION, handle);
-    }
-
-    /**
-     * Creates an intent to be invoked when the user opts to "send sms" from the missed call
-     * notification.
-     */
-    private PendingIntent createSendSmsFromNotificationPendingIntent(Uri handle) {
-        return createTelecomPendingIntent(
-                TelecomBroadcastIntentProcessor.ACTION_SEND_SMS_FROM_NOTIFICATION,
-                Uri.fromParts(Constants.SCHEME_SMSTO, handle.getSchemeSpecificPart(), null));
-    }
-
-    /**
-     * Creates generic pending intent from the specified parameters to be received by
-     * {@link TelecomBroadcastIntentProcessor}.
-     *
-     * @param action The intent action.
-     * @param data The intent data.
-     */
-    private PendingIntent createTelecomPendingIntent(String action, Uri data) {
-        Intent intent = new Intent(action, data, mContext, TelecomBroadcastIntentProcessor.class);
-        return PendingIntent.getBroadcast(mContext, 0, intent, 0);
-    }
-
-    /**
-     * Configures a notification to emit the blinky notification light.
-     */
-    private void configureLedOnNotification(Notification notification) {
-        notification.flags |= Notification.FLAG_SHOW_LIGHTS;
-        notification.defaults |= Notification.DEFAULT_LIGHTS;
-    }
-
-    /**
-     * Adds the missed call notification on startup if there are unread missed calls.
-     */
-    private void updateOnStartup() {
-        Log.d(this, "updateOnStartup()...");
-
-        // instantiate query handler
-        AsyncQueryHandler queryHandler = new AsyncQueryHandler(mContext.getContentResolver()) {
-            @Override
-            protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
-                Log.d(MissedCallNotifier.this, "onQueryComplete()...");
-                if (cursor != null) {
-                    try {
-                        while (cursor.moveToNext()) {
-                            // Get data about the missed call from the cursor
-                            final String handleString = cursor.getString(CALL_LOG_COLUMN_NUMBER);
-                            final int presentation =
-                                    cursor.getInt(CALL_LOG_COLUMN_NUMBER_PRESENTATION);
-                            final long date = cursor.getLong(CALL_LOG_COLUMN_DATE);
-
-                            final Uri handle;
-                            if (presentation != Calls.PRESENTATION_ALLOWED
-                                    || TextUtils.isEmpty(handleString)) {
-                                handle = null;
-                            } else {
-                                handle = Uri.fromParts(PhoneNumberUtils.isUriNumber(handleString) ?
-                                        PhoneAccount.SCHEME_SIP : PhoneAccount.SCHEME_TEL,
-                                                handleString, null);
-                            }
-
-                            // Convert the data to a call object
-                            Call call = new Call(mContext, mCallsManager,
-                                    null, null, null, null, null, true,
-                                    false);
-                            call.setDisconnectCause(new DisconnectCause(DisconnectCause.MISSED));
-                            call.setState(CallState.DISCONNECTED);
-                            call.setCreationTimeMillis(date);
-
-                            // Listen for the update to the caller information before posting the
-                            // notification so that we have the contact info and photo.
-                            call.addListener(new Call.ListenerBase() {
-                                @Override
-                                public void onCallerInfoChanged(Call call) {
-                                    call.removeListener(this);  // No longer need to listen to call
-                                                                // changes after the contact info
-                                                                // is retrieved.
-                                    showMissedCallNotification(call);
-                                }
-                            });
-                            // Set the handle here because that is what triggers the contact info
-                            // query.
-                            call.setHandle(handle, presentation);
-                        }
-                    } finally {
-                        cursor.close();
-                    }
-                }
-            }
-        };
-
-        // setup query spec, look for all Missed calls that are new.
-        StringBuilder where = new StringBuilder("type=");
-        where.append(Calls.MISSED_TYPE);
-        where.append(" AND new=1");
-
-        // start the query
-        queryHandler.startQuery(0, null, Calls.CONTENT_URI, CALL_LOG_PROJECTION,
-                where.toString(), null, Calls.DEFAULT_SORT_ORDER);
-    }
+    void showMissedCallNotification(Call call);
 }
diff --git a/src/com/android/server/telecom/PhoneStateBroadcaster.java b/src/com/android/server/telecom/PhoneStateBroadcaster.java
index 1a1f427..bf0d3b8 100644
--- a/src/com/android/server/telecom/PhoneStateBroadcaster.java
+++ b/src/com/android/server/telecom/PhoneStateBroadcaster.java
@@ -29,10 +29,12 @@
  */
 final class PhoneStateBroadcaster extends CallsManagerListenerBase {
 
+    private final CallsManager mCallsManager;
     private final ITelephonyRegistry mRegistry;
     private int mCurrentState = TelephonyManager.CALL_STATE_IDLE;
 
-    public PhoneStateBroadcaster() {
+    public PhoneStateBroadcaster(CallsManager callsManager) {
+        mCallsManager = callsManager;
         mRegistry = ITelephonyRegistry.Stub.asInterface(ServiceManager.getService(
                 "telephony.registry"));
         if (mRegistry == null) {
@@ -44,7 +46,7 @@
     public void onCallStateChanged(Call call, int oldState, int newState) {
         if ((newState == CallState.DIALING || newState == CallState.ACTIVE
                 || newState == CallState.ON_HOLD) &&
-                !TelecomSystem.getInstance().getCallsManager().hasRingingCall()) {
+                !mCallsManager.hasRingingCall()) {
             /*
              * EXTRA_STATE_RINGING takes precedence over EXTRA_STATE_OFFHOOK, so if there is
              * already a ringing call, don't broadcast EXTRA_STATE_OFFHOOK.
@@ -64,11 +66,10 @@
     public void onCallRemoved(Call call) {
         // Recalculate the current phone state based on the consolidated state of the remaining
         // calls in the call list.
-        final CallsManager callsManager = TelecomSystem.getInstance().getCallsManager();
         int callState = TelephonyManager.CALL_STATE_IDLE;
-        if (callsManager.hasRingingCall()) {
+        if (mCallsManager.hasRingingCall()) {
             callState = TelephonyManager.CALL_STATE_RINGING;
-        } else if (callsManager.getFirstCallWithState(CallState.DIALING, CallState.ACTIVE,
+        } else if (mCallsManager.getFirstCallWithState(CallState.DIALING, CallState.ACTIVE,
                     CallState.ON_HOLD) != null) {
             callState = TelephonyManager.CALL_STATE_OFFHOOK;
         }
diff --git a/src/com/android/server/telecom/ProximitySensorManager.java b/src/com/android/server/telecom/ProximitySensorManager.java
index d18e4ac..5fddb89 100644
--- a/src/com/android/server/telecom/ProximitySensorManager.java
+++ b/src/com/android/server/telecom/ProximitySensorManager.java
@@ -26,8 +26,9 @@
     private static final String TAG = ProximitySensorManager.class.getSimpleName();
 
     private final PowerManager.WakeLock mProximityWakeLock;
+    private final CallsManager mCallsManager;
 
-    public ProximitySensorManager(Context context) {
+    public ProximitySensorManager(Context context, CallsManager callsManager) {
         PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
 
         if (pm.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
@@ -36,12 +37,14 @@
         } else {
             mProximityWakeLock = null;
         }
+
+        mCallsManager = callsManager;
         Log.d(this, "onCreate: mProximityWakeLock: ", mProximityWakeLock);
     }
 
     @Override
     public void onCallRemoved(Call call) {
-        if (TelecomSystem.getInstance().getCallsManager().getCalls().isEmpty()) {
+        if (mCallsManager.getCalls().isEmpty()) {
             Log.i(this, "All calls removed, resetting proximity sensor to default state");
             turnOff(true);
         }
@@ -52,7 +55,7 @@
      * Turn the proximity sensor on.
      */
     void turnOn() {
-        if (TelecomSystem.getInstance().getCallsManager().getCalls().isEmpty()) {
+        if (mCallsManager.getCalls().isEmpty()) {
             Log.w(this, "Asking to turn on prox sensor without a call? I don't think so.");
             return;
         }
diff --git a/src/com/android/server/telecom/ProximitySensorManagerFactory.java b/src/com/android/server/telecom/ProximitySensorManagerFactory.java
new file mode 100644
index 0000000..b73636f
--- /dev/null
+++ b/src/com/android/server/telecom/ProximitySensorManagerFactory.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2015, 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.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.content.Context;
+
+/**
+ * This is a TEMPORARY fix to make the {@link ProximitySensorManager} object injectable for testing.
+ * Class {@link ProximitySensorManager} itself is not testable because it grabs lots of special
+ * stuff from its {@code Context} that cannot be conveniently mocked.
+ *
+ * TODO: Replace with a better design.
+ */
+public interface ProximitySensorManagerFactory {
+
+    ProximitySensorManager create(Context context, CallsManager callsManager);
+
+}
diff --git a/src/com/android/server/telecom/RespondViaSmsManager.java b/src/com/android/server/telecom/RespondViaSmsManager.java
index 3a3432e..874ca4c 100644
--- a/src/com/android/server/telecom/RespondViaSmsManager.java
+++ b/src/com/android/server/telecom/RespondViaSmsManager.java
@@ -44,6 +44,8 @@
     private static final int MSG_CANNED_TEXT_MESSAGES_READY = 1;
     private static final int MSG_SHOW_SENT_TOAST = 2;
 
+    private final CallsManager mCallsManager;
+
     private final Handler mHandler = new Handler() {
         @Override
         public void handleMessage(Message msg) {
@@ -80,7 +82,9 @@
         }
     };
 
-    public RespondViaSmsManager() {}
+    public RespondViaSmsManager(CallsManager callsManager) {
+        mCallsManager = callsManager;
+    }
 
     /**
      * Read the (customizable) canned responses from SharedPreferences,
@@ -139,9 +143,7 @@
     @Override
     public void onIncomingCallRejected(Call call, boolean rejectWithMessage, String textMessage) {
         if (rejectWithMessage && call.getHandle() != null) {
-            PhoneAccountRegistrar phoneAccountRegistrar =
-                    TelecomSystem.getInstance().getCallsManager().getPhoneAccountRegistrar();
-            int subId = phoneAccountRegistrar.getSubscriptionIdForPhoneAccount(
+            int subId = mCallsManager.getPhoneAccountRegistrar().getSubscriptionIdForPhoneAccount(
                     call.getTargetPhoneAccount());
             rejectCallWithMessage(call.getContext(), call.getHandle().getSchemeSpecificPart(),
                     textMessage, subId);
diff --git a/src/com/android/server/telecom/ServiceBinder.java b/src/com/android/server/telecom/ServiceBinder.java
index 8baf3ec..a4bcab0 100644
--- a/src/com/android/server/telecom/ServiceBinder.java
+++ b/src/com/android/server/telecom/ServiceBinder.java
@@ -21,8 +21,6 @@
 import android.content.Intent;
 import android.content.ServiceConnection;
 import android.os.IBinder;
-import android.os.IInterface;
-import android.os.Process;
 import android.os.UserHandle;
 import android.text.TextUtils;
 import android.util.ArraySet;
diff --git a/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java b/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java
index 3110d46..2b22ea9 100644
--- a/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java
+++ b/src/com/android/server/telecom/TelecomBroadcastIntentProcessor.java
@@ -20,27 +20,25 @@
 import android.content.Intent;
 import android.os.UserHandle;
 
-/**
- * Handles miscellaneous Telecom broadcast intents. This should be visible from outside, but
- * should not be in the "exported" state.
- */
 public final class TelecomBroadcastIntentProcessor {
     /** The action used to send SMS response for the missed call notification. */
-    static final String ACTION_SEND_SMS_FROM_NOTIFICATION =
+    public static final String ACTION_SEND_SMS_FROM_NOTIFICATION =
             "com.android.server.telecom.ACTION_SEND_SMS_FROM_NOTIFICATION";
 
     /** The action used to call a handle back for the missed call notification. */
-    static final String ACTION_CALL_BACK_FROM_NOTIFICATION =
+    public static final String ACTION_CALL_BACK_FROM_NOTIFICATION =
             "com.android.server.telecom.ACTION_CALL_BACK_FROM_NOTIFICATION";
 
     /** The action used to clear missed calls. */
-    static final String ACTION_CLEAR_MISSED_CALLS =
+    public static final String ACTION_CLEAR_MISSED_CALLS =
             "com.android.server.telecom.ACTION_CLEAR_MISSED_CALLS";
 
     private final Context mContext;
+    private final CallsManager mCallsManager;
 
-    public TelecomBroadcastIntentProcessor(Context context) {
+    public TelecomBroadcastIntentProcessor(Context context, CallsManager callsManager) {
         mContext = context;
+        mCallsManager = callsManager;
     }
 
     public void processIntent(Intent intent) {
@@ -48,8 +46,7 @@
 
         Log.v(this, "Action received: %s.", action);
 
-        MissedCallNotifier missedCallNotifier = TelecomSystem.getInstance()
-                .getCallsManager().getMissedCallNotifier();
+        MissedCallNotifier missedCallNotifier = mCallsManager.getMissedCallNotifier();
 
         // Send an SMS from the missed call notification.
         if (ACTION_SEND_SMS_FROM_NOTIFICATION.equals(action)) {
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index 476411c..9c0b707 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -84,10 +84,10 @@
                 Object result = null;
                 switch (msg.what) {
                     case MSG_SILENCE_RINGER:
-                        TelecomSystem.getInstance().getCallsManager().getRinger().silence();
+                        mCallsManager.getRinger().silence();
                         break;
                     case MSG_SHOW_CALL_SCREEN:
-                        TelecomSystem.getInstance().getCallsManager().getInCallController().bringToForeground(msg.arg1 == 1);
+                        mCallsManager.getInCallController().bringToForeground(msg.arg1 == 1);
                         break;
                     case MSG_END_CALL:
                         result = endCallInternal();
@@ -96,13 +96,13 @@
                         acceptRingingCallInternal();
                         break;
                     case MSG_CANCEL_MISSED_CALLS_NOTIFICATION:
-                        TelecomSystem.getInstance().getMissedCallNotifier().clearMissedCalls();
+                        mCallsManager.getMissedCallNotifier().clearMissedCalls();
                         break;
                     case MSG_IS_TTY_SUPPORTED:
-                        result = TelecomSystem.getInstance().getCallsManager().isTtySupported();
+                        result = mCallsManager.isTtySupported();
                         break;
                     case MSG_GET_CURRENT_TTY_MODE:
-                        result = TelecomSystem.getInstance().getCallsManager().getCurrentTtyMode();
+                        result = mCallsManager.getCurrentTtyMode();
                         break;
                     case MSG_NEW_INCOMING_CALL:
                         if (request.arg == null || !(request.arg instanceof Intent)) {
@@ -140,13 +140,17 @@
 
     private final MainThreadHandler mMainThreadHandler = new MainThreadHandler();
 
-    private AppOpsManager mAppOpsManager;
-    private UserManager mUserManager;
-    private PackageManager mPackageManager;
-    private TelecomBinderImpl mBinderImpl;
-    private CallsManager mCallsManager;
+    private final AppOpsManager mAppOpsManager;
+    private final UserManager mUserManager;
+    private final PackageManager mPackageManager;
+    private final TelecomBinderImpl mBinderImpl;
+    private final CallsManager mCallsManager;
+    private final PhoneAccountRegistrar mPhoneAccountRegistrar;
 
-    public TelecomServiceImpl(Context context, CallsManager callsManager) {
+    public TelecomServiceImpl(
+            Context context,
+            CallsManager callsManager,
+            PhoneAccountRegistrar phoneAccountRegistrar) {
         mContext = context;
         mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
         mBinderImpl = new TelecomBinderImpl();
@@ -155,6 +159,7 @@
         mPackageManager = mContext.getPackageManager();
 
         mCallsManager = callsManager;
+        mPhoneAccountRegistrar = phoneAccountRegistrar;
     }
 
     public IBinder getBinder() {
@@ -172,7 +177,7 @@
             long token = Binder.clearCallingIdentity();
             try {
                 PhoneAccountHandle defaultOutgoingPhoneAccount =
-                        TelecomSystem.getInstance().getPhoneAccountRegistrar().getDefaultOutgoingPhoneAccount(uriScheme);
+                        mCallsManager.getPhoneAccountRegistrar().getDefaultOutgoingPhoneAccount(uriScheme);
                 // Make sure that the calling user can see this phone account.
                 if (defaultOutgoingPhoneAccount != null
                         && !isVisibleToCaller(defaultOutgoingPhoneAccount)) {
@@ -192,7 +197,7 @@
         public PhoneAccountHandle getUserSelectedOutgoingPhoneAccount() {
             try {
                 PhoneAccountHandle userSelectedOutgoingPhoneAccount =
-                        TelecomSystem.getInstance().getPhoneAccountRegistrar().getUserSelectedOutgoingPhoneAccount();
+                        mCallsManager.getPhoneAccountRegistrar().getUserSelectedOutgoingPhoneAccount();
                 // Make sure that the calling user can see this phone account.
                 if (!isVisibleToCaller(userSelectedOutgoingPhoneAccount)) {
                     Log.w(this, "No account found for the calling user");
@@ -210,7 +215,7 @@
             enforceModifyPermission();
 
             try {
-                TelecomSystem.getInstance().getPhoneAccountRegistrar().setUserSelectedOutgoingPhoneAccount(accountHandle);
+                mCallsManager.getPhoneAccountRegistrar().setUserSelectedOutgoingPhoneAccount(accountHandle);
             } catch (Exception e) {
                 Log.e(this, e, "setUserSelectedOutgoingPhoneAccount");
                 throw e;
@@ -223,7 +228,7 @@
             long token = Binder.clearCallingIdentity();
             try {
                 return filterForAccountsVisibleToCaller(
-                        TelecomSystem.getInstance().getPhoneAccountRegistrar().getCallCapablePhoneAccounts());
+                        mCallsManager.getPhoneAccountRegistrar().getCallCapablePhoneAccounts());
             } catch (Exception e) {
                 Log.e(this, e, "getCallCapablePhoneAccounts");
                 throw e;
@@ -238,7 +243,7 @@
             long token = Binder.clearCallingIdentity();
             try {
                 return filterForAccountsVisibleToCaller(
-                        TelecomSystem.getInstance().getPhoneAccountRegistrar().getCallCapablePhoneAccounts(uriScheme));
+                        mCallsManager.getPhoneAccountRegistrar().getCallCapablePhoneAccounts(uriScheme));
             } catch (Exception e) {
                 Log.e(this, e, "getPhoneAccountsSupportingScheme %s", uriScheme);
                 throw e;
@@ -251,7 +256,7 @@
         public List<PhoneAccountHandle> getPhoneAccountsForPackage(String packageName) {
             try {
                 return filterForAccountsVisibleToCaller(
-                        TelecomSystem.getInstance().getPhoneAccountRegistrar().getPhoneAccountsForPackage(packageName));
+                        mCallsManager.getPhoneAccountRegistrar().getPhoneAccountsForPackage(packageName));
             } catch (Exception e) {
                 Log.e(this, e, "getPhoneAccountsForPackage %s", packageName);
                 throw e;
@@ -265,7 +270,7 @@
                     Log.w(this, "%s is not visible for the calling user", accountHandle);
                     return null;
                 }
-                return TelecomSystem.getInstance().getPhoneAccountRegistrar().getPhoneAccountInternal(accountHandle);
+                return mCallsManager.getPhoneAccountRegistrar().getPhoneAccountInternal(accountHandle);
             } catch (Exception e) {
                 Log.e(this, e, "getPhoneAccount %s", accountHandle);
                 throw e;
@@ -286,7 +291,7 @@
         @Override
         public List<PhoneAccount> getAllPhoneAccounts() {
             try {
-                List<PhoneAccount> allPhoneAccounts = TelecomSystem.getInstance().getPhoneAccountRegistrar().getAllPhoneAccounts();
+                List<PhoneAccount> allPhoneAccounts = mCallsManager.getPhoneAccountRegistrar().getAllPhoneAccounts();
                 List<PhoneAccount> profilePhoneAccounts = new ArrayList<>(allPhoneAccounts.size());
                 for (PhoneAccount phoneAccount : profilePhoneAccounts) {
                     if (isVisibleToCaller(phoneAccount)) {
@@ -304,7 +309,7 @@
         public List<PhoneAccountHandle> getAllPhoneAccountHandles() {
             try {
                 return filterForAccountsVisibleToCaller(
-                        TelecomSystem.getInstance().getPhoneAccountRegistrar().getAllPhoneAccountHandles());
+                        mCallsManager.getPhoneAccountRegistrar().getAllPhoneAccountHandles());
             } catch (Exception e) {
                 Log.e(this, e, "getAllPhoneAccounts");
                 throw e;
@@ -314,7 +319,7 @@
         @Override
         public PhoneAccountHandle getSimCallManager() {
             try {
-                PhoneAccountHandle accountHandle = TelecomSystem.getInstance().getPhoneAccountRegistrar().getSimCallManager();
+                PhoneAccountHandle accountHandle = mCallsManager.getPhoneAccountRegistrar().getSimCallManager();
                 if (!isVisibleToCaller(accountHandle)) {
                     Log.w(this, "%s is not visible for the calling user", accountHandle);
                     return null;
@@ -331,7 +336,7 @@
             enforceModifyPermission();
 
             try {
-                TelecomSystem.getInstance().getPhoneAccountRegistrar().setSimCallManager(accountHandle);
+                mCallsManager.getPhoneAccountRegistrar().setSimCallManager(accountHandle);
             } catch (Exception e) {
                 Log.e(this, e, "setSimCallManager");
                 throw e;
@@ -344,7 +349,7 @@
             long token = Binder.clearCallingIdentity();
             try {
                 return filterForAccountsVisibleToCaller(
-                        TelecomSystem.getInstance().getPhoneAccountRegistrar().getConnectionManagerPhoneAccounts());
+                        mCallsManager.getPhoneAccountRegistrar().getConnectionManagerPhoneAccounts());
             } catch (Exception e) {
                 Log.e(this, e, "getSimCallManagers");
                 throw e;
@@ -372,7 +377,7 @@
                 }
                 enforceUserHandleMatchesCaller(account.getAccountHandle());
 
-                TelecomSystem.getInstance().getPhoneAccountRegistrar().registerPhoneAccount(account);
+                mPhoneAccountRegistrar.registerPhoneAccount(account);
 
                 // Broadcast an intent indicating the phone account which was registered.
                 long token = Binder.clearCallingIdentity();
@@ -395,7 +400,7 @@
                 enforcePhoneAccountModificationForPackage(
                         accountHandle.getComponentName().getPackageName());
                 enforceUserHandleMatchesCaller(accountHandle);
-                TelecomSystem.getInstance().getPhoneAccountRegistrar().unregisterPhoneAccount(accountHandle);
+                mCallsManager.getPhoneAccountRegistrar().unregisterPhoneAccount(accountHandle);
             } catch (Exception e) {
                 Log.e(this, e, "unregisterPhoneAccount %s", accountHandle);
                 throw e;
@@ -406,7 +411,7 @@
         public void clearAccounts(String packageName) {
             try {
                 enforcePhoneAccountModificationForPackage(packageName);
-                TelecomSystem.getInstance().getPhoneAccountRegistrar().clearAccounts(packageName, Binder.getCallingUserHandle());
+                mCallsManager.getPhoneAccountRegistrar().clearAccounts(packageName, Binder.getCallingUserHandle());
             } catch (Exception e) {
                 Log.e(this, e, "clearAccounts %s", packageName);
                 throw e;
@@ -424,7 +429,7 @@
                     Log.w(this, "%s is not visible for the calling user", accountHandle);
                     return false;
                 }
-                return TelecomSystem.getInstance().getPhoneAccountRegistrar().isVoiceMailNumber(accountHandle, number);
+                return mCallsManager.getPhoneAccountRegistrar().isVoiceMailNumber(accountHandle, number);
             } catch (Exception e) {
                 Log.e(this, e, "getSubscriptionIdForPhoneAccount");
                 throw e;
@@ -445,7 +450,7 @@
 
                 int subId = SubscriptionManager.getDefaultVoiceSubId();
                 if (accountHandle != null) {
-                    subId = TelecomSystem.getInstance().getPhoneAccountRegistrar().getSubscriptionIdForPhoneAccount(accountHandle);
+                    subId = mCallsManager.getPhoneAccountRegistrar().getSubscriptionIdForPhoneAccount(accountHandle);
                 }
                 return !TextUtils.isEmpty(getTelephonyManager().getVoiceMailNumber(subId));
             } catch (Exception e) {
@@ -502,7 +507,7 @@
             enforceReadPermission();
             // Do not use sendRequest() with this method since it could cause a deadlock with
             // audio service, which we call into from the main thread: AudioManager.setMode().
-            final int callState = TelecomSystem.getInstance().getCallsManager().getCallState();
+            final int callState = mCallsManager.getCallState();
             return callState == TelephonyManager.CALL_STATE_OFFHOOK
                     || callState == TelephonyManager.CALL_STATE_RINGING;
         }
@@ -513,7 +518,7 @@
         @Override
         public boolean isRinging() {
             enforceReadPermission();
-            return TelecomSystem.getInstance().getCallsManager().getCallState() == TelephonyManager.CALL_STATE_RINGING;
+            return mCallsManager.getCallState() == TelephonyManager.CALL_STATE_RINGING;
         }
 
         /**
@@ -521,7 +526,7 @@
          */
         @Override
         public int getCallState() {
-            return TelecomSystem.getInstance().getCallsManager().getCallState();
+            return mCallsManager.getCallState();
         }
 
         /**
@@ -596,7 +601,8 @@
             long token = Binder.clearCallingIdentity();
             boolean retval = false;
             try {
-                int subId = TelecomSystem.getInstance().getPhoneAccountRegistrar().getSubscriptionIdForPhoneAccount(accountHandle);
+                int subId = mCallsManager.getPhoneAccountRegistrar()
+                        .getSubscriptionIdForPhoneAccount(accountHandle);
                 retval = getTelephonyManager().handlePinMmiForSubscriber(subId, dialString);
             } finally {
                 Binder.restoreCallingIdentity(token);
@@ -621,7 +627,8 @@
             long token = Binder.clearCallingIdentity();
             String retval = "content://icc/adn/";
             try {
-                long subId = TelecomSystem.getInstance().getPhoneAccountRegistrar().getSubscriptionIdForPhoneAccount(accountHandle);
+                long subId = mCallsManager.getPhoneAccountRegistrar()
+                        .getSubscriptionIdForPhoneAccount(accountHandle);
                 retval = retval + "subId/" + subId;
             } finally {
                 Binder.restoreCallingIdentity(token);
@@ -723,15 +730,15 @@
             }
 
             final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");
-            if (TelecomSystem.getInstance().getCallsManager() != null) {
+            if (mCallsManager != null) {
                 pw.println("CallsManager: ");
                 pw.increaseIndent();
-                TelecomSystem.getInstance().getCallsManager().dump(pw);
+                mCallsManager.dump(pw);
                 pw.decreaseIndent();
 
                 pw.println("PhoneAccountRegistrar: ");
                 pw.increaseIndent();
-                TelecomSystem.getInstance().getPhoneAccountRegistrar().dump(pw);
+                mCallsManager.getPhoneAccountRegistrar().dump(pw);
                 pw.decreaseIndent();
             }
         }
@@ -746,7 +753,8 @@
             return false;
         }
 
-        return isVisibleToCaller(TelecomSystem.getInstance().getPhoneAccountRegistrar().getPhoneAccountInternal(accountHandle));
+        return isVisibleToCaller(mCallsManager.getPhoneAccountRegistrar()
+                .getPhoneAccountInternal(accountHandle));
     }
 
     private boolean isVisibleToCaller(PhoneAccount account) {
@@ -825,7 +833,7 @@
     }
 
     private void acceptRingingCallInternal() {
-        Call call = TelecomSystem.getInstance().getCallsManager().getFirstCallWithState(CallState.RINGING);
+        Call call = mCallsManager.getFirstCallWithState(CallState.RINGING);
         if (call != null) {
             call.answer(call.getVideoState());
         }
@@ -834,9 +842,9 @@
     private boolean endCallInternal() {
         // Always operate on the foreground call if one exists, otherwise get the first call in
         // priority order by call-state.
-        Call call = TelecomSystem.getInstance().getCallsManager().getForegroundCall();
+        Call call = mCallsManager.getForegroundCall();
         if (call == null) {
-            call = TelecomSystem.getInstance().getCallsManager().getFirstCallWithState(
+            call = mCallsManager.getFirstCallWithState(
                     CallState.ACTIVE,
                     CallState.DIALING,
                     CallState.RINGING,
diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index 3634b8a..1506d27 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -77,25 +77,38 @@
         INSTANCE = instance;
     }
 
-    public TelecomSystem(Context context) {
+    public TelecomSystem(
+            Context context,
+            MissedCallNotifier missedCallNotifier,
+            HeadsetMediaButtonFactory headsetMediaButtonFactory,
+            ProximitySensorManagerFactory proximitySensorManagerFactory,
+            InCallWakeLockControllerFactory inCallWakeLockControllerFactory) {
         mContext = context.getApplicationContext();
 
-        mMissedCallNotifier = new MissedCallNotifier(mContext);
+        mMissedCallNotifier = missedCallNotifier;
         mPhoneAccountRegistrar = new PhoneAccountRegistrar(mContext);
 
-        mRespondViaSmsManager = new RespondViaSmsManager();
-
         mCallsManager = new CallsManager(
-                mContext, mMissedCallNotifier, mPhoneAccountRegistrar, mRespondViaSmsManager);
-        Log.i(this, "CallsManager initialized");
+                mContext,
+                mMissedCallNotifier,
+                mPhoneAccountRegistrar,
+                headsetMediaButtonFactory,
+                proximitySensorManagerFactory,
+                inCallWakeLockControllerFactory);
+
+        mRespondViaSmsManager = new RespondViaSmsManager(mCallsManager);
+        mCallsManager.setRespondViaSmsManager(mRespondViaSmsManager);
+
         mMissedCallNotifier.setCallsManager(mCallsManager);
 
         mContext.registerReceiver(mUserSwitchedReceiver, USER_SWITCHED_FILTER);
         mBluetoothPhoneServiceImpl =
-                new BluetoothPhoneServiceImpl(context, mCallsManager, mPhoneAccountRegistrar);
-        mCallIntentProcessor = new CallIntentProcessor(context, mCallsManager);
-        mTelecomBroadcastIntentProcessor = new TelecomBroadcastIntentProcessor(context);
-        mTelecomServiceImpl = new TelecomServiceImpl(context, mCallsManager);
+                new BluetoothPhoneServiceImpl(mContext, mCallsManager, mPhoneAccountRegistrar);
+        mCallIntentProcessor = new CallIntentProcessor(mContext, mCallsManager);
+        mTelecomBroadcastIntentProcessor = new TelecomBroadcastIntentProcessor(
+                mContext, mCallsManager);
+        mTelecomServiceImpl =
+                new TelecomServiceImpl(mContext, mCallsManager, mPhoneAccountRegistrar);
     }
 
     public MissedCallNotifier getMissedCallNotifier() {
diff --git a/src/com/android/server/telecom/components/TelecomService.java b/src/com/android/server/telecom/components/TelecomService.java
index 0b8238b..8d5190b 100644
--- a/src/com/android/server/telecom/components/TelecomService.java
+++ b/src/com/android/server/telecom/components/TelecomService.java
@@ -18,11 +18,20 @@
 
 import android.app.Service;
 import android.bluetooth.BluetoothAdapter;
+import android.content.Context;
 import android.content.Intent;
 import android.os.IBinder;
 
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.HeadsetMediaButton;
+import com.android.server.telecom.HeadsetMediaButtonFactory;
+import com.android.server.telecom.InCallWakeLockControllerFactory;
+import com.android.server.telecom.ProximitySensorManagerFactory;
+import com.android.server.telecom.InCallWakeLockController;
 import com.android.server.telecom.Log;
+import com.android.server.telecom.ProximitySensorManager;
 import com.android.server.telecom.TelecomSystem;
+import com.android.server.telecom.ui.MissedCallNotifierImpl;
 
 /**
  * Implementation of the ITelecom interface.
@@ -34,7 +43,32 @@
         Log.d(this, "onBind");
         // We are guaranteed that the TelecomService will be started before any other
         // components in this package because it is started and kept running by the system.
-        TelecomSystem.setInstance(new TelecomSystem(this));
+        TelecomSystem.setInstance(
+                new TelecomSystem(
+                        this,
+                        new MissedCallNotifierImpl(getApplicationContext()),
+                        new HeadsetMediaButtonFactory() {
+                            @Override
+                            public HeadsetMediaButton create(Context context,
+                                    CallsManager callsManager) {
+                                return new HeadsetMediaButton(context, callsManager);
+                            }
+                        },
+                        new ProximitySensorManagerFactory() {
+                            @Override
+                            public ProximitySensorManager create(
+                                    Context context,
+                                    CallsManager callsManager) {
+                                return new ProximitySensorManager(context, callsManager);
+                            }
+                        },
+                        new InCallWakeLockControllerFactory() {
+                            @Override
+                            public InCallWakeLockController create(Context context,
+                                    CallsManager callsManager) {
+                                return new InCallWakeLockController(context, callsManager);
+                            }
+                        }));
         // Start the BluetoothPhoneService
         if (BluetoothAdapter.getDefaultAdapter() != null) {
             startService(new Intent(this, BluetoothPhoneService.class));
diff --git a/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java b/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
new file mode 100644
index 0000000..cd62d91
--- /dev/null
+++ b/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
@@ -0,0 +1,369 @@
+/*
+ * Copyright 2014, 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.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.ui;
+
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.CallsManagerListenerBase;
+import com.android.server.telecom.Constants;
+import com.android.server.telecom.MissedCallNotifier;
+import com.android.server.telecom.Log;
+import com.android.server.telecom.R;
+import com.android.server.telecom.TelecomBroadcastIntentProcessor;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.TaskStackBuilder;
+import android.content.AsyncQueryHandler;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.UserHandle;
+import android.provider.CallLog.Calls;
+import android.telecom.CallState;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccount;
+import android.telephony.PhoneNumberUtils;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
+import android.text.TextUtils;
+
+// TODO: Needed for move to system service: import com.android.internal.R;
+
+/**
+ * Creates a notification for calls that the user missed (neither answered nor rejected).
+ *
+ * TODO: Make TelephonyManager.clearMissedCalls call into this class.
+ *
+ * TODO: Reduce dependencies in this implementation; remove the need to create a new Call
+ *     simply to look up caller metadata, and if possible, make it unnecessary to get a
+ *     direct reference to the CallsManager. Try to make this class simply handle the UI
+ *     and Android-framework entanglements of missed call notification.
+ */
+public class MissedCallNotifierImpl extends CallsManagerListenerBase implements
+        MissedCallNotifier {
+
+    private static final String[] CALL_LOG_PROJECTION = new String[] {
+        Calls._ID,
+        Calls.NUMBER,
+        Calls.NUMBER_PRESENTATION,
+        Calls.DATE,
+        Calls.DURATION,
+        Calls.TYPE,
+    };
+
+    private static final int CALL_LOG_COLUMN_ID = 0;
+    private static final int CALL_LOG_COLUMN_NUMBER = 1;
+    private static final int CALL_LOG_COLUMN_NUMBER_PRESENTATION = 2;
+    private static final int CALL_LOG_COLUMN_DATE = 3;
+    private static final int CALL_LOG_COLUMN_DURATION = 4;
+    private static final int CALL_LOG_COLUMN_TYPE = 5;
+
+    private static final int MISSED_CALL_NOTIFICATION_ID = 1;
+
+    private final Context mContext;
+    private CallsManager mCallsManager;
+    private final NotificationManager mNotificationManager;
+
+    // Used to track the number of missed calls.
+    private int mMissedCallCount = 0;
+
+    public MissedCallNotifierImpl(Context context) {
+        mContext = context;
+        mNotificationManager =
+                (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+
+        updateOnStartup();
+    }
+
+    public void setCallsManager(CallsManager callsManager) {
+        this.mCallsManager = callsManager;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void onCallStateChanged(Call call, int oldState, int newState) {
+        if (oldState == CallState.RINGING && newState == CallState.DISCONNECTED &&
+                call.getDisconnectCause().getCode() == DisconnectCause.MISSED) {
+            showMissedCallNotification(call);
+        }
+    }
+
+    /** Clears missed call notification and marks the call log's missed calls as read. */
+    public void clearMissedCalls() {
+        AsyncTask.execute(new Runnable() {
+            @Override
+            public void run() {
+                // Clear the list of new missed calls from the call log.
+                ContentValues values = new ContentValues();
+                values.put(Calls.NEW, 0);
+                values.put(Calls.IS_READ, 1);
+                StringBuilder where = new StringBuilder();
+                where.append(Calls.NEW);
+                where.append(" = 1 AND ");
+                where.append(Calls.TYPE);
+                where.append(" = ?");
+                mContext.getContentResolver().update(Calls.CONTENT_URI, values, where.toString(),
+                        new String[]{ Integer.toString(Calls.MISSED_TYPE) });
+            }
+        });
+        cancelMissedCallNotification();
+    }
+
+    /**
+     * Create a system notification for the missed call.
+     *
+     * @param call The missed call.
+     */
+    public void showMissedCallNotification(Call call) {
+        mMissedCallCount++;
+
+        final int titleResId;
+        final String expandedText;  // The text in the notification's line 1 and 2.
+
+        // Display the first line of the notification:
+        // 1 missed call: <caller name || handle>
+        // More than 1 missed call: <number of calls> + "missed calls"
+        if (mMissedCallCount == 1) {
+            titleResId = R.string.notification_missedCallTitle;
+            expandedText = getNameForCall(call);
+        } else {
+            titleResId = R.string.notification_missedCallsTitle;
+            expandedText =
+                    mContext.getString(R.string.notification_missedCallsMsg, mMissedCallCount);
+        }
+
+        // Create the notification.
+        Notification.Builder builder = new Notification.Builder(mContext);
+        builder.setSmallIcon(android.R.drawable.stat_notify_missed_call)
+                .setColor(mContext.getResources().getColor(R.color.theme_color))
+                .setWhen(call.getCreationTimeMillis())
+                .setContentTitle(mContext.getText(titleResId))
+                .setContentText(expandedText)
+                .setContentIntent(createCallLogPendingIntent())
+                .setAutoCancel(true)
+                .setDeleteIntent(createClearMissedCallsPendingIntent());
+
+        Uri handleUri = call.getHandle();
+        String handle = handleUri == null ? null : handleUri.getSchemeSpecificPart();
+
+        // Add additional actions when there is only 1 missed call, like call-back and SMS.
+        if (mMissedCallCount == 1) {
+            Log.d(this, "Add actions with number %s.", Log.piiHandle(handle));
+
+            if (!TextUtils.isEmpty(handle)
+                    && !TextUtils.equals(handle, mContext.getString(R.string.handle_restricted))) {
+                builder.addAction(R.drawable.stat_sys_phone_call,
+                        mContext.getString(R.string.notification_missedCall_call_back),
+                        createCallBackPendingIntent(handleUri));
+
+                builder.addAction(R.drawable.ic_text_holo_dark,
+                        mContext.getString(R.string.notification_missedCall_message),
+                        createSendSmsFromNotificationPendingIntent(handleUri));
+            }
+
+            Bitmap photoIcon = call.getPhotoIcon();
+            if (photoIcon != null) {
+                builder.setLargeIcon(photoIcon);
+            } else {
+                Drawable photo = call.getPhoto();
+                if (photo != null && photo instanceof BitmapDrawable) {
+                    builder.setLargeIcon(((BitmapDrawable) photo).getBitmap());
+                }
+            }
+        } else {
+            Log.d(this, "Suppress actions. handle: %s, missedCalls: %d.", Log.piiHandle(handle),
+                    mMissedCallCount);
+        }
+
+        Notification notification = builder.build();
+        configureLedOnNotification(notification);
+
+        Log.i(this, "Adding missed call notification for %s.", call);
+        mNotificationManager.notifyAsUser(
+                null /* tag */ , MISSED_CALL_NOTIFICATION_ID, notification, UserHandle.CURRENT);
+    }
+
+    /** Cancels the "missed call" notification. */
+    private void cancelMissedCallNotification() {
+        // Reset the number of missed calls to 0.
+        mMissedCallCount = 0;
+        mNotificationManager.cancel(MISSED_CALL_NOTIFICATION_ID);
+    }
+
+    /**
+     * Returns the name to use in the missed call notification.
+     */
+    private String getNameForCall(Call call) {
+        String handle = call.getHandle() == null ? null : call.getHandle().getSchemeSpecificPart();
+        String name = call.getName();
+
+        if (!TextUtils.isEmpty(name) && TextUtils.isGraphic(name)) {
+            return name;
+        } else if (!TextUtils.isEmpty(handle)) {
+            // A handle should always be displayed LTR using {@link BidiFormatter} regardless of the
+            // content of the rest of the notification.
+            // TODO: Does this apply to SIP addresses?
+            BidiFormatter bidiFormatter = BidiFormatter.getInstance();
+            return bidiFormatter.unicodeWrap(handle, TextDirectionHeuristics.LTR);
+        } else {
+            // Use "unknown" if the call is unidentifiable.
+            return mContext.getString(R.string.unknown);
+        }
+    }
+
+    /**
+     * Creates a new pending intent that sends the user to the call log.
+     *
+     * @return The pending intent.
+     */
+    private PendingIntent createCallLogPendingIntent() {
+        Intent intent = new Intent(Intent.ACTION_VIEW, null);
+        intent.setType(Calls.CONTENT_TYPE);
+
+        TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(mContext);
+        taskStackBuilder.addNextIntent(intent);
+
+        return taskStackBuilder.getPendingIntent(0, 0);
+    }
+
+    /**
+     * Creates an intent to be invoked when the missed call notification is cleared.
+     */
+    private PendingIntent createClearMissedCallsPendingIntent() {
+        return createTelecomPendingIntent(
+                TelecomBroadcastIntentProcessor.ACTION_CLEAR_MISSED_CALLS, null);
+    }
+
+    /**
+     * Creates an intent to be invoked when the user opts to "call back" from the missed call
+     * notification.
+     *
+     * @param handle The handle to call back.
+     */
+    private PendingIntent createCallBackPendingIntent(Uri handle) {
+        return createTelecomPendingIntent(
+                TelecomBroadcastIntentProcessor.ACTION_CALL_BACK_FROM_NOTIFICATION, handle);
+    }
+
+    /**
+     * Creates an intent to be invoked when the user opts to "send sms" from the missed call
+     * notification.
+     */
+    private PendingIntent createSendSmsFromNotificationPendingIntent(Uri handle) {
+        return createTelecomPendingIntent(
+                TelecomBroadcastIntentProcessor.ACTION_SEND_SMS_FROM_NOTIFICATION,
+                Uri.fromParts(Constants.SCHEME_SMSTO, handle.getSchemeSpecificPart(), null));
+    }
+
+    /**
+     * Creates generic pending intent from the specified parameters to be received by
+     * {@link TelecomBroadcastIntentProcessor}.
+     *
+     * @param action The intent action.
+     * @param data The intent data.
+     */
+    private PendingIntent createTelecomPendingIntent(String action, Uri data) {
+        Intent intent = new Intent(action, data, mContext, TelecomBroadcastIntentProcessor.class);
+        return PendingIntent.getBroadcast(mContext, 0, intent, 0);
+    }
+
+    /**
+     * Configures a notification to emit the blinky notification light.
+     */
+    private void configureLedOnNotification(Notification notification) {
+        notification.flags |= Notification.FLAG_SHOW_LIGHTS;
+        notification.defaults |= Notification.DEFAULT_LIGHTS;
+    }
+
+    /**
+     * Adds the missed call notification on startup if there are unread missed calls.
+     */
+    private void updateOnStartup() {
+        Log.d(this, "updateOnStartup()...");
+
+        // instantiate query handler
+        AsyncQueryHandler queryHandler = new AsyncQueryHandler(mContext.getContentResolver()) {
+            @Override
+            protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+                Log.d(MissedCallNotifierImpl.this, "onQueryComplete()...");
+                if (cursor != null) {
+                    try {
+                        while (cursor.moveToNext()) {
+                            // Get data about the missed call from the cursor
+                            final String handleString = cursor.getString(CALL_LOG_COLUMN_NUMBER);
+                            final int presentation =
+                                    cursor.getInt(CALL_LOG_COLUMN_NUMBER_PRESENTATION);
+                            final long date = cursor.getLong(CALL_LOG_COLUMN_DATE);
+
+                            final Uri handle;
+                            if (presentation != Calls.PRESENTATION_ALLOWED
+                                    || TextUtils.isEmpty(handleString)) {
+                                handle = null;
+                            } else {
+                                handle = Uri.fromParts(PhoneNumberUtils.isUriNumber(handleString) ?
+                                        PhoneAccount.SCHEME_SIP : PhoneAccount.SCHEME_TEL,
+                                                handleString, null);
+                            }
+
+                            // Convert the data to a call object
+                            Call call = new Call(mContext, mCallsManager,
+                                    null, null, null, null, null, true,
+                                    false);
+                            call.setDisconnectCause(new DisconnectCause(DisconnectCause.MISSED));
+                            call.setState(CallState.DISCONNECTED);
+                            call.setCreationTimeMillis(date);
+
+                            // Listen for the update to the caller information before posting the
+                            // notification so that we have the contact info and photo.
+                            call.addListener(new Call.ListenerBase() {
+                                @Override
+                                public void onCallerInfoChanged(Call call) {
+                                    call.removeListener(this);  // No longer need to listen to call
+                                                                // changes after the contact info
+                                                                // is retrieved.
+                                    showMissedCallNotification(call);
+                                }
+                            });
+                            // Set the handle here because that is what triggers the contact info
+                            // query.
+                            call.setHandle(handle, presentation);
+                        }
+                    } finally {
+                        cursor.close();
+                    }
+                }
+            }
+        };
+
+        // setup query spec, look for all Missed calls that are new.
+        StringBuilder where = new StringBuilder("type=");
+        where.append(Calls.MISSED_TYPE);
+        where.append(" AND new=1");
+
+        // start the query
+        queryHandler.startQuery(0, null, Calls.CONTENT_URI, CALL_LOG_PROJECTION,
+                where.toString(), null, Calls.DEFAULT_SORT_ORDER);
+    }
+}
diff --git a/tests/src/com/android/server/telecom/tests/ComponentContextHolder.java b/tests/src/com/android/server/telecom/tests/ComponentContextHolder.java
new file mode 100644
index 0000000..692da37
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/ComponentContextHolder.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2015 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.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.tests;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
+
+import com.android.internal.telecom.IConnectionService;
+import com.android.internal.telecom.IInCallService;
+
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.IContentProvider;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.media.AudioManager;
+import android.os.IInterface;
+import android.os.UserHandle;
+import android.telecom.ConnectionService;
+import android.telecom.InCallService;
+import android.telecom.PhoneAccount;
+import android.telephony.TelephonyManager;
+import android.test.mock.MockContext;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.when;
+
+/**
+ * Controls a test {@link Context} as would be provided by the Android framework to an
+ * {@code Activity}, {@code Service} or other system-instantiated component.
+ *
+ * The {@link Context} created by this object is "hollow" but its {@code applicationContext}
+ * property points to an application context implementing all the nontrivial functionality.
+ */
+public class ComponentContextHolder implements TestDoubleHolder<Context> {
+
+    public class TestApplicationContext extends MockContext {
+        @Override
+        public PackageManager getPackageManager() {
+            return mPackageManager;
+        }
+
+        @Override
+        public File getFilesDir() {
+            try {
+                return File.createTempFile("temp", "temp").getParentFile();
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        @Override
+        public boolean bindServiceAsUser(
+                Intent serviceIntent,
+                ServiceConnection connection,
+                int flags,
+                UserHandle userHandle) {
+            // TODO: Implement "as user" functionality
+            return bindService(serviceIntent, connection, flags);
+        }
+
+        @Override
+        public boolean bindService(
+                Intent serviceIntent,
+                ServiceConnection connection,
+                int flags) {
+            if (mServiceByServiceConnection.containsKey(connection)) {
+                throw new RuntimeException("ServiceConnection already bound: " + connection);
+            }
+            IInterface service = mServiceByComponentName.get(serviceIntent.getComponent());
+            if (service == null) {
+                throw new RuntimeException("ServiceConnection not found: "
+                        + serviceIntent.getComponent());
+            }
+            mServiceByServiceConnection.put(connection, service);
+            connection.onServiceConnected(serviceIntent.getComponent(), service.asBinder());
+            return true;
+        }
+
+        @Override
+        public void unbindService(
+                ServiceConnection connection) {
+            IInterface service = mServiceByServiceConnection.remove(connection);
+            if (service == null) {
+                throw new RuntimeException("ServiceConnection not found: " + connection);
+            }
+            connection.onServiceDisconnected(mComponentNameByService.get(service));
+        }
+
+        @Override
+        public Object getSystemService(String name) {
+            switch (name) {
+                case Context.AUDIO_SERVICE:
+                    return mAudioManager;
+                case Context.TELEPHONY_SERVICE:
+                    return mTelephonyManager;
+                default:
+                    return null;
+            }
+        }
+
+        @Override
+        public Resources getResources() {
+            return mResources;
+        }
+
+        @Override
+        public String getOpPackageName() {
+            return "test";
+        }
+
+        @Override
+        public ContentResolver getContentResolver() {
+            return new ContentResolver(this) {
+                @Override
+                protected IContentProvider acquireProvider(Context c, String name) {
+                    return null;
+                }
+
+                @Override
+                public boolean releaseProvider(IContentProvider icp) {
+                    return false;
+                }
+
+                @Override
+                protected IContentProvider acquireUnstableProvider(Context c, String name) {
+                    return null;
+                }
+
+                @Override
+                public boolean releaseUnstableProvider(IContentProvider icp) {
+                    return false;
+                }
+
+                @Override
+                public void unstableProviderDied(IContentProvider icp) {
+
+                }
+            };
+        }
+
+        @Override
+        public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
+            // TODO -- this is called by WiredHeadsetManager!!!
+            return null;
+        }
+
+        @Override
+        public void sendBroadcast(Intent intent) {
+            // TODO -- need to ensure this is captured
+        }
+
+        @Override
+        public void sendBroadcast(Intent intent, String receiverPermission) {
+            // TODO -- need to ensure this is captured
+        }
+    };
+
+    private final Multimap<String, ComponentName> mComponentNamesByAction =
+            ArrayListMultimap.create();
+    private final Map<ComponentName, IInterface> mServiceByComponentName = new HashMap<>();
+    private final Map<ComponentName, ServiceInfo> mServiceInfoByComponentName = new HashMap<>();
+    private final Map<IInterface, ComponentName> mComponentNameByService = new HashMap<>();
+    private final Map<ServiceConnection, IInterface> mServiceByServiceConnection = new HashMap<>();
+
+    private final Context mContext = new MockContext() {
+        @Override
+        public Context getApplicationContext() {
+            return mApplicationContextSpy;
+        }
+    };
+
+    // The application context is the most important object this class provides to the system
+    // under test.
+    private final Context mApplicationContext = new TestApplicationContext();
+
+    // We then create a spy on the application context allowing standard Mockito-style
+    // when(...) logic to be used to add specific little responses where needed.
+
+    private final Context mApplicationContextSpy = Mockito.spy(mApplicationContext);
+    private final PackageManager mPackageManager = Mockito.mock(PackageManager.class);
+    private final AudioManager mAudioManager = Mockito.mock(AudioManager.class);
+    private final TelephonyManager mTelephonyManager = Mockito.mock(TelephonyManager.class);
+    private final Resources mResources = Mockito.mock(Resources.class);
+    private final Configuration mResourceConfiguration = new Configuration();
+
+    public ComponentContextHolder() {
+        MockitoAnnotations.initMocks(this);
+        when(mResources.getConfiguration()).thenReturn(mResourceConfiguration);
+        mResourceConfiguration.setLocale(Locale.TAIWAN);
+
+        // TODO: Move into actual tests
+        when(mAudioManager.isWiredHeadsetOn()).thenReturn(false);
+
+        doAnswer(new Answer<List<ResolveInfo>>() {
+            @Override
+            public List<ResolveInfo> answer(InvocationOnMock invocation) throws Throwable {
+                return doQueryIntentServices(
+                        (Intent) invocation.getArguments()[0],
+                        (Integer) invocation.getArguments()[1]);
+            }
+        }).when(mPackageManager).queryIntentServices((Intent) any(), anyInt());
+
+        doAnswer(new Answer<List<ResolveInfo>>() {
+            @Override
+            public List<ResolveInfo> answer(InvocationOnMock invocation) throws Throwable {
+                return doQueryIntentServices(
+                        (Intent) invocation.getArguments()[0],
+                        (Integer) invocation.getArguments()[1]);
+            }
+        }).when(mPackageManager).queryIntentServicesAsUser((Intent) any(), anyInt(), anyInt());
+
+        when(mTelephonyManager.getSubIdForPhoneAccount((PhoneAccount) any())).thenReturn(1);
+    }
+
+    @Override
+    public Context getTestDouble() {
+        return mContext;
+    }
+
+    public void addConnectionService(
+            ComponentName componentName,
+            IConnectionService service)
+            throws Exception {
+        addService(ConnectionService.SERVICE_INTERFACE, componentName, service);
+        ServiceInfo serviceInfo = new ServiceInfo();
+        serviceInfo.permission = android.Manifest.permission.BIND_CONNECTION_SERVICE;
+        serviceInfo.packageName = componentName.getPackageName();
+        serviceInfo.name = componentName.getClassName();
+        mServiceInfoByComponentName.put(componentName, serviceInfo);
+    }
+
+    public void addInCallService(
+            ComponentName componentName,
+            IInCallService service)
+            throws Exception {
+        addService(InCallService.SERVICE_INTERFACE, componentName, service);
+        ServiceInfo serviceInfo = new ServiceInfo();
+        serviceInfo.permission = android.Manifest.permission.BIND_INCALL_SERVICE;
+        serviceInfo.packageName = componentName.getPackageName();
+        serviceInfo.name = componentName.getClassName();
+        mServiceInfoByComponentName.put(componentName, serviceInfo);
+    }
+
+    public void putResource(int id, String value) {
+        when(mResources.getString(eq(id))).thenReturn(value);
+    }
+
+    private void addService(String action, ComponentName name, IInterface service) {
+        mComponentNamesByAction.put(action, name);
+        mServiceByComponentName.put(name, service);
+        mComponentNameByService.put(service, name);
+    }
+
+    private List<ResolveInfo> doQueryIntentServices(Intent intent, int flags) {
+        List<ResolveInfo> result = new ArrayList<>();
+        for (ComponentName componentName : mComponentNamesByAction.get(intent.getAction())) {
+            ResolveInfo resolveInfo = new ResolveInfo();
+            resolveInfo.serviceInfo = mServiceInfoByComponentName.get(componentName);
+            result.add(resolveInfo);
+        }
+        return result;
+    }
+}
diff --git a/tests/src/com/android/server/telecom/tests/ConnectionServiceHolder.java b/tests/src/com/android/server/telecom/tests/ConnectionServiceHolder.java
new file mode 100644
index 0000000..668d6c2
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/ConnectionServiceHolder.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2015 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.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.tests;
+
+import com.android.internal.telecom.IConnectionService;
+import com.android.internal.telecom.IConnectionServiceAdapter;
+
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.telecom.AudioState;
+import android.telecom.ConnectionRequest;
+import android.telecom.PhoneAccountHandle;
+
+/**
+ * Controls a test {@link IConnectionService} as would be provided by a source of connectivity
+ * to the Telecom framework.
+ */
+public class ConnectionServiceHolder implements TestDoubleHolder<IConnectionService> {
+
+    private final IConnectionService mConnectionService = new IConnectionService.Stub() {
+        @Override
+        public void addConnectionServiceAdapter(IConnectionServiceAdapter adapter)
+                throws RemoteException {
+        }
+
+        @Override
+        public void removeConnectionServiceAdapter(IConnectionServiceAdapter adapter)
+                throws RemoteException {
+
+        }
+
+        @Override
+        public void createConnection(PhoneAccountHandle connectionManagerPhoneAccount,
+                String callId,
+                ConnectionRequest request, boolean isIncoming, boolean isUnknown)
+                throws RemoteException {
+
+        }
+
+        @Override
+        public void abort(String callId) throws RemoteException {
+
+        }
+
+        @Override
+        public void answerVideo(String callId, int videoState) throws RemoteException {
+
+        }
+
+        @Override
+        public void answer(String callId) throws RemoteException {
+
+        }
+
+        @Override
+        public void reject(String callId) throws RemoteException {
+
+        }
+
+        @Override
+        public void disconnect(String callId) throws RemoteException {
+
+        }
+
+        @Override
+        public void hold(String callId) throws RemoteException {
+
+        }
+
+        @Override
+        public void unhold(String callId) throws RemoteException {
+
+        }
+
+        @Override
+        public void onAudioStateChanged(String activeCallId, AudioState audioState)
+                throws RemoteException {
+
+        }
+
+        @Override
+        public void playDtmfTone(String callId, char digit) throws RemoteException {
+
+        }
+
+        @Override
+        public void stopDtmfTone(String callId) throws RemoteException {
+
+        }
+
+        @Override
+        public void conference(String conferenceCallId, String callId) throws RemoteException {
+
+        }
+
+        @Override
+        public void splitFromConference(String callId) throws RemoteException {
+
+        }
+
+        @Override
+        public void mergeConference(String conferenceCallId) throws RemoteException {
+
+        }
+
+        @Override
+        public void swapConference(String conferenceCallId) throws RemoteException {
+
+        }
+
+        @Override
+        public void onPostDialContinue(String callId, boolean proceed) throws RemoteException {
+
+        }
+
+        @Override
+        public IBinder asBinder() {
+            return null;
+        }
+    };
+
+    @Override
+    public IConnectionService getTestDouble() {
+        return mConnectionService;
+    }
+
+}
diff --git a/tests/src/com/android/server/telecom/tests/MockitoHelper.java b/tests/src/com/android/server/telecom/tests/MockitoHelper.java
new file mode 100644
index 0000000..32b91f9
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/MockitoHelper.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2015 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.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.tests;
+
+import android.content.Context;
+import android.util.Log;
+
+/**
+ * Helper for Mockito-based test cases.
+ */
+public final class MockitoHelper {
+    private static final String TAG = "MockitoHelper";
+    private static final String DEXCACHE = "dexmaker.dexcache";
+
+    private ClassLoader mOriginalClassLoader;
+    private Thread mContextThread;
+
+    /**
+     * Creates a new helper, which in turn will set the context classloader so
+     * it can load Mockito resources.
+     *
+     * @param packageClass test case class
+     */
+    public void setUp(Context context, Class<?> packageClass) throws Exception {
+        // makes a copy of the context classloader
+        mContextThread = Thread.currentThread();
+        mOriginalClassLoader = mContextThread.getContextClassLoader();
+        ClassLoader newClassLoader = packageClass.getClassLoader();
+        Log.v(TAG, "Changing context classloader from " + mOriginalClassLoader
+                + " to " + newClassLoader);
+        mContextThread.setContextClassLoader(newClassLoader);
+        System.setProperty(DEXCACHE, context.getCacheDir().toString());
+    }
+
+    /**
+     * Restores the context classloader to the previous value.
+     */
+    public void tearDown() throws Exception {
+        Log.v(TAG, "Restoring context classloader to " + mOriginalClassLoader);
+        mContextThread.setContextClassLoader(mOriginalClassLoader);
+        System.clearProperty(DEXCACHE);
+    }
+}
\ No newline at end of file
diff --git a/tests/src/com/android/server/telecom/tests/SimpleTelecomTest.java b/tests/src/com/android/server/telecom/tests/SimpleTelecomTest.java
new file mode 100644
index 0000000..8e19a55
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/SimpleTelecomTest.java
@@ -0,0 +1,425 @@
+/*
+ * Copyright (C) 2015 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.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom.tests;
+
+import com.android.internal.telecom.IConnectionService;
+import com.android.internal.telecom.IConnectionServiceAdapter;
+import com.android.internal.telecom.IInCallAdapter;
+import com.android.internal.telecom.IInCallService;
+import com.android.internal.telecom.IVideoProvider;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.HeadsetMediaButton;
+import com.android.server.telecom.HeadsetMediaButtonFactory;
+import com.android.server.telecom.InCallWakeLockControllerFactory;
+import com.android.server.telecom.MissedCallNotifier;
+import com.android.server.telecom.ProximitySensorManagerFactory;
+import com.android.server.telecom.InCallWakeLockController;
+import com.android.server.telecom.ProximitySensorManager;
+import com.android.server.telecom.TelecomSystem;
+
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.telecom.CallState;
+import android.telecom.Connection;
+import android.telecom.ConnectionRequest;
+import android.telecom.DisconnectCause;
+import android.telecom.ParcelableCall;
+import android.telecom.ParcelableConnection;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.StatusHints;
+import android.telecom.TelecomManager;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Collections;
+import java.util.List;
+
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class SimpleTelecomTest extends AndroidTestCase {
+
+    private static final String TAG = "Telecom-TEST";
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Telecom specific mock objects
+
+    @Mock
+    MissedCallNotifier mMissedCallNotifier;
+    @Mock
+    HeadsetMediaButtonFactory mHeadsetMediaButtonFactory;
+    @Mock
+    ProximitySensorManagerFactory mProximitySensorManagerFactory;
+    @Mock
+    InCallWakeLockControllerFactory mInCallWakeLockControllerFactory;
+    @Mock HeadsetMediaButton mHeadsetMediaButton;
+    @Mock ProximitySensorManager mProximitySensorManager;
+    @Mock InCallWakeLockController mInCallWakeLockController;
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Connection service
+
+    PhoneAccount mTestPhoneAccount = PhoneAccount.builder(
+            new PhoneAccountHandle(
+                    new ComponentName("connection-service-package", "connection-service-class"),
+                    "test-account-id"),
+            "test phone account")
+            .addSupportedUriScheme("tel")
+            .setCapabilities(
+                    PhoneAccount.CAPABILITY_CALL_PROVIDER |
+                    PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)
+            .build();
+    @Mock IConnectionService.Stub mConnectionService;
+    IConnectionServiceAdapter mConnectionServiceAdapter;
+
+    ///////////////////////////////////////////////////////////////////////////
+    // In-Call service
+
+    ComponentName mIncallComponentName = new ComponentName("incall-package", "incall-class");
+    @Mock IInCallService.Stub mInCallService;
+    IInCallAdapter mIInCallAdapter;
+
+    private ComponentContextHolder mContextHolder;
+    private TelecomSystem mSystem;
+
+    private ConnectionRequest mConnectionRequest;
+    private String mConnectionId;
+
+    private ParcelableCall mParcelableCall;
+
+    private Looper mMainLooper;
+    private Looper mTestLooper;
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Captured values for outgoing call processing
+
+    Intent mNewOutgoingCallIntent;
+    BroadcastReceiver mNewOutgoingCallReceiver;
+
+    private MockitoHelper mMockitoHelper = new MockitoHelper();
+
+    @Override
+    public void setUp() throws Exception {
+        mMockitoHelper.setUp(getContext(), getClass());
+
+        mMainLooper = Looper.getMainLooper();
+        mTestLooper = Looper.myLooper();
+
+        mContextHolder = new ComponentContextHolder();
+        MockitoAnnotations.initMocks(this);
+
+        mContextHolder.putResource(
+                com.android.server.telecom.R.string.ui_default_package,
+                mIncallComponentName.getPackageName());
+        mContextHolder.putResource(
+                com.android.server.telecom.R.string.incall_default_class,
+                mIncallComponentName.getClassName());
+
+        com.android.server.telecom.Log.setTag(TAG);
+
+        when(mHeadsetMediaButtonFactory.create(
+                any(Context.class),
+                any(CallsManager.class)))
+                .thenReturn(mHeadsetMediaButton);
+
+        when(mInCallWakeLockControllerFactory.create(
+                any(Context.class),
+                any(CallsManager.class)))
+                .thenReturn(mInCallWakeLockController);
+
+        when(mProximitySensorManagerFactory.create((Context) any(), (CallsManager) any()))
+                .thenReturn(mProximitySensorManager);
+
+        // Set up connection service
+
+        mContextHolder.addConnectionService(
+                mTestPhoneAccount.getAccountHandle().getComponentName(),
+                mConnectionService);
+        when(mConnectionService.asBinder()).thenReturn(mConnectionService);
+        when(mConnectionService.queryLocalInterface(anyString()))
+                .thenReturn(mConnectionService);
+
+        // Set up in-call service
+
+        mContextHolder.addInCallService(
+                mIncallComponentName,
+                mInCallService);
+        when(mInCallService.asBinder()).thenReturn(mInCallService);
+        when(mInCallService.queryLocalInterface(anyString()))
+                .thenReturn(mInCallService);
+
+        runOnMainThreadAndWait(new Runnable() {
+            @Override
+            public void run() {
+                mSystem = new TelecomSystem(
+                        mContextHolder.getTestDouble(),
+                        mMissedCallNotifier,
+                        mHeadsetMediaButtonFactory,
+                        mProximitySensorManagerFactory,
+                        mInCallWakeLockControllerFactory);
+                mSystem.getPhoneAccountRegistrar().registerPhoneAccount(mTestPhoneAccount);
+            }
+        });
+    }
+
+    @Override
+    public void tearDown() throws Exception {
+        mMockitoHelper.tearDown();
+        mSystem = null;
+    }
+
+    public void testSimpleOutgoingCall() throws Exception {
+
+        // Arrange to receive the first set of notifications when Telecom receives an Intent
+        // to make an outgoing call
+        doAnswer(new Answer<Object>() {
+            @Override
+            public Object answer(final InvocationOnMock invocation) {
+                mIInCallAdapter = (IInCallAdapter) invocation.getArguments()[0];
+                return null;
+            }
+        }).when(mInCallService).setInCallAdapter((IInCallAdapter) any());
+        verify(mInCallService, never()).addCall((ParcelableCall) any());
+
+        doAnswer(new Answer<Object>() {
+            @Override
+            public Object answer(final InvocationOnMock invocation) {
+                mNewOutgoingCallIntent = (Intent) invocation.getArguments()[0];
+                mNewOutgoingCallReceiver = (BroadcastReceiver) invocation.getArguments()[3];
+                return null;
+            }
+        }).when(mContextHolder.getTestDouble().getApplicationContext())
+                .sendOrderedBroadcastAsUser(
+                        any(Intent.class),
+                        any(UserHandle.class),
+                        anyString(),
+                        any(BroadcastReceiver.class),
+                        any(Handler.class),
+                        anyInt(),
+                        anyString(),
+                        any(Bundle.class));
+
+        doAnswer(new Answer<Object>() {
+            @Override
+            public Object answer(final InvocationOnMock invocation) {
+                mParcelableCall = (ParcelableCall) invocation.getArguments()[0];
+                return null;
+            }
+        }).when(mInCallService).addCall((ParcelableCall) any());
+
+        doAnswer(new Answer<Object>() {
+            @Override
+            public Object answer(final InvocationOnMock invocation) {
+                mParcelableCall = (ParcelableCall) invocation.getArguments()[0];
+                return null;
+            }
+        }).when(mInCallService).updateCall((ParcelableCall) any());
+
+        runOnMainThreadAndWait(new Runnable() {
+            @Override
+            public void run() {
+                // Start an outgoing phone call
+                String number = "650-555-1212";
+                Intent actionCallIntent = new Intent();
+                actionCallIntent.setData(Uri.parse("tel:" + number));
+                actionCallIntent.putExtra(Intent.EXTRA_PHONE_NUMBER, number);
+                actionCallIntent.putExtra(
+                        TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
+                        mTestPhoneAccount.getAccountHandle());
+                actionCallIntent.setAction(Intent.ACTION_CALL);
+                mSystem.getCallIntentProcessor().processIntent(actionCallIntent);
+            }
+        });
+
+        // Sanity check that the in-call adapter is now set
+        assertNotNull(mIInCallAdapter);
+        assertNotNull(mNewOutgoingCallIntent);
+        assertNotNull(mNewOutgoingCallReceiver);
+
+        // Arrange to receive the Connection Service adapter
+        doAnswer(new Answer<Object>() {
+            @Override
+            public Object answer(InvocationOnMock invocation) {
+                mConnectionServiceAdapter = (IConnectionServiceAdapter) invocation
+                        .getArguments()[0];
+                return null;
+            }
+        }).when(mConnectionService).addConnectionServiceAdapter((IConnectionServiceAdapter) any());
+
+        doAnswer(new Answer<Object>() {
+            @Override
+            public Object answer(InvocationOnMock invocation) {
+                mConnectionId = (String) invocation.getArguments()[1];
+                mConnectionRequest = (ConnectionRequest) invocation.getArguments()[2];
+                return null;
+            }
+        }).when(mConnectionService).createConnection(
+                any(PhoneAccountHandle.class),
+                anyString(),
+                any(ConnectionRequest.class),
+                anyBoolean(),
+                anyBoolean());
+
+        // Pass on the new outgoing call Intent
+        runOnMainThreadAndWait(new Runnable() {
+            @Override
+            public void run() {
+                // Set a dummy PendingResult so the BroadcastReceiver agrees to accept onReceive()
+                mNewOutgoingCallReceiver.setPendingResult(
+                        new BroadcastReceiver.PendingResult(0, "", null, 0, true, false, null, 0));
+                mNewOutgoingCallReceiver.setResultData(
+                        mNewOutgoingCallIntent.getStringExtra(Intent.EXTRA_PHONE_NUMBER));
+                mNewOutgoingCallReceiver.onReceive(
+                        mContextHolder.getTestDouble(),
+                        mNewOutgoingCallIntent);
+            }
+        });
+
+        runOnMainThreadAndWait(new Runnable() {
+            @Override
+            public void run() {
+                assertNotNull(mConnectionServiceAdapter);
+                assertNotNull(mConnectionRequest);
+                assertNotNull(mConnectionId);
+            }
+        });
+
+        mConnectionServiceAdapter.handleCreateConnectionComplete(
+                mConnectionId,
+                mConnectionRequest,
+                new ParcelableConnection(
+                        mConnectionRequest.getAccountHandle(),
+                        Connection.STATE_DIALING,
+                        0,
+                        (Uri) null,
+                        0,
+                        "caller display name",
+                        0,
+                        (IVideoProvider) null,
+                        0,
+                        false,
+                        false,
+                        (StatusHints) null,
+                        (DisconnectCause) null,
+                        (List<String>) Collections.EMPTY_LIST));
+        mConnectionServiceAdapter.setDialing(mConnectionId);
+        mConnectionServiceAdapter.setActive(mConnectionId);
+
+        runOnMainThreadAndWait(new Runnable() {
+            @Override
+            public void run() {
+                assertNotNull(mParcelableCall);
+                assertEquals(CallState.ACTIVE, mParcelableCall.getState());
+            }
+        });
+
+        runOnMainThreadAndWait(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    mIInCallAdapter.disconnectCall(mParcelableCall.getId());
+                } catch (RemoteException e) {
+                    throw new RuntimeException(e);
+                }
+            }
+        });
+
+        runOnMainThreadAndWait(new Runnable() {
+            @Override
+            public void run() {
+                assertNotNull(mParcelableCall);
+                assertEquals(CallState.ACTIVE, mParcelableCall.getState());
+                try {
+                    verify(mConnectionService).disconnect(mConnectionId);
+                } catch (RemoteException e) {
+                    throw new RuntimeException(e);
+                }
+            }
+        });
+
+        mConnectionServiceAdapter.setDisconnected(
+                mConnectionId,
+                new DisconnectCause(DisconnectCause.LOCAL));
+
+        runOnMainThreadAndWait(new Runnable() {
+            @Override
+            public void run() {
+                assertEquals(CallState.DISCONNECTED, mParcelableCall.getState());
+            }
+        });
+    }
+
+    private void runOnMainThreadAndWait(Runnable task) {
+        runOn(mMainLooper, task);
+    }
+
+    private void runOnTestThreadAndWait(Runnable task) {
+        runOn(mTestLooper, task);
+    }
+
+    private  void runOn(Looper looper, final Runnable task) {
+        final Object lock = new Object();
+        synchronized (lock) {
+            new Handler(looper).post(new Runnable() {
+                @Override
+                public void run() {
+                    task.run();
+                    synchronized (lock) {
+                        lock.notifyAll();
+                    }
+                }
+            });
+            try {
+                lock.wait();
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
+    private String exceptionToString(Throwable t) {
+        StringWriter sw = new StringWriter();
+        t.printStackTrace(new PrintWriter(sw));
+        return sw.toString();
+    }
+
+    private void log(String msg) {
+        Log.i(TAG, getClass().getSimpleName() + " - " + msg);
+    }
+}
diff --git a/tests/src/com/android/server/telecom/tests/MockConnectionService.java b/tests/src/com/android/server/telecom/tests/TestDoubleHolder.java
similarity index 63%
rename from tests/src/com/android/server/telecom/tests/MockConnectionService.java
rename to tests/src/com/android/server/telecom/tests/TestDoubleHolder.java
index 62448cd..7c34f84 100644
--- a/tests/src/com/android/server/telecom/tests/MockConnectionService.java
+++ b/tests/src/com/android/server/telecom/tests/TestDoubleHolder.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2014 The Android Open Source Project
+ * Copyright (C) 2015 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.
@@ -16,11 +16,17 @@
 
 package com.android.server.telecom.tests;
 
-import android.telecom.ConnectionService;
-
 /**
- * A non-functional {@link android.telecom.ConnectionService} to use for unit tests.
+ * An object that provides a control interface for configuring a test double.
+ *
+ * TODO: Come up with a better name for this.
  */
-public class MockConnectionService extends ConnectionService {
+public interface TestDoubleHolder <T> {
 
+    /**
+     * Obtain the actual test double provided by this holder.
+     *
+     * @return the test double.
+     */
+    T getTestDouble();
 }
diff --git a/tests/src/com/android/server/telecom/tests/unit/PhoneAccountRegistrarTest.java b/tests/src/com/android/server/telecom/tests/unit/PhoneAccountRegistrarTest.java
index e63e79f..0224566 100644
--- a/tests/src/com/android/server/telecom/tests/unit/PhoneAccountRegistrarTest.java
+++ b/tests/src/com/android/server/telecom/tests/unit/PhoneAccountRegistrarTest.java
@@ -357,4 +357,4 @@
         s.simCallManager = new PhoneAccountHandle(new ComponentName("pkg0", "cls0"), "id1");
         return s;
     }
-}
\ No newline at end of file
+}