Renaming Telecomm to Telecom.

- Changing package from android.telecomm to android.telecom
- Changing package from com.android.telecomm to
com.android.server.telecomm.
- Renaming TelecommManager to TelecomManager.

Bug: 17364651
Change-Id: Ib7b20ba6348948afb391450b4eef8919261f3272
diff --git a/src/com/android/server/telecom/AsyncResultCallback.java b/src/com/android/server/telecom/AsyncResultCallback.java
new file mode 100644
index 0000000..edbda3b
--- /dev/null
+++ b/src/com/android/server/telecom/AsyncResultCallback.java
@@ -0,0 +1,24 @@
+/*
+ * 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;
+
+/**
+ * Generic result interface for use with async method callback.
+ */
+interface AsyncResultCallback<T> {
+    void onResult(T result, int errorCode, String errorMsg);
+}
diff --git a/src/com/android/server/telecom/AsyncRingtonePlayer.java b/src/com/android/server/telecom/AsyncRingtonePlayer.java
new file mode 100644
index 0000000..ad57b00
--- /dev/null
+++ b/src/com/android/server/telecom/AsyncRingtonePlayer.java
@@ -0,0 +1,161 @@
+/*
+ * 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;
+
+import android.media.AudioManager;
+import android.media.Ringtone;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.provider.Settings;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * Plays the default ringtone. Uses {@link Ringtone} in a separate thread so that this class can be
+ * used from the main thread.
+ */
+class AsyncRingtonePlayer {
+    // Message codes used with the ringtone thread.
+    static final int EVENT_PLAY = 1;
+    static final int EVENT_STOP = 2;
+
+    /** Handler running on the ringtone thread. */
+    private Handler mHandler;
+
+    /** The current ringtone. Only used by the ringtone thread. */
+    private Ringtone mRingtone;
+
+    /** Plays the ringtone. */
+    void play(Uri ringtone) {
+        Log.d(this, "Posting play.");
+        postMessage(EVENT_PLAY, true /* shouldCreateHandler */, ringtone);
+    }
+
+    /** Stops playing the ringtone. */
+    void stop() {
+        Log.d(this, "Posting stop.");
+        postMessage(EVENT_STOP, false /* shouldCreateHandler */, null);
+    }
+
+    /**
+     * Posts a message to the ringtone-thread handler. Creates the handler if specified by the
+     * parameter shouldCreateHandler.
+     *
+     * @param messageCode The message to post.
+     * @param shouldCreateHandler True when a handler should be created to handle this message.
+     */
+    private void postMessage(int messageCode, boolean shouldCreateHandler, Uri ringtone) {
+        synchronized(this) {
+            if (mHandler == null && shouldCreateHandler) {
+                mHandler = getNewHandler();
+            }
+
+            if (mHandler == null) {
+                Log.d(this, "Message %d skipped because there is no handler.", messageCode);
+            } else {
+                mHandler.obtainMessage(messageCode, ringtone).sendToTarget();
+            }
+        }
+    }
+
+    /**
+     * Creates a new ringtone Handler running in its own thread.
+     */
+    private Handler getNewHandler() {
+        Preconditions.checkState(mHandler == null);
+
+        HandlerThread thread = new HandlerThread("ringtone-player");
+        thread.start();
+
+        return new Handler(thread.getLooper()) {
+            @Override
+            public void handleMessage(Message msg) {
+                switch(msg.what) {
+                    case EVENT_PLAY:
+                        handlePlay((Uri) msg.obj);
+                        break;
+                    case EVENT_STOP:
+                        handleStop();
+                        break;
+                }
+            }
+        };
+    }
+
+    /**
+     * Starts the actual playback of the ringtone. Executes on ringtone-thread.
+     */
+    private void handlePlay(Uri ringtoneUri) {
+        ThreadUtil.checkNotOnMainThread();
+        Log.i(this, "Play ringtone.");
+
+        if (mRingtone == null) {
+            mRingtone = getRingtone(ringtoneUri);
+
+            // Cancel everything if there is no ringtone.
+            if (mRingtone == null) {
+                handleStop();
+                return;
+            }
+        }
+
+        if (mRingtone.isPlaying()) {
+            Log.d(this, "Ringtone already playing.");
+        } else {
+            mRingtone.play();
+            Log.d(this, "Ringtone.play() invoked.");
+        }
+    }
+
+    /**
+     * Stops the playback of the ringtone. Executes on the ringtone-thread.
+     */
+    private void handleStop() {
+        ThreadUtil.checkNotOnMainThread();
+        Log.i(this, "Stop ringtone.");
+
+        if (mRingtone != null) {
+            Log.d(this, "Ringtone.stop() invoked.");
+            mRingtone.stop();
+            mRingtone = null;
+        }
+
+        synchronized(this) {
+            if (mHandler.hasMessages(EVENT_PLAY)) {
+                Log.v(this, "Keeping alive ringtone thread for pending messages.");
+            } else {
+                mHandler.removeMessages(EVENT_STOP);
+                mHandler.getLooper().quitSafely();
+                mHandler = null;
+                Log.v(this, "Handler cleared.");
+            }
+        }
+    }
+
+    private Ringtone getRingtone(Uri ringtoneUri) {
+        if (ringtoneUri == null) {
+            ringtoneUri = Settings.System.DEFAULT_RINGTONE_URI;
+        }
+
+        Ringtone ringtone = RingtoneManager.getRingtone(TelecomApp.getInstance(), ringtoneUri);
+        ringtone.setStreamType(AudioManager.STREAM_RING);
+        return ringtone;
+    }
+}
diff --git a/src/com/android/server/telecom/BluetoothManager.java b/src/com/android/server/telecom/BluetoothManager.java
new file mode 100644
index 0000000..9b5fd26
--- /dev/null
+++ b/src/com/android/server/telecom/BluetoothManager.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 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;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadset;
+import android.bluetooth.BluetoothProfile;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.SystemClock;
+
+import java.util.List;
+
+/**
+ * Listens to and caches bluetooth headset state.  Used By the CallAudioManager for maintaining
+ * overall audio state. Also provides method for connecting the bluetooth headset to the phone call.
+ */
+public class BluetoothManager {
+
+    private final BluetoothProfile.ServiceListener mBluetoothProfileServiceListener =
+            new BluetoothProfile.ServiceListener() {
+                @Override
+                public void onServiceConnected(int profile, BluetoothProfile proxy) {
+                    mBluetoothHeadset = (BluetoothHeadset) proxy;
+                    Log.v(this, "- Got BluetoothHeadset: " + mBluetoothHeadset);
+                }
+
+                @Override
+                public void onServiceDisconnected(int profile) {
+                    mBluetoothHeadset = null;
+                }
+           };
+
+    /**
+     * Receiver for misc intent broadcasts the BluetoothManager cares about.
+     */
+    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+
+            if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) {
+                int bluetoothHeadsetState = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
+                                                          BluetoothHeadset.STATE_DISCONNECTED);
+                Log.d(this, "mReceiver: HEADSET_STATE_CHANGED_ACTION");
+                Log.d(this, "==> new state: %s ", bluetoothHeadsetState);
+                updateBluetoothState();
+            } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
+                int bluetoothHeadsetAudioState =
+                        intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
+                                           BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
+                Log.d(this, "mReceiver: HEADSET_AUDIO_STATE_CHANGED_ACTION");
+                Log.d(this, "==> new state: %s", bluetoothHeadsetAudioState);
+                updateBluetoothState();
+            }
+        }
+    };
+
+    private final BluetoothAdapter mBluetoothAdapter;
+    private final CallAudioManager mCallAudioManager;
+
+    private BluetoothHeadset mBluetoothHeadset;
+    private boolean mBluetoothConnectionPending = false;
+    private long mBluetoothConnectionRequestTime;
+
+
+    public BluetoothManager(Context context, CallAudioManager callAudioManager) {
+        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+        mCallAudioManager = callAudioManager;
+
+        if (mBluetoothAdapter != null) {
+            mBluetoothAdapter.getProfileProxy(context, mBluetoothProfileServiceListener,
+                                    BluetoothProfile.HEADSET);
+        }
+
+        // Register for misc other intent broadcasts.
+        IntentFilter intentFilter =
+                new IntentFilter(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
+        intentFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
+        context.registerReceiver(mReceiver, intentFilter);
+    }
+
+    //
+    // Bluetooth helper methods.
+    //
+    // - BluetoothAdapter is the Bluetooth system service.  If
+    //   getDefaultAdapter() returns null
+    //   then the device is not BT capable.  Use BluetoothDevice.isEnabled()
+    //   to see if BT is enabled on the device.
+    //
+    // - BluetoothHeadset is the API for the control connection to a
+    //   Bluetooth Headset.  This lets you completely connect/disconnect a
+    //   headset (which we don't do from the Phone UI!) but also lets you
+    //   get the address of the currently active headset and see whether
+    //   it's currently connected.
+
+    /**
+     * @return true if the Bluetooth on/off switch in the UI should be
+     *         available to the user (i.e. if the device is BT-capable
+     *         and a headset is connected.)
+     */
+    boolean isBluetoothAvailable() {
+        Log.v(this, "isBluetoothAvailable()...");
+
+        // There's no need to ask the Bluetooth system service if BT is enabled:
+        //
+        //    BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+        //    if ((adapter == null) || !adapter.isEnabled()) {
+        //        Log.d(this, "  ==> FALSE (BT not enabled)");
+        //        return false;
+        //    }
+        //    Log.d(this, "  - BT enabled!  device name " + adapter.getName()
+        //                 + ", address " + adapter.getAddress());
+        //
+        // ...since we already have a BluetoothHeadset instance.  We can just
+        // call isConnected() on that, and assume it'll be false if BT isn't
+        // enabled at all.
+
+        // Check if there's a connected headset, using the BluetoothHeadset API.
+        boolean isConnected = false;
+        if (mBluetoothHeadset != null) {
+            List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
+
+            if (deviceList.size() > 0) {
+                isConnected = true;
+                for (int i = 0; i < deviceList.size(); i++) {
+                    BluetoothDevice device = deviceList.get(i);
+                    Log.v(this, "state = " + mBluetoothHeadset.getConnectionState(device)
+                            + "for headset: " + device);
+                }
+            }
+        }
+
+        Log.v(this, "  ==> " + isConnected);
+        return isConnected;
+    }
+
+    /**
+     * @return true if a BT Headset is available, and its audio is currently connected.
+     */
+    boolean isBluetoothAudioConnected() {
+        if (mBluetoothHeadset == null) {
+            Log.v(this, "isBluetoothAudioConnected: ==> FALSE (null mBluetoothHeadset)");
+            return false;
+        }
+        List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
+
+        if (deviceList.isEmpty()) {
+            return false;
+        }
+        for (int i = 0; i < deviceList.size(); i++) {
+            BluetoothDevice device = deviceList.get(i);
+            boolean isAudioOn = mBluetoothHeadset.isAudioConnected(device);
+            Log.v(this, "isBluetoothAudioConnected: ==> isAudioOn = " + isAudioOn
+                    + "for headset: " + device);
+            if (isAudioOn) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Helper method used to control the onscreen "Bluetooth" indication;
+     *
+     * @return true if a BT device is available and its audio is currently connected,
+     *              <b>or</b> if we issued a BluetoothHeadset.connectAudio()
+     *              call within the last 5 seconds (which presumably means
+     *              that the BT audio connection is currently being set
+     *              up, and will be connected soon.)
+     */
+    /* package */ boolean isBluetoothAudioConnectedOrPending() {
+        if (isBluetoothAudioConnected()) {
+            Log.v(this, "isBluetoothAudioConnectedOrPending: ==> TRUE (really connected)");
+            return true;
+        }
+
+        // If we issued a connectAudio() call "recently enough", even
+        // if BT isn't actually connected yet, let's still pretend BT is
+        // on.  This makes the onscreen indication more responsive.
+        if (mBluetoothConnectionPending) {
+            long timeSinceRequest =
+                    SystemClock.elapsedRealtime() - mBluetoothConnectionRequestTime;
+            if (timeSinceRequest < 5000 /* 5 seconds */) {
+                Log.v(this, "isBluetoothAudioConnectedOrPending: ==> TRUE (requested "
+                             + timeSinceRequest + " msec ago)");
+                return true;
+            } else {
+                Log.v(this, "isBluetoothAudioConnectedOrPending: ==> FALSE (request too old: "
+                             + timeSinceRequest + " msec ago)");
+                mBluetoothConnectionPending = false;
+                return false;
+            }
+        }
+
+        Log.v(this, "isBluetoothAudioConnectedOrPending: ==> FALSE");
+        return false;
+    }
+
+    /**
+     * Notified audio manager of a change to the bluetooth state.
+     */
+    void updateBluetoothState() {
+        mCallAudioManager.onBluetoothStateChange(this);
+    }
+
+    void connectBluetoothAudio() {
+        Log.v(this, "connectBluetoothAudio()...");
+        if (mBluetoothHeadset != null) {
+            mBluetoothHeadset.connectAudio();
+        }
+
+        // Watch out: The bluetooth connection doesn't happen instantly;
+        // the connectAudio() call returns instantly but does its real
+        // work in another thread.  The mBluetoothConnectionPending flag
+        // is just a little trickery to ensure that the onscreen UI updates
+        // instantly. (See isBluetoothAudioConnectedOrPending() above.)
+        mBluetoothConnectionPending = true;
+        mBluetoothConnectionRequestTime = SystemClock.elapsedRealtime();
+    }
+
+    void disconnectBluetoothAudio() {
+        Log.v(this, "disconnectBluetoothAudio()...");
+        if (mBluetoothHeadset != null) {
+            mBluetoothHeadset.disconnectAudio();
+        }
+        mBluetoothConnectionPending = false;
+    }
+
+    private void dumpBluetoothState() {
+        Log.d(this, "============== dumpBluetoothState() =============");
+        Log.d(this, "= isBluetoothAvailable: " + isBluetoothAvailable());
+        Log.d(this, "= isBluetoothAudioConnected: " + isBluetoothAudioConnected());
+        Log.d(this, "= isBluetoothAudioConnectedOrPending: " +
+                isBluetoothAudioConnectedOrPending());
+        Log.d(this, "=");
+        if (mBluetoothAdapter != null) {
+            if (mBluetoothHeadset != null) {
+                List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
+
+                if (deviceList.size() > 0) {
+                    BluetoothDevice device = deviceList.get(0);
+                    Log.d(this, "= BluetoothHeadset.getCurrentDevice: " + device);
+                    Log.d(this, "= BluetoothHeadset.State: "
+                        + mBluetoothHeadset.getConnectionState(device));
+                    Log.d(this, "= BluetoothHeadset audio connected: " +
+                        mBluetoothHeadset.isAudioConnected(device));
+                }
+            } else {
+                Log.d(this, "= mBluetoothHeadset is null");
+            }
+        } else {
+            Log.d(this, "= mBluetoothAdapter is null; device is not BT capable");
+        }
+    }
+}
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
new file mode 100644
index 0000000..6f7177b
--- /dev/null
+++ b/src/com/android/server/telecom/Call.java
@@ -0,0 +1,1232 @@
+/*
+ * Copyright (C) 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;
+
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.provider.ContactsContract.Contacts;
+import android.telecom.CallState;
+import android.telecom.Connection;
+import android.telecom.GatewayInfo;
+import android.telecom.ParcelableConnection;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.PhoneCapabilities;
+import android.telecom.Response;
+import android.telecom.StatusHints;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
+import android.telephony.DisconnectCause;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+
+import com.android.internal.telecom.IVideoProvider;
+import com.android.internal.telephony.CallerInfo;
+import com.android.internal.telephony.CallerInfoAsyncQuery;
+import com.android.internal.telephony.CallerInfoAsyncQuery.OnQueryCompleteListener;
+import com.android.internal.telephony.SmsApplication;
+import com.android.server.telecom.ContactsAsyncHelper.OnImageLoadCompleteListener;
+
+import com.google.common.base.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ *  Encapsulates all aspects of a given phone call throughout its lifecycle, starting
+ *  from the time the call intent was received by Telecom (vs. the time the call was
+ *  connected etc).
+ */
+final class Call implements CreateConnectionResponse {
+    /**
+     * Listener for events on the call.
+     */
+    interface Listener {
+        void onSuccessfulOutgoingCall(Call call, int callState);
+        void onFailedOutgoingCall(Call call, int errorCode, String errorMsg);
+        void onSuccessfulIncomingCall(Call call);
+        void onFailedIncomingCall(Call call);
+        void onRingbackRequested(Call call, boolean ringbackRequested);
+        void onPostDialWait(Call call, String remaining);
+        void onCallCapabilitiesChanged(Call call);
+        void onParentChanged(Call call);
+        void onChildrenChanged(Call call);
+        void onCannedSmsResponsesLoaded(Call call);
+        void onVideoCallProviderChanged(Call call);
+        void onCallerInfoChanged(Call call);
+        void onIsVoipAudioModeChanged(Call call);
+        void onStatusHintsChanged(Call call);
+        void onHandleChanged(Call call);
+        void onCallerDisplayNameChanged(Call call);
+        void onVideoStateChanged(Call call);
+        void onTargetPhoneAccountChanged(Call call);
+        void onConnectionManagerPhoneAccountChanged(Call call);
+        void onPhoneAccountChanged(Call call);
+        void onConferenceableCallsChanged(Call call);
+    }
+
+    abstract static class ListenerBase implements Listener {
+        @Override
+        public void onSuccessfulOutgoingCall(Call call, int callState) {}
+        @Override
+        public void onFailedOutgoingCall(Call call, int errorCode, String errorMsg) {}
+        @Override
+        public void onSuccessfulIncomingCall(Call call) {}
+        @Override
+        public void onFailedIncomingCall(Call call) {}
+        @Override
+        public void onRingbackRequested(Call call, boolean ringbackRequested) {}
+        @Override
+        public void onPostDialWait(Call call, String remaining) {}
+        @Override
+        public void onCallCapabilitiesChanged(Call call) {}
+        @Override
+        public void onParentChanged(Call call) {}
+        @Override
+        public void onChildrenChanged(Call call) {}
+        @Override
+        public void onCannedSmsResponsesLoaded(Call call) {}
+        @Override
+        public void onVideoCallProviderChanged(Call call) {}
+        @Override
+        public void onCallerInfoChanged(Call call) {}
+        @Override
+        public void onIsVoipAudioModeChanged(Call call) {}
+        @Override
+        public void onStatusHintsChanged(Call call) {}
+        @Override
+        public void onHandleChanged(Call call) {}
+        @Override
+        public void onCallerDisplayNameChanged(Call call) {}
+        @Override
+        public void onVideoStateChanged(Call call) {}
+        @Override
+        public void onTargetPhoneAccountChanged(Call call) {}
+        @Override
+        public void onConnectionManagerPhoneAccountChanged(Call call) {}
+        @Override
+        public void onPhoneAccountChanged(Call call) {}
+        @Override
+        public void onConferenceableCallsChanged(Call call) {}
+    }
+
+    private static final OnQueryCompleteListener sCallerInfoQueryListener =
+            new OnQueryCompleteListener() {
+                /** ${inheritDoc} */
+                @Override
+                public void onQueryComplete(int token, Object cookie, CallerInfo callerInfo) {
+                    if (cookie != null) {
+                        ((Call) cookie).setCallerInfo(callerInfo, token);
+                    }
+                }
+            };
+
+    private static final OnImageLoadCompleteListener sPhotoLoadListener =
+            new OnImageLoadCompleteListener() {
+                /** ${inheritDoc} */
+                @Override
+                public void onImageLoadComplete(
+                        int token, Drawable photo, Bitmap photoIcon, Object cookie) {
+                    if (cookie != null) {
+                        ((Call) cookie).setPhoto(photo, photoIcon, token);
+                    }
+                }
+            };
+
+    private final Runnable mDirectToVoicemailRunnable = new Runnable() {
+        @Override
+        public void run() {
+            processDirectToVoicemail();
+        }
+    };
+
+    /** True if this is an incoming call. */
+    private final boolean mIsIncoming;
+
+    /**
+     * The time this call was created. Beyond logging and such, may also be used for bookkeeping
+     * and specifically for marking certain call attempts as failed attempts.
+     */
+    private final long mCreationTimeMillis = System.currentTimeMillis();
+
+    /** The gateway information associated with this call. This stores the original call handle
+     * that the user is attempting to connect to via the gateway, the actual handle to dial in
+     * order to connect the call via the gateway, as well as the package name of the gateway
+     * service. */
+    private GatewayInfo mGatewayInfo;
+
+    private PhoneAccountHandle mConnectionManagerPhoneAccountHandle;
+
+    private PhoneAccountHandle mTargetPhoneAccountHandle;
+
+    private final Handler mHandler = new Handler();
+
+    private final List<Call> mConferenceableCalls = new ArrayList<>();
+
+    private PhoneAccountHandle mPhoneAccountHandle;
+
+    private long mConnectTimeMillis = 0;
+
+    /** The state of the call. */
+    private int mState;
+
+    /** The handle with which to establish this call. */
+    private Uri mHandle;
+
+    /**
+     * The presentation requirements for the handle. See {@link TelecomManager} for valid values.
+     */
+    private int mHandlePresentation;
+
+    /** The caller display name (CNAP) set by the connection service. */
+    private String mCallerDisplayName;
+
+    /**
+     * The presentation requirements for the handle. See {@link TelecomManager} for valid values.
+     */
+    private int mCallerDisplayNamePresentation;
+
+    /**
+     * The connection service which is attempted or already connecting this call.
+     */
+    private ConnectionServiceWrapper mConnectionService;
+
+    private boolean mIsEmergencyCall;
+
+    private boolean mSpeakerphoneOn;
+
+    /**
+     * Tracks the video states which were applicable over the duration of a call.
+     * See {@link VideoProfile} for a list of valid video states.
+     */
+    private int mVideoStateHistory;
+
+    private int mVideoState;
+
+    /**
+     * Disconnect cause for the call. Only valid if the state of the call is STATE_DISCONNECTED.
+     * See {@link android.telephony.DisconnectCause}.
+     */
+    private int mDisconnectCause = DisconnectCause.NOT_VALID;
+
+    /**
+     * Additional disconnect information provided by the connection service.
+     */
+    private String mDisconnectMessage;
+
+    /** Info used by the connection services. */
+    private Bundle mExtras = Bundle.EMPTY;
+
+    /** Set of listeners on this call.
+     *
+     * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
+     * load factor before resizing, 1 means we only expect a single thread to
+     * access the map so make only a single shard
+     */
+    private final Set<Listener> mListeners = Collections.newSetFromMap(
+            new ConcurrentHashMap<Listener, Boolean>(8, 0.9f, 1));
+
+    private CreateConnectionProcessor mCreateConnectionProcessor;
+
+    /** Caller information retrieved from the latest contact query. */
+    private CallerInfo mCallerInfo;
+
+    /** The latest token used with a contact info query. */
+    private int mQueryToken = 0;
+
+    /** Whether this call is requesting that Telecom play the ringback tone on its behalf. */
+    private boolean mRingbackRequested = false;
+
+    /** Whether direct-to-voicemail query is pending. */
+    private boolean mDirectToVoicemailQueryPending;
+
+    private int mCallCapabilities;
+
+    private boolean mIsConference = false;
+
+    private Call mParentCall = null;
+
+    private List<Call> mChildCalls = new LinkedList<>();
+
+    /** Set of text message responses allowed for this call, if applicable. */
+    private List<String> mCannedSmsResponses = Collections.EMPTY_LIST;
+
+    /** Whether an attempt has been made to load the text message responses. */
+    private boolean mCannedSmsResponsesLoadingStarted = false;
+
+    private IVideoProvider mVideoProvider;
+
+    private boolean mIsVoipAudioMode;
+    private StatusHints mStatusHints;
+    private final ConnectionServiceRepository mRepository;
+
+    /**
+     * Persists the specified parameters and initializes the new instance.
+     *
+     * @param handle The handle to dial.
+     * @param gatewayInfo Gateway information to use for the call.
+     * @param connectionManagerPhoneAccountHandle Account to use for the service managing the call.
+     *         This account must be one that was registered with the
+     *         {@link PhoneAccount#CAPABILITY_CONNECTION_MANAGER} flag.
+     * @param targetPhoneAccountHandle Account information to use for the call. This account must be
+     *         one that was registered with the {@link PhoneAccount#CAPABILITY_CALL_PROVIDER} flag.
+     * @param isIncoming True if this is an incoming call.
+     */
+    Call(
+            ConnectionServiceRepository repository,
+            Uri handle,
+            GatewayInfo gatewayInfo,
+            PhoneAccountHandle connectionManagerPhoneAccountHandle,
+            PhoneAccountHandle targetPhoneAccountHandle,
+            boolean isIncoming,
+            boolean isConference) {
+        mState = isConference ? CallState.ACTIVE : CallState.NEW;
+        mRepository = repository;
+        setHandle(handle);
+        setHandle(handle, TelecomManager.PRESENTATION_ALLOWED);
+        mGatewayInfo = gatewayInfo;
+        mConnectionManagerPhoneAccountHandle = connectionManagerPhoneAccountHandle;
+        mTargetPhoneAccountHandle = targetPhoneAccountHandle;
+        mIsIncoming = isIncoming;
+        mIsConference = isConference;
+        maybeLoadCannedSmsResponses();
+    }
+
+    void addListener(Listener listener) {
+        mListeners.add(listener);
+    }
+
+    void removeListener(Listener listener) {
+        if (listener != null) {
+            mListeners.remove(listener);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        String component = null;
+        if (mConnectionService != null && mConnectionService.getComponentName() != null) {
+            component = mConnectionService.getComponentName().flattenToShortString();
+        }
+
+        return String.format(Locale.US, "[%s, %s, %s, %s, %d]", System.identityHashCode(this),
+                mState, component, Log.piiHandle(mHandle), getVideoState());
+    }
+
+    int getState() {
+        return mState;
+    }
+
+    /**
+     * Sets the call state. Although there exists the notion of appropriate state transitions
+     * (see {@link CallState}), in practice those expectations break down when cellular systems
+     * 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) {
+        if (mState != newState) {
+            Log.v(this, "setState %s -> %s", mState, newState);
+            mState = newState;
+            maybeLoadCannedSmsResponses();
+
+            if (mState == CallState.DISCONNECTED) {
+                fixParentAfterDisconnect();
+            }
+        }
+    }
+
+    void setRingbackRequested(boolean ringbackRequested) {
+        mRingbackRequested = ringbackRequested;
+        for (Listener l : mListeners) {
+            l.onRingbackRequested(this, mRingbackRequested);
+        }
+    }
+
+    boolean isRingbackRequested() {
+        return mRingbackRequested;
+    }
+
+    boolean isConference() {
+        return mIsConference;
+    }
+
+    Uri getHandle() {
+        return mHandle;
+    }
+
+    int getHandlePresentation() {
+        return mHandlePresentation;
+    }
+
+
+    void setHandle(Uri handle) {
+        setHandle(handle, TelecomManager.PRESENTATION_ALLOWED);
+    }
+
+    void setHandle(Uri handle, int presentation) {
+        if (!Objects.equals(handle, mHandle) || presentation != mHandlePresentation) {
+            mHandle = handle;
+            mHandlePresentation = presentation;
+            mIsEmergencyCall = mHandle != null && PhoneNumberUtils.isLocalEmergencyNumber(
+                    TelecomApp.getInstance(), mHandle.getSchemeSpecificPart());
+            startCallerInfoLookup();
+            for (Listener l : mListeners) {
+                l.onHandleChanged(this);
+            }
+        }
+    }
+
+    String getCallerDisplayName() {
+        return mCallerDisplayName;
+    }
+
+    int getCallerDisplayNamePresentation() {
+        return mCallerDisplayNamePresentation;
+    }
+
+    void setCallerDisplayName(String callerDisplayName, int presentation) {
+        if (!TextUtils.equals(callerDisplayName, mCallerDisplayName) ||
+                presentation != mCallerDisplayNamePresentation) {
+            mCallerDisplayName = callerDisplayName;
+            mCallerDisplayNamePresentation = presentation;
+            for (Listener l : mListeners) {
+                l.onCallerDisplayNameChanged(this);
+            }
+        }
+    }
+
+    String getName() {
+        return mCallerInfo == null ? null : mCallerInfo.name;
+    }
+
+    Bitmap getPhotoIcon() {
+        return mCallerInfo == null ? null : mCallerInfo.cachedPhotoIcon;
+    }
+
+    Drawable getPhoto() {
+        return mCallerInfo == null ? null : mCallerInfo.cachedPhoto;
+    }
+
+    /**
+     * @param disconnectCause The reason for the disconnection, any of
+     *         {@link android.telephony.DisconnectCause}.
+     * @param disconnectMessage Optional message about the disconnect.
+     */
+    void setDisconnectCause(int disconnectCause, String disconnectMessage) {
+        // TODO: Consider combining this method with a setDisconnected() method that is totally
+        // separate from setState.
+        mDisconnectCause = disconnectCause;
+        mDisconnectMessage = disconnectMessage;
+    }
+
+    int getDisconnectCause() {
+        return mDisconnectCause;
+    }
+
+    String getDisconnectMessage() {
+        return mDisconnectMessage;
+    }
+
+    boolean isEmergencyCall() {
+        return mIsEmergencyCall;
+    }
+
+    /**
+     * @return The original handle this call is associated with. In-call services should use this
+     * handle when indicating in their UI the handle that is being called.
+     */
+    public Uri getOriginalHandle() {
+        if (mGatewayInfo != null && !mGatewayInfo.isEmpty()) {
+            return mGatewayInfo.getOriginalAddress();
+        }
+        return getHandle();
+    }
+
+    GatewayInfo getGatewayInfo() {
+        return mGatewayInfo;
+    }
+
+    void setGatewayInfo(GatewayInfo gatewayInfo) {
+        mGatewayInfo = gatewayInfo;
+    }
+
+    PhoneAccountHandle getConnectionManagerPhoneAccount() {
+        return mConnectionManagerPhoneAccountHandle;
+    }
+
+    void setConnectionManagerPhoneAccount(PhoneAccountHandle accountHandle) {
+        if (!Objects.equals(mConnectionManagerPhoneAccountHandle, accountHandle)) {
+            mConnectionManagerPhoneAccountHandle = accountHandle;
+            for (Listener l : mListeners) {
+                l.onConnectionManagerPhoneAccountChanged(this);
+            }
+        }
+
+    }
+
+    PhoneAccountHandle getTargetPhoneAccount() {
+        return mTargetPhoneAccountHandle;
+    }
+
+    void setTargetPhoneAccount(PhoneAccountHandle accountHandle) {
+        if (!Objects.equals(mTargetPhoneAccountHandle, accountHandle)) {
+            mTargetPhoneAccountHandle = accountHandle;
+            for (Listener l : mListeners) {
+                l.onTargetPhoneAccountChanged(this);
+            }
+        }
+    }
+
+    boolean isIncoming() {
+        return mIsIncoming;
+    }
+
+    /**
+     * @return The "age" of this call object in milliseconds, which typically also represents the
+     *     period since this call was added to the set pending outgoing calls, see
+     *     mCreationTimeMillis.
+     */
+    long getAgeMillis() {
+        return System.currentTimeMillis() - mCreationTimeMillis;
+    }
+
+    /**
+     * @return The time when this call object was created and added to the set of pending outgoing
+     *     calls.
+     */
+    long getCreationTimeMillis() {
+        return mCreationTimeMillis;
+    }
+
+    long getConnectTimeMillis() {
+        return mConnectTimeMillis;
+    }
+
+    void setConnectTimeMillis(long connectTimeMillis) {
+        mConnectTimeMillis = connectTimeMillis;
+    }
+
+    int getCallCapabilities() {
+        return mCallCapabilities;
+    }
+
+    void setCallCapabilities(int callCapabilities) {
+        Log.v(this, "setCallCapabilities: %s", PhoneCapabilities.toString(callCapabilities));
+        if (mCallCapabilities != callCapabilities) {
+           mCallCapabilities = callCapabilities;
+            for (Listener l : mListeners) {
+                l.onCallCapabilitiesChanged(this);
+            }
+        }
+    }
+
+    Call getParentCall() {
+        return mParentCall;
+    }
+
+    List<Call> getChildCalls() {
+        return mChildCalls;
+    }
+
+    ConnectionServiceWrapper getConnectionService() {
+        return mConnectionService;
+    }
+
+    void setConnectionService(ConnectionServiceWrapper service) {
+        Preconditions.checkNotNull(service);
+
+        clearConnectionService();
+
+        service.incrementAssociatedCallCount();
+        mConnectionService = service;
+        mConnectionService.addCall(this);
+    }
+
+    /**
+     * Clears the associated connection service.
+     */
+    void clearConnectionService() {
+        if (mConnectionService != null) {
+            ConnectionServiceWrapper serviceTemp = mConnectionService;
+            mConnectionService = null;
+            serviceTemp.removeCall(this);
+
+            // Decrementing the count can cause the service to unbind, which itself can trigger the
+            // service-death code.  Since the service death code tries to clean up any associated
+            // calls, we need to make sure to remove that information (e.g., removeCall()) before
+            // we decrement. Technically, invoking removeCall() prior to decrementing is all that is
+            // necessary, but cleaning up mConnectionService prior to triggering an unbind is good
+            // to do.
+            decrementAssociatedCallCount(serviceTemp);
+        }
+    }
+
+    private void processDirectToVoicemail() {
+        if (mDirectToVoicemailQueryPending) {
+            if (mCallerInfo != null && mCallerInfo.shouldSendToVoicemail) {
+                Log.i(this, "Directing call to voicemail: %s.", this);
+                // TODO: Once we move State handling from CallsManager to Call, we
+                // will not need to set STATE_RINGING state prior to calling reject.
+                setState(CallState.RINGING);
+                reject(false, null);
+            } else {
+                // TODO: Make this class (not CallsManager) responsible for changing
+                // the call state to STATE_RINGING.
+
+                // TODO: Replace this with state transition to STATE_RINGING.
+                for (Listener l : mListeners) {
+                    l.onSuccessfulIncomingCall(this);
+                }
+            }
+
+            mDirectToVoicemailQueryPending = false;
+        }
+    }
+
+    /**
+     * Starts the create connection sequence. Upon completion, there should exist an active
+     * connection through a connection service (or the call will have failed).
+     */
+    void startCreateConnection() {
+        Preconditions.checkState(mCreateConnectionProcessor == null);
+        mCreateConnectionProcessor = new CreateConnectionProcessor(this, mRepository, this);
+        mCreateConnectionProcessor.process();
+    }
+
+    @Override
+    public void handleCreateConnectionSuccess(
+            CallIdMapper idMapper,
+            ParcelableConnection connection) {
+        Log.v(this, "handleCreateConnectionSuccessful %s", connection);
+        mCreateConnectionProcessor = null;
+        setTargetPhoneAccount(connection.getPhoneAccount());
+        setHandle(connection.getHandle(), connection.getHandlePresentation());
+        setCallerDisplayName(
+                connection.getCallerDisplayName(), connection.getCallerDisplayNamePresentation());
+        setCallCapabilities(connection.getCapabilities());
+        setVideoProvider(connection.getVideoProvider());
+        setVideoState(connection.getVideoState());
+        setRingbackRequested(connection.isRingbackRequested());
+        setIsVoipAudioMode(connection.getIsVoipAudioMode());
+        setStatusHints(connection.getStatusHints());
+
+        mConferenceableCalls.clear();
+        for (String id : connection.getConferenceableConnectionIds()) {
+            mConferenceableCalls.add(idMapper.getCall(id));
+        }
+
+        if (mIsIncoming) {
+            // We do not handle incoming calls immediately when they are verified by the connection
+            // service. We allow the caller-info-query code to execute first so that we can read the
+            // direct-to-voicemail property before deciding if we want to show the incoming call to
+            // the user or if we want to reject the call.
+            mDirectToVoicemailQueryPending = true;
+
+            // Timeout the direct-to-voicemail lookup execution so that we dont wait too long before
+            // showing the user the incoming call screen.
+            mHandler.postDelayed(mDirectToVoicemailRunnable, Timeouts.getDirectToVoicemailMillis());
+        } else {
+            for (Listener l : mListeners) {
+                l.onSuccessfulOutgoingCall(this,
+                        getStateFromConnectionState(connection.getState()));
+            }
+        }
+    }
+
+    @Override
+    public void handleCreateConnectionFailure(int code, String msg) {
+        mCreateConnectionProcessor = null;
+        clearConnectionService();
+        setDisconnectCause(code, msg);
+        CallsManager.getInstance().markCallAsDisconnected(this, code, msg);
+
+        if (mIsIncoming) {
+            for (Listener listener : mListeners) {
+                listener.onFailedIncomingCall(this);
+            }
+        } else {
+            for (Listener listener : mListeners) {
+                listener.onFailedOutgoingCall(this, code, msg);
+            }
+        }
+    }
+
+    /**
+     * Plays the specified DTMF tone.
+     */
+    void playDtmfTone(char digit) {
+        if (mConnectionService == null) {
+            Log.w(this, "playDtmfTone() request on a call without a connection service.");
+        } else {
+            Log.i(this, "Send playDtmfTone to connection service for call %s", this);
+            mConnectionService.playDtmfTone(this, digit);
+        }
+    }
+
+    /**
+     * Stops playing any currently playing DTMF tone.
+     */
+    void stopDtmfTone() {
+        if (mConnectionService == null) {
+            Log.w(this, "stopDtmfTone() request on a call without a connection service.");
+        } else {
+            Log.i(this, "Send stopDtmfTone to connection service for call %s", this);
+            mConnectionService.stopDtmfTone(this);
+        }
+    }
+
+    /**
+     * Attempts to disconnect the call through the connection service.
+     */
+    void disconnect() {
+        if (mState == CallState.NEW || mState == CallState.PRE_DIAL_WAIT ||
+                mState == CallState.CONNECTING) {
+            Log.v(this, "Aborting call %s", this);
+            abort();
+        } else if (mState != CallState.ABORTED && mState != CallState.DISCONNECTED) {
+            if (mConnectionService == null) {
+                Log.e(this, new Exception(), "disconnect() request on a call without a"
+                        + " connection service.");
+            } else {
+                Log.i(this, "Send disconnect to connection service for call: %s", this);
+                // The call isn't officially disconnected until the connection service
+                // confirms that the call was actually disconnected. Only then is the
+                // association between call and connection service severed, see
+                // {@link CallsManager#markCallAsDisconnected}.
+                mConnectionService.disconnect(this);
+            }
+        }
+    }
+
+    void abort() {
+        if (mCreateConnectionProcessor != null) {
+            mCreateConnectionProcessor.abort();
+        } else if (mState == CallState.NEW || mState == CallState.PRE_DIAL_WAIT
+                || mState == CallState.CONNECTING) {
+            handleCreateConnectionFailure(DisconnectCause.OUTGOING_CANCELED, null);
+        } else {
+            Log.v(this, "Cannot abort a call which isn't either PRE_DIAL_WAIT or CONNECTING");
+        }
+    }
+
+    /**
+     * Answers the call if it is ringing.
+     *
+     * @param videoState The video state in which to answer the call.
+     */
+    void answer(int videoState) {
+        Preconditions.checkNotNull(mConnectionService);
+
+        // Check to verify that the call is still in the ringing state. A call can change states
+        // between the time the user hits 'answer' and Telecom receives the command.
+        if (isRinging("answer")) {
+            // At this point, we are asking the connection service to answer but we don't assume
+            // that it will work. Instead, we wait until confirmation from the connectino service
+            // that the call is in a non-STATE_RINGING state before changing the UI. See
+            // {@link ConnectionServiceAdapter#setActive} and other set* methods.
+            mConnectionService.answer(this, videoState);
+        }
+    }
+
+    /**
+     * Rejects the call if it is ringing.
+     *
+     * @param rejectWithMessage Whether to send a text message as part of the call rejection.
+     * @param textMessage An optional text message to send as part of the rejection.
+     */
+    void reject(boolean rejectWithMessage, String textMessage) {
+        Preconditions.checkNotNull(mConnectionService);
+
+        // Check to verify that the call is still in the ringing state. A call can change states
+        // between the time the user hits 'reject' and Telecomm receives the command.
+        if (isRinging("reject")) {
+            mConnectionService.reject(this);
+        }
+    }
+
+    /**
+     * Puts the call on hold if it is currently active.
+     */
+    void hold() {
+        Preconditions.checkNotNull(mConnectionService);
+
+        if (mState == CallState.ACTIVE) {
+            mConnectionService.hold(this);
+        }
+    }
+
+    /**
+     * Releases the call from hold if it is currently active.
+     */
+    void unhold() {
+        Preconditions.checkNotNull(mConnectionService);
+
+        if (mState == CallState.ON_HOLD) {
+            mConnectionService.unhold(this);
+        }
+    }
+
+    /** Checks if this is a live call or not. */
+    boolean isAlive() {
+        switch (mState) {
+            case CallState.NEW:
+            case CallState.RINGING:
+            case CallState.DISCONNECTED:
+            case CallState.ABORTED:
+                return false;
+            default:
+                return true;
+        }
+    }
+
+    boolean isActive() {
+        return mState == CallState.ACTIVE;
+    }
+
+    Bundle getExtras() {
+        return mExtras;
+    }
+
+    void setExtras(Bundle extras) {
+        mExtras = extras;
+    }
+
+    /**
+     * @return the uri of the contact associated with this call.
+     */
+    Uri getContactUri() {
+        if (mCallerInfo == null || !mCallerInfo.contactExists) {
+            return null;
+        }
+        return Contacts.getLookupUri(mCallerInfo.contactIdOrZero, mCallerInfo.lookupKey);
+    }
+
+    Uri getRingtone() {
+        return mCallerInfo == null ? null : mCallerInfo.contactRingtoneUri;
+    }
+
+    void onPostDialWait(String remaining) {
+        for (Listener l : mListeners) {
+            l.onPostDialWait(this, remaining);
+        }
+    }
+
+    void postDialContinue(boolean proceed) {
+        mConnectionService.onPostDialContinue(this, proceed);
+    }
+
+    void conferenceWith(Call otherCall) {
+        if (mConnectionService == null) {
+            Log.w(this, "conference requested on a call without a connection service.");
+        } else {
+            mConnectionService.conference(this, otherCall);
+        }
+    }
+
+    void splitFromConference() {
+        if (mConnectionService == null) {
+            Log.w(this, "splitting from conference call without a connection service");
+        } else {
+            mConnectionService.splitFromConference(this);
+        }
+    }
+
+    void mergeConference() {
+        if (mConnectionService == null) {
+            Log.w(this, "merging conference calls without a connection service.");
+        } else if (can(PhoneCapabilities.MERGE_CONFERENCE)) {
+            mConnectionService.mergeConference(this);
+        }
+    }
+
+    void swapConference() {
+        if (mConnectionService == null) {
+            Log.w(this, "swapping conference calls without a connection service.");
+        } else if (can(PhoneCapabilities.SWAP_CONFERENCE)) {
+            mConnectionService.swapConference(this);
+        }
+    }
+
+    void setParentCall(Call parentCall) {
+        if (parentCall == this) {
+            Log.e(this, new Exception(), "setting the parent to self");
+            return;
+        }
+        if (parentCall == mParentCall) {
+            // nothing to do
+            return;
+        }
+        Preconditions.checkState(parentCall == null || mParentCall == null);
+
+        Call oldParent = mParentCall;
+        if (mParentCall != null) {
+            mParentCall.removeChildCall(this);
+        }
+        mParentCall = parentCall;
+        if (mParentCall != null) {
+            mParentCall.addChildCall(this);
+        }
+
+        for (Listener l : mListeners) {
+            l.onParentChanged(this);
+        }
+    }
+
+    void setConferenceableCalls(List<Call> conferenceableCalls) {
+        mConferenceableCalls.clear();
+        mConferenceableCalls.addAll(conferenceableCalls);
+    }
+
+    List<Call> getConferenceableCalls() {
+        return mConferenceableCalls;
+    }
+
+    private boolean can(int capability) {
+        return (mCallCapabilities & capability) == capability;
+    }
+
+    private void addChildCall(Call call) {
+        if (!mChildCalls.contains(call)) {
+            mChildCalls.add(call);
+
+            for (Listener l : mListeners) {
+                l.onChildrenChanged(this);
+            }
+        }
+    }
+
+    private void removeChildCall(Call call) {
+        if (mChildCalls.remove(call)) {
+            for (Listener l : mListeners) {
+                l.onChildrenChanged(this);
+            }
+        }
+    }
+
+    /**
+     * Return whether the user can respond to this {@code Call} via an SMS message.
+     *
+     * @return true if the "Respond via SMS" feature should be enabled
+     * for this incoming call.
+     *
+     * The general rule is that we *do* allow "Respond via SMS" except for
+     * the few (relatively rare) cases where we know for sure it won't
+     * work, namely:
+     *   - a bogus or blank incoming number
+     *   - a call from a SIP address
+     *   - a "call presentation" that doesn't allow the number to be revealed
+     *
+     * In all other cases, we allow the user to respond via SMS.
+     *
+     * Note that this behavior isn't perfect; for example we have no way
+     * to detect whether the incoming call is from a landline (with most
+     * networks at least), so we still enable this feature even though
+     * SMSes to that number will silently fail.
+     */
+    boolean isRespondViaSmsCapable() {
+        if (mState != CallState.RINGING) {
+            return false;
+        }
+
+        if (getHandle() == null) {
+            // No incoming number known or call presentation is "PRESENTATION_RESTRICTED", in
+            // other words, the user should not be able to see the incoming phone number.
+            return false;
+        }
+
+        if (PhoneNumberUtils.isUriNumber(getHandle().toString())) {
+            // The incoming number is actually a URI (i.e. a SIP address),
+            // not a regular PSTN phone number, and we can't send SMSes to
+            // SIP addresses.
+            // (TODO: That might still be possible eventually, though. Is
+            // there some SIP-specific equivalent to sending a text message?)
+            return false;
+        }
+
+        // Is there a valid SMS application on the phone?
+        if (SmsApplication.getDefaultRespondViaMessageApplication(TelecomApp.getInstance(),
+                true /*updateIfNeeded*/) == null) {
+            return false;
+        }
+
+        // TODO: with some carriers (in certain countries) you *can* actually
+        // tell whether a given number is a mobile phone or not. So in that
+        // case we could potentially return false here if the incoming call is
+        // from a land line.
+
+        // If none of the above special cases apply, it's OK to enable the
+        // "Respond via SMS" feature.
+        return true;
+    }
+
+    List<String> getCannedSmsResponses() {
+        return mCannedSmsResponses;
+    }
+
+    /**
+     * We need to make sure that before we move a call to the disconnected state, it no
+     * longer has any parent/child relationships.  We want to do this to ensure that the InCall
+     * Service always has the right data in the right order.  We also want to do it in telecom so
+     * that the insurance policy lives in the framework side of things.
+     */
+    private void fixParentAfterDisconnect() {
+        setParentCall(null);
+    }
+
+    /**
+     * @return True if the call is ringing, else logs the action name.
+     */
+    private boolean isRinging(String actionName) {
+        if (mState == CallState.RINGING) {
+            return true;
+        }
+
+        Log.i(this, "Request to %s a non-ringing call %s", actionName, this);
+        return false;
+    }
+
+    @SuppressWarnings("rawtypes")
+    private void decrementAssociatedCallCount(ServiceBinder binder) {
+        if (binder != null) {
+            binder.decrementAssociatedCallCount();
+        }
+    }
+
+    /**
+     * Looks up contact information based on the current handle.
+     */
+    private void startCallerInfoLookup() {
+        String number = mHandle == null ? null : mHandle.getSchemeSpecificPart();
+
+        mQueryToken++;  // Updated so that previous queries can no longer set the information.
+        mCallerInfo = null;
+        if (!TextUtils.isEmpty(number)) {
+            Log.v(this, "Looking up information for: %s.", Log.piiHandle(number));
+            CallerInfoAsyncQuery.startQuery(
+                    mQueryToken,
+                    TelecomApp.getInstance(),
+                    number,
+                    sCallerInfoQueryListener,
+                    this);
+        }
+    }
+
+    /**
+     * Saves the specified caller info if the specified token matches that of the last query
+     * that was made.
+     *
+     * @param callerInfo The new caller information to set.
+     * @param token The token used with this query.
+     */
+    private void setCallerInfo(CallerInfo callerInfo, int token) {
+        Preconditions.checkNotNull(callerInfo);
+
+        if (mQueryToken == token) {
+            mCallerInfo = callerInfo;
+            Log.i(this, "CallerInfo received for %s: %s", Log.piiHandle(mHandle), callerInfo);
+
+            if (mCallerInfo.contactDisplayPhotoUri != null) {
+                Log.d(this, "Searching person uri %s for call %s",
+                        mCallerInfo.contactDisplayPhotoUri, this);
+                ContactsAsyncHelper.startObtainPhotoAsync(
+                        token,
+                        TelecomApp.getInstance(),
+                        mCallerInfo.contactDisplayPhotoUri,
+                        sPhotoLoadListener,
+                        this);
+                // Do not call onCallerInfoChanged yet in this case.  We call it in setPhoto().
+            } else {
+                for (Listener l : mListeners) {
+                    l.onCallerInfoChanged(this);
+                }
+            }
+
+            processDirectToVoicemail();
+        }
+    }
+
+    CallerInfo getCallerInfo() {
+        return mCallerInfo;
+    }
+
+    /**
+     * Saves the specified photo information if the specified token matches that of the last query.
+     *
+     * @param photo The photo as a drawable.
+     * @param photoIcon The photo as a small icon.
+     * @param token The token used with this query.
+     */
+    private void setPhoto(Drawable photo, Bitmap photoIcon, int token) {
+        if (mQueryToken == token) {
+            mCallerInfo.cachedPhoto = photo;
+            mCallerInfo.cachedPhotoIcon = photoIcon;
+
+            for (Listener l : mListeners) {
+                l.onCallerInfoChanged(this);
+            }
+        }
+    }
+
+    private void maybeLoadCannedSmsResponses() {
+        if (mIsIncoming && isRespondViaSmsCapable() && !mCannedSmsResponsesLoadingStarted) {
+            Log.d(this, "maybeLoadCannedSmsResponses: starting task to load messages");
+            mCannedSmsResponsesLoadingStarted = true;
+            RespondViaSmsManager.getInstance().loadCannedTextMessages(
+                    new Response<Void, List<String>>() {
+                        @Override
+                        public void onResult(Void request, List<String>... result) {
+                            if (result.length > 0) {
+                                Log.d(this, "maybeLoadCannedSmsResponses: got %s", result[0]);
+                                mCannedSmsResponses = result[0];
+                                for (Listener l : mListeners) {
+                                    l.onCannedSmsResponsesLoaded(Call.this);
+                                }
+                            }
+                        }
+
+                        @Override
+                        public void onError(Void request, int code, String msg) {
+                            Log.w(Call.this, "Error obtaining canned SMS responses: %d %s", code,
+                                    msg);
+                        }
+                    }
+            );
+        } else {
+            Log.d(this, "maybeLoadCannedSmsResponses: doing nothing");
+        }
+    }
+
+    /**
+     * Sets speakerphone option on when call begins.
+     */
+    public void setStartWithSpeakerphoneOn(boolean startWithSpeakerphone) {
+        mSpeakerphoneOn = startWithSpeakerphone;
+    }
+
+    /**
+     * Returns speakerphone option.
+     *
+     * @return Whether or not speakerphone should be set automatically when call begins.
+     */
+    public boolean getStartWithSpeakerphoneOn() {
+        return mSpeakerphoneOn;
+    }
+
+    /**
+     * Sets a video call provider for the call.
+     */
+    public void setVideoProvider(IVideoProvider videoProvider) {
+        mVideoProvider = videoProvider;
+        for (Listener l : mListeners) {
+            l.onVideoCallProviderChanged(Call.this);
+        }
+    }
+
+    /**
+     * @return Return the {@link Connection.VideoProvider} binder.
+     */
+    public IVideoProvider getVideoProvider() {
+        return mVideoProvider;
+    }
+
+    /**
+     * The current video state for the call.
+     * Valid values: see {@link VideoProfile.VideoState}.
+     */
+    public int getVideoState() {
+        return mVideoState;
+    }
+
+    /**
+     * Returns the video states which were applicable over the duration of a call.
+     * See {@link VideoProfile} for a list of valid video states.
+     *
+     * @return The video states applicable over the duration of the call.
+     */
+    public int getVideoStateHistory() {
+        return mVideoStateHistory;
+    }
+
+    /**
+     * Determines the current video state for the call.
+     * For an outgoing call determines the desired video state for the call.
+     * Valid values: see {@link VideoProfile.VideoState}
+     *
+     * @param videoState The video state for the call.
+     */
+    public void setVideoState(int videoState) {
+        // Track which video states were applicable over the duration of the call.
+        mVideoStateHistory = mVideoStateHistory | videoState;
+
+        mVideoState = videoState;
+        for (Listener l : mListeners) {
+            l.onVideoStateChanged(this);
+        }
+    }
+
+    public boolean getIsVoipAudioMode() {
+        return mIsVoipAudioMode;
+    }
+
+    public void setIsVoipAudioMode(boolean audioModeIsVoip) {
+        mIsVoipAudioMode = audioModeIsVoip;
+        for (Listener l : mListeners) {
+            l.onIsVoipAudioModeChanged(this);
+        }
+    }
+
+    public StatusHints getStatusHints() {
+        return mStatusHints;
+    }
+
+    public void setStatusHints(StatusHints statusHints) {
+        mStatusHints = statusHints;
+        for (Listener l : mListeners) {
+            l.onStatusHintsChanged(this);
+        }
+    }
+
+    static int getStateFromConnectionState(int state) {
+        switch (state) {
+            case Connection.STATE_INITIALIZING:
+                return CallState.CONNECTING;
+            case Connection.STATE_ACTIVE:
+                return CallState.ACTIVE;
+            case Connection.STATE_DIALING:
+                return CallState.DIALING;
+            case Connection.STATE_DISCONNECTED:
+                return CallState.DISCONNECTED;
+            case Connection.STATE_HOLDING:
+                return CallState.ON_HOLD;
+            case Connection.STATE_NEW:
+                return CallState.NEW;
+            case Connection.STATE_RINGING:
+                return CallState.RINGING;
+        }
+        return CallState.DISCONNECTED;
+    }
+}
diff --git a/src/com/android/server/telecom/CallActivity.java b/src/com/android/server/telecom/CallActivity.java
new file mode 100644
index 0000000..96ffb96
--- /dev/null
+++ b/src/com/android/server/telecom/CallActivity.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.UserManager;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telephony.DisconnectCause;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.widget.Toast;
+
+/**
+ * Activity that handles system CALL actions and forwards them to {@link CallsManager}.
+ * Handles all three CALL action types: CALL, CALL_PRIVILEGED, and CALL_EMERGENCY.
+ *
+ * Pre-L, the only way apps were were allowed to make outgoing emergency calls was the
+ * ACTION_CALL_PRIVILEGED action (which requires the system only CALL_PRIVILEGED permission).
+ *
+ * In L, any app that has the CALL_PRIVILEGED permission can continue to make outgoing emergency
+ * calls via ACTION_CALL_PRIVILEGED.
+ *
+ * In addition, the default dialer (identified via {@link android.telecom.TelecomManager#getDefaultPhoneApp()}
+ * will also be granted the ability to make emergency outgoing calls using the CALL action. In
+ * order to do this, it must call startActivityForResult on the CALL intent to allow its package
+ * name to be passed to {@link CallActivity}. Calling startActivity will continue to work on all
+ * non-emergency numbers just like it did pre-L.
+ */
+public class CallActivity extends Activity {
+
+    private CallsManager mCallsManager = CallsManager.getInstance();
+    private boolean mIsVoiceCapable;
+
+    /**
+     * {@inheritDoc}
+     *
+     * This method is the single point of entry for the CALL intent, which is used by built-in apps
+     * like Contacts & Dialer, as well as 3rd party apps to initiate outgoing calls.
+     */
+    @Override
+    protected void onCreate(Bundle bundle) {
+        super.onCreate(bundle);
+
+        mIsVoiceCapable = isVoiceCapable();
+
+        // TODO: This activity will be displayed until the next screen which could be
+        // the in-call UI and error dialog or potentially a call-type selection dialog.
+        // Traditionally, this has been a black screen with a spinner. We need to reevaluate if this
+        // is still desired and add back if necessary. Currently, the activity is set to NoDisplay
+        // theme which means it shows no UI.
+
+        Intent intent = getIntent();
+        Configuration configuration = getResources().getConfiguration();
+
+        Log.d(this, "onCreate: this = %s, bundle = %s", this, bundle);
+        Log.d(this, " - intent = %s", intent);
+        Log.d(this, " - configuration = %s", configuration);
+
+        // TODO: Figure out if there is something to restore from bundle.
+        // See OutgoingCallBroadcaster in services/Telephony for more.
+
+        processIntent(intent);
+
+        // This activity does not have associated UI, so close.
+        finish();
+
+        Log.d(this, "onCreate: end");
+    }
+
+    /**
+     * Processes intents sent to the activity.
+     *
+     * @param intent The intent.
+     */
+    private void processIntent(Intent intent) {
+        // Ensure call intents are not processed on devices that are not capable of calling.
+        if (!mIsVoiceCapable) {
+            setResult(RESULT_CANCELED);
+            return;
+        }
+
+        String action = intent.getAction();
+
+        // TODO: Check for non-voice capable devices before reading any intents.
+
+        if (Intent.ACTION_CALL.equals(action) ||
+                Intent.ACTION_CALL_PRIVILEGED.equals(action) ||
+                Intent.ACTION_CALL_EMERGENCY.equals(action)) {
+            processOutgoingCallIntent(intent);
+        } else if (TelecomManager.ACTION_INCOMING_CALL.equals(action)) {
+            processIncomingCallIntent(intent);
+        }
+    }
+
+    /**
+     * Processes CALL, CALL_PRIVILEGED, and CALL_EMERGENCY intents.
+     *
+     * @param intent Call intent containing data about the handle to call.
+     */
+    private void processOutgoingCallIntent(Intent intent) {
+        Uri handle = intent.getData();
+        String scheme = handle.getScheme();
+        String uriString = handle.getSchemeSpecificPart();
+
+        if (!PhoneAccount.SCHEME_VOICEMAIL.equals(scheme)) {
+            handle = Uri.fromParts(PhoneNumberUtils.isUriNumber(uriString) ?
+                    PhoneAccount.SCHEME_SIP : PhoneAccount.SCHEME_TEL, uriString, null);
+        }
+
+        UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE);
+        if (userManager.hasUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS)
+                && !TelephonyUtil.shouldProcessAsEmergency(this, handle)) {
+            // Only emergency calls are allowed for users with the DISALLOW_OUTGOING_CALLS
+            // restriction.
+            Toast.makeText(this, getResources().getString(R.string.outgoing_call_not_allowed),
+                    Toast.LENGTH_SHORT).show();
+            Log.d(this, "Rejecting non-emergency phone call due to DISALLOW_OUTGOING_CALLS "
+                    + "restriction");
+            return;
+        }
+
+        PhoneAccountHandle phoneAccountHandle = intent.getParcelableExtra(
+                TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE);
+
+        Bundle clientExtras = null;
+        if (intent.hasExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS)) {
+            clientExtras = intent.getBundleExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS);
+        }
+        if (clientExtras == null) {
+            clientExtras = Bundle.EMPTY;
+        }
+
+        // Send to CallsManager to ensure the InCallUI gets kicked off before the broadcast returns
+        Call call = mCallsManager.startOutgoingCall(handle, phoneAccountHandle, clientExtras);
+
+        if (call == null) {
+            setResult(RESULT_CANCELED);
+        } else {
+            NewOutgoingCallIntentBroadcaster broadcaster = new NewOutgoingCallIntentBroadcaster(
+                    mCallsManager, call, intent, isDefaultDialer());
+            final int result = broadcaster.processIntent();
+            final boolean success = result == DisconnectCause.NOT_DISCONNECTED;
+
+            if (!success && call != null) {
+                disconnectCallAndShowErrorDialog(call, result);
+            }
+            setResult(success ? RESULT_OK : RESULT_CANCELED);
+        }
+    }
+
+    /**
+     * Processes INCOMING_CALL intents. Grabs the connection service information from the intent
+     * extra and forwards that to the CallsManager to start the incoming call flow.
+     *
+     * @param intent The incoming call intent.
+     */
+    private void processIncomingCallIntent(Intent intent) {
+        PhoneAccountHandle phoneAccountHandle = intent.getParcelableExtra(
+                TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE);
+        if (phoneAccountHandle == null) {
+            Log.w(this, "Rejecting incoming call due to null phone account");
+            return;
+        }
+        if (phoneAccountHandle.getComponentName() == null) {
+            Log.w(this, "Rejecting incoming call due to null component name");
+            return;
+        }
+
+        Bundle clientExtras = null;
+        if (intent.hasExtra(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS)) {
+            clientExtras = intent.getBundleExtra(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS);
+        }
+        if (clientExtras == null) {
+            clientExtras = Bundle.EMPTY;
+        }
+
+        Log.d(this, "Processing incoming call from connection service [%s]",
+                phoneAccountHandle.getComponentName());
+        mCallsManager.processIncomingCallIntent(phoneAccountHandle, clientExtras);
+    }
+
+    private boolean isDefaultDialer() {
+        final String packageName = getCallingPackage();
+        if (TextUtils.isEmpty(packageName)) {
+            return false;
+        }
+
+        final TelecomManager telecomManager =
+                (TelecomManager) getSystemService(Context.TELECOM_SERVICE);
+        final ComponentName defaultPhoneApp = telecomManager.getDefaultPhoneApp();
+        return (defaultPhoneApp != null
+                && TextUtils.equals(defaultPhoneApp.getPackageName(), packageName));
+    }
+
+    /**
+     * Returns whether the device is voice-capable (e.g. a phone vs a tablet).
+     *
+     * @return {@code True} if the device is voice-capable.
+     */
+    private boolean isVoiceCapable() {
+        return getApplicationContext().getResources().getBoolean(
+                com.android.internal.R.bool.config_voice_capable);
+    }
+
+    private void disconnectCallAndShowErrorDialog(Call call, int errorCode) {
+        call.disconnect();
+        final Intent errorIntent = new Intent(this, ErrorDialogActivity.class);
+        int errorMessageId = -1;
+        switch (errorCode) {
+            case DisconnectCause.INVALID_NUMBER:
+                errorMessageId = R.string.outgoing_call_error_no_phone_number_supplied;
+                break;
+            case DisconnectCause.VOICEMAIL_NUMBER_MISSING:
+                errorIntent.putExtra(ErrorDialogActivity.SHOW_MISSING_VOICEMAIL_NO_DIALOG_EXTRA,
+                        true);
+                break;
+        }
+        if (errorMessageId != -1) {
+            errorIntent.putExtra(ErrorDialogActivity.ERROR_MESSAGE_ID_EXTRA, errorMessageId);
+        }
+        startActivity(errorIntent);
+    }
+}
diff --git a/src/com/android/server/telecom/CallAudioManager.java b/src/com/android/server/telecom/CallAudioManager.java
new file mode 100644
index 0000000..312f58f
--- /dev/null
+++ b/src/com/android/server/telecom/CallAudioManager.java
@@ -0,0 +1,495 @@
+/*
+ * Copyright (C) 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;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.telecom.AudioState;
+import android.telecom.CallState;
+
+import com.google.common.base.Preconditions;
+
+import java.util.Objects;
+
+/**
+ * This class manages audio modes, streams and other properties.
+ */
+final class CallAudioManager extends CallsManagerListenerBase
+        implements WiredHeadsetManager.Listener {
+    private static final int STREAM_NONE = -1;
+
+    private final StatusBarNotifier mStatusBarNotifier;
+    private final AudioManager mAudioManager;
+    private final BluetoothManager mBluetoothManager;
+    private final WiredHeadsetManager mWiredHeadsetManager;
+
+    private AudioState mAudioState;
+    private int mAudioFocusStreamType;
+    private boolean mIsRinging;
+    private boolean mIsTonePlaying;
+    private boolean mWasSpeakerOn;
+    private int mMostRecentlyUsedMode = AudioManager.MODE_IN_CALL;
+
+    CallAudioManager(Context context, StatusBarNotifier statusBarNotifier,
+            WiredHeadsetManager wiredHeadsetManager) {
+        mStatusBarNotifier = statusBarNotifier;
+        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        mBluetoothManager = new BluetoothManager(context, this);
+        mWiredHeadsetManager = wiredHeadsetManager;
+        mWiredHeadsetManager.addListener(this);
+
+        saveAudioState(getInitialAudioState(null));
+        mAudioFocusStreamType = STREAM_NONE;
+    }
+
+    AudioState getAudioState() {
+        return mAudioState;
+    }
+
+    @Override
+    public void onCallAdded(Call call) {
+        onCallUpdated(call);
+
+        if (hasFocus() && getForegroundCall() == call) {
+            if (!call.isIncoming()) {
+                // Unmute new outgoing call.
+                setSystemAudioState(false, mAudioState.route, mAudioState.supportedRouteMask);
+            }
+        }
+    }
+
+    @Override
+    public void onCallRemoved(Call call) {
+        // If we didn't already have focus, there's nothing to do.
+        if (hasFocus()) {
+            if (CallsManager.getInstance().getCalls().isEmpty()) {
+                Log.v(this, "all calls removed, reseting system audio to default state");
+                setInitialAudioState(null, false /* force */);
+                mWasSpeakerOn = false;
+            }
+            updateAudioStreamAndMode();
+        }
+    }
+
+    @Override
+    public void onCallStateChanged(Call call, int oldState, int newState) {
+        onCallUpdated(call);
+    }
+
+    @Override
+    public void onIncomingCallAnswered(Call call) {
+        int route = mAudioState.route;
+
+        // 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 = CallsManager.getInstance().getCalls().size() == 1;
+        if (isOnlyCall && mBluetoothManager.isBluetoothAvailable()) {
+            mBluetoothManager.connectBluetoothAudio();
+            route = AudioState.ROUTE_BLUETOOTH;
+        }
+
+        setSystemAudioState(false /* isMute */, route, mAudioState.supportedRouteMask);
+    }
+
+    @Override
+    public void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall) {
+        onCallUpdated(newForegroundCall);
+        // Ensure that the foreground call knows about the latest audio state.
+        updateAudioForForegroundCall();
+    }
+
+    @Override
+    public void onIsVoipAudioModeChanged(Call call) {
+        updateAudioStreamAndMode();
+    }
+
+    /**
+      * Updates the audio route when the headset plugged in state changes. For example, if audio is
+      * being routed over speakerphone and a headset is plugged in then switch to wired headset.
+      */
+    @Override
+    public void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn) {
+        // This can happen even when there are no calls and we don't have focus.
+        if (!hasFocus()) {
+            return;
+        }
+
+        int newRoute = AudioState.ROUTE_EARPIECE;
+        if (newIsPluggedIn) {
+            newRoute = AudioState.ROUTE_WIRED_HEADSET;
+        } else if (mWasSpeakerOn) {
+            Call call = getForegroundCall();
+            if (call != null && call.isAlive()) {
+                // Restore the speaker state.
+                newRoute = AudioState.ROUTE_SPEAKER;
+            }
+        }
+        setSystemAudioState(mAudioState.isMuted, newRoute, calculateSupportedRoutes());
+    }
+
+    void toggleMute() {
+        mute(!mAudioState.isMuted);
+    }
+
+    void mute(boolean shouldMute) {
+        if (!hasFocus()) {
+            return;
+        }
+
+        Log.v(this, "mute, shouldMute: %b", shouldMute);
+
+        // Don't mute if there are any emergency calls.
+        if (CallsManager.getInstance().hasEmergencyCall()) {
+            shouldMute = false;
+            Log.v(this, "ignoring mute for emergency call");
+        }
+
+        if (mAudioState.isMuted != shouldMute) {
+            setSystemAudioState(shouldMute, mAudioState.route, mAudioState.supportedRouteMask);
+        }
+    }
+
+    /**
+     * Changed the audio route, for example from earpiece to speaker phone.
+     *
+     * @param route The new audio route to use. See {@link AudioState}.
+     */
+    void setAudioRoute(int route) {
+        // This can happen even when there are no calls and we don't have focus.
+        if (!hasFocus()) {
+            return;
+        }
+
+        Log.v(this, "setAudioRoute, route: %s", AudioState.audioRouteToString(route));
+
+        // Change ROUTE_WIRED_OR_EARPIECE to a single entry.
+        int newRoute = selectWiredOrEarpiece(route, mAudioState.supportedRouteMask);
+
+        // If route is unsupported, do nothing.
+        if ((mAudioState.supportedRouteMask | newRoute) == 0) {
+            Log.wtf(this, "Asking to set to a route that is unsupported: %d", newRoute);
+            return;
+        }
+
+        if (mAudioState.route != newRoute) {
+            // Remember the new speaker state so it can be restored when the user plugs and unplugs
+            // a headset.
+            mWasSpeakerOn = newRoute == AudioState.ROUTE_SPEAKER;
+            setSystemAudioState(mAudioState.isMuted, newRoute, mAudioState.supportedRouteMask);
+        }
+    }
+
+    void setIsRinging(boolean isRinging) {
+        if (mIsRinging != isRinging) {
+            Log.v(this, "setIsRinging %b -> %b", mIsRinging, isRinging);
+            mIsRinging = isRinging;
+            updateAudioStreamAndMode();
+        }
+    }
+
+    /**
+     * Sets the tone playing status. Some tones can play even when there are no live calls and this
+     * status indicates that we should keep audio focus even for tones that play beyond the life of
+     * calls.
+     *
+     * @param isPlayingNew The status to set.
+     */
+    void setIsTonePlaying(boolean isPlayingNew) {
+        ThreadUtil.checkOnMainThread();
+
+        if (mIsTonePlaying != isPlayingNew) {
+            Log.v(this, "mIsTonePlaying %b -> %b.", mIsTonePlaying, isPlayingNew);
+            mIsTonePlaying = isPlayingNew;
+            updateAudioStreamAndMode();
+        }
+    }
+
+    /**
+     * Updates the audio routing according to the bluetooth state.
+     */
+    void onBluetoothStateChange(BluetoothManager bluetoothManager) {
+        // This can happen even when there are no calls and we don't have focus.
+        if (!hasFocus()) {
+            return;
+        }
+
+        int supportedRoutes = calculateSupportedRoutes();
+        int newRoute = mAudioState.route;
+        if (bluetoothManager.isBluetoothAudioConnectedOrPending()) {
+            newRoute = AudioState.ROUTE_BLUETOOTH;
+        } else if (mAudioState.route == AudioState.ROUTE_BLUETOOTH) {
+            newRoute = selectWiredOrEarpiece(AudioState.ROUTE_WIRED_OR_EARPIECE, supportedRoutes);
+            // Do not switch to speaker when bluetooth disconnects.
+            mWasSpeakerOn = false;
+        }
+
+        setSystemAudioState(mAudioState.isMuted, newRoute, supportedRoutes);
+    }
+
+    boolean isBluetoothAudioOn() {
+        return mBluetoothManager.isBluetoothAudioConnected();
+    }
+
+    boolean isBluetoothDeviceAvailable() {
+        return mBluetoothManager.isBluetoothAvailable();
+    }
+
+    private void saveAudioState(AudioState audioState) {
+        mAudioState = audioState;
+        mStatusBarNotifier.notifyMute(mAudioState.isMuted);
+        mStatusBarNotifier.notifySpeakerphone(mAudioState.route == AudioState.ROUTE_SPEAKER);
+    }
+
+    private void onCallUpdated(Call call) {
+        boolean wasNotVoiceCall = mAudioFocusStreamType != AudioManager.STREAM_VOICE_CALL;
+        updateAudioStreamAndMode();
+
+        // If we transition from not voice call to voice call, we need to set an initial state.
+        if (wasNotVoiceCall && mAudioFocusStreamType == AudioManager.STREAM_VOICE_CALL) {
+            setInitialAudioState(call, true /* force */);
+        }
+    }
+
+    private void setSystemAudioState(boolean isMuted, int route, int supportedRouteMask) {
+        setSystemAudioState(false /* force */, isMuted, route, supportedRouteMask);
+    }
+
+    private void setSystemAudioState(
+            boolean force, boolean isMuted, int route, int supportedRouteMask) {
+        if (!hasFocus()) {
+            return;
+        }
+
+        AudioState oldAudioState = mAudioState;
+        saveAudioState(new AudioState(isMuted, route, supportedRouteMask));
+        if (!force && Objects.equals(oldAudioState, mAudioState)) {
+            return;
+        }
+        Log.i(this, "changing audio state from %s to %s", oldAudioState, mAudioState);
+
+        // Mute.
+        if (mAudioState.isMuted != mAudioManager.isMicrophoneMute()) {
+            Log.i(this, "changing microphone mute state to: %b", mAudioState.isMuted);
+            mAudioManager.setMicrophoneMute(mAudioState.isMuted);
+        }
+
+        // Audio route.
+        if (mAudioState.route == AudioState.ROUTE_BLUETOOTH) {
+            turnOnSpeaker(false);
+            turnOnBluetooth(true);
+        } else if (mAudioState.route == AudioState.ROUTE_SPEAKER) {
+            turnOnBluetooth(false);
+            turnOnSpeaker(true);
+        } else if (mAudioState.route == AudioState.ROUTE_EARPIECE ||
+                mAudioState.route == AudioState.ROUTE_WIRED_HEADSET) {
+            turnOnBluetooth(false);
+            turnOnSpeaker(false);
+        }
+
+        if (!oldAudioState.equals(mAudioState)) {
+            CallsManager.getInstance().onAudioStateChanged(oldAudioState, mAudioState);
+            updateAudioForForegroundCall();
+        }
+    }
+
+    private void turnOnSpeaker(boolean on) {
+        // Wired headset and earpiece work the same way
+        if (mAudioManager.isSpeakerphoneOn() != on) {
+            Log.i(this, "turning speaker phone %s", on);
+            mAudioManager.setSpeakerphoneOn(on);
+        }
+    }
+
+    private void turnOnBluetooth(boolean on) {
+        if (mBluetoothManager.isBluetoothAvailable()) {
+            boolean isAlreadyOn = mBluetoothManager.isBluetoothAudioConnectedOrPending();
+            if (on != isAlreadyOn) {
+                Log.i(this, "connecting bluetooth %s", on);
+                if (on) {
+                    mBluetoothManager.connectBluetoothAudio();
+                } else {
+                    mBluetoothManager.disconnectBluetoothAudio();
+                }
+            }
+        }
+    }
+
+    private void updateAudioStreamAndMode() {
+        Log.v(this, "updateAudioStreamAndMode, mIsRinging: %b, mIsTonePlaying: %b", mIsRinging,
+                mIsTonePlaying);
+        if (mIsRinging) {
+            requestAudioFocusAndSetMode(AudioManager.STREAM_RING, AudioManager.MODE_RINGTONE);
+        } else {
+            Call call = getForegroundCall();
+            if (call != null) {
+                int mode = call.getIsVoipAudioMode() ?
+                        AudioManager.MODE_IN_COMMUNICATION : AudioManager.MODE_IN_CALL;
+                requestAudioFocusAndSetMode(AudioManager.STREAM_VOICE_CALL, mode);
+            } else if (mIsTonePlaying) {
+                // There is no call, however, we are still playing a tone, so keep focus.
+                // Since there is no call from which to determine the mode, use the most
+                // recently used mode instead.
+                requestAudioFocusAndSetMode(
+                        AudioManager.STREAM_VOICE_CALL, mMostRecentlyUsedMode);
+            } else if (!hasRingingForegroundCall()) {
+                abandonAudioFocus();
+            } else {
+                // mIsRinging is false, but there is a foreground ringing call present. Don't
+                // abandon audio focus immediately to prevent audio focus from getting lost between
+                // the time it takes for the foreground call to transition from RINGING to ACTIVE/
+                // DISCONNECTED. When the call eventually transitions to the next state, audio
+                // focus will be correctly abandoned by the if clause above.
+            }
+        }
+    }
+
+    private void requestAudioFocusAndSetMode(int stream, int mode) {
+        Log.v(this, "requestAudioFocusAndSetMode, stream: %d -> %d", mAudioFocusStreamType, stream);
+        Preconditions.checkState(stream != STREAM_NONE);
+
+        // Even if we already have focus, if the stream is different we update audio manager to give
+        // it a hint about the purpose of our focus.
+        if (mAudioFocusStreamType != stream) {
+            Log.v(this, "requesting audio focus for stream: %d", stream);
+            mAudioManager.requestAudioFocusForCall(stream,
+                    AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+        }
+        mAudioFocusStreamType = stream;
+
+        setMode(mode);
+    }
+
+    private void abandonAudioFocus() {
+        if (hasFocus()) {
+            setMode(AudioManager.MODE_NORMAL);
+            Log.v(this, "abandoning audio focus");
+            mAudioManager.abandonAudioFocusForCall();
+            mAudioFocusStreamType = STREAM_NONE;
+        }
+    }
+
+    /**
+     * Sets the audio mode.
+     *
+     * @param newMode Mode constant from AudioManager.MODE_*.
+     */
+    private void setMode(int newMode) {
+        Preconditions.checkState(hasFocus());
+        int oldMode = mAudioManager.getMode();
+        Log.v(this, "Request to change audio mode from %d to %d", oldMode, newMode);
+        if (oldMode != newMode) {
+            mAudioManager.setMode(newMode);
+            mMostRecentlyUsedMode = newMode;
+        }
+    }
+
+    private int selectWiredOrEarpiece(int route, int supportedRouteMask) {
+        // Since they are mutually exclusive and one is ALWAYS valid, we allow a special input of
+        // ROUTE_WIRED_OR_EARPIECE so that callers dont have to make a call to check which is
+        // supported before calling setAudioRoute.
+        if (route == AudioState.ROUTE_WIRED_OR_EARPIECE) {
+            route = AudioState.ROUTE_WIRED_OR_EARPIECE & supportedRouteMask;
+            if (route == 0) {
+                Log.wtf(this, "One of wired headset or earpiece should always be valid.");
+                // assume earpiece in this case.
+                route = AudioState.ROUTE_EARPIECE;
+            }
+        }
+        return route;
+    }
+
+    private int calculateSupportedRoutes() {
+        int routeMask = AudioState.ROUTE_SPEAKER;
+
+        if (mWiredHeadsetManager.isPluggedIn()) {
+            routeMask |= AudioState.ROUTE_WIRED_HEADSET;
+        } else {
+            routeMask |= AudioState.ROUTE_EARPIECE;
+        }
+
+        if (mBluetoothManager.isBluetoothAvailable()) {
+            routeMask |=  AudioState.ROUTE_BLUETOOTH;
+        }
+
+        return routeMask;
+    }
+
+    private AudioState getInitialAudioState(Call call) {
+        int supportedRouteMask = calculateSupportedRoutes();
+        int route = selectWiredOrEarpiece(
+                AudioState.ROUTE_WIRED_OR_EARPIECE, supportedRouteMask);
+
+        // We want the UI to indicate that "bluetooth is in use" in two slightly different cases:
+        // (a) The obvious case: if a bluetooth headset is currently in use for an ongoing call.
+        // (b) The not-so-obvious case: if an incoming call is ringing, and we expect that audio
+        //     *will* be routed to a bluetooth headset once the call is answered. In this case, just
+        //     check if the headset is available. Note this only applies when we are dealing with
+        //     the first call.
+        if (call != null && mBluetoothManager.isBluetoothAvailable()) {
+            switch(call.getState()) {
+                case CallState.ACTIVE:
+                case CallState.ON_HOLD:
+                case CallState.DIALING:
+                case CallState.RINGING:
+                    route = AudioState.ROUTE_BLUETOOTH;
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        return new AudioState(false, route, supportedRouteMask);
+    }
+
+    private void setInitialAudioState(Call call, boolean force) {
+        AudioState audioState = getInitialAudioState(call);
+        Log.v(this, "setInitialAudioState %s, %s", audioState, call);
+        setSystemAudioState(
+                force, audioState.isMuted, audioState.route, audioState.supportedRouteMask);
+    }
+
+    private void updateAudioForForegroundCall() {
+        Call call = CallsManager.getInstance().getForegroundCall();
+        if (call != null && call.getConnectionService() != null) {
+            call.getConnectionService().onAudioStateChanged(call, mAudioState);
+        }
+    }
+
+    /**
+     * Returns the current foreground call in order to properly set the audio mode.
+     */
+    private Call getForegroundCall() {
+        Call call = CallsManager.getInstance().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}.
+        if (call != null && call.getState() == CallState.RINGING) {
+            call = null;
+        }
+        return call;
+    }
+
+    private boolean hasRingingForegroundCall() {
+        Call call = CallsManager.getInstance().getForegroundCall();
+        return call != null && call.getState() == CallState.RINGING;
+    }
+
+    private boolean hasFocus() {
+        return mAudioFocusStreamType != STREAM_NONE;
+    }
+}
diff --git a/src/com/android/server/telecom/CallIdMapper.java b/src/com/android/server/telecom/CallIdMapper.java
new file mode 100644
index 0000000..40a50a5
--- /dev/null
+++ b/src/com/android/server/telecom/CallIdMapper.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 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;
+
+import com.google.common.collect.HashBiMap;
+
+/** Utility to map {@link Call} objects to unique IDs. IDs are generated when a call is added. */
+class CallIdMapper {
+    private final HashBiMap<String, Call> mCalls = HashBiMap.create();
+    private final String mCallIdPrefix;
+    private static int sIdCount;
+
+    CallIdMapper(String callIdPrefix) {
+        ThreadUtil.checkOnMainThread();
+        mCallIdPrefix = callIdPrefix + "@";
+    }
+
+    void replaceCall(Call newCall, Call callToReplace) {
+        ThreadUtil.checkOnMainThread();
+
+        // Use the old call's ID for the new call.
+        String callId = getCallId(callToReplace);
+        mCalls.put(callId, newCall);
+    }
+
+    void addCall(Call call, String id) {
+        if (call == null) {
+            return;
+        }
+        ThreadUtil.checkOnMainThread();
+        mCalls.put(id, call);
+    }
+
+    void addCall(Call call) {
+        ThreadUtil.checkOnMainThread();
+        addCall(call, getNewId());
+    }
+
+    void removeCall(Call call) {
+        if (call == null) {
+            return;
+        }
+        ThreadUtil.checkOnMainThread();
+        mCalls.inverse().remove(call);
+    }
+
+    void removeCall(String callId) {
+        ThreadUtil.checkOnMainThread();
+        mCalls.remove(callId);
+    }
+
+    String getCallId(Call call) {
+        if (call == null) {
+            return null;
+        }
+        ThreadUtil.checkOnMainThread();
+        return mCalls.inverse().get(call);
+    }
+
+    Call getCall(Object objId) {
+        ThreadUtil.checkOnMainThread();
+
+        String callId = null;
+        if (objId instanceof String) {
+            callId = (String) objId;
+        }
+        if (!isValidCallId(callId) && !isValidConferenceId(callId)) {
+            return null;
+        }
+
+        return mCalls.get(callId);
+    }
+
+    void clear() {
+        mCalls.clear();
+    }
+
+    boolean isValidCallId(String callId) {
+        // Note, no need for thread check, this method is thread safe.
+        return callId != null && callId.startsWith(mCallIdPrefix);
+    }
+
+    boolean isValidConferenceId(String callId) {
+        return callId != null;
+    }
+
+    String getNewId() {
+        sIdCount++;
+        return mCallIdPrefix + sIdCount;
+    }
+}
diff --git a/src/com/android/server/telecom/CallLogManager.java b/src/com/android/server/telecom/CallLogManager.java
new file mode 100644
index 0000000..a29ef54
--- /dev/null
+++ b/src/com/android/server/telecom/CallLogManager.java
@@ -0,0 +1,295 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.CallLog.Calls;
+import android.telecom.CallState;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.VideoProfile;
+import android.telephony.DisconnectCause;
+import android.telephony.PhoneNumberUtils;
+
+import com.android.internal.telephony.CallerInfo;
+import com.android.internal.telephony.PhoneConstants;
+
+/**
+ * Helper class that provides functionality to write information about calls and their associated
+ * caller details to the call log. All logging activity will be performed asynchronously in a
+ * background thread to avoid blocking on the main thread.
+ */
+final class CallLogManager extends CallsManagerListenerBase {
+    /**
+     * Parameter object to hold the arguments to add a call in the call log DB.
+     */
+    private static class AddCallArgs {
+        /**
+         * @param callerInfo Caller details.
+         * @param number The phone number to be logged.
+         * @param presentation Number presentation of the phone number to be logged.
+         * @param callType The type of call (e.g INCOMING_TYPE). @see
+         *     {@link android.provider.CallLog} for the list of values.
+         * @param features The features of the call (e.g. FEATURES_VIDEO). @see
+         *     {@link android.provider.CallLog} for the list of values.
+         * @param creationDate Time when the call was created (milliseconds since epoch).
+         * @param durationInMillis Duration of the call (milliseconds).
+         * @param dataUsage Data usage in bytes, or null if not applicable.
+         */
+        public AddCallArgs(Context context, CallerInfo callerInfo, String number,
+                int presentation, int callType, int features, PhoneAccountHandle accountHandle,
+                long creationDate, long durationInMillis, Long dataUsage) {
+            this.context = context;
+            this.callerInfo = callerInfo;
+            this.number = number;
+            this.presentation = presentation;
+            this.callType = callType;
+            this.features = features;
+            this.accountHandle = accountHandle;
+            this.timestamp = creationDate;
+            this.durationInSec = (int)(durationInMillis / 1000);
+            this.dataUsage = dataUsage;
+        }
+        // Since the members are accessed directly, we don't use the
+        // mXxxx notation.
+        public final Context context;
+        public final CallerInfo callerInfo;
+        public final String number;
+        public final int presentation;
+        public final int callType;
+        public final int features;
+        public final PhoneAccountHandle accountHandle;
+        public final long timestamp;
+        public final int durationInSec;
+        public final Long dataUsage;
+    }
+
+    private static final String TAG = CallLogManager.class.getSimpleName();
+
+    private final Context mContext;
+
+    public CallLogManager(Context context) {
+        mContext = context;
+    }
+
+    @Override
+    public void onCallStateChanged(Call call, int oldState, int newState) {
+        boolean isNewlyDisconnected =
+                newState == CallState.DISCONNECTED || newState == CallState.ABORTED;
+        boolean isCallCanceled = isNewlyDisconnected &&
+                call.getDisconnectCause() == DisconnectCause.OUTGOING_CANCELED;
+
+        // Log newly disconnected calls only if:
+        // 1) It was not in the "choose account" phase when disconnected
+        // 2) It is a conference call
+        // 3) Call was not explicitly canceled
+        if (isNewlyDisconnected &&
+                (oldState != CallState.PRE_DIAL_WAIT &&
+                 !call.isConference() &&
+                 !isCallCanceled)) {
+            int type;
+            if (!call.isIncoming()) {
+                type = Calls.OUTGOING_TYPE;
+            } else if (oldState == CallState.RINGING) {
+                type = Calls.MISSED_TYPE;
+            } else {
+                type = Calls.INCOMING_TYPE;
+            }
+            logCall(call, type);
+        }
+    }
+
+    /**
+     * Logs a call to the call log based on the {@link Call} object passed in.
+     *
+     * @param call The call object being logged
+     * @param callLogType The type of call log entry to log this call as. See:
+     *     {@link android.provider.CallLog.Calls#INCOMING_TYPE}
+     *     {@link android.provider.CallLog.Calls#OUTGOING_TYPE}
+     *     {@link android.provider.CallLog.Calls#MISSED_TYPE}
+     */
+    private void logCall(Call call, int callLogType) {
+        final long creationTime = call.getCreationTimeMillis();
+        final long age = call.getAgeMillis();
+
+        final String logNumber = getLogNumber(call);
+
+        Log.d(TAG, "logNumber set to: %s", Log.pii(logNumber));
+
+        final int presentation = getPresentation(call);
+        final PhoneAccountHandle accountHandle = call.getTargetPhoneAccount();
+
+        // TODO(vt): Once data usage is available, wire it up here.
+        int callFeatures = getCallFeatures(call.getVideoStateHistory());
+        logCall(call.getCallerInfo(), logNumber, presentation, callLogType, callFeatures,
+                accountHandle, creationTime, age, null);
+    }
+
+    /**
+     * Inserts a call into the call log, based on the parameters passed in.
+     *
+     * @param callerInfo Caller details.
+     * @param number The number the call was made to or from.
+     * @param presentation
+     * @param callType The type of call.
+     * @param features The features of the call.
+     * @param start The start time of the call, in milliseconds.
+     * @param duration The duration of the call, in milliseconds.
+     * @param dataUsage The data usage for the call, null if not applicable.
+     */
+    private void logCall(
+            CallerInfo callerInfo,
+            String number,
+            int presentation,
+            int callType,
+            int features,
+            PhoneAccountHandle accountHandle,
+            long start,
+            long duration,
+            Long dataUsage) {
+        boolean isEmergencyNumber = PhoneNumberUtils.isLocalEmergencyNumber(mContext, number);
+
+        // On some devices, to avoid accidental redialing of emergency numbers, we *never* log
+        // emergency calls to the Call Log.  (This behavior is set on a per-product basis, based
+        // on carrier requirements.)
+        final boolean okToLogEmergencyNumber =
+                mContext.getResources().getBoolean(R.bool.allow_emergency_numbers_in_call_log);
+
+        // Don't log emergency numbers if the device doesn't allow it.
+        final boolean isOkToLogThisCall = !isEmergencyNumber || okToLogEmergencyNumber;
+
+        if (isOkToLogThisCall) {
+            Log.d(TAG, "Logging Calllog entry: " + callerInfo + ", "
+                    + Log.pii(number) + "," + presentation + ", " + callType
+                    + ", " + start + ", " + duration);
+            AddCallArgs args = new AddCallArgs(mContext, callerInfo, number, presentation,
+                    callType, features, accountHandle, start, duration, dataUsage);
+            logCallAsync(args);
+        } else {
+          Log.d(TAG, "Not adding emergency call to call log.");
+        }
+    }
+
+    /**
+     * Based on the video state of the call, determines the call features applicable for the call.
+     *
+     * @param videoState The video state.
+     * @return The call features.
+     */
+    private static int getCallFeatures(int videoState) {
+        if ((videoState & VideoProfile.VideoState.TX_ENABLED)
+                == VideoProfile.VideoState.TX_ENABLED) {
+            return Calls.FEATURES_VIDEO;
+        }
+        return 0;
+    }
+
+    /**
+     * Retrieve the phone number from the call, and then process it before returning the
+     * actual number that is to be logged.
+     *
+     * @param call The phone connection.
+     * @return the phone number to be logged.
+     */
+    private String getLogNumber(Call call) {
+        Uri handle = call.getOriginalHandle();
+
+        if (handle == null) {
+            return null;
+        }
+
+        String handleString = handle.getSchemeSpecificPart();
+        if (!PhoneNumberUtils.isUriNumber(handleString)) {
+            handleString = PhoneNumberUtils.stripSeparators(handleString);
+        }
+        return handleString;
+    }
+
+    /**
+     * Gets the presentation from the {@link Call}.
+     *
+     * TODO: There needs to be a way to pass information from
+     * Connection.getNumberPresentation() into a {@link Call} object. Until then, always return
+     * PhoneConstants.PRESENTATION_ALLOWED. On top of that, we might need to introduce
+     * getNumberPresentation to the ContactInfo object as well.
+     *
+     * @param call The call object to retrieve caller details from.
+     * @return The number presentation constant to insert into the call logs.
+     */
+    private int getPresentation(Call call) {
+        return PhoneConstants.PRESENTATION_ALLOWED;
+    }
+
+    /**
+     * Adds the call defined by the parameters in the provided AddCallArgs to the CallLogProvider
+     * using an AsyncTask to avoid blocking the main thread.
+     *
+     * @param args Prepopulated call details.
+     * @return A handle to the AsyncTask that will add the call to the call log asynchronously.
+     */
+    public AsyncTask<AddCallArgs, Void, Uri[]> logCallAsync(AddCallArgs args) {
+        return new LogCallAsyncTask().execute(args);
+    }
+
+    /**
+     * Helper AsyncTask to access the call logs database asynchronously since database operations
+     * can take a long time depending on the system's load. Since it extends AsyncTask, it uses
+     * its own thread pool.
+     */
+    private class LogCallAsyncTask extends AsyncTask<AddCallArgs, Void, Uri[]> {
+        @Override
+        protected Uri[] doInBackground(AddCallArgs... callList) {
+            int count = callList.length;
+            Uri[] result = new Uri[count];
+            for (int i = 0; i < count; i++) {
+                AddCallArgs c = callList[i];
+
+                try {
+                    // May block.
+                    result[i] = Calls.addCall(c.callerInfo, c.context, c.number, c.presentation,
+                            c.callType, c.features, c.accountHandle, c.timestamp, c.durationInSec,
+                            c.dataUsage, true /* addForAllUsers */);
+                } catch (Exception e) {
+                    // This is very rare but may happen in legitimate cases.
+                    // E.g. If the phone is encrypted and thus write request fails, it may cause
+                    // some kind of Exception (right now it is IllegalArgumentException, but this
+                    // might change).
+                    //
+                    // We don't want to crash the whole process just because of that, so just log
+                    // it instead.
+                    Log.e(TAG, e, "Exception raised during adding CallLog entry.");
+                    result[i] = null;
+                }
+            }
+            return result;
+        }
+
+        /**
+         * Performs a simple sanity check to make sure the call was written in the database.
+         * Typically there is only one result per call so it is easy to identify which one failed.
+         */
+        @Override
+        protected void onPostExecute(Uri[] result) {
+            for (Uri uri : result) {
+                if (uri == null) {
+                    Log.w(TAG, "Failed to write call to the log.");
+                }
+            }
+        }
+    }
+}
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
new file mode 100644
index 0000000..d0aa49f
--- /dev/null
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -0,0 +1,871 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.telecom.AudioState;
+import android.telecom.CallState;
+import android.telecom.GatewayInfo;
+import android.telecom.ParcelableConference;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.DisconnectCause;
+import android.telephony.TelephonyManager;
+
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Singleton.
+ *
+ * NOTE: by design most APIs are package private, use the relevant adapter/s to allow
+ * access from other packages specifically refraining from passing the CallsManager instance
+ * beyond the com.android.server.telecom package boundary.
+ */
+public final class CallsManager extends Call.ListenerBase {
+
+    // TODO: Consider renaming this CallsManagerPlugin.
+    interface CallsManagerListener {
+        void onCallAdded(Call call);
+        void onCallRemoved(Call call);
+        void onCallStateChanged(Call call, int oldState, int newState);
+        void onConnectionServiceChanged(
+                Call call,
+                ConnectionServiceWrapper oldService,
+                ConnectionServiceWrapper newService);
+        void onIncomingCallAnswered(Call call);
+        void onIncomingCallRejected(Call call, boolean rejectWithMessage, String textMessage);
+        void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall);
+        void onAudioStateChanged(AudioState oldAudioState, AudioState newAudioState);
+        void onRingbackRequested(Call call, boolean ringback);
+        void onIsConferencedChanged(Call call);
+        void onIsVoipAudioModeChanged(Call call);
+        void onVideoStateChanged(Call call);
+    }
+
+    private static final CallsManager INSTANCE = new CallsManager();
+
+    /**
+     * The main call repository. Keeps an instance of all live calls. New incoming and outgoing
+     * calls are added to the map and removed when the calls move to the disconnected state.
+    *
+     * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
+     * load factor before resizing, 1 means we only expect a single thread to
+     * access the map so make only a single shard
+     */
+    private final Set<Call> mCalls = Collections.newSetFromMap(
+            new ConcurrentHashMap<Call, Boolean>(8, 0.9f, 1));
+
+    private final ConnectionServiceRepository mConnectionServiceRepository =
+            new ConnectionServiceRepository();
+    private final DtmfLocalTonePlayer mDtmfLocalTonePlayer = new DtmfLocalTonePlayer();
+    private final InCallController mInCallController = new InCallController();
+    private final CallAudioManager mCallAudioManager;
+    private final Ringer mRinger;
+    // For this set initial table size to 16 because we add 13 listeners in
+    // the CallsManager constructor.
+    private final Set<CallsManagerListener> mListeners = Collections.newSetFromMap(
+            new ConcurrentHashMap<CallsManagerListener, Boolean>(16, 0.9f, 1));
+    private final HeadsetMediaButton mHeadsetMediaButton;
+    private final WiredHeadsetManager mWiredHeadsetManager;
+    private final TtyManager mTtyManager;
+    private final ProximitySensorManager mProximitySensorManager;
+
+    /**
+     * The call the user is currently interacting with. This is the call that should have audio
+     * focus and be visible in the in-call UI.
+     */
+    private Call mForegroundCall;
+
+    /** Singleton accessor. */
+    static CallsManager getInstance() {
+        return INSTANCE;
+    }
+
+    /**
+     * Initializes the required Telecom components.
+     */
+    private CallsManager() {
+        TelecomApp app = TelecomApp.getInstance();
+
+        StatusBarNotifier statusBarNotifier = new StatusBarNotifier(app, this);
+        mWiredHeadsetManager = new WiredHeadsetManager(app);
+        mCallAudioManager = new CallAudioManager(app, statusBarNotifier, mWiredHeadsetManager);
+        InCallTonePlayer.Factory playerFactory = new InCallTonePlayer.Factory(mCallAudioManager);
+        mRinger = new Ringer(mCallAudioManager, this, playerFactory, app);
+        mHeadsetMediaButton = new HeadsetMediaButton(app, this);
+        mTtyManager = new TtyManager(app, mWiredHeadsetManager);
+        mProximitySensorManager = new ProximitySensorManager(app);
+
+        mListeners.add(statusBarNotifier);
+        mListeners.add(new CallLogManager(app));
+        mListeners.add(new PhoneStateBroadcaster());
+        mListeners.add(mInCallController);
+        mListeners.add(mRinger);
+        mListeners.add(new RingbackPlayer(this, playerFactory));
+        mListeners.add(new InCallToneMonitor(playerFactory, this));
+        mListeners.add(mCallAudioManager);
+        mListeners.add(app.getMissedCallNotifier());
+        mListeners.add(mDtmfLocalTonePlayer);
+        mListeners.add(mHeadsetMediaButton);
+        mListeners.add(RespondViaSmsManager.getInstance());
+        mListeners.add(mProximitySensorManager);
+    }
+
+    @Override
+    public void onSuccessfulOutgoingCall(Call call, int callState) {
+        Log.v(this, "onSuccessfulOutgoingCall, %s", call);
+
+        setCallState(call, callState);
+        if (!mCalls.contains(call)) {
+            // Call was not added previously in startOutgoingCall due to it being a potential MMI
+            // code, so add it now.
+            addCall(call);
+        }
+
+        // The call's ConnectionService has been updated.
+        for (CallsManagerListener listener : mListeners) {
+            listener.onConnectionServiceChanged(call, null, call.getConnectionService());
+        }
+
+        markCallAsDialing(call);
+    }
+
+    @Override
+    public void onFailedOutgoingCall(Call call, int errorCode, String errorMsg) {
+        Log.v(this, "onFailedOutgoingCall, call: %s", call);
+
+        // TODO: Replace disconnect cause with more specific disconnect causes.
+        markCallAsDisconnected(call, errorCode, errorMsg);
+    }
+
+    @Override
+    public void onSuccessfulIncomingCall(Call call) {
+        Log.d(this, "onSuccessfulIncomingCall");
+        setCallState(call, CallState.RINGING);
+        addCall(call);
+    }
+
+    @Override
+    public void onFailedIncomingCall(Call call) {
+        setCallState(call, CallState.DISCONNECTED);
+        call.removeListener(this);
+    }
+
+    @Override
+    public void onRingbackRequested(Call call, boolean ringback) {
+        for (CallsManagerListener listener : mListeners) {
+            listener.onRingbackRequested(call, ringback);
+        }
+    }
+
+    @Override
+    public void onPostDialWait(Call call, String remaining) {
+        mInCallController.onPostDialWait(call, remaining);
+    }
+
+    @Override
+    public void onParentChanged(Call call) {
+        for (CallsManagerListener listener : mListeners) {
+            listener.onIsConferencedChanged(call);
+        }
+    }
+
+    @Override
+    public void onChildrenChanged(Call call) {
+        for (CallsManagerListener listener : mListeners) {
+            listener.onIsConferencedChanged(call);
+        }
+    }
+
+    @Override
+    public void onIsVoipAudioModeChanged(Call call) {
+        for (CallsManagerListener listener : mListeners) {
+            listener.onIsVoipAudioModeChanged(call);
+        }
+    }
+
+    @Override
+    public void onVideoStateChanged(Call call) {
+        for (CallsManagerListener listener : mListeners) {
+            listener.onVideoStateChanged(call);
+        }
+    }
+
+    ImmutableCollection<Call> getCalls() {
+        return ImmutableList.copyOf(mCalls);
+    }
+
+    Call getForegroundCall() {
+        return mForegroundCall;
+    }
+
+    Ringer getRinger() {
+        return mRinger;
+    }
+
+    InCallController getInCallController() {
+        return mInCallController;
+    }
+
+    boolean hasEmergencyCall() {
+        for (Call call : mCalls) {
+            if (call.isEmergencyCall()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    AudioState getAudioState() {
+        return mCallAudioManager.getAudioState();
+    }
+
+    boolean isTtySupported() {
+        return mTtyManager.isTtySupported();
+    }
+
+    int getCurrentTtyMode() {
+        return mTtyManager.getCurrentTtyMode();
+    }
+
+    /**
+     * Starts the process to attach the call to a connection service.
+     *
+     * @param phoneAccountHandle The phone account which contains the component name of the
+     *        connection service to use for this call.
+     * @param extras The optional extras Bundle passed with the intent used for the incoming call.
+     */
+    void processIncomingCallIntent(PhoneAccountHandle phoneAccountHandle, Bundle extras) {
+        Log.d(this, "processIncomingCallIntent");
+        Uri handle = extras.getParcelable(TelephonyManager.EXTRA_INCOMING_NUMBER);
+        Call call = new Call(
+                mConnectionServiceRepository,
+                handle,
+                null /* gatewayInfo */,
+                null /* connectionManagerPhoneAccount */,
+                phoneAccountHandle,
+                true /* isIncoming */,
+                false /* isConference */);
+
+        call.setExtras(extras);
+        // TODO: Move this to be a part of addCall()
+        call.addListener(this);
+        call.startCreateConnection();
+    }
+
+    /**
+     * Kicks off the first steps to creating an outgoing call so that InCallUI can launch.
+     *
+     * @param handle Handle to connect the call with.
+     * @param phoneAccountHandle The phone account which contains the component name of the
+     *        connection service to use for this call.
+     * @param extras The optional extras Bundle passed with the intent used for the incoming call.
+     */
+    Call startOutgoingCall(Uri handle, PhoneAccountHandle phoneAccountHandle, Bundle extras) {
+        // We only allow a single outgoing call at any given time. Before placing a call, make sure
+        // there doesn't already exist another outgoing call.
+        Call call = getFirstCallWithState(CallState.NEW, CallState.DIALING,
+                CallState.CONNECTING, CallState.PRE_DIAL_WAIT);
+
+        if (call != null) {
+            Log.i(this, "Canceling simultaneous outgoing call.");
+            return null;
+        }
+
+        TelecomApp app = TelecomApp.getInstance();
+
+        // Only dial with the requested phoneAccount if it is still valid. Otherwise treat this call
+        // as if a phoneAccount was not specified (does the default behavior instead).
+        // Note: We will not attempt to dial with a requested phoneAccount if it is disabled.
+        if (phoneAccountHandle != null) {
+            List<PhoneAccountHandle> enabledAccounts =
+                    app.getPhoneAccountRegistrar().getEnabledPhoneAccounts(handle.getScheme());
+            if (!enabledAccounts.contains(phoneAccountHandle)) {
+                phoneAccountHandle = null;
+            }
+        }
+
+        if (phoneAccountHandle == null) {
+            // No preset account, check if default exists that supports the URI scheme for the
+            // handle.
+            PhoneAccountHandle defaultAccountHandle =
+                    app.getPhoneAccountRegistrar().getDefaultOutgoingPhoneAccount(
+                            handle.getScheme());
+            if (defaultAccountHandle != null) {
+                phoneAccountHandle = defaultAccountHandle;
+            }
+        }
+
+        // Create a call with original handle. The handle may be changed when the call is attached
+        // to a connection service, but in most cases will remain the same.
+        call = new Call(
+                mConnectionServiceRepository,
+                handle,
+                null /* gatewayInfo */,
+                null /* connectionManagerPhoneAccount */,
+                phoneAccountHandle,
+                false /* isIncoming */,
+                false /* isConference */);
+        call.setExtras(extras);
+
+        final boolean emergencyCall = TelephonyUtil.shouldProcessAsEmergency(app, call.getHandle());
+        if (phoneAccountHandle == null && !emergencyCall) {
+            // This is the state where the user is expected to select an account
+            call.setState(CallState.PRE_DIAL_WAIT);
+        } else {
+            call.setState(CallState.CONNECTING);
+        }
+
+        if (!isPotentialMMICode(handle)) {
+            addCall(call);
+        } else {
+            call.addListener(this);
+        }
+
+        return call;
+    }
+
+    /**
+     * Attempts to issue/connect the specified call.
+     *
+     * @param handle Handle to connect the call with.
+     * @param gatewayInfo Optional gateway information that can be used to route the call to the
+     *        actual dialed handle via a gateway provider. May be null.
+     * @param speakerphoneOn Whether or not to turn the speakerphone on once the call connects.
+     * @param videoState The desired video state for the outgoing call.
+     */
+    void placeOutgoingCall(Call call, Uri handle, GatewayInfo gatewayInfo, boolean speakerphoneOn,
+            int videoState) {
+        if (call == null) {
+            // don't do anything if the call no longer exists
+            Log.i(this, "Canceling unknown call.");
+            return;
+        }
+
+        final Uri uriHandle = (gatewayInfo == null) ? handle : gatewayInfo.getGatewayAddress();
+
+        if (gatewayInfo == null) {
+            Log.i(this, "Creating a new outgoing call with handle: %s", Log.piiHandle(uriHandle));
+        } else {
+            Log.i(this, "Creating a new outgoing call with gateway handle: %s, original handle: %s",
+                    Log.pii(uriHandle), Log.pii(handle));
+        }
+
+        call.setHandle(uriHandle);
+        call.setGatewayInfo(gatewayInfo);
+        call.setStartWithSpeakerphoneOn(speakerphoneOn);
+        call.setVideoState(videoState);
+
+        TelecomApp app = TelecomApp.getInstance();
+        final boolean emergencyCall = TelephonyUtil.shouldProcessAsEmergency(app, call.getHandle());
+        if (emergencyCall) {
+            // Emergency -- CreateConnectionProcessor will choose accounts automatically
+            call.setTargetPhoneAccount(null);
+        }
+
+        if (call.getTargetPhoneAccount() != null || emergencyCall) {
+            // If the account has been set, proceed to place the outgoing call.
+            // Otherwise the connection will be initiated when the account is set by the user.
+            call.startCreateConnection();
+        }
+    }
+
+    /**
+     * Attempts to start a conference call for the specified call.
+     *
+     * @param call The call to conference.
+     * @param otherCall The other call to conference with.
+     */
+    void conference(Call call, Call otherCall) {
+        call.conferenceWith(otherCall);
+    }
+
+    /**
+     * Instructs Telecom to answer the specified call. Intended to be invoked by the in-call
+     * app through {@link InCallAdapter} after Telecom notifies it of an incoming call followed by
+     * the user opting to answer said call.
+     *
+     * @param call The call to answer.
+     * @param videoState The video state in which to answer the call.
+     */
+    void answerCall(Call call, int videoState) {
+        if (!mCalls.contains(call)) {
+            Log.i(this, "Request to answer a non-existent call %s", call);
+        } else {
+            // If the foreground call is not the ringing call and it is currently isActive() or
+            // STATE_DIALING, put it on hold before answering the call.
+            if (mForegroundCall != null && mForegroundCall != call &&
+                    (mForegroundCall.isActive() ||
+                     mForegroundCall.getState() == CallState.DIALING)) {
+                Log.v(this, "Holding active/dialing call %s before answering incoming call %s.",
+                        mForegroundCall, call);
+                mForegroundCall.hold();
+                // TODO: Wait until we get confirmation of the active call being
+                // on-hold before answering the new call.
+                // TODO: Import logic from CallManager.acceptCall()
+            }
+
+            for (CallsManagerListener listener : mListeners) {
+                listener.onIncomingCallAnswered(call);
+            }
+
+            // We do not update the UI until we get confirmation of the answer() through
+            // {@link #markCallAsActive}.
+            call.answer(videoState);
+        }
+    }
+
+    /**
+     * Instructs Telecom to reject the specified call. Intended to be invoked by the in-call
+     * app through {@link InCallAdapter} after Telecom notifies it of an incoming call followed by
+     * the user opting to reject said call.
+     */
+    void rejectCall(Call call, boolean rejectWithMessage, String textMessage) {
+        if (!mCalls.contains(call)) {
+            Log.i(this, "Request to reject a non-existent call %s", call);
+        } else {
+            for (CallsManagerListener listener : mListeners) {
+                listener.onIncomingCallRejected(call, rejectWithMessage, textMessage);
+            }
+            call.reject(rejectWithMessage, textMessage);
+        }
+    }
+
+    /**
+     * Instructs Telecom to play the specified DTMF tone within the specified call.
+     *
+     * @param digit The DTMF digit to play.
+     */
+    void playDtmfTone(Call call, char digit) {
+        if (!mCalls.contains(call)) {
+            Log.i(this, "Request to play DTMF in a non-existent call %s", call);
+        } else {
+            call.playDtmfTone(digit);
+            mDtmfLocalTonePlayer.playTone(call, digit);
+        }
+    }
+
+    /**
+     * Instructs Telecom to stop the currently playing DTMF tone, if any.
+     */
+    void stopDtmfTone(Call call) {
+        if (!mCalls.contains(call)) {
+            Log.i(this, "Request to stop DTMF in a non-existent call %s", call);
+        } else {
+            call.stopDtmfTone();
+            mDtmfLocalTonePlayer.stopTone(call);
+        }
+    }
+
+    /**
+     * Instructs Telecom to continue (or not) the current post-dial DTMF string, if any.
+     */
+    void postDialContinue(Call call, boolean proceed) {
+        if (!mCalls.contains(call)) {
+            Log.i(this, "Request to continue post-dial string in a non-existent call %s", call);
+        } else {
+            call.postDialContinue(proceed);
+        }
+    }
+
+    /**
+     * Instructs Telecom to disconnect the specified call. Intended to be invoked by the
+     * in-call app through {@link InCallAdapter} for an ongoing call. This is usually triggered by
+     * the user hitting the end-call button.
+     */
+    void disconnectCall(Call call) {
+        Log.v(this, "disconnectCall %s", call);
+
+        if (!mCalls.contains(call)) {
+            Log.w(this, "Unknown call (%s) asked to disconnect", call);
+        } else {
+            call.disconnect();
+        }
+    }
+
+    /**
+     * Instructs Telecom to disconnect all calls.
+     */
+    void disconnectAllCalls() {
+        Log.v(this, "disconnectAllCalls");
+
+        for (Call call : mCalls) {
+            disconnectCall(call);
+        }
+    }
+
+
+    /**
+     * Instructs Telecom to put the specified call on hold. Intended to be invoked by the
+     * in-call app through {@link InCallAdapter} for an ongoing call. This is usually triggered by
+     * the user hitting the hold button during an active call.
+     */
+    void holdCall(Call call) {
+        if (!mCalls.contains(call)) {
+            Log.w(this, "Unknown call (%s) asked to be put on hold", call);
+        } else {
+            Log.d(this, "Putting call on hold: (%s)", call);
+            call.hold();
+        }
+    }
+
+    /**
+     * Instructs Telecom to release the specified call from hold. Intended to be invoked by
+     * the in-call app through {@link InCallAdapter} for an ongoing call. This is usually triggered
+     * by the user hitting the hold button during a held call.
+     */
+    void unholdCall(Call call) {
+        if (!mCalls.contains(call)) {
+            Log.w(this, "Unknown call (%s) asked to be removed from hold", call);
+        } else {
+            Log.d(this, "unholding call: (%s)", call);
+            for (Call c : mCalls) {
+                if (c != null && c.isAlive() && c != call) {
+                    c.hold();
+                }
+            }
+            call.unhold();
+        }
+    }
+
+    /** Called by the in-call UI to change the mute state. */
+    void mute(boolean shouldMute) {
+        mCallAudioManager.mute(shouldMute);
+    }
+
+    /**
+      * Called by the in-call UI to change the audio route, for example to change from earpiece to
+      * speaker phone.
+      */
+    void setAudioRoute(int route) {
+        mCallAudioManager.setAudioRoute(route);
+    }
+
+    /** Called by the in-call UI to turn the proximity sensor on. */
+    void turnOnProximitySensor() {
+        mProximitySensorManager.turnOn();
+    }
+
+    /**
+     * Called by the in-call UI to turn the proximity sensor off.
+     * @param screenOnImmediately If true, the screen will be turned on immediately. Otherwise,
+     *        the screen will be kept off until the proximity sensor goes negative.
+     */
+    void turnOffProximitySensor(boolean screenOnImmediately) {
+        mProximitySensorManager.turnOff(screenOnImmediately);
+    }
+
+    void phoneAccountSelected(Call call, PhoneAccountHandle account) {
+        if (!mCalls.contains(call)) {
+            Log.i(this, "Attemped to add account to unknown call %s", call);
+        } else {
+            call.setTargetPhoneAccount(account);
+            call.startCreateConnection();
+        }
+    }
+
+    /** Called when the audio state changes. */
+    void onAudioStateChanged(AudioState oldAudioState, AudioState newAudioState) {
+        Log.v(this, "onAudioStateChanged, audioState: %s -> %s", oldAudioState, newAudioState);
+        for (CallsManagerListener listener : mListeners) {
+            listener.onAudioStateChanged(oldAudioState, newAudioState);
+        }
+    }
+
+    void markCallAsRinging(Call call) {
+        setCallState(call, CallState.RINGING);
+    }
+
+    void markCallAsDialing(Call call) {
+        setCallState(call, CallState.DIALING);
+    }
+
+    void markCallAsActive(Call call) {
+        if (call.getConnectTimeMillis() == 0) {
+            call.setConnectTimeMillis(System.currentTimeMillis());
+        }
+        setCallState(call, CallState.ACTIVE);
+
+        if (call.getStartWithSpeakerphoneOn()) {
+            setAudioRoute(AudioState.ROUTE_SPEAKER);
+        }
+    }
+
+    void markCallAsOnHold(Call call) {
+        setCallState(call, CallState.ON_HOLD);
+    }
+
+    /**
+     * Marks the specified call as STATE_DISCONNECTED and notifies the in-call app. If this was the last
+     * live call, then also disconnect from the in-call controller.
+     *
+     * @param disconnectCause The disconnect reason, see {@link android.telephony.DisconnectCause}.
+     * @param disconnectMessage Optional message about the disconnect.
+     */
+    void markCallAsDisconnected(Call call, int disconnectCause, String disconnectMessage) {
+        call.setDisconnectCause(disconnectCause, disconnectMessage);
+        setCallState(call, CallState.DISCONNECTED);
+        removeCall(call);
+    }
+
+    /**
+     * Removes an existing disconnected call, and notifies the in-call app.
+     */
+    void markCallAsRemoved(Call call) {
+        removeCall(call);
+    }
+
+    /**
+     * Cleans up any calls currently associated with the specified connection service when the
+     * service binder disconnects unexpectedly.
+     *
+     * @param service The connection service that disconnected.
+     */
+    void handleConnectionServiceDeath(ConnectionServiceWrapper service) {
+        if (service != null) {
+            for (Call call : mCalls) {
+                if (call.getConnectionService() == service) {
+                    markCallAsDisconnected(call, DisconnectCause.ERROR_UNSPECIFIED, null);
+                }
+            }
+        }
+    }
+
+    boolean hasAnyCalls() {
+        return !mCalls.isEmpty();
+    }
+
+    boolean hasActiveOrHoldingCall() {
+        return getFirstCallWithState(CallState.ACTIVE, CallState.ON_HOLD) != null;
+    }
+
+    boolean hasRingingCall() {
+        return getFirstCallWithState(CallState.RINGING) != null;
+    }
+
+    boolean onMediaButton(int type) {
+        if (hasAnyCalls()) {
+            if (HeadsetMediaButton.SHORT_PRESS == type) {
+                Call ringingCall = getFirstCallWithState(CallState.RINGING);
+                if (ringingCall == null) {
+                    mCallAudioManager.toggleMute();
+                    return true;
+                } else {
+                    ringingCall.answer(ringingCall.getVideoState());
+                    return true;
+                }
+            } else if (HeadsetMediaButton.LONG_PRESS == type) {
+                Log.d(this, "handleHeadsetHook: longpress -> hangup");
+                Call callToHangup = getFirstCallWithState(
+                        CallState.RINGING, CallState.DIALING, CallState.ACTIVE, CallState.ON_HOLD);
+                if (callToHangup != null) {
+                    callToHangup.disconnect();
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Checks to see if the specified call is the only high-level call and if so, enable the
+     * "Add-call" button. We allow you to add a second call but not a third or beyond.
+     *
+     * @param call The call to test for add-call.
+     * @return Whether the add-call feature should be enabled for the call.
+     */
+    protected boolean isAddCallCapable(Call call) {
+        if (call.getParentCall() != null) {
+            // Never true for child calls.
+            return false;
+        }
+
+        // Loop through all the other calls and there exists a top level (has no parent) call
+        // that is not the specified call, return false.
+        for (Call otherCall : mCalls) {
+            if (call != otherCall && otherCall.getParentCall() == null) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Returns the first call that it finds with the given states. The states are treated as having
+     * priority order so that any call with the first state will be returned before any call with
+     * states listed later in the parameter list.
+     */
+    Call getFirstCallWithState(int... states) {
+        for (int currentState : states) {
+            // check the foreground first
+            if (mForegroundCall != null && mForegroundCall.getState() == currentState) {
+                return mForegroundCall;
+            }
+
+            for (Call call : mCalls) {
+                if (currentState == call.getState()) {
+                    return call;
+                }
+            }
+        }
+        return null;
+    }
+
+    Call createConferenceCall(
+            PhoneAccountHandle phoneAccount,
+            ParcelableConference parcelableConference) {
+        Call call = new Call(
+                mConnectionServiceRepository,
+                null /* handle */,
+                null /* gatewayInfo */,
+                null /* connectionManagerPhoneAccount */,
+                phoneAccount,
+                false /* isIncoming */,
+                true /* isConference */);
+
+        setCallState(call, Call.getStateFromConnectionState(parcelableConference.getState()));
+        if (call.getState() == CallState.ACTIVE) {
+            call.setConnectTimeMillis(System.currentTimeMillis());
+        }
+        call.setCallCapabilities(parcelableConference.getCapabilities());
+
+        // TODO: Move this to be a part of addCall()
+        call.addListener(this);
+        addCall(call);
+        return call;
+    }
+
+
+    /**
+     * Adds the specified call to the main list of live calls.
+     *
+     * @param call The call to add.
+     */
+    private void addCall(Call call) {
+        Log.v(this, "addCall(%s)", call);
+
+        call.addListener(this);
+        mCalls.add(call);
+
+        // TODO: Update mForegroundCall prior to invoking
+        // onCallAdded for calls which immediately take the foreground (like the first call).
+        for (CallsManagerListener listener : mListeners) {
+            listener.onCallAdded(call);
+        }
+        updateForegroundCall();
+    }
+
+    private void removeCall(Call call) {
+        Log.v(this, "removeCall(%s)", call);
+
+        call.setParentCall(null);  // need to clean up parent relationship before destroying.
+        call.removeListener(this);
+        call.clearConnectionService();
+
+        boolean shouldNotify = false;
+        if (mCalls.contains(call)) {
+            mCalls.remove(call);
+            shouldNotify = true;
+        }
+
+        // Only broadcast changes for calls that are being tracked.
+        if (shouldNotify) {
+            for (CallsManagerListener listener : mListeners) {
+                listener.onCallRemoved(call);
+            }
+            updateForegroundCall();
+        }
+    }
+
+    /**
+     * Sets the specified state on the specified call.
+     *
+     * @param call The call.
+     * @param newState The new state of the call.
+     */
+    private void setCallState(Call call, int newState) {
+        if (call == null) {
+            return;
+        }
+        int oldState = call.getState();
+        Log.i(this, "setCallState %s -> %s, call: %s", CallState.toString(oldState),
+                CallState.toString(newState), call);
+        if (newState != oldState) {
+            // Unfortunately, in the telephony world the radio is king. So if the call notifies
+            // us that the call is in a particular state, we allow it even if it doesn't make
+            // sense (e.g., STATE_ACTIVE -> STATE_RINGING).
+            // TODO: Consider putting a stop to the above and turning CallState
+            // into a well-defined state machine.
+            // TODO: Define expected state transitions here, and log when an
+            // unexpected transition occurs.
+            call.setState(newState);
+
+            // Only broadcast state change for calls that are being tracked.
+            if (mCalls.contains(call)) {
+                for (CallsManagerListener listener : mListeners) {
+                    listener.onCallStateChanged(call, oldState, newState);
+                }
+                updateForegroundCall();
+            }
+        }
+    }
+
+    /**
+     * Checks which call should be visible to the user and have audio focus.
+     */
+    private void updateForegroundCall() {
+        Call newForegroundCall = null;
+        for (Call call : mCalls) {
+            // TODO: Foreground-ness needs to be explicitly set. No call, regardless
+            // of its state will be foreground by default and instead the connection service should
+            // be notified when its calls enter and exit foreground state. Foreground will mean that
+            // the call should play audio and listen to microphone if it wants.
+
+            // Active calls have priority.
+            if (call.isActive()) {
+                newForegroundCall = call;
+                break;
+            }
+
+            if (call.isAlive() || call.getState() == CallState.RINGING) {
+                newForegroundCall = call;
+                // Don't break in case there's an active call that has priority.
+            }
+        }
+
+        if (newForegroundCall != mForegroundCall) {
+            Log.v(this, "Updating foreground call, %s -> %s.", mForegroundCall, newForegroundCall);
+            Call oldForegroundCall = mForegroundCall;
+            mForegroundCall = newForegroundCall;
+
+            for (CallsManagerListener listener : mListeners) {
+                listener.onForegroundCallChanged(oldForegroundCall, mForegroundCall);
+            }
+        }
+    }
+
+    private boolean isPotentialMMICode(Uri handle) {
+        return (handle != null && handle.getSchemeSpecificPart() != null
+                && handle.getSchemeSpecificPart().contains("#"));
+    }
+}
diff --git a/src/com/android/server/telecom/CallsManagerListenerBase.java b/src/com/android/server/telecom/CallsManagerListenerBase.java
new file mode 100644
index 0000000..93b2a24
--- /dev/null
+++ b/src/com/android/server/telecom/CallsManagerListenerBase.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 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;
+
+import android.telecom.AudioState;
+
+/**
+ * Provides a default implementation for listeners of CallsManager.
+ */
+class CallsManagerListenerBase implements CallsManager.CallsManagerListener {
+    @Override
+    public void onCallAdded(Call call) {
+    }
+
+    @Override
+    public void onCallRemoved(Call call) {
+    }
+
+    @Override
+    public void onCallStateChanged(Call call, int oldState, int newState) {
+    }
+
+    @Override
+    public void onConnectionServiceChanged(
+            Call call,
+            ConnectionServiceWrapper oldService,
+            ConnectionServiceWrapper newService) {
+    }
+
+    @Override
+    public void onIncomingCallAnswered(Call call) {
+    }
+
+    @Override
+    public void onIncomingCallRejected(Call call, boolean rejectWithMessage, String textMessage) {
+    }
+
+    @Override
+    public void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall) {
+    }
+
+    @Override
+    public void onAudioStateChanged(AudioState oldAudioState, AudioState newAudioState) {
+    }
+
+    @Override
+    public void onRingbackRequested(Call call, boolean ringback) {
+    }
+
+    @Override
+    public void onIsConferencedChanged(Call call) {
+    }
+
+    @Override
+    public void onIsVoipAudioModeChanged(Call call) {
+    }
+
+    @Override
+    public void onVideoStateChanged(Call call) {
+    }
+}
diff --git a/src/com/android/server/telecom/ConnectionServiceRepository.java b/src/com/android/server/telecom/ConnectionServiceRepository.java
new file mode 100644
index 0000000..d7a6d3e
--- /dev/null
+++ b/src/com/android/server/telecom/ConnectionServiceRepository.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.content.ComponentName;
+
+import java.util.HashMap;
+
+/**
+ * Searches for and returns connection services.
+ */
+final class ConnectionServiceRepository
+        implements ServiceBinder.Listener<ConnectionServiceWrapper> {
+    private final HashMap<ComponentName, ConnectionServiceWrapper> mServiceCache =
+            new HashMap<ComponentName, ConnectionServiceWrapper>();
+
+    ConnectionServiceRepository() {
+    }
+
+    ConnectionServiceWrapper getService(ComponentName componentName) {
+        ConnectionServiceWrapper service = mServiceCache.get(componentName);
+        if (service == null) {
+            service = new ConnectionServiceWrapper(
+                    componentName,
+                    this,
+                    TelecomApp.getInstance().getPhoneAccountRegistrar());
+            service.addListener(this);
+            mServiceCache.put(componentName, service);
+        }
+        return service;
+    }
+
+    /**
+     * Removes the specified service from the cache when the service unbinds.
+     *
+     * {@inheritDoc}
+     */
+    @Override
+    public void onUnbind(ConnectionServiceWrapper service) {
+        mServiceCache.remove(service.getComponentName());
+    }
+}
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
new file mode 100644
index 0000000..669f9a5
--- /dev/null
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -0,0 +1,1003 @@
+/*
+ * 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;
+
+import android.content.ComponentName;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.RemoteException;
+import android.telecom.AudioState;
+import android.telecom.Connection;
+import android.telecom.ConnectionRequest;
+import android.telecom.ConnectionService;
+import android.telecom.GatewayInfo;
+import android.telecom.ParcelableConference;
+import android.telecom.ParcelableConnection;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.StatusHints;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
+import android.telephony.DisconnectCause;
+
+import com.android.internal.os.SomeArgs;
+import com.android.internal.telecom.IConnectionService;
+import com.android.internal.telecom.IConnectionServiceAdapter;
+import com.android.internal.telecom.IVideoProvider;
+import com.android.internal.telecom.RemoteServiceCallback;
+import com.google.common.base.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Wrapper for {@link IConnectionService}s, handles binding to {@link IConnectionService} and keeps
+ * track of when the object can safely be unbound. Other classes should not use
+ * {@link IConnectionService} directly and instead should use this class to invoke methods of
+ * {@link IConnectionService}.
+ */
+final class ConnectionServiceWrapper extends ServiceBinder<IConnectionService> {
+    private static final int MSG_HANDLE_CREATE_CONNECTION_COMPLETE = 1;
+    private static final int MSG_SET_ACTIVE = 2;
+    private static final int MSG_SET_RINGING = 3;
+    private static final int MSG_SET_DIALING = 4;
+    private static final int MSG_SET_DISCONNECTED = 5;
+    private static final int MSG_SET_ON_HOLD = 6;
+    private static final int MSG_SET_RINGBACK_REQUESTED = 7;
+    private static final int MSG_SET_CALL_CAPABILITIES = 8;
+    private static final int MSG_SET_IS_CONFERENCED = 9;
+    private static final int MSG_ADD_CONFERENCE_CALL = 10;
+    private static final int MSG_REMOVE_CALL = 11;
+    private static final int MSG_ON_POST_DIAL_WAIT = 12;
+    private static final int MSG_QUERY_REMOTE_CALL_SERVICES = 13;
+    private static final int MSG_SET_VIDEO_PROVIDER = 14;
+    private static final int MSG_SET_IS_VOIP_AUDIO_MODE = 15;
+    private static final int MSG_SET_STATUS_HINTS = 16;
+    private static final int MSG_SET_ADDRESS = 17;
+    private static final int MSG_SET_CALLER_DISPLAY_NAME = 18;
+    private static final int MSG_SET_VIDEO_STATE = 19;
+    private static final int MSG_SET_CONFERENCEABLE_CONNECTIONS = 20;
+
+    private final Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            Call call;
+            switch (msg.what) {
+                case MSG_HANDLE_CREATE_CONNECTION_COMPLETE: {
+                    SomeArgs args = (SomeArgs) msg.obj;
+                    try {
+                        String callId = (String) args.arg1;
+                        ConnectionRequest request = (ConnectionRequest) args.arg2;
+                        ParcelableConnection connection = (ParcelableConnection) args.arg3;
+                        handleCreateConnectionComplete(callId, request, connection);
+                    } finally {
+                        args.recycle();
+                    }
+                    break;
+                }
+                case MSG_SET_ACTIVE:
+                    call = mCallIdMapper.getCall(msg.obj);
+                    if (call != null) {
+                        mCallsManager.markCallAsActive(call);
+                    } else {
+                        //Log.w(this, "setActive, unknown call id: %s", msg.obj);
+                    }
+                    break;
+                case MSG_SET_RINGING:
+                    call = mCallIdMapper.getCall(msg.obj);
+                    if (call != null) {
+                        mCallsManager.markCallAsRinging(call);
+                    } else {
+                        //Log.w(this, "setRinging, unknown call id: %s", msg.obj);
+                    }
+                    break;
+                case MSG_SET_DIALING:
+                    call = mCallIdMapper.getCall(msg.obj);
+                    if (call != null) {
+                        mCallsManager.markCallAsDialing(call);
+                    } else {
+                        //Log.w(this, "setDialing, unknown call id: %s", msg.obj);
+                    }
+                    break;
+                case MSG_SET_DISCONNECTED: {
+                    SomeArgs args = (SomeArgs) msg.obj;
+                    try {
+                        call = mCallIdMapper.getCall(args.arg1);
+                        String disconnectMessage = (String) args.arg2;
+                        int disconnectCause = args.argi1;
+                        Log.d(this, "disconnect call %s %s", args.arg1, call);
+                        if (call != null) {
+                            mCallsManager.markCallAsDisconnected(call, disconnectCause,
+                                    disconnectMessage);
+                        } else {
+                            //Log.w(this, "setDisconnected, unknown call id: %s", args.arg1);
+                        }
+                    } finally {
+                        args.recycle();
+                    }
+                    break;
+                }
+                case MSG_SET_ON_HOLD:
+                    call = mCallIdMapper.getCall(msg.obj);
+                    if (call != null) {
+                        mCallsManager.markCallAsOnHold(call);
+                    } else {
+                        //Log.w(this, "setOnHold, unknown call id: %s", msg.obj);
+                    }
+                    break;
+                case MSG_SET_RINGBACK_REQUESTED: {
+                    call = mCallIdMapper.getCall(msg.obj);
+                    if (call != null) {
+                        call.setRingbackRequested(msg.arg1 == 1);
+                    } else {
+                        //Log.w(this, "setRingback, unknown call id: %s", args.arg1);
+                    }
+                    break;
+                }
+                case MSG_SET_CALL_CAPABILITIES: {
+                    call = mCallIdMapper.getCall(msg.obj);
+                    if (call != null) {
+                        call.setCallCapabilities(msg.arg1);
+                    } else {
+                        //Log.w(ConnectionServiceWrapper.this,
+                        //      "setCallCapabilities, unknown call id: %s", msg.obj);
+                    }
+                    break;
+                }
+                case MSG_SET_IS_CONFERENCED: {
+                    SomeArgs args = (SomeArgs) msg.obj;
+                    try {
+                        Call childCall = mCallIdMapper.getCall(args.arg1);
+                        Log.d(this, "SET_IS_CONFERENCE: %s %s", args.arg1, args.arg2);
+                        if (childCall != null) {
+                            String conferenceCallId = (String) args.arg2;
+                            if (conferenceCallId == null) {
+                                Log.d(this, "unsetting parent: %s", args.arg1);
+                                childCall.setParentCall(null);
+                            } else {
+                                Call conferenceCall = mCallIdMapper.getCall(conferenceCallId);
+                                childCall.setParentCall(conferenceCall);
+                            }
+                        } else {
+                            //Log.w(this, "setIsConferenced, unknown call id: %s", args.arg1);
+                        }
+                    } finally {
+                        args.recycle();
+                    }
+                    break;
+                }
+                case MSG_ADD_CONFERENCE_CALL: {
+                    SomeArgs args = (SomeArgs) msg.obj;
+                    try {
+                        String id = (String) args.arg1;
+                        if (mCallIdMapper.getCall(id) != null) {
+                            Log.w(this, "Attempting to add a conference call using an existing " +
+                                    "call id %s", id);
+                            break;
+                        }
+                        ParcelableConference parcelableConference =
+                                (ParcelableConference) args.arg2;
+                        // need to create a new Call
+                        Call conferenceCall = mCallsManager.createConferenceCall(
+                                null, parcelableConference);
+                        mCallIdMapper.addCall(conferenceCall, id);
+                        conferenceCall.setConnectionService(ConnectionServiceWrapper.this);
+
+                        Log.d(this, "adding children to conference %s",
+                                parcelableConference.getConnectionIds());
+                        for (String callId : parcelableConference.getConnectionIds()) {
+                            Call childCall = mCallIdMapper.getCall(callId);
+                            Log.d(this, "found child: %s", callId);
+                            if (childCall != null) {
+                                childCall.setParentCall(conferenceCall);
+                            }
+                        }
+                    } finally {
+                        args.recycle();
+                    }
+                    break;
+                }
+                case MSG_REMOVE_CALL: {
+                    call = mCallIdMapper.getCall(msg.obj);
+                    if (call != null) {
+                        if (call.isActive()) {
+                            mCallsManager.markCallAsDisconnected(
+                                    call, DisconnectCause.NORMAL, null);
+                        } else {
+                            mCallsManager.markCallAsRemoved(call);
+                        }
+                    }
+                    break;
+                }
+                case MSG_ON_POST_DIAL_WAIT: {
+                    SomeArgs args = (SomeArgs) msg.obj;
+                    try {
+                        call = mCallIdMapper.getCall(args.arg1);
+                        if (call != null) {
+                            String remaining = (String) args.arg2;
+                            call.onPostDialWait(remaining);
+                        } else {
+                            //Log.w(this, "onPostDialWait, unknown call id: %s", args.arg1);
+                        }
+                    } finally {
+                        args.recycle();
+                    }
+                    break;
+                }
+                case MSG_QUERY_REMOTE_CALL_SERVICES: {
+                    queryRemoteConnectionServices((RemoteServiceCallback) msg.obj);
+                    break;
+                }
+                case MSG_SET_VIDEO_PROVIDER: {
+                    SomeArgs args = (SomeArgs) msg.obj;
+                    try {
+                        call = mCallIdMapper.getCall(args.arg1);
+                        IVideoProvider videoProvider = (IVideoProvider) args.arg2;
+                        if (call != null) {
+                            call.setVideoProvider(videoProvider);
+                        }
+                    } finally {
+                        args.recycle();
+                    }
+                    break;
+                }
+                case MSG_SET_IS_VOIP_AUDIO_MODE: {
+                    call = mCallIdMapper.getCall(msg.obj);
+                    if (call != null) {
+                        call.setIsVoipAudioMode(msg.arg1 == 1);
+                    }
+                    break;
+                }
+                case MSG_SET_STATUS_HINTS: {
+                    SomeArgs args = (SomeArgs) msg.obj;
+                    try {
+                        call = mCallIdMapper.getCall(args.arg1);
+                        StatusHints statusHints = (StatusHints) args.arg2;
+                        if (call != null) {
+                            call.setStatusHints(statusHints);
+                        }
+                    } finally {
+                        args.recycle();
+                    }
+                    break;
+                }
+                case MSG_SET_ADDRESS: {
+                    SomeArgs args = (SomeArgs) msg.obj;
+                    try {
+                        call = mCallIdMapper.getCall(args.arg1);
+                        if (call != null) {
+                            call.setHandle((Uri) args.arg2, args.argi1);
+                        }
+                    } finally {
+                        args.recycle();
+                    }
+                    break;
+                }
+                case MSG_SET_CALLER_DISPLAY_NAME: {
+                    SomeArgs args = (SomeArgs) msg.obj;
+                    try {
+                        call = mCallIdMapper.getCall(args.arg1);
+                        if (call != null) {
+                            call.setCallerDisplayName((String) args.arg2, args.argi1);
+                        }
+                    } finally {
+                        args.recycle();
+                    }
+                    break;
+                }
+                case MSG_SET_VIDEO_STATE: {
+                    call = mCallIdMapper.getCall(msg.obj);
+                    if (call != null) {
+                        call.setVideoState(msg.arg1);
+                    }
+                    break;
+                }
+                case MSG_SET_CONFERENCEABLE_CONNECTIONS: {
+                    SomeArgs args = (SomeArgs) msg.obj;
+                    try {
+                        call = mCallIdMapper.getCall(args.arg1);
+                        if (call != null ){
+                            @SuppressWarnings("unchecked")
+                            List<String> conferenceableIds = (List<String>) args.arg2;
+                            List<Call> conferenceableCalls =
+                                    new ArrayList<>(conferenceableIds.size());
+                            for (String otherId : (List<String>) args.arg2) {
+                                Call otherCall = mCallIdMapper.getCall(otherId);
+                                if (otherCall != null && otherCall != call) {
+                                    conferenceableCalls.add(otherCall);
+                                }
+                            }
+                            call.setConferenceableCalls(conferenceableCalls);
+                        }
+                    } finally {
+                        args.recycle();
+                    }
+                    break;
+                }
+            }
+        }
+    };
+
+    private final class Adapter extends IConnectionServiceAdapter.Stub {
+
+        @Override
+        public void handleCreateConnectionComplete(
+                String callId,
+                ConnectionRequest request,
+                ParcelableConnection connection) {
+            logIncoming("handleCreateConnectionComplete %s", request);
+            if (mCallIdMapper.isValidCallId(callId)) {
+                SomeArgs args = SomeArgs.obtain();
+                args.arg1 = callId;
+                args.arg2 = request;
+                args.arg3 = connection;
+                mHandler.obtainMessage(MSG_HANDLE_CREATE_CONNECTION_COMPLETE, args)
+                        .sendToTarget();
+            }
+        }
+
+        @Override
+        public void setActive(String callId) {
+            logIncoming("setActive %s", callId);
+            if (mCallIdMapper.isValidCallId(callId) || mCallIdMapper.isValidConferenceId(callId)) {
+                mHandler.obtainMessage(MSG_SET_ACTIVE, callId).sendToTarget();
+            }
+        }
+
+        @Override
+        public void setRinging(String callId) {
+            logIncoming("setRinging %s", callId);
+            if (mCallIdMapper.isValidCallId(callId)) {
+                mHandler.obtainMessage(MSG_SET_RINGING, callId).sendToTarget();
+            }
+        }
+
+        @Override
+        public void setVideoProvider(String callId, IVideoProvider videoProvider) {
+            logIncoming("setVideoProvider %s", callId);
+            if (mCallIdMapper.isValidCallId(callId)) {
+                SomeArgs args = SomeArgs.obtain();
+                args.arg1 = callId;
+                args.arg2 = videoProvider;
+                mHandler.obtainMessage(MSG_SET_VIDEO_PROVIDER, args).sendToTarget();
+            }
+        }
+
+        @Override
+        public void setDialing(String callId) {
+            logIncoming("setDialing %s", callId);
+            if (mCallIdMapper.isValidCallId(callId)) {
+                mHandler.obtainMessage(MSG_SET_DIALING, callId).sendToTarget();
+            }
+        }
+
+        @Override
+        public void setDisconnected(
+                String callId, int disconnectCause, String disconnectMessage) {
+            logIncoming("setDisconnected %s %d %s", callId, disconnectCause, disconnectMessage);
+            if (mCallIdMapper.isValidCallId(callId) || mCallIdMapper.isValidConferenceId(callId)) {
+                Log.d(this, "disconnect call %s", callId);
+                SomeArgs args = SomeArgs.obtain();
+                args.arg1 = callId;
+                args.arg2 = disconnectMessage;
+                args.argi1 = disconnectCause;
+                mHandler.obtainMessage(MSG_SET_DISCONNECTED, args).sendToTarget();
+            }
+        }
+
+        @Override
+        public void setOnHold(String callId) {
+            logIncoming("setOnHold %s", callId);
+            if (mCallIdMapper.isValidCallId(callId) || mCallIdMapper.isValidConferenceId(callId)) {
+                mHandler.obtainMessage(MSG_SET_ON_HOLD, callId).sendToTarget();
+            }
+        }
+
+        @Override
+        public void setRingbackRequested(String callId, boolean ringback) {
+            logIncoming("setRingbackRequested %s %b", callId, ringback);
+            if (mCallIdMapper.isValidCallId(callId)) {
+                mHandler.obtainMessage(MSG_SET_RINGBACK_REQUESTED, ringback ? 1 : 0, 0, callId)
+                        .sendToTarget();
+            }
+        }
+
+        @Override
+        public void removeCall(String callId) {
+            logIncoming("removeCall %s", callId);
+            if (mCallIdMapper.isValidCallId(callId) || mCallIdMapper.isValidConferenceId(callId)) {
+                mHandler.obtainMessage(MSG_REMOVE_CALL, callId).sendToTarget();
+                mHandler.obtainMessage(MSG_REMOVE_CALL, callId);
+            }
+        }
+
+        @Override
+        public void setCallCapabilities(String callId, int callCapabilities) {
+            logIncoming("setCallCapabilities %s %d", callId, callCapabilities);
+            if (mCallIdMapper.isValidCallId(callId) || mCallIdMapper.isValidConferenceId(callId)) {
+                mHandler.obtainMessage(MSG_SET_CALL_CAPABILITIES, callCapabilities, 0, callId)
+                        .sendToTarget();
+            } else {
+                Log.w(this, "ID not valid for setCallCapabilities");
+            }
+        }
+
+        @Override
+        public void setIsConferenced(String callId, String conferenceCallId) {
+            logIncoming("setIsConferenced %s %s", callId, conferenceCallId);
+            if (mCallIdMapper.isValidCallId(callId)) {
+                SomeArgs args = SomeArgs.obtain();
+                args.arg1 = callId;
+                args.arg2 = conferenceCallId;
+                mHandler.obtainMessage(MSG_SET_IS_CONFERENCED, args).sendToTarget();
+            }
+        }
+
+        @Override
+        public void addConferenceCall(String callId, ParcelableConference parcelableConference) {
+            logIncoming("addConferenceCall %s %s", callId, parcelableConference);
+            // We do not check call Ids here because we do not yet know the call ID for new
+            // conference calls.
+            SomeArgs args = SomeArgs.obtain();
+            args.arg1 = callId;
+            args.arg2 = parcelableConference;
+            mHandler.obtainMessage(MSG_ADD_CONFERENCE_CALL, args).sendToTarget();
+        }
+
+        @Override
+        public void onPostDialWait(String callId, String remaining) throws RemoteException {
+            logIncoming("onPostDialWait %s %s", callId, remaining);
+            if (mCallIdMapper.isValidCallId(callId)) {
+                SomeArgs args = SomeArgs.obtain();
+                args.arg1 = callId;
+                args.arg2 = remaining;
+                mHandler.obtainMessage(MSG_ON_POST_DIAL_WAIT, args).sendToTarget();
+            }
+        }
+
+        @Override
+        public void queryRemoteConnectionServices(RemoteServiceCallback callback) {
+            logIncoming("queryRemoteCSs");
+            mHandler.obtainMessage(MSG_QUERY_REMOTE_CALL_SERVICES, callback).sendToTarget();
+        }
+
+        @Override
+        public void setVideoState(String callId, int videoState) {
+            logIncoming("setVideoState %s %d", callId, videoState);
+            if (mCallIdMapper.isValidCallId(callId)) {
+                mHandler.obtainMessage(MSG_SET_VIDEO_STATE, videoState, 0, callId).sendToTarget();
+            }
+        }
+
+        @Override
+        public void setIsVoipAudioMode(String callId, boolean isVoip) {
+            logIncoming("setIsVoipAudioMode %s %b", callId, isVoip);
+            if (mCallIdMapper.isValidCallId(callId)) {
+                mHandler.obtainMessage(MSG_SET_IS_VOIP_AUDIO_MODE, isVoip ? 1 : 0, 0,
+                        callId).sendToTarget();
+            }
+        }
+
+        @Override
+        public void setStatusHints(String callId, StatusHints statusHints) {
+            logIncoming("setStatusHints %s %s", callId, statusHints);
+            if (mCallIdMapper.isValidCallId(callId)) {
+                SomeArgs args = SomeArgs.obtain();
+                args.arg1 = callId;
+                args.arg2 = statusHints;
+                mHandler.obtainMessage(MSG_SET_STATUS_HINTS, args).sendToTarget();
+            }
+        }
+
+        @Override
+        public void setAddress(String callId, Uri address, int presentation) {
+            logIncoming("setAddress %s %s %d", callId, address, presentation);
+            if (mCallIdMapper.isValidCallId(callId)) {
+                SomeArgs args = SomeArgs.obtain();
+                args.arg1 = callId;
+                args.arg2 = address;
+                args.argi1 = presentation;
+                mHandler.obtainMessage(MSG_SET_ADDRESS, args).sendToTarget();
+            }
+        }
+
+        @Override
+        public void setCallerDisplayName(
+                String callId, String callerDisplayName, int presentation) {
+            logIncoming("setCallerDisplayName %s %s %d", callId, callerDisplayName, presentation);
+            if (mCallIdMapper.isValidCallId(callId)) {
+                SomeArgs args = SomeArgs.obtain();
+                args.arg1 = callId;
+                args.arg2 = callerDisplayName;
+                args.argi1 = presentation;
+                mHandler.obtainMessage(MSG_SET_CALLER_DISPLAY_NAME, args).sendToTarget();
+            }
+        }
+
+        @Override
+        public void setConferenceableConnections(
+                String callId, List<String> conferenceableCallIds) {
+            logIncoming("setConferenceableConnections %s %s", callId, conferenceableCallIds);
+            if (mCallIdMapper.isValidCallId(callId)) {
+                SomeArgs args = SomeArgs.obtain();
+                args.arg1 = callId;
+                args.arg2 = conferenceableCallIds;
+                mHandler.obtainMessage(MSG_SET_CONFERENCEABLE_CONNECTIONS, args).sendToTarget();
+            }
+        }
+    }
+
+    private final Adapter mAdapter = new Adapter();
+    private final CallsManager mCallsManager = CallsManager.getInstance();
+    /**
+     * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
+     * load factor before resizing, 1 means we only expect a single thread to
+     * access the map so make only a single shard
+     */
+    private final Set<Call> mPendingConferenceCalls = Collections.newSetFromMap(
+            new ConcurrentHashMap<Call, Boolean>(8, 0.9f, 1));
+    private final CallIdMapper mCallIdMapper = new CallIdMapper("ConnectionService");
+    private final Map<String, CreateConnectionResponse> mPendingResponses = new HashMap<>();
+
+    private Binder mBinder = new Binder();
+    private IConnectionService mServiceInterface;
+    private final ConnectionServiceRepository mConnectionServiceRepository;
+
+    /**
+     * Creates a connection service.
+     *
+     * @param componentName The component name of the service with which to bind.
+     * @param connectionServiceRepository Connection service repository.
+     * @param phoneAccountRegistrar Phone account registrar
+     */
+    ConnectionServiceWrapper(
+            ComponentName componentName,
+            ConnectionServiceRepository connectionServiceRepository,
+            PhoneAccountRegistrar phoneAccountRegistrar) {
+        super(ConnectionService.SERVICE_INTERFACE, componentName);
+        mConnectionServiceRepository = connectionServiceRepository;
+        phoneAccountRegistrar.addListener(new PhoneAccountRegistrar.Listener() {
+            // TODO -- Upon changes to PhoneAccountRegistrar, need to re-wire connections
+            // To do this, we must proxy remote ConnectionService objects
+        });
+    }
+
+    /** See {@link IConnectionService#addConnectionServiceAdapter}. */
+    private void addConnectionServiceAdapter(IConnectionServiceAdapter adapter) {
+        if (isServiceValid("addConnectionServiceAdapter")) {
+            try {
+                logOutgoing("addConnectionServiceAdapter %s", adapter);
+                mServiceInterface.addConnectionServiceAdapter(adapter);
+            } catch (RemoteException e) {
+            }
+        }
+    }
+
+    /**
+     * Creates a new connection for a new outgoing call or to attach to an existing incoming call.
+     */
+    void createConnection(final Call call, final CreateConnectionResponse response) {
+        Log.d(this, "createConnection(%s) via %s.", call, getComponentName());
+        BindCallback callback = new BindCallback() {
+            @Override
+            public void onSuccess() {
+                String callId = mCallIdMapper.getCallId(call);
+                mPendingResponses.put(callId, response);
+
+                GatewayInfo gatewayInfo = call.getGatewayInfo();
+                Bundle extras = call.getExtras();
+                if (gatewayInfo != null && gatewayInfo.getGatewayProviderPackageName() != null &&
+                        gatewayInfo.getOriginalAddress() != null) {
+                    extras = (Bundle) extras.clone();
+                    extras.putString(
+                            TelecomManager.GATEWAY_PROVIDER_PACKAGE,
+                            gatewayInfo.getGatewayProviderPackageName());
+                    extras.putParcelable(
+                            TelecomManager.GATEWAY_ORIGINAL_ADDRESS,
+                            gatewayInfo.getOriginalAddress());
+                }
+
+                try {
+                    mServiceInterface.createConnection(
+                            call.getConnectionManagerPhoneAccount(),
+                            callId,
+                            new ConnectionRequest(
+                                    call.getTargetPhoneAccount(),
+                                    call.getHandle(),
+                                    extras,
+                                    call.getVideoState()),
+                            call.isIncoming());
+                } catch (RemoteException e) {
+                    Log.e(this, e, "Failure to createConnection -- %s", getComponentName());
+                    mPendingResponses.remove(callId).handleCreateConnectionFailure(
+                            DisconnectCause.OUTGOING_FAILURE, e.toString());
+                }
+            }
+
+            @Override
+            public void onFailure() {
+                Log.e(this, new Exception(), "Failure to call %s", getComponentName());
+                response.handleCreateConnectionFailure(DisconnectCause.OUTGOING_FAILURE, null);
+            }
+        };
+
+        mBinder.bind(callback);
+    }
+
+    /** @see ConnectionService#abort(String) */
+    void abort(Call call) {
+        // Clear out any pending outgoing call data
+        final String callId = mCallIdMapper.getCallId(call);
+
+        // If still bound, tell the connection service to abort.
+        if (callId != null && isServiceValid("abort")) {
+            try {
+                logOutgoing("abort %s", callId);
+                mServiceInterface.abort(callId);
+            } catch (RemoteException e) {
+            }
+        }
+
+        removeCall(call, DisconnectCause.LOCAL, null);
+    }
+
+    /** @see ConnectionService#hold(String) */
+    void hold(Call call) {
+        final String callId = mCallIdMapper.getCallId(call);
+        if (callId != null && isServiceValid("hold")) {
+            try {
+                logOutgoing("hold %s", callId);
+                mServiceInterface.hold(callId);
+            } catch (RemoteException e) {
+            }
+        }
+    }
+
+    /** @see ConnectionService#unhold(String) */
+    void unhold(Call call) {
+        final String callId = mCallIdMapper.getCallId(call);
+        if (callId != null && isServiceValid("unhold")) {
+            try {
+                logOutgoing("unhold %s", callId);
+                mServiceInterface.unhold(callId);
+            } catch (RemoteException e) {
+            }
+        }
+    }
+
+    /** @see ConnectionService#onAudioStateChanged(String,AudioState) */
+    void onAudioStateChanged(Call activeCall, AudioState audioState) {
+        final String callId = mCallIdMapper.getCallId(activeCall);
+        if (callId != null && isServiceValid("onAudioStateChanged")) {
+            try {
+                logOutgoing("onAudioStateChanged %s %s", callId, audioState);
+                mServiceInterface.onAudioStateChanged(callId, audioState);
+            } catch (RemoteException e) {
+            }
+        }
+    }
+
+    /** @see ConnectionService#disconnect(String) */
+    void disconnect(Call call) {
+        final String callId = mCallIdMapper.getCallId(call);
+        if (callId != null && isServiceValid("disconnect")) {
+            try {
+                logOutgoing("disconnect %s", callId);
+                mServiceInterface.disconnect(callId);
+            } catch (RemoteException e) {
+            }
+        }
+    }
+
+    /** @see ConnectionService#answer(String,int) */
+    void answer(Call call, int videoState) {
+        final String callId = mCallIdMapper.getCallId(call);
+        if (callId != null && isServiceValid("answer")) {
+            try {
+                logOutgoing("answer %s %d", callId, videoState);
+                if (videoState == VideoProfile.VideoState.AUDIO_ONLY) {
+                    mServiceInterface.answer(callId);
+                } else {
+                    mServiceInterface.answerVideo(callId, videoState);
+                }
+            } catch (RemoteException e) {
+            }
+        }
+    }
+
+    /** @see ConnectionService#reject(String) */
+    void reject(Call call) {
+        final String callId = mCallIdMapper.getCallId(call);
+        if (callId != null && isServiceValid("reject")) {
+            try {
+                logOutgoing("reject %s", callId);
+                mServiceInterface.reject(callId);
+            } catch (RemoteException e) {
+            }
+        }
+    }
+
+    /** @see ConnectionService#playDtmfTone(String,char) */
+    void playDtmfTone(Call call, char digit) {
+        final String callId = mCallIdMapper.getCallId(call);
+        if (callId != null && isServiceValid("playDtmfTone")) {
+            try {
+                logOutgoing("playDtmfTone %s %c", callId, digit);
+                mServiceInterface.playDtmfTone(callId, digit);
+            } catch (RemoteException e) {
+            }
+        }
+    }
+
+    /** @see ConnectionService#stopDtmfTone(String) */
+    void stopDtmfTone(Call call) {
+        final String callId = mCallIdMapper.getCallId(call);
+        if (callId != null && isServiceValid("stopDtmfTone")) {
+            try {
+                logOutgoing("stopDtmfTone %s",callId);
+                mServiceInterface.stopDtmfTone(callId);
+            } catch (RemoteException e) {
+            }
+        }
+    }
+
+    void addCall(Call call) {
+        if (mCallIdMapper.getCallId(call) == null) {
+            mCallIdMapper.addCall(call);
+        }
+    }
+
+    /**
+     * Associates newCall with this connection service by replacing callToReplace.
+     */
+    void replaceCall(Call newCall, Call callToReplace) {
+        Preconditions.checkState(callToReplace.getConnectionService() == this);
+        mCallIdMapper.replaceCall(newCall, callToReplace);
+    }
+
+    void removeCall(Call call) {
+        removeCall(call, DisconnectCause.ERROR_UNSPECIFIED, null);
+    }
+
+    void removeCall(String callId, int disconnectCause, String disconnectMessage) {
+        CreateConnectionResponse response = mPendingResponses.remove(callId);
+        if (response != null) {
+            response.handleCreateConnectionFailure(disconnectCause, disconnectMessage);
+        }
+
+        mCallIdMapper.removeCall(callId);
+    }
+
+    void removeCall(Call call, int disconnectCause, String disconnectMessage) {
+        CreateConnectionResponse response = mPendingResponses.remove(mCallIdMapper.getCallId(call));
+        if (response != null) {
+            response.handleCreateConnectionFailure(disconnectCause, disconnectMessage);
+        }
+
+        mCallIdMapper.removeCall(call);
+    }
+
+    void onPostDialContinue(Call call, boolean proceed) {
+        final String callId = mCallIdMapper.getCallId(call);
+        if (callId != null && isServiceValid("onPostDialContinue")) {
+            try {
+                logOutgoing("onPostDialContinue %s %b", callId, proceed);
+                mServiceInterface.onPostDialContinue(callId, proceed);
+            } catch (RemoteException ignored) {
+            }
+        }
+    }
+
+    void conference(final Call call, Call otherCall) {
+        final String callId = mCallIdMapper.getCallId(call);
+        final String otherCallId = mCallIdMapper.getCallId(otherCall);
+        if (callId != null && otherCallId != null && isServiceValid("conference")) {
+            try {
+                logOutgoing("conference %s %s", callId, otherCallId);
+                mServiceInterface.conference(callId, otherCallId);
+            } catch (RemoteException ignored) {
+            }
+        }
+    }
+
+    void splitFromConference(Call call) {
+        final String callId = mCallIdMapper.getCallId(call);
+        if (callId != null && isServiceValid("splitFromConference")) {
+            try {
+                logOutgoing("splitFromConference %s", callId);
+                mServiceInterface.splitFromConference(callId);
+            } catch (RemoteException ignored) {
+            }
+        }
+    }
+
+    void mergeConference(Call call) {
+        final String callId = mCallIdMapper.getCallId(call);
+        if (callId != null && isServiceValid("mergeConference")) {
+            try {
+                logOutgoing("mergeConference %s", callId);
+                mServiceInterface.mergeConference(callId);
+            } catch (RemoteException ignored) {
+            }
+        }
+    }
+
+    void swapConference(Call call) {
+        final String callId = mCallIdMapper.getCallId(call);
+        if (callId != null && isServiceValid("swapConference")) {
+            try {
+                logOutgoing("swapConference %s", callId);
+                mServiceInterface.swapConference(callId);
+            } catch (RemoteException ignored) {
+            }
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void setServiceInterface(IBinder binder) {
+        if (binder == null) {
+            // We have lost our service connection. Notify the world that this service is done.
+            // We must notify the adapter before CallsManager. The adapter will force any pending
+            // 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();
+            CallsManager.getInstance().handleConnectionServiceDeath(this);
+            mServiceInterface = null;
+        } else {
+            mServiceInterface = IConnectionService.Stub.asInterface(binder);
+            addConnectionServiceAdapter(mAdapter);
+        }
+    }
+
+    private void handleCreateConnectionComplete(
+            String callId,
+            ConnectionRequest request,
+            ParcelableConnection connection) {
+        // TODO: Note we are not using parameter "request", which is a side effect of our tacit
+        // assumption that we have at most one outgoing connection attempt per ConnectionService.
+        // This may not continue to be the case.
+        if (connection.getState() == Connection.STATE_DISCONNECTED) {
+            // A connection that begins in the DISCONNECTED state is an indication of
+            // failure to connect; we handle all failures uniformly
+            removeCall(callId, connection.getDisconnectCause(), connection.getDisconnectMessage());
+        } else {
+            // Successful connection
+            if (mPendingResponses.containsKey(callId)) {
+                mPendingResponses.remove(callId)
+                        .handleCreateConnectionSuccess(mCallIdMapper, connection);
+            }
+        }
+    }
+
+    /**
+     * Called when the associated connection service dies.
+     */
+    private void handleConnectionServiceDeath() {
+        if (!mPendingResponses.isEmpty()) {
+            CreateConnectionResponse[] responses = mPendingResponses.values().toArray(
+                    new CreateConnectionResponse[mPendingResponses.values().size()]);
+            mPendingResponses.clear();
+            for (int i = 0; i < responses.length; i++) {
+                responses[i].handleCreateConnectionFailure(DisconnectCause.ERROR_UNSPECIFIED, null);
+            }
+        }
+        mCallIdMapper.clear();
+    }
+
+    private void logIncoming(String msg, Object... params) {
+        Log.d(this, "ConnectionService -> Telecom: " + msg, params);
+    }
+
+    private void logOutgoing(String msg, Object... params) {
+        Log.d(this, "Telecom -> ConnectionService: " + msg, params);
+    }
+
+    private void queryRemoteConnectionServices(final RemoteServiceCallback callback) {
+        PhoneAccountRegistrar registrar = TelecomApp.getInstance().getPhoneAccountRegistrar();
+
+        // Only give remote connection services to this connection service if it is listed as
+        // the connection manager.
+        PhoneAccountHandle simCallManager = registrar.getSimCallManager();
+        Log.d(this, "queryRemoteConnectionServices finds simCallManager = %s", simCallManager);
+        if (simCallManager == null ||
+                !simCallManager.getComponentName().equals(getComponentName())) {
+            noRemoteServices(callback);
+            return;
+        }
+
+        // Make a list of ConnectionServices that are listed as being associated with SIM accounts
+        final Set<ConnectionServiceWrapper> simServices = Collections.newSetFromMap(
+                new ConcurrentHashMap<ConnectionServiceWrapper, Boolean>(8, 0.9f, 1));
+        for (PhoneAccountHandle handle : registrar.getEnabledPhoneAccounts()) {
+            PhoneAccount account = registrar.getPhoneAccount(handle);
+            if ((account.getCapabilities() & PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION) != 0) {
+                ConnectionServiceWrapper service =
+                        mConnectionServiceRepository.getService(handle.getComponentName());
+                if (service != null) {
+                    simServices.add(service);
+                }
+            }
+        }
+
+        final List<ComponentName> simServiceComponentNames = new ArrayList<>();
+        final List<IBinder> simServiceBinders = new ArrayList<>();
+
+        Log.v(this, "queryRemoteConnectionServices, simServices = %s", simServices);
+
+        for (ConnectionServiceWrapper simService : simServices) {
+            if (simService == this) {
+                // Only happens in the unlikely case that a SIM service is also a SIM call manager
+                continue;
+            }
+
+            final ConnectionServiceWrapper currentSimService = simService;
+
+            currentSimService.mBinder.bind(new BindCallback() {
+                @Override
+                public void onSuccess() {
+                    Log.d(this, "Adding simService %s", currentSimService.getComponentName());
+                    simServiceComponentNames.add(currentSimService.getComponentName());
+                    simServiceBinders.add(currentSimService.mServiceInterface.asBinder());
+                    maybeComplete();
+                }
+
+                @Override
+                public void onFailure() {
+                    Log.d(this, "Failed simService %s", currentSimService.getComponentName());
+                    // We know maybeComplete() will always be a no-op from now on, so go ahead and
+                    // signal failure of the entire request
+                    noRemoteServices(callback);
+                }
+
+                private void maybeComplete() {
+                    if (simServiceComponentNames.size() == simServices.size()) {
+                        setRemoteServices(callback, simServiceComponentNames, simServiceBinders);
+                    }
+                }
+            });
+        }
+    }
+
+    private void setRemoteServices(
+            RemoteServiceCallback callback,
+            List<ComponentName> componentNames,
+            List<IBinder> binders) {
+        try {
+            callback.onResult(componentNames, binders);
+        } catch (RemoteException e) {
+            Log.e(this, e, "Contacting ConnectionService %s",
+                    ConnectionServiceWrapper.this.getComponentName());
+        }
+    }
+
+    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());
+        }
+    }
+}
diff --git a/src/com/android/server/telecom/Constants.java b/src/com/android/server/telecom/Constants.java
new file mode 100644
index 0000000..bc6e350
--- /dev/null
+++ b/src/com/android/server/telecom/Constants.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 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;
+
+/**
+ * App-wide constants for the phone app.
+ *
+ * Any constants that need to be shared between two or more classes within
+ * the com.android.phone package should be defined here.  (Constants that
+ * are private to only one class can go in that class's .java file.)
+ */
+public class Constants {
+    //
+    // URI schemes
+    //
+
+    public static final String SCHEME_SMSTO = "smsto";
+}
diff --git a/src/com/android/server/telecom/ContactsAsyncHelper.java b/src/com/android/server/telecom/ContactsAsyncHelper.java
new file mode 100644
index 0000000..aa99ac3
--- /dev/null
+++ b/src/com/android/server/telecom/ContactsAsyncHelper.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 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;
+
+import android.app.Notification;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Helper class for loading contacts photo asynchronously.
+ */
+public final class ContactsAsyncHelper {
+    private static final String LOG_TAG = ContactsAsyncHelper.class.getSimpleName();
+
+    /**
+     * Interface for a WorkerHandler result return.
+     */
+    public interface OnImageLoadCompleteListener {
+        /**
+         * Called when the image load is complete.
+         *
+         * @param token Integer passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int,
+         * Context, Uri, OnImageLoadCompleteListener, Object)}.
+         * @param photo Drawable object obtained by the async load.
+         * @param photoIcon Bitmap object obtained by the async load.
+         * @param cookie Object passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int,
+         * Context, Uri, OnImageLoadCompleteListener, Object)}. Can be null iff. the original
+         * cookie is null.
+         */
+        public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon,
+                Object cookie);
+    }
+
+    // constants
+    private static final int EVENT_LOAD_IMAGE = 1;
+
+    private static final Handler sResultHandler = new Handler(Looper.getMainLooper()) {
+        /** Called when loading is done. */
+        @Override
+        public void handleMessage(Message msg) {
+            WorkerArgs args = (WorkerArgs) msg.obj;
+            switch (msg.arg1) {
+                case EVENT_LOAD_IMAGE:
+                    if (args.listener != null) {
+                        Log.d(this, "Notifying listener: " + args.listener.toString() +
+                                " image: " + args.displayPhotoUri + " completed");
+                        args.listener.onImageLoadComplete(msg.what, args.photo, args.photoIcon,
+                                args.cookie);
+                    }
+                    break;
+                default:
+            }
+        }
+    };
+
+    /** Handler run on a worker thread to load photo asynchronously. */
+    private static final Handler sThreadHandler;
+
+    static {
+        HandlerThread thread = new HandlerThread("ContactsAsyncWorker");
+        thread.start();
+        sThreadHandler = new WorkerHandler(thread.getLooper());
+    }
+
+    private ContactsAsyncHelper() {}
+
+    private static final class WorkerArgs {
+        public Context context;
+        public Uri displayPhotoUri;
+        public Drawable photo;
+        public Bitmap photoIcon;
+        public Object cookie;
+        public OnImageLoadCompleteListener listener;
+    }
+
+    /**
+     * Thread worker class that handles the task of opening the stream and loading
+     * the images.
+     */
+    private static class WorkerHandler extends Handler {
+        public WorkerHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            WorkerArgs args = (WorkerArgs) msg.obj;
+
+            switch (msg.arg1) {
+                case EVENT_LOAD_IMAGE:
+                    InputStream inputStream = null;
+                    try {
+                        try {
+                            inputStream = args.context.getContentResolver()
+                                    .openInputStream(args.displayPhotoUri);
+                        } catch (Exception e) {
+                            Log.e(this, e, "Error opening photo input stream");
+                        }
+
+                        if (inputStream != null) {
+                            args.photo = Drawable.createFromStream(inputStream,
+                                    args.displayPhotoUri.toString());
+
+                            // This assumes Drawable coming from contact database is usually
+                            // BitmapDrawable and thus we can have (down)scaled version of it.
+                            args.photoIcon = getPhotoIconWhenAppropriate(args.context, args.photo);
+
+                            Log.d(this, "Loading image: " + msg.arg1 +
+                                    " token: " + msg.what + " image URI: " + args.displayPhotoUri);
+                        } else {
+                            args.photo = null;
+                            args.photoIcon = null;
+                            Log.d(this, "Problem with image: " + msg.arg1 +
+                                    " token: " + msg.what + " image URI: " + args.displayPhotoUri +
+                                    ", using default image.");
+                        }
+                    } finally {
+                        if (inputStream != null) {
+                            try {
+                                inputStream.close();
+                            } catch (IOException e) {
+                                Log.e(this, e, "Unable to close input stream.");
+                            }
+                        }
+                    }
+                    break;
+                default:
+            }
+
+            // send the reply to the enclosing class.
+            Message reply = sResultHandler.obtainMessage(msg.what);
+            reply.arg1 = msg.arg1;
+            reply.obj = msg.obj;
+            reply.sendToTarget();
+        }
+
+        /**
+         * Returns a Bitmap object suitable for {@link Notification}'s large icon. This might
+         * return null when the given Drawable isn't BitmapDrawable, or if the system fails to
+         * create a scaled Bitmap for the Drawable.
+         */
+        private Bitmap getPhotoIconWhenAppropriate(Context context, Drawable photo) {
+            if (!(photo instanceof BitmapDrawable)) {
+                return null;
+            }
+            int iconSize = context.getResources()
+                    .getDimensionPixelSize(R.dimen.notification_icon_size);
+            Bitmap orgBitmap = ((BitmapDrawable) photo).getBitmap();
+            int orgWidth = orgBitmap.getWidth();
+            int orgHeight = orgBitmap.getHeight();
+            int longerEdge = orgWidth > orgHeight ? orgWidth : orgHeight;
+            // We want downscaled one only when the original icon is too big.
+            if (longerEdge > iconSize) {
+                float ratio = ((float) longerEdge) / iconSize;
+                int newWidth = (int) (orgWidth / ratio);
+                int newHeight = (int) (orgHeight / ratio);
+                // If the longer edge is much longer than the shorter edge, the latter may
+                // become 0 which will cause a crash.
+                if (newWidth <= 0 || newHeight <= 0) {
+                    Log.w(this, "Photo icon's width or height become 0.");
+                    return null;
+                }
+
+                // It is sure ratio >= 1.0f in any case and thus the newly created Bitmap
+                // should be smaller than the original.
+                return Bitmap.createScaledBitmap(orgBitmap, newWidth, newHeight, true);
+            } else {
+                return orgBitmap;
+            }
+        }
+    }
+
+    /**
+     * Starts an asynchronous image load. After finishing the load,
+     * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)}
+     * will be called.
+     *
+     * @param token Arbitrary integer which will be returned as the first argument of
+     * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)}
+     * @param context Context object used to do the time-consuming operation.
+     * @param displayPhotoUri Uri to be used to fetch the photo
+     * @param listener Callback object which will be used when the asynchronous load is done.
+     * Can be null, which means only the asynchronous load is done while there's no way to
+     * obtain the loaded photos.
+     * @param cookie Arbitrary object the caller wants to remember, which will become the
+     * fourth argument of {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable,
+     * Bitmap, Object)}. Can be null, at which the callback will also has null for the argument.
+     */
+    public static final void startObtainPhotoAsync(int token, Context context, Uri displayPhotoUri,
+            OnImageLoadCompleteListener listener, Object cookie) {
+        ThreadUtil.checkOnMainThread();
+
+        // in case the source caller info is null, the URI will be null as well.
+        // just update using the placeholder image in this case.
+        if (displayPhotoUri == null) {
+            Log.wtf(LOG_TAG, "Uri is missing");
+            return;
+        }
+
+        // Added additional Cookie field in the callee to handle arguments
+        // sent to the callback function.
+
+        // setup arguments
+        WorkerArgs args = new WorkerArgs();
+        args.cookie = cookie;
+        args.context = context;
+        args.displayPhotoUri = displayPhotoUri;
+        args.listener = listener;
+
+        // setup message arguments
+        Message msg = sThreadHandler.obtainMessage(token);
+        msg.arg1 = EVENT_LOAD_IMAGE;
+        msg.obj = args;
+
+        Log.d(LOG_TAG, "Begin loading image: " + args.displayPhotoUri +
+                ", displaying default image for now.");
+
+        // notify the thread to begin working
+        sThreadHandler.sendMessage(msg);
+    }
+
+
+}
diff --git a/src/com/android/server/telecom/CreateConnectionProcessor.java b/src/com/android/server/telecom/CreateConnectionProcessor.java
new file mode 100644
index 0000000..e4c5463
--- /dev/null
+++ b/src/com/android/server/telecom/CreateConnectionProcessor.java
@@ -0,0 +1,307 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.telecom.ParcelableConnection;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.DisconnectCause;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * This class creates connections to place new outgoing calls to attached to an existing incoming
+ * call. In either case, this class cycles through a set of connection services until:
+ *   - a connection service returns a newly created connection in which case the call is displayed
+ *     to the user
+ *   - a connection service cancels the process, in which case the call is aborted
+ */
+final class CreateConnectionProcessor {
+
+    // Describes information required to attempt to make a phone call
+    private static class CallAttemptRecord {
+        // The PhoneAccount describing the target connection service which we will
+        // contact in order to process an attempt
+        public final PhoneAccountHandle connectionManagerPhoneAccount;
+        // The PhoneAccount which we will tell the target connection service to use
+        // for attempting to make the actual phone call
+        public final PhoneAccountHandle targetPhoneAccount;
+
+        public CallAttemptRecord(
+                PhoneAccountHandle connectionManagerPhoneAccount,
+                PhoneAccountHandle targetPhoneAccount) {
+            this.connectionManagerPhoneAccount = connectionManagerPhoneAccount;
+            this.targetPhoneAccount = targetPhoneAccount;
+        }
+
+        @Override
+        public String toString() {
+            return "CallAttemptRecord("
+                    + Objects.toString(connectionManagerPhoneAccount) + ","
+                    + Objects.toString(targetPhoneAccount) + ")";
+        }
+
+        /**
+         * Determines if this instance of {@code CallAttemptRecord} has the same underlying
+         * {@code PhoneAccountHandle}s as another instance.
+         *
+         * @param obj The other instance to compare against.
+         * @return {@code True} if the {@code CallAttemptRecord}s are equal.
+         */
+        @Override
+        public boolean equals(Object obj) {
+            if (obj instanceof CallAttemptRecord) {
+                CallAttemptRecord other = (CallAttemptRecord) obj;
+                return Objects.equals(connectionManagerPhoneAccount,
+                        other.connectionManagerPhoneAccount) &&
+                        Objects.equals(targetPhoneAccount, other.targetPhoneAccount);
+            }
+            return false;
+        }
+    }
+
+    private final Call mCall;
+    private final ConnectionServiceRepository mRepository;
+    private List<CallAttemptRecord> mAttemptRecords;
+    private Iterator<CallAttemptRecord> mAttemptRecordIterator;
+    private CreateConnectionResponse mResponse;
+    private int mLastErrorCode = DisconnectCause.OUTGOING_FAILURE;
+    private String mLastErrorMsg;
+
+    CreateConnectionProcessor(
+            Call call, ConnectionServiceRepository repository, CreateConnectionResponse response) {
+        mCall = call;
+        mRepository = repository;
+        mResponse = response;
+    }
+
+    void process() {
+        Log.v(this, "process");
+        mAttemptRecords = new ArrayList<>();
+        if (mCall.getTargetPhoneAccount() != null) {
+            mAttemptRecords.add(new CallAttemptRecord(
+                    mCall.getTargetPhoneAccount(), mCall.getTargetPhoneAccount()));
+        }
+        adjustAttemptsForConnectionManager();
+        adjustAttemptsForEmergency();
+        mAttemptRecordIterator = mAttemptRecords.iterator();
+        attemptNextPhoneAccount();
+    }
+
+    void abort() {
+        Log.v(this, "abort");
+
+        // Clear the response first to prevent attemptNextConnectionService from attempting any
+        // more services.
+        CreateConnectionResponse response = mResponse;
+        mResponse = null;
+
+        ConnectionServiceWrapper service = mCall.getConnectionService();
+        if (service != null) {
+            service.abort(mCall);
+            mCall.clearConnectionService();
+        }
+        if (response != null) {
+            response.handleCreateConnectionFailure(DisconnectCause.OUTGOING_CANCELED, null);
+        }
+    }
+
+    private void attemptNextPhoneAccount() {
+        Log.v(this, "attemptNextPhoneAccount");
+        PhoneAccountRegistrar registrar = TelecomApp.getInstance().getPhoneAccountRegistrar();
+        CallAttemptRecord attempt = null;
+        if (mAttemptRecordIterator.hasNext()) {
+            attempt = mAttemptRecordIterator.next();
+
+            if (!registrar.phoneAccountHasPermission(attempt.connectionManagerPhoneAccount)) {
+                Log.w(this,
+                        "Connection mgr does not have BIND_CONNECTION_SERVICE for attempt: %s",
+                        attempt);
+                attemptNextPhoneAccount();
+                return;
+            }
+
+            // If the target PhoneAccount differs from the ConnectionManager phone acount, ensure it
+            // also has BIND_CONNECTION_SERVICE permission.
+            if (!attempt.connectionManagerPhoneAccount.equals(attempt.targetPhoneAccount) &&
+                    !registrar.phoneAccountHasPermission(attempt.targetPhoneAccount)) {
+                Log.w(this,
+                        "Target PhoneAccount does not have BIND_CONNECTION_SERVICE for attempt: %s",
+                        attempt);
+                attemptNextPhoneAccount();
+                return;
+            }
+        }
+
+        if (mResponse != null && attempt != null) {
+            Log.i(this, "Trying attempt %s", attempt);
+            ConnectionServiceWrapper service =
+                    mRepository.getService(
+                            attempt.connectionManagerPhoneAccount.getComponentName());
+            if (service == null) {
+                Log.i(this, "Found no connection service for attempt %s", attempt);
+                attemptNextPhoneAccount();
+            } else {
+                mCall.setConnectionManagerPhoneAccount(attempt.connectionManagerPhoneAccount);
+                mCall.setTargetPhoneAccount(attempt.targetPhoneAccount);
+                mCall.setConnectionService(service);
+                Log.i(this, "Attempting to call from %s", service.getComponentName());
+                service.createConnection(mCall, new Response(service));
+            }
+        } else {
+            Log.v(this, "attemptNextPhoneAccount, no more accounts, failing");
+            if (mResponse != null) {
+                mResponse.handleCreateConnectionFailure(mLastErrorCode, mLastErrorMsg);
+                mResponse = null;
+                mCall.clearConnectionService();
+            }
+        }
+    }
+
+    private boolean shouldSetConnectionManager() {
+        Context context = TelecomApp.getInstance();
+        if (!context.getResources().getBoolean(R.bool.connection_manager_enabled)) {
+            // Connection Manager support has been turned off, disregard.
+            return false;
+        }
+
+        if (mAttemptRecords.size() == 0) {
+            return false;
+        }
+
+        if (mAttemptRecords.size() > 1) {
+            Log.d(this, "shouldSetConnectionManager, error, mAttemptRecords should not have more "
+                    + "than 1 record");
+            return false;
+        }
+
+        PhoneAccountRegistrar registrar = TelecomApp.getInstance().getPhoneAccountRegistrar();
+        PhoneAccountHandle connectionManager = registrar.getSimCallManager();
+        if (connectionManager == null) {
+            return false;
+        }
+
+        PhoneAccountHandle targetPhoneAccountHandle = mAttemptRecords.get(0).targetPhoneAccount;
+        if (Objects.equals(connectionManager, targetPhoneAccountHandle)) {
+            return false;
+        }
+
+        // Connection managers are only allowed to manage SIM subscriptions.
+        PhoneAccount targetPhoneAccount = registrar.getPhoneAccount(targetPhoneAccountHandle);
+        boolean isSimSubscription = (targetPhoneAccount.getCapabilities() &
+                PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION) != 0;
+        if (!isSimSubscription) {
+            return false;
+        }
+
+        return true;
+    }
+
+    // If there exists a registered connection manager then use it.
+    private void adjustAttemptsForConnectionManager() {
+        if (shouldSetConnectionManager()) {
+            CallAttemptRecord record = new CallAttemptRecord(
+                    TelecomApp.getInstance().getPhoneAccountRegistrar().getSimCallManager(),
+                    mAttemptRecords.get(0).targetPhoneAccount);
+            Log.v(this, "setConnectionManager, changing %s -> %s",
+                    mAttemptRecords.get(0).targetPhoneAccount, record);
+            mAttemptRecords.set(0, record);
+        } else {
+            Log.v(this, "setConnectionManager, not changing");
+        }
+    }
+
+    // If we are possibly attempting to call a local emergency number, ensure that the
+    // plain PSTN connection services are listed, and nothing else.
+    private void adjustAttemptsForEmergency()  {
+        if (TelephonyUtil.shouldProcessAsEmergency(TelecomApp.getInstance(), mCall.getHandle())) {
+            Log.i(this, "Emergency number detected");
+            mAttemptRecords.clear();
+            List<PhoneAccount> allAccounts = TelecomApp.getInstance()
+                    .getPhoneAccountRegistrar().getAllPhoneAccounts();
+            // First, add SIM phone accounts which can place emergency calls.
+            for (PhoneAccount phoneAccount : allAccounts) {
+                if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS) &&
+                        phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
+                    Log.i(this, "Will try PSTN account %s for emergency",
+                            phoneAccount.getAccountHandle());
+                    mAttemptRecords.add(
+                            new CallAttemptRecord(
+                                    phoneAccount.getAccountHandle(),
+                                    phoneAccount.getAccountHandle()));
+                }
+            }
+
+            // Next, add the connection manager account as a backup if it can place emergency calls.
+            PhoneAccountHandle callManagerHandle = TelecomApp.getInstance()
+                    .getPhoneAccountRegistrar().getSimCallManager();
+            if (callManagerHandle != null) {
+                PhoneAccount callManager = TelecomApp.getInstance()
+                        .getPhoneAccountRegistrar().getPhoneAccount(callManagerHandle);
+                if (callManager.hasCapabilities(PhoneAccount.CAPABILITY_PLACE_EMERGENCY_CALLS)) {
+                    CallAttemptRecord callAttemptRecord = new CallAttemptRecord(callManagerHandle,
+                            TelecomApp.getInstance().getPhoneAccountRegistrar().
+                                    getDefaultOutgoingPhoneAccount(mCall.getHandle().getScheme())
+                    );
+
+                    if (!mAttemptRecords.contains(callAttemptRecord)) {
+                        Log.i(this, "Will try Connection Manager account %s for emergency",
+                                callManager);
+                        mAttemptRecords.add(callAttemptRecord);
+                    }
+                }
+            }
+        }
+    }
+
+    private class Response implements CreateConnectionResponse {
+        private final ConnectionServiceWrapper mService;
+
+        Response(ConnectionServiceWrapper service) {
+            mService = service;
+        }
+
+        @Override
+        public void handleCreateConnectionSuccess(
+                CallIdMapper idMapper,
+                ParcelableConnection connection) {
+            if (mResponse == null) {
+                // Nobody is listening for this connection attempt any longer; ask the responsible
+                // ConnectionService to tear down any resources associated with the call
+                mService.abort(mCall);
+            } else {
+                // Success -- share the good news and remember that we are no longer interested
+                // in hearing about any more attempts
+                mResponse.handleCreateConnectionSuccess(idMapper, connection);
+                mResponse = null;
+            }
+        }
+
+        @Override
+        public void handleCreateConnectionFailure(int code, String msg) {
+            // Failure of some sort; record the reasons for failure and try again if possible
+            Log.d(CreateConnectionProcessor.this, "Connection failed: %d (%s)", code, msg);
+            mLastErrorCode = code;
+            mLastErrorMsg = msg;
+            attemptNextPhoneAccount();
+        }
+    }
+}
diff --git a/src/com/android/server/telecom/CreateConnectionResponse.java b/src/com/android/server/telecom/CreateConnectionResponse.java
new file mode 100644
index 0000000..a1ffa38
--- /dev/null
+++ b/src/com/android/server/telecom/CreateConnectionResponse.java
@@ -0,0 +1,27 @@
+/*
+ * 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;
+
+import android.telecom.ParcelableConnection;
+
+/**
+ * A callback for providing the result of creating a connection.
+ */
+interface CreateConnectionResponse {
+    void handleCreateConnectionSuccess(CallIdMapper idMapper, ParcelableConnection connection);
+    void handleCreateConnectionFailure(int code, String message);
+}
diff --git a/src/com/android/server/telecom/DtmfLocalTonePlayer.java b/src/com/android/server/telecom/DtmfLocalTonePlayer.java
new file mode 100644
index 0000000..eb3f9a0
--- /dev/null
+++ b/src/com/android/server/telecom/DtmfLocalTonePlayer.java
@@ -0,0 +1,155 @@
+/*
+ * 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;
+
+import android.media.AudioManager;
+import android.media.ToneGenerator;
+import android.provider.Settings;
+
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Map;
+
+/**
+ * Plays DTMF tones locally for the caller to hear. In order to reduce (1) the amount of times we
+ * check the "play local tones" setting and (2) the length of time we keep the tone generator, this
+ * class employs a concept of a call "session" that starts and stops when the foreground call
+ * changes.
+ */
+class DtmfLocalTonePlayer extends CallsManagerListenerBase {
+    private static final Map<Character, Integer> TONE_MAP =
+            ImmutableMap.<Character, Integer>builder()
+                    .put('1', ToneGenerator.TONE_DTMF_1)
+                    .put('2', ToneGenerator.TONE_DTMF_2)
+                    .put('3', ToneGenerator.TONE_DTMF_3)
+                    .put('4', ToneGenerator.TONE_DTMF_4)
+                    .put('5', ToneGenerator.TONE_DTMF_5)
+                    .put('6', ToneGenerator.TONE_DTMF_6)
+                    .put('7', ToneGenerator.TONE_DTMF_7)
+                    .put('8', ToneGenerator.TONE_DTMF_8)
+                    .put('9', ToneGenerator.TONE_DTMF_9)
+                    .put('0', ToneGenerator.TONE_DTMF_0)
+                    .put('#', ToneGenerator.TONE_DTMF_P)
+                    .put('*', ToneGenerator.TONE_DTMF_S)
+                    .build();
+
+    /** Generator used to actually play the tone. */
+    private ToneGenerator mToneGenerator;
+
+    /** The current call associated with an existing dtmf session. */
+    private Call mCall;
+
+    /** {@inheritDoc} */
+    @Override
+    public void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall) {
+        endDtmfSession(oldForegroundCall);
+        startDtmfSession(newForegroundCall);
+    }
+
+    /**
+     * Starts playing the dtmf tone specified by c.
+     *
+     * @param call The associated call.
+     * @param c The digit to play.
+     */
+    void playTone(Call call, char c) {
+        // Do nothing if it is not the right call.
+        if (mCall != call) {
+            return;
+        }
+
+        if (mToneGenerator == null) {
+            Log.d(this, "playTone: mToneGenerator == null, %c.", c);
+        } else {
+            Log.d(this, "starting local tone: %c.", c);
+            if (TONE_MAP.containsKey(c)) {
+                mToneGenerator.startTone(TONE_MAP.get(c), -1 /* toneDuration */);
+            }
+        }
+    }
+
+    /**
+     * Stops any currently playing dtmf tone.
+     *
+     * @param call The associated call.
+     */
+    void stopTone(Call call) {
+        // Do nothing if it's not the right call.
+        if (mCall != call) {
+            return;
+        }
+
+        if (mToneGenerator == null) {
+            Log.d(this, "stopTone: mToneGenerator == null.");
+        } else {
+            Log.d(this, "stopping local tone.");
+            mToneGenerator.stopTone();
+        }
+    }
+
+    /**
+     * Runs initialization requires to play local tones during a call.
+     *
+     * @param call The call associated with this dtmf session.
+     */
+    private void startDtmfSession(Call call) {
+        if (call == null) {
+            return;
+        }
+        TelecomApp app = TelecomApp.getInstance();
+
+        final boolean areLocalTonesEnabled;
+        if (app.getResources().getBoolean(R.bool.allow_local_dtmf_tones)) {
+            areLocalTonesEnabled = Settings.System.getInt(
+                    app.getContentResolver(), Settings.System.DTMF_TONE_WHEN_DIALING, 1) == 1;
+        } else {
+            areLocalTonesEnabled = false;
+        }
+
+        mCall = call;
+
+        if (areLocalTonesEnabled) {
+            if (mToneGenerator == null) {
+                try {
+                    mToneGenerator = new ToneGenerator(AudioManager.STREAM_DTMF, 80);
+                } catch (RuntimeException e) {
+                    Log.e(this, e, "Error creating local tone generator.");
+                    mToneGenerator = null;
+                }
+            }
+        }
+    }
+
+    /**
+     * Releases resources needed for playing local dtmf tones.
+     *
+     * @param call The call associated with the session to end.
+     */
+    private void endDtmfSession(Call call) {
+        if (call != null && mCall == call) {
+            // Do a stopTone() in case the sessions ends before we are told to stop the tone.
+            stopTone(call);
+
+            mCall = null;
+
+            if (mToneGenerator != null) {
+                mToneGenerator.release();
+                mToneGenerator = null;
+            }
+        }
+    }
+}
diff --git a/src/com/android/server/telecom/ErrorDialogActivity.java b/src/com/android/server/telecom/ErrorDialogActivity.java
new file mode 100644
index 0000000..0e6bca0
--- /dev/null
+++ b/src/com/android/server/telecom/ErrorDialogActivity.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.telecom;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+
+/**
+ * Used to display an error dialog from within the Telecom service when an outgoing call fails
+ */
+public class ErrorDialogActivity extends Activity {
+    private static final String TAG = ErrorDialogActivity.class.getSimpleName();
+
+    public static final String SHOW_MISSING_VOICEMAIL_NO_DIALOG_EXTRA = "show_missing_voicemail";
+    public static final String ERROR_MESSAGE_ID_EXTRA = "error_message_id";
+
+    /**
+     * Intent action to bring up Voicemail Provider settings.
+     */
+    public static final String ACTION_ADD_VOICEMAIL =
+            "com.android.phone.CallFeaturesSetting.ADD_VOICEMAIL";
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        final boolean showVoicemailDialog = getIntent().getBooleanExtra(
+                SHOW_MISSING_VOICEMAIL_NO_DIALOG_EXTRA, false);
+
+        if (showVoicemailDialog) {
+            showMissingVoicemailErrorDialog();
+        } else {
+            final int error = getIntent().getIntExtra(ERROR_MESSAGE_ID_EXTRA, -1);
+            if (error == -1) {
+                Log.w(TAG, "ErrorDialogActivity called with no error type extra.");
+                finish();
+            } else {
+                showGenericErrorDialog(error);
+            }
+        }
+    }
+
+    private void showGenericErrorDialog(int resid) {
+        final CharSequence msg = getResources().getText(resid);
+        final DialogInterface.OnClickListener clickListener;
+        final DialogInterface.OnCancelListener cancelListener;
+
+        clickListener = new DialogInterface.OnClickListener() {
+            @Override
+            public void onClick(DialogInterface dialog, int which) {
+                finish();
+            }
+        };
+
+        cancelListener = new DialogInterface.OnCancelListener() {
+            @Override
+            public void onCancel(DialogInterface dialog) {
+                finish();
+            }
+        };
+
+        final AlertDialog errorDialog = new AlertDialog.Builder(this)
+                .setMessage(msg).setPositiveButton(android.R.string.ok, clickListener)
+                        .setOnCancelListener(cancelListener).create();
+
+        errorDialog.show();
+    }
+
+    private void showMissingVoicemailErrorDialog() {
+        new AlertDialog.Builder(this)
+                .setTitle(R.string.no_vm_number)
+                .setMessage(R.string.no_vm_number_msg)
+                .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+                        @Override
+                        public void onClick(DialogInterface dialog, int which) {
+                            finish();
+                        }})
+                .setNegativeButton(R.string.add_vm_number_str,
+                        new DialogInterface.OnClickListener() {
+                                @Override
+                                public void onClick(DialogInterface dialog, int which) {
+                                    addVoiceMailNumberPanel(dialog);
+                                }})
+                .setOnCancelListener(new DialogInterface.OnCancelListener() {
+                        @Override
+                        public void onCancel(DialogInterface dialog) {
+                            finish();
+                        }}).show();
+    }
+
+
+    private void addVoiceMailNumberPanel(DialogInterface dialog) {
+        if (dialog != null) {
+            dialog.dismiss();
+        }
+
+        // Navigate to the Voicemail setting in the Call Settings activity.
+        Intent intent = new Intent(ACTION_ADD_VOICEMAIL);
+        startActivity(intent);
+        finish();
+    }
+
+    @Override
+    public void finish() {
+        super.finish();
+        // Don't show the return to previous task animation to avoid showing a black screen.
+        // Just dismiss the dialog and undim the previous activity immediately.
+        overridePendingTransition(0, 0);
+    }
+}
diff --git a/src/com/android/server/telecom/HeadsetMediaButton.java b/src/com/android/server/telecom/HeadsetMediaButton.java
new file mode 100644
index 0000000..f0ea1e9
--- /dev/null
+++ b/src/com/android/server/telecom/HeadsetMediaButton.java
@@ -0,0 +1,103 @@
+/*
+ * 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;
+
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioAttributes;
+import android.media.session.MediaSession;
+import android.view.KeyEvent;
+
+/**
+ * Static class to handle listening to the headset media buttons.
+ */
+final class HeadsetMediaButton extends CallsManagerListenerBase {
+
+    // Types of media button presses
+    static final int SHORT_PRESS = 1;
+    static final int LONG_PRESS = 2;
+
+    private static final AudioAttributes AUDIO_ATTRIBUTES = new AudioAttributes.Builder()
+            .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
+            .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION).build();
+
+    private final MediaSession.Callback mSessionCallback = new MediaSession.Callback() {
+        @Override
+        public boolean onMediaButtonEvent(Intent intent) {
+            KeyEvent event = (KeyEvent) intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
+            Log.v(this, "SessionCallback.onMediaButton()...  event = %s.", event);
+            if ((event != null) && (event.getKeyCode() == KeyEvent.KEYCODE_HEADSETHOOK)) {
+                Log.v(this, "SessionCallback: HEADSETHOOK");
+                boolean consumed = handleHeadsetHook(event);
+                Log.v(this, "==> handleHeadsetHook(): consumed = %b.", consumed);
+                return consumed;
+            }
+            return true;
+        }
+    };
+
+    private final CallsManager mCallsManager;
+
+    private final MediaSession mSession;
+
+    HeadsetMediaButton(Context context, CallsManager callsManager) {
+        mCallsManager = callsManager;
+
+        // Create a MediaSession but don't enable it yet. This is a
+        // replacement for MediaButtonReceiver
+        mSession = new MediaSession(context, HeadsetMediaButton.class.getSimpleName());
+        mSession.setCallback(mSessionCallback);
+        mSession.setFlags(MediaSession.FLAG_EXCLUSIVE_GLOBAL_PRIORITY
+                | MediaSession.FLAG_HANDLES_MEDIA_BUTTONS);
+        mSession.setPlaybackToLocal(AUDIO_ATTRIBUTES);
+    }
+
+    /**
+     * Handles the wired headset button while in-call.
+     *
+     * @return true if we consumed the event.
+     */
+    private boolean handleHeadsetHook(KeyEvent event) {
+        Log.d(this, "handleHeadsetHook()...%s %s", event.getAction(), event.getRepeatCount());
+
+        if (event.isLongPress()) {
+            return mCallsManager.onMediaButton(LONG_PRESS);
+        } else if (event.getAction() == KeyEvent.ACTION_UP && event.getRepeatCount() == 0) {
+            return mCallsManager.onMediaButton(SHORT_PRESS);
+        }
+
+        return true;
+    }
+
+    /** ${inheritDoc} */
+    @Override
+    public void onCallAdded(Call call) {
+        if (!mSession.isActive()) {
+            mSession.setActive(true);
+        }
+    }
+
+    /** ${inheritDoc} */
+    @Override
+    public void onCallRemoved(Call call) {
+        if (!mCallsManager.hasAnyCalls()) {
+            if (mSession.isActive()) {
+                mSession.setActive(false);
+            }
+        }
+    }
+}
diff --git a/src/com/android/server/telecom/InCallAdapter.java b/src/com/android/server/telecom/InCallAdapter.java
new file mode 100644
index 0000000..5a93464
--- /dev/null
+++ b/src/com/android/server/telecom/InCallAdapter.java
@@ -0,0 +1,347 @@
+/*
+ * Copyright (C) 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;
+
+import android.os.Handler;
+import android.os.Message;
+import android.telecom.PhoneAccountHandle;
+
+import com.android.internal.os.SomeArgs;
+import com.android.internal.telecom.IInCallAdapter;
+
+/**
+ * Receives call commands and updates from in-call app and passes them through to CallsManager.
+ * {@link InCallController} creates an instance of this class and passes it to the in-call app after
+ * binding to it. This adapter can receive commands and updates until the in-call app is unbound.
+ */
+class InCallAdapter extends IInCallAdapter.Stub {
+    private static final int MSG_ANSWER_CALL = 0;
+    private static final int MSG_REJECT_CALL = 1;
+    private static final int MSG_PLAY_DTMF_TONE = 2;
+    private static final int MSG_STOP_DTMF_TONE = 3;
+    private static final int MSG_POST_DIAL_CONTINUE = 4;
+    private static final int MSG_DISCONNECT_CALL = 5;
+    private static final int MSG_HOLD_CALL = 6;
+    private static final int MSG_UNHOLD_CALL = 7;
+    private static final int MSG_MUTE = 8;
+    private static final int MSG_SET_AUDIO_ROUTE = 9;
+    private static final int MSG_CONFERENCE = 10;
+    private static final int MSG_SPLIT_FROM_CONFERENCE = 11;
+    private static final int MSG_SWAP_WITH_BACKGROUND_CALL = 12;
+    private static final int MSG_PHONE_ACCOUNT_SELECTED = 13;
+    private static final int MSG_TURN_ON_PROXIMITY_SENSOR = 14;
+    private static final int MSG_TURN_OFF_PROXIMITY_SENSOR = 15;
+    private static final int MSG_MERGE_CONFERENCE = 16;
+    private static final int MSG_SWAP_CONFERENCE = 17;
+
+    private final class InCallAdapterHandler extends Handler {
+        @Override
+        public void handleMessage(Message msg) {
+            Call call;
+            switch (msg.what) {
+                case MSG_ANSWER_CALL: {
+                    SomeArgs args = (SomeArgs) msg.obj;
+                    try {
+                        call = mCallIdMapper.getCall(args.arg1);
+                        int videoState = (int) args.arg2;
+                        if (call != null) {
+                            mCallsManager.answerCall(call, videoState);
+                        } else {
+                            Log.w(this, "answerCall, unknown call id: %s", msg.obj);
+                        }
+                    } finally {
+                        args.recycle();
+                    }
+                    break;
+                }
+                case MSG_REJECT_CALL: {
+                    SomeArgs args = (SomeArgs) msg.obj;
+                    try {
+                        call = mCallIdMapper.getCall(args.arg1);
+                        boolean rejectWithMessage = args.argi1 == 1;
+                        String textMessage = (String) args.arg2;
+                        if (call != null) {
+                            mCallsManager.rejectCall(call, rejectWithMessage, textMessage);
+                        } else {
+                            Log.w(this, "setRingback, unknown call id: %s", args.arg1);
+                        }
+                    } finally {
+                        args.recycle();
+                    }
+                    break;
+                }
+                case MSG_PLAY_DTMF_TONE:
+                    call = mCallIdMapper.getCall(msg.obj);
+                    if (call != null) {
+                        mCallsManager.playDtmfTone(call, (char) msg.arg1);
+                    } else {
+                        Log.w(this, "playDtmfTone, unknown call id: %s", msg.obj);
+                    }
+                    break;
+                case MSG_STOP_DTMF_TONE:
+                    call = mCallIdMapper.getCall(msg.obj);
+                    if (call != null) {
+                        mCallsManager.stopDtmfTone(call);
+                    } else {
+                        Log.w(this, "stopDtmfTone, unknown call id: %s", msg.obj);
+                    }
+                    break;
+                case MSG_POST_DIAL_CONTINUE:
+                    call = mCallIdMapper.getCall(msg.obj);
+                    mCallsManager.postDialContinue(call, msg.arg1 == 1);
+                    call = mCallIdMapper.getCall(msg.obj);
+                    if (call != null) {
+                        mCallsManager.postDialContinue(call, msg.arg1 == 1);
+                    } else {
+                        Log.w(this, "postDialContinue, unknown call id: %s", msg.obj);
+                    }
+                    break;
+                case MSG_DISCONNECT_CALL:
+                    call = mCallIdMapper.getCall(msg.obj);
+                    if (call != null) {
+                        mCallsManager.disconnectCall(call);
+                    } else {
+                        Log.w(this, "disconnectCall, unknown call id: %s", msg.obj);
+                    }
+                    break;
+                case MSG_HOLD_CALL:
+                    call = mCallIdMapper.getCall(msg.obj);
+                    if (call != null) {
+                        mCallsManager.holdCall(call);
+                    } else {
+                        Log.w(this, "holdCall, unknown call id: %s", msg.obj);
+                    }
+                    break;
+                case MSG_UNHOLD_CALL:
+                    call = mCallIdMapper.getCall(msg.obj);
+                    if (call != null) {
+                        mCallsManager.unholdCall(call);
+                    } else {
+                        Log.w(this, "unholdCall, unknown call id: %s", msg.obj);
+                    }
+                    break;
+                case MSG_PHONE_ACCOUNT_SELECTED: {
+                    SomeArgs args = (SomeArgs) msg.obj;
+                    try {
+                        call = mCallIdMapper.getCall(args.arg1);
+                        if (call != null) {
+                            mCallsManager.phoneAccountSelected(call, (PhoneAccountHandle) args.arg2);
+                        } else {
+                            Log.w(this, "phoneAccountSelected, unknown call id: %s", args.arg1);
+                        }
+                    } finally {
+                        args.recycle();
+                    }
+                    break;
+                }
+                case MSG_MUTE:
+                    mCallsManager.mute(msg.arg1 == 1);
+                    break;
+                case MSG_SET_AUDIO_ROUTE:
+                    mCallsManager.setAudioRoute(msg.arg1);
+                    break;
+                case MSG_CONFERENCE: {
+                    SomeArgs args = (SomeArgs) msg.obj;
+                    try {
+                        call = mCallIdMapper.getCall(args.arg1);
+                        Call otherCall = mCallIdMapper.getCall(args.arg2);
+                        if (call != null && otherCall != null) {
+                            mCallsManager.conference(call, otherCall);
+                        } else {
+                            Log.w(this, "conference, unknown call id: %s", msg.obj);
+                        }
+                    } finally {
+                        args.recycle();
+                    }
+                    break;
+                }
+                case MSG_SPLIT_FROM_CONFERENCE:
+                    call = mCallIdMapper.getCall(msg.obj);
+                    if (call != null) {
+                        call.splitFromConference();
+                    } else {
+                        Log.w(this, "splitFromConference, unknown call id: %s", msg.obj);
+                    }
+                    break;
+                case MSG_TURN_ON_PROXIMITY_SENSOR:
+                    mCallsManager.turnOnProximitySensor();
+                    break;
+                case MSG_TURN_OFF_PROXIMITY_SENSOR:
+                    mCallsManager.turnOffProximitySensor((boolean) msg.obj);
+                    break;
+                case MSG_MERGE_CONFERENCE:
+                    call = mCallIdMapper.getCall(msg.obj);
+                    if (call != null) {
+                        call.mergeConference();
+                    } else {
+                        Log.w(this, "mergeConference, unknown call id: %s", msg.obj);
+                    }
+                    break;
+                case MSG_SWAP_CONFERENCE:
+                    call = mCallIdMapper.getCall(msg.obj);
+                    if (call != null) {
+                        call.swapConference();
+                    } else {
+                        Log.w(this, "swapConference, unknown call id: %s", msg.obj);
+                    }
+                    break;
+            }
+        }
+    }
+
+    private final CallsManager mCallsManager;
+    private final Handler mHandler = new InCallAdapterHandler();
+    private final CallIdMapper mCallIdMapper;
+
+    /** Persists the specified parameters. */
+    public InCallAdapter(CallsManager callsManager, CallIdMapper callIdMapper) {
+        ThreadUtil.checkOnMainThread();
+        mCallsManager = callsManager;
+        mCallIdMapper = callIdMapper;
+    }
+
+    @Override
+    public void answerCall(String callId, int videoState) {
+        Log.d(this, "answerCall(%s,%d)", callId, videoState);
+        if (mCallIdMapper.isValidCallId(callId)) {
+            SomeArgs args = SomeArgs.obtain();
+            args.arg1 = callId;
+            args.arg2 = videoState;
+            mHandler.obtainMessage(MSG_ANSWER_CALL, args).sendToTarget();
+        }
+    }
+
+    @Override
+    public void rejectCall(String callId, boolean rejectWithMessage, String textMessage) {
+        Log.d(this, "rejectCall(%s,%b,%s)", callId, rejectWithMessage, textMessage);
+        if (mCallIdMapper.isValidCallId(callId)) {
+            SomeArgs args = SomeArgs.obtain();
+            args.arg1 = callId;
+            args.argi1 = rejectWithMessage ? 1 : 0;
+            args.arg2 = textMessage;
+            mHandler.obtainMessage(MSG_REJECT_CALL, args).sendToTarget();
+        }
+    }
+
+    @Override
+    public void playDtmfTone(String callId, char digit) {
+        Log.d(this, "playDtmfTone(%s,%c)", callId, digit);
+        if (mCallIdMapper.isValidCallId(callId)) {
+            mHandler.obtainMessage(MSG_PLAY_DTMF_TONE, (int) digit, 0, callId).sendToTarget();
+        }
+    }
+
+    @Override
+    public void stopDtmfTone(String callId) {
+        Log.d(this, "stopDtmfTone(%s)", callId);
+        if (mCallIdMapper.isValidCallId(callId)) {
+            mHandler.obtainMessage(MSG_STOP_DTMF_TONE, callId).sendToTarget();
+        }
+    }
+
+    @Override
+    public void postDialContinue(String callId, boolean proceed) {
+        Log.d(this, "postDialContinue(%s)", callId);
+        if (mCallIdMapper.isValidCallId(callId)) {
+            mHandler.obtainMessage(MSG_POST_DIAL_CONTINUE, proceed ? 1 : 0, 0, callId).sendToTarget();
+        }
+    }
+
+    @Override
+    public void disconnectCall(String callId) {
+        Log.v(this, "disconnectCall: %s", callId);
+        if (mCallIdMapper.isValidCallId(callId)) {
+            mHandler.obtainMessage(MSG_DISCONNECT_CALL, callId).sendToTarget();
+        }
+    }
+
+    @Override
+    public void holdCall(String callId) {
+        if (mCallIdMapper.isValidCallId(callId)) {
+            mHandler.obtainMessage(MSG_HOLD_CALL, callId).sendToTarget();
+        }
+    }
+
+    @Override
+    public void unholdCall(String callId) {
+        if (mCallIdMapper.isValidCallId(callId)) {
+            mHandler.obtainMessage(MSG_UNHOLD_CALL, callId).sendToTarget();
+        }
+    }
+
+    @Override
+    public void phoneAccountSelected(String callId, PhoneAccountHandle accountHandle) {
+        if (mCallIdMapper.isValidCallId(callId)) {
+            SomeArgs args = SomeArgs.obtain();
+            args.arg1 = callId;
+            args.arg2 = accountHandle;
+            mHandler.obtainMessage(MSG_PHONE_ACCOUNT_SELECTED, args).sendToTarget();
+        }
+    }
+
+    @Override
+    public void mute(boolean shouldMute) {
+        mHandler.obtainMessage(MSG_MUTE, shouldMute ? 1 : 0, 0).sendToTarget();
+    }
+
+    @Override
+    public void setAudioRoute(int route) {
+        mHandler.obtainMessage(MSG_SET_AUDIO_ROUTE, route, 0).sendToTarget();
+    }
+
+    @Override
+    public void conference(String callId, String otherCallId) {
+        if (mCallIdMapper.isValidCallId(callId) &&
+                mCallIdMapper.isValidCallId(otherCallId)) {
+            SomeArgs args = SomeArgs.obtain();
+            args.arg1 = callId;
+            args.arg2 = otherCallId;
+            mHandler.obtainMessage(MSG_CONFERENCE, args).sendToTarget();
+        }
+    }
+
+    @Override
+    public void splitFromConference(String callId) {
+        if (mCallIdMapper.isValidCallId(callId)) {
+            mHandler.obtainMessage(MSG_SPLIT_FROM_CONFERENCE, callId).sendToTarget();
+        }
+    }
+
+    @Override
+    public void mergeConference(String callId) {
+        if (mCallIdMapper.isValidCallId(callId)) {
+            mHandler.obtainMessage(MSG_MERGE_CONFERENCE, callId).sendToTarget();
+        }
+    }
+
+    @Override
+    public void swapConference(String callId) {
+        if (mCallIdMapper.isValidCallId(callId)) {
+            mHandler.obtainMessage(MSG_SWAP_CONFERENCE, callId).sendToTarget();
+        }
+    }
+
+    @Override
+    public void turnOnProximitySensor() {
+        mHandler.obtainMessage(MSG_TURN_ON_PROXIMITY_SENSOR).sendToTarget();
+    }
+
+    @Override
+    public void turnOffProximitySensor(boolean screenOnImmediately) {
+        mHandler.obtainMessage(MSG_TURN_OFF_PROXIMITY_SENSOR, screenOnImmediately).sendToTarget();
+    }
+}
diff --git a/src/com/android/server/telecom/InCallController.java b/src/com/android/server/telecom/InCallController.java
new file mode 100644
index 0000000..26ac4a5
--- /dev/null
+++ b/src/com/android/server/telecom/InCallController.java
@@ -0,0 +1,514 @@
+/*
+ * Copyright (C) 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;
+
+import android.Manifest;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.telecom.AudioState;
+import android.telecom.CallProperties;
+import android.telecom.CallState;
+import android.telecom.InCallService;
+import android.telecom.ParcelableCall;
+import android.telecom.PhoneCapabilities;
+import android.telecom.TelecomManager;
+import android.util.ArrayMap;
+
+import com.android.internal.telecom.IInCallService;
+import com.google.common.collect.ImmutableCollection;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Binds to {@link IInCallService} and provides the service to {@link CallsManager} through which it
+ * can send updates to the in-call app. This class is created and owned by CallsManager and retains
+ * a binding to the {@link IInCallService} (implemented by the in-call app).
+ */
+public final class InCallController extends CallsManagerListenerBase {
+    /**
+     * Used to bind to the in-call app and triggers the start of communication between
+     * this class and in-call app.
+     */
+    private class InCallServiceConnection implements ServiceConnection {
+        /** {@inheritDoc} */
+        @Override public void onServiceConnected(ComponentName name, IBinder service) {
+            Log.d(this, "onServiceConnected: %s", name);
+            onConnected(name, service);
+        }
+
+        /** {@inheritDoc} */
+        @Override public void onServiceDisconnected(ComponentName name) {
+            Log.d(this, "onDisconnected: %s", name);
+            onDisconnected(name);
+        }
+    }
+
+    private final Call.Listener mCallListener = new Call.ListenerBase() {
+        @Override
+        public void onCallCapabilitiesChanged(Call call) {
+            updateCall(call);
+        }
+
+        @Override
+        public void onCannedSmsResponsesLoaded(Call call) {
+            updateCall(call);
+        }
+
+        @Override
+        public void onVideoCallProviderChanged(Call call) {
+            updateCall(call);
+        }
+
+        @Override
+        public void onStatusHintsChanged(Call call) {
+            updateCall(call);
+        }
+
+        @Override
+        public void onHandleChanged(Call call) {
+            updateCall(call);
+        }
+
+        @Override
+        public void onCallerDisplayNameChanged(Call call) {
+            updateCall(call);
+        }
+
+        @Override
+        public void onVideoStateChanged(Call call) {
+            updateCall(call);
+        }
+
+        @Override
+        public void onTargetPhoneAccountChanged(Call call) {
+            updateCall(call);
+        }
+
+        @Override
+        public void onConferenceableCallsChanged(Call call) {
+            updateCall(call);
+        }
+    };
+
+    /**
+     * Maintains a binding connection to the in-call app(s).
+     * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
+     * load factor before resizing, 1 means we only expect a single thread to
+     * access the map so make only a single shard
+     */
+    private final Map<ComponentName, InCallServiceConnection> mServiceConnections =
+            new ConcurrentHashMap<ComponentName, InCallServiceConnection>(8, 0.9f, 1);
+
+    /** The in-call app implementations, see {@link IInCallService}. */
+    private final Map<ComponentName, IInCallService> mInCallServices = new ArrayMap<>();
+
+    private final CallIdMapper mCallIdMapper = new CallIdMapper("InCall");
+
+    /** The {@link ComponentName} of the default InCall UI. */
+    private final ComponentName mInCallComponentName;
+
+    public InCallController() {
+        Context context = TelecomApp.getInstance();
+        Resources resources = context.getResources();
+
+        mInCallComponentName = new ComponentName(
+                resources.getString(R.string.ui_default_package),
+                resources.getString(R.string.incall_default_class));
+    }
+
+    @Override
+    public void onCallAdded(Call call) {
+        if (mInCallServices.isEmpty()) {
+            bind();
+        } else {
+            Log.i(this, "onCallAdded: %s", call);
+            // Track the call if we don't already know about it.
+            addCall(call);
+
+            for (Map.Entry<ComponentName, IInCallService> entry : mInCallServices.entrySet()) {
+                ComponentName componentName = entry.getKey();
+                IInCallService inCallService = entry.getValue();
+
+                ParcelableCall parcelableCall = toParcelableCall(call,
+                        componentName.equals(mInCallComponentName) /* includeVideoProvider */);
+                try {
+                    inCallService.addCall(parcelableCall);
+                } catch (RemoteException ignored) {
+                }
+            }
+        }
+    }
+
+    @Override
+    public void onCallRemoved(Call call) {
+        Log.i(this, "onCallRemoved: %s", call);
+        if (CallsManager.getInstance().getCalls().isEmpty()) {
+            // TODO: Wait for all messages to be delivered to the service before unbinding.
+            unbind();
+        }
+        call.removeListener(mCallListener);
+        mCallIdMapper.removeCall(call);
+    }
+
+    @Override
+    public void onCallStateChanged(Call call, int oldState, int newState) {
+        updateCall(call);
+    }
+
+    @Override
+    public void onConnectionServiceChanged(
+            Call call,
+            ConnectionServiceWrapper oldService,
+            ConnectionServiceWrapper newService) {
+        updateCall(call);
+    }
+
+    @Override
+    public void onAudioStateChanged(AudioState oldAudioState, AudioState newAudioState) {
+        if (!mInCallServices.isEmpty()) {
+            Log.i(this, "Calling onAudioStateChanged, audioState: %s -> %s", oldAudioState,
+                    newAudioState);
+            for (IInCallService inCallService : mInCallServices.values()) {
+                try {
+                    inCallService.onAudioStateChanged(newAudioState);
+                } catch (RemoteException ignored) {
+                }
+            }
+        }
+    }
+
+    void onPostDialWait(Call call, String remaining) {
+        if (!mInCallServices.isEmpty()) {
+            Log.i(this, "Calling onPostDialWait, remaining = %s", remaining);
+            for (IInCallService inCallService : mInCallServices.values()) {
+                try {
+                    inCallService.setPostDialWait(mCallIdMapper.getCallId(call), remaining);
+                } catch (RemoteException ignored) {
+                }
+            }
+        }
+    }
+
+    @Override
+    public void onIsConferencedChanged(Call call) {
+        Log.d(this, "onIsConferencedChanged %s", call);
+        updateCall(call);
+    }
+
+    void bringToForeground(boolean showDialpad) {
+        if (!mInCallServices.isEmpty()) {
+            for (IInCallService inCallService : mInCallServices.values()) {
+                try {
+                    inCallService.bringToForeground(showDialpad);
+                } catch (RemoteException ignored) {
+                }
+            }
+        } else {
+            Log.w(this, "Asking to bring unbound in-call UI to foreground.");
+        }
+    }
+
+    /**
+     * Unbinds an existing bound connection to the in-call app.
+     */
+    private void unbind() {
+        ThreadUtil.checkOnMainThread();
+        Iterator<Map.Entry<ComponentName, InCallServiceConnection>> iterator =
+            mServiceConnections.entrySet().iterator();
+        while (iterator.hasNext()) {
+            Log.i(this, "Unbinding from InCallService %s");
+            TelecomApp.getInstance().unbindService(iterator.next().getValue());
+            iterator.remove();
+        }
+        mInCallServices.clear();
+    }
+
+    /**
+     * Binds to the in-call app if not already connected by binding directly to the saved
+     * component name of the {@link IInCallService} implementation.
+     */
+    private void bind() {
+        ThreadUtil.checkOnMainThread();
+        if (mInCallServices.isEmpty()) {
+            mServiceConnections.clear();
+            Context context = TelecomApp.getInstance();
+            PackageManager packageManager = TelecomApp.getInstance().getPackageManager();
+            Intent serviceIntent = new Intent(InCallService.SERVICE_INTERFACE);
+
+            for (ResolveInfo entry : packageManager.queryIntentServices(serviceIntent, 0)) {
+                ServiceInfo serviceInfo = entry.serviceInfo;
+                if (serviceInfo != null) {
+                    boolean hasServiceBindPermission = serviceInfo.permission != null &&
+                            serviceInfo.permission.equals(
+                                    Manifest.permission.BIND_INCALL_SERVICE);
+                    boolean hasControlInCallPermission = packageManager.checkPermission(
+                            Manifest.permission.CONTROL_INCALL_EXPERIENCE,
+                            serviceInfo.packageName) == PackageManager.PERMISSION_GRANTED;
+
+                    if (!hasServiceBindPermission) {
+                        Log.w(this, "InCallService does not have BIND_INCALL_SERVICE permission: " +
+                                serviceInfo.packageName);
+                        continue;
+                    }
+
+                    if (!hasControlInCallPermission) {
+                        Log.w(this,
+                                "InCall UI does not have CONTROL_INCALL_EXPERIENCE permission: " +
+                                        serviceInfo.packageName);
+                        continue;
+                    }
+
+                    Log.i(this, "Attempting to bind to InCall " + serviceInfo.packageName);
+                    InCallServiceConnection inCallServiceConnection = new InCallServiceConnection();
+                    ComponentName componentName = new ComponentName(serviceInfo.packageName,
+                            serviceInfo.name);
+
+                    if (!mServiceConnections.containsKey(componentName)) {
+                        Intent intent = new Intent(InCallService.SERVICE_INTERFACE);
+                        intent.setComponent(componentName);
+
+                        if (context.bindServiceAsUser(intent, inCallServiceConnection,
+                                Context.BIND_AUTO_CREATE, UserHandle.CURRENT)) {
+                            mServiceConnections.put(componentName, inCallServiceConnection);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Persists the {@link IInCallService} instance and starts the communication between
+     * this class and in-call app by sending the first update to in-call app. This method is
+     * called after a successful binding connection is established.
+     *
+     * @param componentName The service {@link ComponentName}.
+     * @param service The {@link IInCallService} implementation.
+     */
+    private void onConnected(ComponentName componentName, IBinder service) {
+        ThreadUtil.checkOnMainThread();
+
+        Log.i(this, "onConnected to %s", componentName);
+
+        IInCallService inCallService = IInCallService.Stub.asInterface(service);
+
+        try {
+            inCallService.setInCallAdapter(new InCallAdapter(CallsManager.getInstance(),
+                    mCallIdMapper));
+            mInCallServices.put(componentName, inCallService);
+        } catch (RemoteException e) {
+            Log.e(this, e, "Failed to set the in-call adapter.");
+            return;
+        }
+
+        // Upon successful connection, send the state of the world to the service.
+        ImmutableCollection<Call> calls = CallsManager.getInstance().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,
+                            componentName.equals(mInCallComponentName) /* includeVideoProvider */));
+                } catch (RemoteException ignored) {
+                }
+            }
+            onAudioStateChanged(null, CallsManager.getInstance().getAudioState());
+        } else {
+            unbind();
+        }
+    }
+
+    /**
+     * Cleans up an instance of in-call app after the service has been unbound.
+     *
+     * @param disconnectedComponent The {@link ComponentName} of the service which disconnected.
+     */
+    private void onDisconnected(ComponentName disconnectedComponent) {
+        Log.i(this, "onDisconnected from %s", disconnectedComponent);
+        ThreadUtil.checkOnMainThread();
+        Context context = TelecomApp.getInstance();
+
+        if (mInCallServices.containsKey(disconnectedComponent)) {
+            mInCallServices.remove(disconnectedComponent);
+        }
+
+        if (mServiceConnections.containsKey(disconnectedComponent)) {
+            // One of the services that we were bound to has disconnected. If the default in-call UI
+            // has disconnected, disconnect all calls and un-bind all other InCallService
+            // implementations.
+            if (disconnectedComponent.equals(mInCallComponentName)) {
+                Log.i(this, "In-call UI %s disconnected.", disconnectedComponent);
+                CallsManager.getInstance().disconnectAllCalls();
+                unbind();
+            } else {
+                Log.i(this, "In-Call Service %s suddenly disconnected", disconnectedComponent);
+                // Else, if it wasn't the default in-call UI, then one of the other in-call services
+                // disconnected and, well, that's probably their fault.  Clear their state and
+                // ignore.
+                InCallServiceConnection serviceConnection =
+                        mServiceConnections.get(disconnectedComponent);
+
+                // We still need to call unbind even though it disconnected.
+                context.unbindService(serviceConnection);
+
+                mServiceConnections.remove(disconnectedComponent);
+                mInCallServices.remove(disconnectedComponent);
+            }
+        }
+    }
+
+    /**
+     * Informs all {@link InCallService} instances of the updated call information.  Changes to the
+     * video provider are only communicated to the default in-call UI.
+     *
+     * @param call The {@link Call}.
+     */
+    private void updateCall(Call call) {
+        if (!mInCallServices.isEmpty()) {
+            for (Map.Entry<ComponentName, IInCallService> entry : mInCallServices.entrySet()) {
+                ComponentName componentName = entry.getKey();
+                IInCallService inCallService = entry.getValue();
+                ParcelableCall parcelableCall = toParcelableCall(call,
+                        componentName.equals(mInCallComponentName) /* includeVideoProvider */);
+
+                Log.v(this, "updateCall %s ==> %s", call, parcelableCall);
+                try {
+                    inCallService.updateCall(parcelableCall);
+                } catch (RemoteException ignored) {
+                }
+            }
+        }
+    }
+
+    /**
+     * Parcels all information for a {@link Call} into a new {@link ParcelableCall} instance.
+     *
+     * @param call The {@link Call} to parcel.
+     * @param includeVideoProvider When {@code true}, the {@link IVideoProvider} is included in the
+     *      parcelled call.  When {@code false}, the {@link IVideoProvider} is not included.
+     * @return The {@link ParcelableCall} containing all call information from the {@link Call}.
+     */
+    private ParcelableCall toParcelableCall(Call call, boolean includeVideoProvider) {
+        String callId = mCallIdMapper.getCallId(call);
+
+        int capabilities = call.getCallCapabilities();
+        if (CallsManager.getInstance().isAddCallCapable(call)) {
+            capabilities |= PhoneCapabilities.ADD_CALL;
+        }
+
+        // Disable mute and add call for emergency calls.
+        if (call.isEmergencyCall()) {
+            capabilities &= ~PhoneCapabilities.MUTE;
+            capabilities &= ~PhoneCapabilities.ADD_CALL;
+        }
+
+        int properties = call.isConference() ? CallProperties.CONFERENCE : 0;
+
+        int state = call.getState();
+        if (state == CallState.ABORTED) {
+            state = CallState.DISCONNECTED;
+        }
+
+        String parentCallId = null;
+        Call parentCall = call.getParentCall();
+        if (parentCall != null) {
+            parentCallId = mCallIdMapper.getCallId(parentCall);
+        }
+
+        long connectTimeMillis = call.getConnectTimeMillis();
+        List<Call> childCalls = call.getChildCalls();
+        List<String> childCallIds = new ArrayList<>();
+        if (!childCalls.isEmpty()) {
+            connectTimeMillis = Long.MAX_VALUE;
+            for (Call child : childCalls) {
+                connectTimeMillis = Math.min(child.getConnectTimeMillis(), connectTimeMillis);
+                childCallIds.add(mCallIdMapper.getCallId(child));
+            }
+        }
+
+        if (call.isRespondViaSmsCapable()) {
+            capabilities |= PhoneCapabilities.RESPOND_VIA_TEXT;
+        }
+
+        Uri handle = call.getHandlePresentation() == TelecomManager.PRESENTATION_ALLOWED ?
+                call.getHandle() : null;
+        String callerDisplayName = call.getCallerDisplayNamePresentation() ==
+                TelecomManager.PRESENTATION_ALLOWED ?  call.getCallerDisplayName() : null;
+
+        List<Call> conferenceableCalls = call.getConferenceableCalls();
+        List<String> conferenceableCallIds = new ArrayList<String>(conferenceableCalls.size());
+        for (Call otherCall : conferenceableCalls) {
+            String otherId = mCallIdMapper.getCallId(otherCall);
+            if (otherId != null) {
+                conferenceableCallIds.add(otherId);
+            }
+        }
+
+        return new ParcelableCall(
+                callId,
+                state,
+                call.getDisconnectCause(),
+                call.getDisconnectMessage(),
+                call.getCannedSmsResponses(),
+                capabilities,
+                properties,
+                connectTimeMillis,
+                handle,
+                call.getHandlePresentation(),
+                callerDisplayName,
+                call.getCallerDisplayNamePresentation(),
+                call.getGatewayInfo(),
+                call.getTargetPhoneAccount(),
+                includeVideoProvider ? call.getVideoProvider() : null,
+                parentCallId,
+                childCallIds,
+                call.getStatusHints(),
+                call.getVideoState(),
+                conferenceableCallIds,
+                call.getExtras());
+    }
+
+    /**
+     * Adds the call to the list of calls tracked by the {@link InCallController}.
+     * @param call The call to add.
+     */
+    private void addCall(Call call) {
+        if (mCallIdMapper.getCallId(call) == null) {
+            mCallIdMapper.addCall(call);
+            call.addListener(mCallListener);
+        }
+    }
+}
diff --git a/src/com/android/server/telecom/InCallToneMonitor.java b/src/com/android/server/telecom/InCallToneMonitor.java
new file mode 100644
index 0000000..2ffb499
--- /dev/null
+++ b/src/com/android/server/telecom/InCallToneMonitor.java
@@ -0,0 +1,96 @@
+/*
+ * 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;
+
+import android.telecom.CallState;
+import android.telephony.DisconnectCause;
+
+import java.util.Collection;
+
+/**
+ * Monitors events from CallsManager and plays in-call tones for events which require them, such as
+ * different type of call disconnections (busy tone, congestion tone, etc).
+ */
+public final class InCallToneMonitor extends CallsManagerListenerBase {
+    private final InCallTonePlayer.Factory mPlayerFactory;
+
+    private final CallsManager mCallsManager;
+
+    InCallToneMonitor(InCallTonePlayer.Factory playerFactory, CallsManager callsManager) {
+        mPlayerFactory = playerFactory;
+        mCallsManager = callsManager;
+    }
+
+    @Override
+    public void onCallStateChanged(Call call, int oldState, int newState) {
+        if (mCallsManager.getForegroundCall() != call) {
+            // We only play tones for foreground calls.
+            return;
+        }
+
+        if (newState == CallState.DISCONNECTED) {
+            int toneToPlay = InCallTonePlayer.TONE_INVALID;
+
+            Log.v(this, "Disconnect cause: %d.", call.getDisconnectCause());
+
+            switch(call.getDisconnectCause()) {
+                case DisconnectCause.BUSY:
+                    toneToPlay = InCallTonePlayer.TONE_BUSY;
+                    break;
+                case DisconnectCause.CONGESTION:
+                    toneToPlay = InCallTonePlayer.TONE_CONGESTION;
+                    break;
+                case DisconnectCause.CDMA_REORDER:
+                    toneToPlay = InCallTonePlayer.TONE_REORDER;
+                    break;
+                case DisconnectCause.CDMA_INTERCEPT:
+                    toneToPlay = InCallTonePlayer.TONE_INTERCEPT;
+                    break;
+                case DisconnectCause.CDMA_DROP:
+                    toneToPlay = InCallTonePlayer.TONE_CDMA_DROP;
+                    break;
+                case DisconnectCause.OUT_OF_SERVICE:
+                    toneToPlay = InCallTonePlayer.TONE_OUT_OF_SERVICE;
+                    break;
+                case DisconnectCause.UNOBTAINABLE_NUMBER:
+                    toneToPlay = InCallTonePlayer.TONE_UNOBTAINABLE_NUMBER;
+                    break;
+                case DisconnectCause.ERROR_UNSPECIFIED:
+                    toneToPlay = InCallTonePlayer.TONE_CALL_ENDED;
+                    break;
+                case DisconnectCause.NORMAL:
+                case DisconnectCause.LOCAL:
+                    // Only play the disconnect sound on normal disconnects if there are no other
+                    // calls present beyond the one that is currently disconnected.
+                    Collection<Call> allCalls = mCallsManager.getCalls();
+                    if (allCalls.size() == 1) {
+                        if (!allCalls.contains(call)) {
+                            Log.wtf(this, "Disconnecting call not found %s.", call);
+                        }
+                        toneToPlay = InCallTonePlayer.TONE_CALL_ENDED;
+                    }
+                    break;
+            }
+
+            Log.d(this, "Found a disconnected call with tone to play %d.", toneToPlay);
+
+            if (toneToPlay != InCallTonePlayer.TONE_INVALID) {
+                mPlayerFactory.createPlayer(toneToPlay).startTone();
+            }
+        }
+    }
+}
diff --git a/src/com/android/server/telecom/InCallTonePlayer.java b/src/com/android/server/telecom/InCallTonePlayer.java
new file mode 100644
index 0000000..2ffe599
--- /dev/null
+++ b/src/com/android/server/telecom/InCallTonePlayer.java
@@ -0,0 +1,260 @@
+/*
+ * 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;
+
+import android.media.AudioManager;
+import android.media.ToneGenerator;
+import android.os.Handler;
+import android.os.Looper;
+
+/**
+ * Play a call-related tone (ringback, busy signal, etc.) through ToneGenerator. To use, create an
+ * instance using InCallTonePlayer.Factory (passing in the TONE_* constant for the tone you want)
+ * and start() it. Implemented on top of {@link Thread} so that the tone plays in its own thread.
+ */
+public final class InCallTonePlayer extends Thread {
+
+    /**
+     * Factory used to create InCallTonePlayers. Exists to aid with testing mocks.
+     */
+    public static class Factory {
+        private final CallAudioManager mCallAudioManager;
+
+        Factory(CallAudioManager callAudioManager) {
+            mCallAudioManager = callAudioManager;
+        }
+
+        InCallTonePlayer createPlayer(int tone) {
+            return new InCallTonePlayer(tone, mCallAudioManager);
+        }
+    }
+
+    // The possible tones that we can play.
+    public static final int TONE_INVALID = 0;
+    public static final int TONE_BUSY = 1;
+    public static final int TONE_CALL_ENDED = 2;
+    public static final int TONE_OTA_CALL_ENDED = 3;
+    public static final int TONE_CALL_WAITING = 4;
+    public static final int TONE_CDMA_DROP = 5;
+    public static final int TONE_CONGESTION = 6;
+    public static final int TONE_INTERCEPT = 7;
+    public static final int TONE_OUT_OF_SERVICE = 8;
+    public static final int TONE_REDIAL = 9;
+    public static final int TONE_REORDER = 10;
+    public static final int TONE_RING_BACK = 11;
+    public static final int TONE_UNOBTAINABLE_NUMBER = 12;
+    public static final int TONE_VOICE_PRIVACY = 13;
+
+    private static final int RELATIVE_VOLUME_EMERGENCY = 100;
+    private static final int RELATIVE_VOLUME_HIPRI = 80;
+    private static final int RELATIVE_VOLUME_LOPRI = 50;
+
+    // Buffer time (in msec) to add on to the tone timeout value. Needed mainly when the timeout
+    // value for a tone is exact duration of the tone itself.
+    private static final int TIMEOUT_BUFFER_MILLIS = 20;
+
+    // The tone state.
+    private static final int STATE_OFF = 0;
+    private static final int STATE_ON = 1;
+    private static final int STATE_STOPPED = 2;
+
+    /**
+     * Keeps count of the number of actively playing tones so that we can notify CallAudioManager
+     * when we need focus and when it can be release. This should only be manipulated from the main
+     * thread.
+     */
+    private static int sTonesPlaying = 0;
+
+    private final CallAudioManager mCallAudioManager;
+
+    private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
+
+    /** The ID of the tone to play. */
+    private final int mToneId;
+
+    /** Current state of the tone player. */
+    private int mState;
+
+    /**
+     * Initializes the tone player. Private; use the {@link Factory} to create tone players.
+     *
+     * @param toneId ID of the tone to play, see TONE_* constants.
+     */
+    private InCallTonePlayer(int toneId, CallAudioManager callAudioManager) {
+        mState = STATE_OFF;
+        mToneId = toneId;
+        mCallAudioManager = callAudioManager;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void run() {
+        ToneGenerator toneGenerator = null;
+        try {
+            Log.d(this, "run(toneId = %s)", mToneId);
+
+            final int toneType;  // Passed to ToneGenerator.startTone.
+            final int toneVolume;  // Passed to the ToneGenerator constructor.
+            final int toneLengthMillis;
+
+            switch (mToneId) {
+                case TONE_BUSY:
+                    // TODO: CDMA-specific tones
+                    toneType = ToneGenerator.TONE_SUP_BUSY;
+                    toneVolume = RELATIVE_VOLUME_HIPRI;
+                    toneLengthMillis = 4000;
+                    break;
+                case TONE_CALL_ENDED:
+                    toneType = ToneGenerator.TONE_PROP_PROMPT;
+                    toneVolume = RELATIVE_VOLUME_HIPRI;
+                    toneLengthMillis = 4000;
+                    break;
+                case TONE_OTA_CALL_ENDED:
+                    // TODO: fill in
+                    throw new IllegalStateException("OTA Call ended NYI.");
+                case TONE_CALL_WAITING:
+                    toneType = ToneGenerator.TONE_SUP_CALL_WAITING;
+                    toneVolume = RELATIVE_VOLUME_HIPRI;
+                    toneLengthMillis = Integer.MAX_VALUE - TIMEOUT_BUFFER_MILLIS;
+                    break;
+                case TONE_CDMA_DROP:
+                    toneType = ToneGenerator.TONE_CDMA_CALLDROP_LITE;
+                    toneVolume = RELATIVE_VOLUME_LOPRI;
+                    toneLengthMillis = 375;
+                    break;
+                case TONE_CONGESTION:
+                    toneType = ToneGenerator.TONE_SUP_CONGESTION;
+                    toneVolume = RELATIVE_VOLUME_HIPRI;
+                    toneLengthMillis = 4000;
+                    break;
+                case TONE_INTERCEPT:
+                    toneType = ToneGenerator.TONE_CDMA_ABBR_INTERCEPT;
+                    toneVolume = RELATIVE_VOLUME_LOPRI;
+                    toneLengthMillis = 500;
+                    break;
+                case TONE_OUT_OF_SERVICE:
+                    toneType = ToneGenerator.TONE_CDMA_CALLDROP_LITE;
+                    toneVolume = RELATIVE_VOLUME_LOPRI;
+                    toneLengthMillis = 375;
+                    break;
+                case TONE_REDIAL:
+                    toneType = ToneGenerator.TONE_CDMA_ALERT_AUTOREDIAL_LITE;
+                    toneVolume = RELATIVE_VOLUME_LOPRI;
+                    toneLengthMillis = 5000;
+                    break;
+                case TONE_REORDER:
+                    toneType = ToneGenerator.TONE_CDMA_REORDER;
+                    toneVolume = RELATIVE_VOLUME_HIPRI;
+                    toneLengthMillis = 5000;
+                    break;
+                case TONE_RING_BACK:
+                    toneType = ToneGenerator.TONE_SUP_RINGTONE;
+                    toneVolume = RELATIVE_VOLUME_HIPRI;
+                    toneLengthMillis = Integer.MAX_VALUE - TIMEOUT_BUFFER_MILLIS;
+                    break;
+                case TONE_UNOBTAINABLE_NUMBER:
+                    toneType = ToneGenerator.TONE_SUP_ERROR;
+                    toneVolume = RELATIVE_VOLUME_HIPRI;
+                    toneLengthMillis = 4000;
+                    break;
+                case TONE_VOICE_PRIVACY:
+                    // TODO: fill in.
+                    throw new IllegalStateException("Voice privacy tone NYI.");
+                default:
+                    throw new IllegalStateException("Bad toneId: " + mToneId);
+            }
+
+            int stream = AudioManager.STREAM_VOICE_CALL;
+            if (mCallAudioManager.isBluetoothAudioOn()) {
+                stream = AudioManager.STREAM_BLUETOOTH_SCO;
+            }
+
+            // If the ToneGenerator creation fails, just continue without it. It is a local audio
+            // signal, and is not as important.
+            try {
+                Log.v(this, "Creating generator");
+                toneGenerator = new ToneGenerator(stream, toneVolume);
+            } catch (RuntimeException e) {
+                Log.w(this, "Failed to create ToneGenerator.", e);
+                return;
+            }
+
+            // TODO: Certain CDMA tones need to check the ringer-volume state before
+            // playing. See CallNotifier.InCallTonePlayer.
+
+            // TODO: Some tones play through the end of a call so we need to inform
+            // CallAudioManager that we want focus the same way that Ringer does.
+
+            synchronized (this) {
+                if (mState != STATE_STOPPED) {
+                    mState = STATE_ON;
+                    toneGenerator.startTone(toneType);
+                    try {
+                        Log.v(this, "Starting tone %d...waiting for %d ms.", mToneId,
+                                toneLengthMillis + TIMEOUT_BUFFER_MILLIS);
+                        wait(toneLengthMillis + TIMEOUT_BUFFER_MILLIS);
+                    } catch (InterruptedException e) {
+                        Log.w(this, "wait interrupted", e);
+                    }
+                }
+            }
+            mState = STATE_OFF;
+        } finally {
+            if (toneGenerator != null) {
+                toneGenerator.release();
+            }
+            cleanUpTonePlayer();
+        }
+    }
+
+    void startTone() {
+        ThreadUtil.checkOnMainThread();
+
+        sTonesPlaying++;
+        if (sTonesPlaying == 1) {
+            mCallAudioManager.setIsTonePlaying(true);
+        }
+
+        start();
+    }
+
+    /**
+     * Stops the tone.
+     */
+    void stopTone() {
+        synchronized (this) {
+            if (mState == STATE_ON) {
+                Log.d(this, "Stopping the tone %d.", mToneId);
+                notify();
+            }
+            mState = STATE_STOPPED;
+        }
+    }
+
+    private void cleanUpTonePlayer() {
+        // Release focus on the main thread.
+        mMainThreadHandler.post(new Runnable() {
+            @Override public void run() {
+                if (sTonesPlaying == 0) {
+                    Log.wtf(this, "Over-releasing focus for tone player.");
+                } else if (--sTonesPlaying == 0) {
+                    mCallAudioManager.setIsTonePlaying(false);
+                }
+            }
+        });
+    }
+}
diff --git a/src/com/android/server/telecom/Log.java b/src/com/android/server/telecom/Log.java
new file mode 100644
index 0000000..ff3c8ce
--- /dev/null
+++ b/src/com/android/server/telecom/Log.java
@@ -0,0 +1,211 @@
+/*
+ * 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;
+
+import android.net.Uri;
+import android.telecom.PhoneAccount;
+import android.telephony.PhoneNumberUtils;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.IllegalFormatException;
+import java.util.Locale;
+
+/**
+ * Manages logging for the entire module.
+ */
+public class Log {
+
+    // Generic tag for all In Call logging
+    private static final String TAG = "Telecom";
+
+    public static final boolean FORCE_LOGGING = false; /* STOP SHIP if true */
+    public static final boolean DEBUG = isLoggable(android.util.Log.DEBUG);
+    public static final boolean INFO = isLoggable(android.util.Log.INFO);
+    public static final boolean VERBOSE = isLoggable(android.util.Log.VERBOSE);
+    public static final boolean WARN = isLoggable(android.util.Log.WARN);
+    public static final boolean ERROR = isLoggable(android.util.Log.ERROR);
+
+    private Log() {}
+
+    public static boolean isLoggable(int level) {
+        return FORCE_LOGGING || android.util.Log.isLoggable(TAG, level);
+    }
+
+    public static void d(String prefix, String format, Object... args) {
+        if (DEBUG) {
+            android.util.Log.d(TAG, buildMessage(prefix, format, args));
+        }
+    }
+
+    public static void d(Object objectPrefix, String format, Object... args) {
+        if (DEBUG) {
+            android.util.Log.d(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args));
+        }
+    }
+
+    public static void i(String prefix, String format, Object... args) {
+        if (INFO) {
+            android.util.Log.i(TAG, buildMessage(prefix, format, args));
+        }
+    }
+
+    public static void i(Object objectPrefix, String format, Object... args) {
+        if (INFO) {
+            android.util.Log.i(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args));
+        }
+    }
+
+    public static void v(String prefix, String format, Object... args) {
+        if (VERBOSE) {
+            android.util.Log.v(TAG, buildMessage(prefix, format, args));
+        }
+    }
+
+    public static void v(Object objectPrefix, String format, Object... args) {
+        if (VERBOSE) {
+            android.util.Log.v(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args));
+        }
+    }
+
+    public static void w(String prefix, String format, Object... args) {
+        if (WARN) {
+            android.util.Log.w(TAG, buildMessage(prefix, format, args));
+        }
+    }
+
+    public static void w(Object objectPrefix, String format, Object... args) {
+        if (WARN) {
+            android.util.Log.w(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args));
+        }
+    }
+
+    public static void e(String prefix, Throwable tr, String format, Object... args) {
+        if (ERROR) {
+            android.util.Log.e(TAG, buildMessage(prefix, format, args), tr);
+        }
+    }
+
+    public static void e(Object objectPrefix, Throwable tr, String format, Object... args) {
+        if (ERROR) {
+            android.util.Log.e(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args),
+                    tr);
+        }
+    }
+
+    public static void wtf(String prefix, Throwable tr, String format, Object... args) {
+        android.util.Log.wtf(TAG, buildMessage(prefix, format, args), tr);
+    }
+
+    public static void wtf(Object objectPrefix, Throwable tr, String format, Object... args) {
+        android.util.Log.wtf(TAG, buildMessage(getPrefixFromObject(objectPrefix), format, args),
+                tr);
+    }
+
+    public static void wtf(String prefix, String format, Object... args) {
+        String msg = buildMessage(prefix, format, args);
+        android.util.Log.wtf(TAG, msg, new IllegalStateException(msg));
+    }
+
+    public static void wtf(Object objectPrefix, String format, Object... args) {
+        String msg = buildMessage(getPrefixFromObject(objectPrefix), format, args);
+        android.util.Log.wtf(TAG, msg, new IllegalStateException(msg));
+    }
+
+    public static String piiHandle(Object pii) {
+        if (pii == null || VERBOSE) {
+            return String.valueOf(pii);
+        }
+
+        if (pii instanceof Uri) {
+            Uri uri = (Uri) pii;
+
+            // All Uri's which are not "tel" go through normal pii() method.
+            if (!PhoneAccount.SCHEME_TEL.equals(uri.getScheme())) {
+                return pii(pii);
+            } else {
+                pii = uri.getSchemeSpecificPart();
+            }
+        }
+
+        String originalString = String.valueOf(pii);
+        StringBuilder stringBuilder = new StringBuilder(originalString.length());
+        for (char c : originalString.toCharArray()) {
+            if (PhoneNumberUtils.isDialable(c)) {
+                stringBuilder.append('*');
+            } else {
+                stringBuilder.append(c);
+            }
+        }
+        return stringBuilder.toString();
+    }
+
+    /**
+     * Redact personally identifiable information for production users.
+     * If we are running in verbose mode, return the original string, otherwise
+     * return a SHA-1 hash of the input string.
+     */
+    public static String pii(Object pii) {
+        if (pii == null || VERBOSE) {
+            return String.valueOf(pii);
+        }
+        return "[" + secureHash(String.valueOf(pii).getBytes()) + "]";
+    }
+
+    private static String secureHash(byte[] input) {
+        MessageDigest messageDigest;
+        try {
+            messageDigest = MessageDigest.getInstance("SHA-1");
+        } catch (NoSuchAlgorithmException e) {
+            return null;
+        }
+        messageDigest.update(input);
+        byte[] result = messageDigest.digest();
+        return encodeHex(result);
+    }
+
+    private static String encodeHex(byte[] bytes) {
+        StringBuffer hex = new StringBuffer(bytes.length * 2);
+
+        for (int i = 0; i < bytes.length; i++) {
+            int byteIntValue = bytes[i] & 0xff;
+            if (byteIntValue < 0x10) {
+                hex.append("0");
+            }
+            hex.append(Integer.toString(byteIntValue, 16));
+        }
+
+        return hex.toString();
+    }
+
+    private static String getPrefixFromObject(Object obj) {
+        return obj == null ? "<null>" : obj.getClass().getSimpleName();
+    }
+
+    private static String buildMessage(String prefix, String format, Object... args) {
+        String msg;
+        try {
+            msg = (args == null || args.length == 0) ? format
+                    : String.format(Locale.US, format, args);
+        } catch (IllegalFormatException ife) {
+            e("Log", ife, "IllegalFormatException: formatString='%s' numArgs=%d", format,
+                    args.length);
+            msg = format + " (An error occurred while formatting the message.)";
+        }
+        return String.format(Locale.US, "%s: %s", prefix, msg);
+    }
+}
diff --git a/src/com/android/server/telecom/MissedCallNotifier.java b/src/com/android/server/telecom/MissedCallNotifier.java
new file mode 100644
index 0000000..553d2e1
--- /dev/null
+++ b/src/com/android/server/telecom/MissedCallNotifier.java
@@ -0,0 +1,322 @@
+/*
+ * 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;
+
+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.provider.CallLog;
+import android.provider.CallLog.Calls;
+import android.telecom.CallState;
+import android.telephony.DisconnectCause;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
+import android.text.TextUtils;
+
+/**
+ * Creates a notification for calls that the user missed (neither answered nor rejected).
+ * TODO: Make TelephonyManager.clearMissedCalls call into this class.
+ * STOPSHIP: Resolve b/13769374 about moving this class to InCall.
+ */
+class MissedCallNotifier extends CallsManagerListenerBase {
+
+    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 MISSED_CALL_NOTIFICATION_ID = 1;
+
+    private final Context mContext;
+    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();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void onCallStateChanged(Call call, int oldState, int newState) {
+        if (oldState == CallState.RINGING && newState == CallState.DISCONNECTED &&
+                call.getDisconnectCause() == DisconnectCause.INCOMING_MISSED) {
+            showMissedCallNotification(call);
+        }
+    }
+
+    /** Clears missed call notification and marks the call log's missed calls as read. */
+    void clearMissedCalls() {
+        // 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.
+     */
+    private 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.notify(MISSED_CALL_NOTIFICATION_ID, notification);
+    }
+
+    /** 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(
+                TelecomBroadcastReceiver.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(
+                TelecomBroadcastReceiver.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(
+                TelecomBroadcastReceiver.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 TelecomBroadcastReceiver}.
+     *
+     * @param action The intent action.
+     * @param data The intent data.
+     */
+    private PendingIntent createTelecomPendingIntent(String action, Uri data) {
+        Intent intent = new Intent(action, data, mContext, TelecomBroadcastReceiver.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
+                            Uri handle = Uri.parse(cursor.getString(
+                                    cursor.getColumnIndexOrThrow(Calls.NUMBER)));
+                            int presentation = cursor.getInt(cursor.getColumnIndexOrThrow(
+                                    Calls.NUMBER_PRESENTATION));
+
+                            if (presentation != Calls.PRESENTATION_ALLOWED) {
+                                handle = null;
+                            }
+
+                            // Convert the data to a call object
+                            Call call = new Call(null, null, null, null, null, true, false);
+                            call.setDisconnectCause(DisconnectCause.INCOMING_MISSED, "");
+                            call.setState(CallState.DISCONNECTED);
+
+                            // 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/src/com/android/server/telecom/MultiLineTitleEditTextPreference.java b/src/com/android/server/telecom/MultiLineTitleEditTextPreference.java
new file mode 100644
index 0000000..d94dc60
--- /dev/null
+++ b/src/com/android/server/telecom/MultiLineTitleEditTextPreference.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2011 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;
+import android.preference.EditTextPreference;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.TextView;
+
+/**
+ * Ultra-simple subclass of EditTextPreference that allows the "title" to wrap
+ * onto multiple lines.
+ *
+ * (By default, the title of an EditTextPreference is singleLine="true"; see
+ * preference_holo.xml under frameworks/base.  But in the "Respond via SMS"
+ * settings UI we want titles to be multi-line, since the customized messages
+ * might be fairly long, and should be able to wrap.)
+ *
+ * TODO: This is pretty cumbersome; it would be nicer for the framework to
+ * either allow modifying the title's attributes in XML, or at least provide
+ * some way from Java (given an EditTextPreference) to reach inside and get a
+ * handle to the "title" TextView.
+ *
+ * TODO: Also, it would reduce clutter if this could be an inner class in
+ * RespondViaSmsManager.java, but then there would be no way to reference the
+ * class from XML.  That's because
+ *    <com.android.server.telecom.MultiLineTitleEditTextPreference ... />
+ * isn't valid XML syntax due to the "$" character.  And Preference
+ * elements don't have a "class" attribute, so you can't do something like
+ * <view class="com.android.server.telecom.Foo$Bar"> as you can with regular views.
+ */
+public class MultiLineTitleEditTextPreference extends EditTextPreference {
+    public MultiLineTitleEditTextPreference(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    public MultiLineTitleEditTextPreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public MultiLineTitleEditTextPreference(Context context) {
+        super(context);
+    }
+
+    // The "title" TextView inside an EditTextPreference defaults to
+    // singleLine="true" (see preference_holo.xml under frameworks/base.)
+    // We override onBindView() purely to look up that TextView and call
+    // setSingleLine(false) on it.
+    @Override
+    protected void onBindView(View view) {
+        super.onBindView(view);
+
+        TextView textView = (TextView) view.findViewById(com.android.internal.R.id.title);
+        if (textView != null) {
+            textView.setSingleLine(false);
+        }
+    }
+}
diff --git a/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java b/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
new file mode 100644
index 0000000..3fbe498
--- /dev/null
+++ b/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
@@ -0,0 +1,419 @@
+/*
+ * Copyright (C) 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;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.UserHandle;
+import android.telecom.GatewayInfo;
+import android.telecom.PhoneAccount;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
+import android.telephony.DisconnectCause;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+
+/**
+ * OutgoingCallIntentBroadcaster receives CALL and CALL_PRIVILEGED Intents, and broadcasts the
+ * ACTION_NEW_OUTGOING_CALL intent. ACTION_NEW_OUTGOING_CALL is an ordered broadcast intent which
+ * contains the phone number being dialed. Applications can use this intent to (1) see which numbers
+ * are being dialed, (2) redirect a call (change the number being dialed), or (3) prevent a call
+ * from being placed.
+ *
+ * After the other applications have had a chance to see the ACTION_NEW_OUTGOING_CALL intent, it
+ * finally reaches the {@link NewOutgoingCallBroadcastIntentReceiver}.
+ *
+ * Calls where no number is present (like for a CDMA "empty flash" or a nonexistent voicemail
+ * number) are exempt from being broadcast.
+ *
+ * Calls to emergency numbers are still broadcast for informative purposes. The call is placed
+ * prior to sending ACTION_NEW_OUTGOING_CALL and cannot be redirected nor prevented.
+ */
+class NewOutgoingCallIntentBroadcaster {
+    /** Required permission for any app that wants to consume ACTION_NEW_OUTGOING_CALL. */
+    private static final String PERMISSION = android.Manifest.permission.PROCESS_OUTGOING_CALLS;
+
+    private static final String EXTRA_ACTUAL_NUMBER_TO_DIAL =
+            "android.telecom.extra.ACTUAL_NUMBER_TO_DIAL";
+
+    /**
+     * Legacy string constants used to retrieve gateway provider extras from intents. These still
+     * need to be copied from the source call intent to the destination intent in order to
+     * support third party gateway providers that are still using old string constants in
+     * Telephony.
+     */
+    public static final String EXTRA_GATEWAY_PROVIDER_PACKAGE =
+            "com.android.phone.extra.GATEWAY_PROVIDER_PACKAGE";
+    public static final String EXTRA_GATEWAY_URI = "com.android.phone.extra.GATEWAY_URI";
+    public static final String EXTRA_GATEWAY_ORIGINAL_URI =
+            "com.android.phone.extra.GATEWAY_ORIGINAL_URI";
+
+    private final CallsManager mCallsManager;
+    private final Call mCall;
+    private final Intent mIntent;
+    /*
+     * Whether or not the outgoing call intent originated from the default phone application. If
+     * so, it will be allowed to make emergency calls, even with the ACTION_CALL intent.
+     */
+    private final boolean mIsDefaultOrSystemPhoneApp;
+
+    NewOutgoingCallIntentBroadcaster(CallsManager callsManager, Call call, Intent intent,
+            boolean isDefaultPhoneApp) {
+        mCallsManager = callsManager;
+        mCall = call;
+        mIntent = intent;
+        mIsDefaultOrSystemPhoneApp = isDefaultPhoneApp;
+    }
+
+    /**
+     * Processes the result of the outgoing call broadcast intent, and performs callbacks to
+     * the OutgoingCallIntentBroadcasterListener as necessary.
+     */
+    private class NewOutgoingCallBroadcastIntentReceiver extends BroadcastReceiver {
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            Log.v(this, "onReceive: %s", intent);
+
+            // Once the NEW_OUTGOING_CALL broadcast is finished, the resultData is used as the
+            // actual number to call. (If null, no call will be placed.)
+            String resultNumber = getResultData();
+            Log.v(this, "- got number from resultData: %s", Log.pii(resultNumber));
+
+            boolean endEarly = false;
+            if (resultNumber == null) {
+                Log.v(this, "Call cancelled (null number), returning...");
+                endEarly = true;
+            } else if (PhoneNumberUtils.isPotentialLocalEmergencyNumber(context, resultNumber)) {
+                Log.w(this, "Cannot modify outgoing call to emergency number %s.", resultNumber);
+                endEarly = true;
+            }
+
+            if (endEarly) {
+                if (mCall != null) {
+                    mCall.disconnect();
+                }
+                return;
+            }
+
+            Uri resultHandleUri = Uri.fromParts(PhoneNumberUtils.isUriNumber(resultNumber) ?
+                    PhoneAccount.SCHEME_SIP : PhoneAccount.SCHEME_TEL, resultNumber, null);
+
+            Uri originalUri = mIntent.getData();
+
+            if (originalUri.getSchemeSpecificPart().equals(resultNumber)) {
+                Log.v(this, "Call number unmodified after new outgoing call intent broadcast.");
+            } else {
+                Log.v(this, "Retrieved modified handle after outgoing call intent broadcast: "
+                        + "Original: %s, Modified: %s",
+                        Log.pii(originalUri),
+                        Log.pii(resultHandleUri));
+            }
+
+            GatewayInfo gatewayInfo = getGateWayInfoFromIntent(intent, resultHandleUri);
+            mCallsManager.placeOutgoingCall(mCall, resultHandleUri, gatewayInfo,
+                    mIntent.getBooleanExtra(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE,
+                            false),
+                    mIntent.getIntExtra(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+                            VideoProfile.VideoState.AUDIO_ONLY));
+        }
+    }
+
+    /**
+     * Processes the supplied intent and starts the outgoing call broadcast process relevant to the
+     * intent.
+     *
+     * This method will handle three kinds of actions:
+     *
+     * - CALL (intent launched by all third party dialers)
+     * - CALL_PRIVILEGED (intent launched by system apps e.g. system Dialer, voice Dialer)
+     * - CALL_EMERGENCY (intent launched by lock screen emergency dialer)
+     *
+     * @return {@link CallActivity#OUTGOING_CALL_SUCCEEDED} if the call succeeded, and an
+     *         appropriate {@link DisconnectCause} if the call did not, describing why it failed.
+     */
+    int processIntent() {
+        Log.v(this, "Processing call intent in OutgoingCallIntentBroadcaster.");
+
+        final Context context = TelecomApp.getInstance();
+
+        Intent intent = mIntent;
+        String action = intent.getAction();
+        final Uri handle = intent.getData();
+
+        if (handle == null) {
+            Log.w(this, "Empty handle obtained from the call intent.");
+            return DisconnectCause.INVALID_NUMBER;
+        }
+
+        boolean isVoicemailNumber = PhoneAccount.SCHEME_VOICEMAIL.equals(handle.getScheme());
+        if (isVoicemailNumber) {
+            if (Intent.ACTION_CALL.equals(action)) {
+                // Voicemail calls will be handled directly by the telephony connection manager
+                Log.i(this, "Placing call immediately instead of waiting for "
+                        + " OutgoingCallBroadcastReceiver: %s", intent);
+
+                boolean speakerphoneOn = mIntent.getBooleanExtra(
+                        TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, false);
+                mCallsManager.placeOutgoingCall(mCall, handle, null, speakerphoneOn,
+                        VideoProfile.VideoState.AUDIO_ONLY);
+
+                return DisconnectCause.NOT_DISCONNECTED;
+            } else {
+                Log.i(this, "Unhandled intent %s. Ignoring and not placing call.", intent);
+                return DisconnectCause.OUTGOING_CANCELED;
+            }
+        }
+
+        String number = PhoneNumberUtils.getNumberFromIntent(intent, context);
+        if (TextUtils.isEmpty(number)) {
+            Log.w(this, "Empty number obtained from the call intent.");
+            return DisconnectCause.NO_PHONE_NUMBER_SUPPLIED;
+        }
+
+        boolean isUriNumber = PhoneNumberUtils.isUriNumber(number);
+        if (!isUriNumber) {
+            number = PhoneNumberUtils.convertKeypadLettersToDigits(number);
+            number = PhoneNumberUtils.stripSeparators(number);
+        }
+
+        final boolean isPotentialEmergencyNumber = isPotentialEmergencyNumber(context, number);
+        Log.v(this, "isPotentialEmergencyNumber = %s", isPotentialEmergencyNumber);
+
+        rewriteCallIntentAction(intent, isPotentialEmergencyNumber);
+        action = intent.getAction();
+        // True for certain types of numbers that are not intended to be intercepted or modified
+        // by third parties (e.g. emergency numbers).
+        boolean callImmediately = false;
+
+        if (Intent.ACTION_CALL.equals(action)) {
+            if (isPotentialEmergencyNumber) {
+                if (!mIsDefaultOrSystemPhoneApp) {
+                    Log.w(this, "Cannot call potential emergency number %s with CALL Intent %s "
+                            + "unless caller is system or default dialer.", number, intent);
+                    launchSystemDialer(context, intent.getData());
+                    return DisconnectCause.OUTGOING_CANCELED;
+                } else {
+                    callImmediately = true;
+                }
+            }
+        } else if (Intent.ACTION_CALL_EMERGENCY.equals(action)) {
+            if (!isPotentialEmergencyNumber) {
+                Log.w(this, "Cannot call non-potential-emergency number %s with EMERGENCY_CALL "
+                        + "Intent %s.", number, intent);
+                return DisconnectCause.OUTGOING_CANCELED;
+            }
+            callImmediately = true;
+        } else {
+            Log.w(this, "Unhandled Intent %s. Ignoring and not placing call.", intent);
+            return DisconnectCause.INVALID_NUMBER;
+        }
+
+        if (callImmediately) {
+            Log.i(this, "Placing call immediately instead of waiting for "
+                    + " OutgoingCallBroadcastReceiver: %s", intent);
+            String scheme = isUriNumber ? PhoneAccount.SCHEME_SIP : PhoneAccount.SCHEME_TEL;
+            boolean speakerphoneOn = mIntent.getBooleanExtra(
+                    TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, false);
+            int videoState = mIntent.getIntExtra(
+                    TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+                    VideoProfile.VideoState.AUDIO_ONLY);
+            mCallsManager.placeOutgoingCall(mCall, Uri.fromParts(scheme, number, null), null,
+                    speakerphoneOn, videoState);
+
+            // Don't return but instead continue and send the ACTION_NEW_OUTGOING_CALL broadcast
+            // so that third parties can still inspect (but not intercept) the outgoing call. When
+            // the broadcast finally reaches the OutgoingCallBroadcastReceiver, we'll know not to
+            // initiate the call again because of the presence of the EXTRA_ALREADY_CALLED extra.
+        }
+
+        broadcastIntent(intent, number, context, !callImmediately);
+        return DisconnectCause.NOT_DISCONNECTED;
+    }
+
+    /**
+     * Sends a new outgoing call ordered broadcast so that third party apps can cancel the
+     * placement of the call or redirect it to a different number.
+     *
+     * @param originalCallIntent The original call intent.
+     * @param number Call number that was stored in the original call intent.
+     * @param context Valid context to send the ordered broadcast using.
+     * @param receiverRequired Whether or not the result from the ordered broadcast should be
+     *     processed using a {@link NewOutgoingCallIntentBroadcaster}.
+     */
+    private void broadcastIntent(
+            Intent originalCallIntent,
+            String number,
+            Context context,
+            boolean receiverRequired) {
+        Intent broadcastIntent = new Intent(Intent.ACTION_NEW_OUTGOING_CALL);
+        if (number != null) {
+            broadcastIntent.putExtra(Intent.EXTRA_PHONE_NUMBER, number);
+        }
+
+        // Force receivers of this broadcast intent to run at foreground priority because we
+        // want to finish processing the broadcast intent as soon as possible.
+        broadcastIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+        Log.v(this, "Broadcasting intent: %s.", broadcastIntent);
+
+        checkAndCopyProviderExtras(originalCallIntent, broadcastIntent);
+
+        context.sendOrderedBroadcastAsUser(
+                broadcastIntent,
+                UserHandle.OWNER,
+                PERMISSION,
+                receiverRequired ? new NewOutgoingCallBroadcastIntentReceiver() : null,
+                null,  // scheduler
+                Activity.RESULT_OK,  // initialCode
+                number,  // initialData: initial value for the result data (number to be modified)
+                null);  // initialExtras
+    }
+
+    /**
+     * Copy all the expected extras set when a 3rd party gateway provider is to be used, from the
+     * source intent to the destination one.
+     *
+     * @param src Intent which may contain the provider's extras.
+     * @param dst Intent where a copy of the extras will be added if applicable.
+     */
+    public void checkAndCopyProviderExtras(Intent src, Intent dst) {
+        if (src == null) {
+            return;
+        }
+        if (hasGatewayProviderExtras(src)) {
+            dst.putExtra(EXTRA_GATEWAY_PROVIDER_PACKAGE,
+                    src.getStringExtra(EXTRA_GATEWAY_PROVIDER_PACKAGE));
+            dst.putExtra(EXTRA_GATEWAY_URI,
+                    src.getStringExtra(EXTRA_GATEWAY_URI));
+            Log.d(this, "Found and copied gateway provider extras to broadcast intent.");
+            return;
+        }
+
+        Log.d(this, "No provider extras found in call intent.");
+    }
+
+    /**
+     * Check if valid gateway provider information is stored as extras in the intent
+     *
+     * @param intent to check for
+     * @return true if the intent has all the gateway information extras needed.
+     */
+    private boolean hasGatewayProviderExtras(Intent intent) {
+        final String name = intent.getStringExtra(EXTRA_GATEWAY_PROVIDER_PACKAGE);
+        final String uriString = intent.getStringExtra(EXTRA_GATEWAY_URI);
+
+        return !TextUtils.isEmpty(name) && !TextUtils.isEmpty(uriString);
+    }
+
+    private static Uri getGatewayUriFromString(String gatewayUriString) {
+        return TextUtils.isEmpty(gatewayUriString) ? null : Uri.parse(gatewayUriString);
+    }
+
+    /**
+     * Extracts gateway provider information from a provided intent..
+     *
+     * @param intent to extract gateway provider information from.
+     * @param trueHandle The actual call handle that the user is trying to dial
+     * @return GatewayInfo object containing extracted gateway provider information as well as
+     *     the actual handle the user is trying to dial.
+     */
+    public static GatewayInfo getGateWayInfoFromIntent(Intent intent, Uri trueHandle) {
+        if (intent == null) {
+            return null;
+        }
+
+        // Check if gateway extras are present.
+        String gatewayPackageName = intent.getStringExtra(EXTRA_GATEWAY_PROVIDER_PACKAGE);
+        Uri gatewayUri = getGatewayUriFromString(intent.getStringExtra(EXTRA_GATEWAY_URI));
+        if (!TextUtils.isEmpty(gatewayPackageName) && gatewayUri != null) {
+            return new GatewayInfo(gatewayPackageName, gatewayUri, trueHandle);
+        }
+
+        return null;
+    }
+
+    private void launchSystemDialer(Context context, Uri handle) {
+        Intent systemDialerIntent = new Intent();
+        final Resources resources = context.getResources();
+        systemDialerIntent.setClassName(
+                resources.getString(R.string.ui_default_package),
+                resources.getString(R.string.dialer_default_class));
+        systemDialerIntent.setAction(Intent.ACTION_DIAL);
+        systemDialerIntent.setData(handle);
+        systemDialerIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        Log.v(this, "calling startActivity for default dialer: %s", systemDialerIntent);
+        context.startActivity(systemDialerIntent);
+    }
+
+    /**
+     * Check whether or not this is an emergency number, in order to enforce the restriction
+     * that only the CALL_PRIVILEGED and CALL_EMERGENCY intents are allowed to make emergency
+     * calls.
+     *
+     * To prevent malicious 3rd party apps from making emergency calls by passing in an
+     * "invalid" number like "9111234" (that isn't technically an emergency number but might
+     * still result in an emergency call with some networks), we use
+     * isPotentialLocalEmergencyNumber instead of isLocalEmergencyNumber.
+     *
+     * @param context Valid context
+     * @param number number to inspect in order to determine whether or not an emergency number
+     * is potentially being dialed
+     * @return True if the handle is potentially an emergency number.
+     */
+    private boolean isPotentialEmergencyNumber(Context context, String number) {
+        Log.v(this, "Checking restrictions for number : %s", Log.pii(number));
+        return (number != null) && PhoneNumberUtils.isPotentialLocalEmergencyNumber(context,number);
+    }
+
+    /**
+     * Given a call intent and whether or not the number to dial is an emergency number, rewrite
+     * the call intent action to an appropriate one.
+     *
+     * @param intent Intent to rewrite the action for
+     * @param isPotentialEmergencyNumber Whether or not the number is potentially an emergency
+     * number.
+     */
+    private void rewriteCallIntentAction(Intent intent, boolean isPotentialEmergencyNumber) {
+        if (CallActivity.class.getName().equals(intent.getComponent().getClassName())) {
+            // If we were launched directly from the CallActivity, not one of its more privileged
+            // aliases, then make sure that only the non-privileged actions are allowed.
+            if (!Intent.ACTION_CALL.equals(intent.getAction())) {
+                Log.w(this, "Attempt to deliver non-CALL action; forcing to CALL");
+                intent.setAction(Intent.ACTION_CALL);
+            }
+        }
+
+        String action = intent.getAction();
+
+        /* Change CALL_PRIVILEGED into CALL or CALL_EMERGENCY as needed. */
+        if (Intent.ACTION_CALL_PRIVILEGED.equals(action)) {
+            if (isPotentialEmergencyNumber) {
+                Log.i(this, "ACTION_CALL_PRIVILEGED is used while the number is a potential"
+                        + " emergency number. Using ACTION_CALL_EMERGENCY as an action instead.");
+                action = Intent.ACTION_CALL_EMERGENCY;
+            } else {
+                action = Intent.ACTION_CALL;
+            }
+            Log.v(this, " - updating action from CALL_PRIVILEGED to %s", action);
+            intent.setAction(action);
+        }
+    }
+}
diff --git a/src/com/android/server/telecom/PhoneAccountBroadcastReceiver.java b/src/com/android/server/telecom/PhoneAccountBroadcastReceiver.java
new file mode 100644
index 0000000..6152bef
--- /dev/null
+++ b/src/com/android/server/telecom/PhoneAccountBroadcastReceiver.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 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;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+
+import java.lang.String;
+
+/**
+ * Captures {@code android.intent.action.ACTION_PACKAGE_FULLY_REMOVED} intents and triggers the
+ * removal of associated {@link android.telecom.PhoneAccount}s via the
+ * {@link com.android.telecom.PhoneAccountRegistrar}.
+ * Note: This class listens for the {@code PACKAGE_FULLY_REMOVED} intent rather than
+ * {@code PACKAGE_REMOVED} as {@code PACKAGE_REMOVED} is triggered on re-installation of the same
+ * package, where {@code PACKAGE_FULLY_REMOVED} is triggered only when an application is completely
+ * uninstalled.  This is desirable as we do not wish to un-register all
+ * {@link android.telecom.PhoneAccount}s associated with a package being re-installed to ensure
+ * the enabled state of the accounts is retained.
+ */
+public class PhoneAccountBroadcastReceiver extends BroadcastReceiver {
+    /**
+     * Receives the intents the class is configured to received.
+     *
+     * @param context The Context in which the receiver is running.
+     * @param intent The Intent being received.
+     */
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        if (Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(intent.getAction())) {
+            Uri uri = intent.getData();
+            if (uri == null) {
+                return;
+            }
+
+            String packageName = uri.getSchemeSpecificPart();
+            handlePackageRemoved(context, packageName);
+        }
+    }
+
+    /**
+     * Handles the removal of a package by calling upon the {@link PhoneAccountRegistrar} to
+     * un-register any {@link android.telecom.PhoneAccount}s associated with the package.
+     *
+     * @param packageName The name of the removed package.
+     */
+    private void handlePackageRemoved(Context context, String packageName) {
+        TelecomApp telecomApp = TelecomApp.getInstance();
+        if (telecomApp == null) {
+            return;
+        }
+
+        PhoneAccountRegistrar registrar = telecomApp.getPhoneAccountRegistrar();
+        if (registrar == null) {
+            return;
+        }
+
+        registrar.clearAccounts(packageName);
+    }
+}
diff --git a/src/com/android/server/telecom/PhoneAccountRegistrar.java b/src/com/android/server/telecom/PhoneAccountRegistrar.java
new file mode 100644
index 0000000..31dd727
--- /dev/null
+++ b/src/com/android/server/telecom/PhoneAccountRegistrar.java
@@ -0,0 +1,1041 @@
+/*
+ * Copyright (C) 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;
+
+import android.Manifest;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.provider.Settings;
+import android.telecom.ConnectionService;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.AtomicFile;
+import android.util.Xml;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.FastXmlSerializer;
+import com.android.internal.util.XmlUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.Integer;
+import java.lang.SecurityException;
+import java.lang.String;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Handles writing and reading PhoneAccountHandle registration entries. This is a simple verbatim
+ * delegate for all the account handling methods on {@link android.telecom.TelecomManager} as implemented in
+ * {@link TelecomServiceImpl}, with the notable exception that {@link TelecomServiceImpl} is
+ * responsible for security checking to make sure that the caller has proper authority over
+ * the {@code ComponentName}s they are declaring in their {@code PhoneAccountHandle}s.
+ */
+public final class PhoneAccountRegistrar {
+
+    public static final PhoneAccountHandle NO_ACCOUNT_SELECTED =
+            new PhoneAccountHandle(new ComponentName("null", "null"), "NO_ACCOUNT_SELECTED");
+
+    public abstract static class Listener {
+        public void onAccountsChanged(PhoneAccountRegistrar registrar) {}
+        public void onDefaultOutgoingChanged(PhoneAccountRegistrar registrar) {}
+        public void onSimCallManagerChanged(PhoneAccountRegistrar registrar) {}
+    }
+
+    private static final String FILE_NAME = "phone-account-registrar-state.xml";
+    @VisibleForTesting
+    public static final int EXPECTED_STATE_VERSION = 3;
+
+    /** Keep in sync with the same in SipSettings.java */
+    private static final String SIP_SHARED_PREFERENCES = "SIP_PREFERENCES";
+
+    private final List<Listener> mListeners = new CopyOnWriteArrayList<>();
+    private final AtomicFile mAtomicFile;
+    private final Context mContext;
+    private State mState;
+
+    public PhoneAccountRegistrar(Context context) {
+        this(context, FILE_NAME);
+    }
+
+    @VisibleForTesting
+    public PhoneAccountRegistrar(Context context, String fileName) {
+        // TODO: Change file location when Telecom is part of system
+        mAtomicFile = new AtomicFile(new File(context.getFilesDir(), fileName));
+        mState = new State();
+        mContext = context;
+        read();
+    }
+
+    /**
+     * Retrieves the default outgoing phone account supporting the specified uriScheme.
+     * @param uriScheme The URI scheme for the outgoing call.
+     * @return The {@link PhoneAccountHandle} to use.
+     */
+    public PhoneAccountHandle getDefaultOutgoingPhoneAccount(String uriScheme) {
+        final PhoneAccountHandle userSelected = getUserSelectedOutgoingPhoneAccount();
+
+        if (userSelected != null) {
+            // If there is a default PhoneAccount, ensure it supports calls to handles with the
+            // specified uriScheme.
+            final PhoneAccount userSelectedAccount = getPhoneAccount(userSelected);
+            if (userSelectedAccount.supportsUriScheme(uriScheme)) {
+                return userSelected;
+            }
+        }
+
+        List<PhoneAccountHandle> outgoing = getEnabledPhoneAccounts(uriScheme);
+        switch (outgoing.size()) {
+            case 0:
+                // There are no accounts, so there can be no default
+                return null;
+            case 1:
+                // There is only one account, which is by definition the default
+                return outgoing.get(0);
+            default:
+                // There are multiple accounts with no selected default
+                return null;
+        }
+    }
+
+    PhoneAccountHandle getUserSelectedOutgoingPhoneAccount() {
+        if (mState.defaultOutgoing != null) {
+            // Return the registered outgoing default iff it still exists (we keep a sticky
+            // default to survive account deletion and re-addition)
+            for (int i = 0; i < mState.accounts.size(); i++) {
+                if (mState.accounts.get(i).getAccountHandle().equals(mState.defaultOutgoing)) {
+                    return mState.defaultOutgoing;
+                }
+            }
+            // At this point, there was a registered default but it has been deleted; proceed
+            // as though there were no default
+        }
+        return null;
+    }
+
+    public void setUserSelectedOutgoingPhoneAccount(PhoneAccountHandle accountHandle) {
+        if (accountHandle == null) {
+            // Asking to clear the default outgoing is a valid request
+            mState.defaultOutgoing = null;
+        } else {
+            boolean found = false;
+            for (PhoneAccount m : mState.accounts) {
+                if (Objects.equals(accountHandle, m.getAccountHandle())) {
+                    found = true;
+                    break;
+                }
+            }
+
+            if (!found) {
+                Log.w(this, "Trying to set nonexistent default outgoing %s",
+                        accountHandle);
+                return;
+            }
+
+            if (!getPhoneAccount(accountHandle).hasCapabilities(
+                    PhoneAccount.CAPABILITY_CALL_PROVIDER)) {
+                Log.w(this, "Trying to set non-call-provider default outgoing %s",
+                        accountHandle);
+                return;
+            }
+
+            mState.defaultOutgoing = accountHandle;
+        }
+
+        write();
+        fireDefaultOutgoingChanged();
+    }
+
+    public void setSimCallManager(PhoneAccountHandle callManager) {
+        if (!isEnabledConnectionManager()) {
+            return;
+        }
+
+        if (callManager != null) {
+            PhoneAccount callManagerAccount = getPhoneAccount(callManager);
+            if (callManagerAccount == null) {
+                Log.d(this, "setSimCallManager: Nonexistent call manager: %s", callManager);
+                return;
+            } else if (!callManagerAccount.hasCapabilities(
+                    PhoneAccount.CAPABILITY_CONNECTION_MANAGER)) {
+                Log.d(this, "setSimCallManager: Not a call manager: %s", callManagerAccount);
+                return;
+            }
+        } else {
+            callManager = NO_ACCOUNT_SELECTED;
+        }
+        mState.simCallManager = callManager;
+
+        write();
+        fireSimCallManagerChanged();
+    }
+
+    public PhoneAccountHandle getSimCallManager() {
+        if (!isEnabledConnectionManager()) {
+            return null;
+        }
+
+        if (mState.simCallManager != null) {
+            if (NO_ACCOUNT_SELECTED.equals(mState.simCallManager)) {
+                return null;
+            }
+            // Return the registered sim call manager iff it still exists (we keep a sticky
+            // setting to survive account deletion and re-addition)
+            for (int i = 0; i < mState.accounts.size(); i++) {
+                if (mState.accounts.get(i).getAccountHandle().equals(mState.simCallManager)) {
+                    return mState.simCallManager;
+                }
+            }
+        }
+
+        // See if the OEM has specified a default one.
+        String defaultConnectionMgr =
+                mContext.getResources().getString(R.string.default_connection_manager_component);
+        if (!TextUtils.isEmpty(defaultConnectionMgr)) {
+            PackageManager pm = mContext.getPackageManager();
+
+            ComponentName componentName = ComponentName.unflattenFromString(defaultConnectionMgr);
+            Intent intent = new Intent(ConnectionService.SERVICE_INTERFACE);
+            intent.setComponent(componentName);
+
+            // Make sure that the component can be resolved.
+            List<ResolveInfo> resolveInfos = pm.queryIntentServices(intent, 0);
+            if (!resolveInfos.isEmpty()) {
+                // See if there is registered PhoneAccount by this component.
+                List<PhoneAccountHandle> handles = getAllPhoneAccountHandles();
+                for (PhoneAccountHandle handle : handles) {
+                    if (componentName.equals(handle.getComponentName())) {
+                        return handle;
+                    }
+                }
+                Log.d(this, "%s does not have a PhoneAccount; not using as default", componentName);
+            } else {
+                Log.d(this, "%s could not be resolved; not using as default", componentName);
+            }
+        } else {
+            Log.v(this, "No default connection manager specified");
+        }
+
+        return null;
+    }
+
+    /**
+     * Retrieves a list of all {@link PhoneAccountHandle}s registered.
+     *
+     * @return The list of {@link PhoneAccountHandle}s.
+     */
+    public List<PhoneAccountHandle> getAllPhoneAccountHandles() {
+        List<PhoneAccountHandle> accountHandles = new ArrayList<>();
+        for (PhoneAccount m : mState.accounts) {
+            accountHandles.add(m.getAccountHandle());
+        }
+        return accountHandles;
+    }
+
+    public List<PhoneAccount> getAllPhoneAccounts() {
+        return new ArrayList<>(mState.accounts);
+    }
+
+    /**
+     * Determines the number of enabled and disabled {@link PhoneAccount}s.
+     *
+     * @return The number of enabled and disabled {@link PhoneAccount}s
+     */
+    public int getAllPhoneAccountsCount() {
+        return mState.accounts.size();
+    }
+
+    /**
+     * Retrieves a list of all enabled call provider phone accounts.
+     *
+     * @return The phone account handles.
+     */
+    public List<PhoneAccountHandle> getEnabledPhoneAccounts() {
+        return getPhoneAccountHandles(PhoneAccount.CAPABILITY_CALL_PROVIDER);
+    }
+
+    /**
+     * Retrieves a list of all enabled phone account call provider phone accounts supporting the
+     * specified URI scheme.
+     *
+     * @param uriScheme The URI scheme.
+     * @return The phone account handles.
+     */
+    public List<PhoneAccountHandle> getEnabledPhoneAccounts(String uriScheme) {
+        return getPhoneAccountHandles(PhoneAccount.CAPABILITY_CALL_PROVIDER, uriScheme,
+                false /* includeDisabled */);
+    }
+
+    /**
+     * Retrieves a list of all enabled phone account handles with the connection manager capability.
+     *
+     * @return The phone account handles.
+     */
+    public List<PhoneAccountHandle> getConnectionManagerPhoneAccounts() {
+        if (isEnabledConnectionManager()) {
+            return getPhoneAccountHandles(PhoneAccount.CAPABILITY_CONNECTION_MANAGER,
+                    null /* supportedUriScheme */, false /* includeDisabled */);
+        }
+        return Collections.emptyList();
+    }
+
+    public PhoneAccount getPhoneAccount(PhoneAccountHandle handle) {
+        for (PhoneAccount m : mState.accounts) {
+            if (Objects.equals(handle, m.getAccountHandle())) {
+                return m;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Changes the enabled state of the {@link PhoneAccount} identified by a
+     * {@link PhoneAccountHandle}.
+     *
+     * @param handle The {@link PhoneAccountHandle}.
+     * @param isEnabled The new enabled state of the {@link PhoneAccount}.
+     */
+    public void setPhoneAccountEnabled(PhoneAccountHandle handle, boolean isEnabled) {
+        PhoneAccount existing = getPhoneAccount(handle);
+        if (existing.isEnabled() == isEnabled) {
+            return;
+        }
+
+        // Do not permit PhoneAccounts which are marked as always enabled to be disabled.
+        if (existing.hasCapabilities(PhoneAccount.CAPABILITY_ALWAYS_ENABLED)) {
+            return;
+        }
+
+        // If we are disabling the current default outgoing phone account or Sim call manager we
+        // need to null out those preferences.
+        if (!isEnabled) {
+            if (mState.defaultOutgoing != null && mState.defaultOutgoing.equals(handle)) {
+                setUserSelectedOutgoingPhoneAccount(null);
+            }
+
+            if (mState.simCallManager != null && mState.simCallManager.equals(handle)) {
+                setSimCallManager(null);
+            }
+        }
+
+        PhoneAccount.Builder builder = existing.toBuilder().setEnabled(isEnabled);
+        PhoneAccount replacement = builder.build();
+        addOrReplacePhoneAccount(replacement);
+
+        // Notify the package which registered this PhoneAccount of its new enabled state.
+        notifyPhoneAccountEnabledStateChanged(replacement.getAccountHandle(), isEnabled);
+    }
+
+    // TODO: Should we implement an artificial limit for # of accounts associated with a single
+    // ComponentName?
+    public void registerPhoneAccount(PhoneAccount account) {
+        // Enforce the requirement that a connection service for a phone account has the correct
+        // permission.
+        if (!phoneAccountHasPermission(account.getAccountHandle())) {
+            Log.w(this, "Phone account %s does not have BIND_CONNECTION_SERVICE permission.",
+                    account.getAccountHandle());
+            throw new SecurityException(
+                    "PhoneAccount connection service requires BIND_CONNECTION_SERVICE permission.");
+        }
+
+        // If there is an existing PhoneAccount already registered with this handle, copy its
+        // enabled state to the new phone account.
+        PhoneAccount existing = getPhoneAccount(account.getAccountHandle());
+        if (existing != null) {
+            account = account.toBuilder().setEnabled(existing.isEnabled()).build();
+        }
+
+        addOrReplacePhoneAccount(account);
+    }
+
+    /**
+     * Adds a {@code PhoneAccount}, replacing an existing one if found.
+     *
+     * @param account The {@code PhoneAccount} to add or replace.
+     */
+    private void addOrReplacePhoneAccount(PhoneAccount account) {
+        mState.accounts.add(account);
+        // Search for duplicates and remove any that are found.
+        for (int i = 0; i < mState.accounts.size() - 1; i++) {
+            if (Objects.equals(
+                    account.getAccountHandle(), mState.accounts.get(i).getAccountHandle())) {
+                // replace existing entry.
+                mState.accounts.remove(i);
+                break;
+            }
+        }
+
+        write();
+        fireAccountsChanged();
+    }
+
+    public void unregisterPhoneAccount(PhoneAccountHandle accountHandle) {
+        for (int i = 0; i < mState.accounts.size(); i++) {
+            if (Objects.equals(accountHandle, mState.accounts.get(i).getAccountHandle())) {
+                mState.accounts.remove(i);
+                break;
+            }
+        }
+
+        write();
+        fireAccountsChanged();
+    }
+
+    /**
+     * Un-registers all phone accounts associated with a specified package.
+     *
+     * @param packageName The package for which phone accounts will be removed.
+     */
+    public void clearAccounts(String packageName) {
+        boolean accountsRemoved = false;
+        Iterator<PhoneAccount> it = mState.accounts.iterator();
+        while (it.hasNext()) {
+            PhoneAccount phoneAccount = it.next();
+            if (Objects.equals(
+                    packageName,
+                    phoneAccount.getAccountHandle().getComponentName().getPackageName())) {
+                Log.i(this, "Removing phone account " + phoneAccount.getLabel());
+                it.remove();
+                accountsRemoved = true;
+            }
+        }
+
+        if (accountsRemoved) {
+            write();
+            fireAccountsChanged();
+        }
+    }
+
+    public void addListener(Listener l) {
+        mListeners.add(l);
+    }
+
+    public void removeListener(Listener l) {
+        if (l != null) {
+            mListeners.remove(l);
+        }
+    }
+
+    private void fireAccountsChanged() {
+        for (Listener l : mListeners) {
+            l.onAccountsChanged(this);
+        }
+    }
+
+    private void fireDefaultOutgoingChanged() {
+        for (Listener l : mListeners) {
+            l.onDefaultOutgoingChanged(this);
+        }
+    }
+
+    private void fireSimCallManagerChanged() {
+        for (Listener l : mListeners) {
+            l.onSimCallManagerChanged(this);
+        }
+    }
+
+    private boolean isEnabledConnectionManager() {
+        return mContext.getResources().getBoolean(R.bool.connection_manager_enabled);
+    }
+
+    /**
+     * Determines if the connection service specified by a {@link PhoneAccountHandle} has the
+     * {@link Manifest.permission#BIND_CONNECTION_SERVICE} permission.
+     *
+     * @param phoneAccountHandle The phone account to check.
+     * @return {@code True} if the phone account has permission.
+     */
+    public boolean phoneAccountHasPermission(PhoneAccountHandle phoneAccountHandle) {
+        PackageManager packageManager = TelecomApp.getInstance().getPackageManager();
+        try {
+            ServiceInfo serviceInfo = packageManager.getServiceInfo(
+                    phoneAccountHandle.getComponentName(), 0);
+
+            return serviceInfo.permission != null &&
+                    serviceInfo.permission.equals(Manifest.permission.BIND_CONNECTION_SERVICE);
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.w(this, "Name not found %s", e);
+            return false;
+        }
+    }
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+
+    /**
+     * Returns a list of phone account handles with the specified flag.
+     *
+     * @param flags Flags which the {@code PhoneAccount} must have.
+     */
+    private List<PhoneAccountHandle> getPhoneAccountHandles(int flags) {
+        return getPhoneAccountHandles(flags, null, false /* includeDisabled */);
+    }
+
+    /**
+     * Returns a list of phone account handles with the specified flag, supporting the specified
+     * URI scheme.  By default, only enabled phone accounts are included, unless the
+     * {@code includeDisabled} parameter is set {@code true}.
+     *
+     * @param flags Flags which the {@code PhoneAccount} must have.
+     * @param uriScheme URI schemes the PhoneAccount must handle.  {@code Null} bypasses the
+     *                  URI scheme check.
+     * @param includeDisabled When {@code true}, the list of phone accounts handles includes those
+     *                        which are marked as disabled.
+     */
+    private List<PhoneAccountHandle> getPhoneAccountHandles(int flags, String uriScheme,
+            boolean includeDisabled) {
+        List<PhoneAccountHandle> accountHandles = new ArrayList<>();
+        for (PhoneAccount m : mState.accounts) {
+            if ((includeDisabled || m.isEnabled()) && m.hasCapabilities(flags) &&
+                    (uriScheme == null || m.supportsUriScheme(uriScheme))) {
+                accountHandles.add(m.getAccountHandle());
+            }
+        }
+        return accountHandles;
+    }
+
+    /**
+     * Notifies the package which registered a {@link PhoneAccount} that it has been enabled.
+     * Only broadcasts the intent if the package has a {@link android.content.BroadcastReceiver}
+     * registered for the intent.
+     *
+     * @param phoneAccountHandle The {@link PhoneAccountHandle} which has been enabled or disabled.
+     * @param isEnabled {@code True} if the {@link PhoneAccount} is enabled, false otherwise.
+     */
+    private void notifyPhoneAccountEnabledStateChanged(PhoneAccountHandle phoneAccountHandle,
+            boolean isEnabled) {
+        Intent intent;
+
+        if (isEnabled) {
+            intent = new Intent(TelecomManager.ACTION_PHONE_ACCOUNT_ENABLED);
+        } else {
+            intent = new Intent(TelecomManager.ACTION_PHONE_ACCOUNT_DISABLED);
+        }
+        intent.setPackage(phoneAccountHandle.getComponentName().getPackageName());
+        intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle);
+
+        if (isReceiverListening(intent)) {
+            Log.i(this, "notifyPhoneAccountEnabledState %s %s", phoneAccountHandle,
+                    (isEnabled ? "enabled" : "disabled"));
+            mContext.sendBroadcast(intent);
+        }
+    }
+
+    /**
+     * Determines there is a {@link android.content.BroadcastReceiver} listening for an
+     * {@link Intent}.
+     *
+     * @param intent The {@link Intent}.
+     * @return {@code True} if there is a listener.
+     */
+    private boolean isReceiverListening(Intent intent) {
+        PackageManager pm = mContext.getPackageManager();
+        final List<ResolveInfo> activities = pm.queryBroadcastReceivers(intent, 0);
+        return !(activities.isEmpty());
+    }
+
+    /**
+     * The state of this {@code PhoneAccountRegistrar}.
+     */
+    @VisibleForTesting
+    public static class State {
+        /**
+         * The account selected by the user to be employed by default for making outgoing calls.
+         * If the user has not made such a selection, then this is null.
+         */
+        public PhoneAccountHandle defaultOutgoing = null;
+
+        /**
+         * A {@code PhoneAccount} having {@link PhoneAccount#CAPABILITY_CONNECTION_MANAGER} which
+         * manages and optimizes a user's PSTN SIM connections.
+         */
+        public PhoneAccountHandle simCallManager;
+
+        /**
+         * The complete list of {@code PhoneAccount}s known to the Telecom subsystem.
+         */
+        public final List<PhoneAccount> accounts = new ArrayList<>();
+
+        /**
+         * The version number of the State data.
+         */
+        public int versionNumber;
+    }
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+    //
+    // State management
+    //
+
+    private void write() {
+        final FileOutputStream os;
+        try {
+            os = mAtomicFile.startWrite();
+            boolean success = false;
+            try {
+                XmlSerializer serializer = new FastXmlSerializer();
+                serializer.setOutput(new BufferedOutputStream(os), "utf-8");
+                writeToXml(mState, serializer);
+                serializer.flush();
+                success = true;
+            } finally {
+                if (success) {
+                    mAtomicFile.finishWrite(os);
+                } else {
+                    mAtomicFile.failWrite(os);
+                }
+            }
+        } catch (IOException e) {
+            Log.e(this, e, "Writing state to XML file");
+        }
+    }
+
+    private void read() {
+        final InputStream is;
+        try {
+            is = mAtomicFile.openRead();
+        } catch (FileNotFoundException ex) {
+            return;
+        }
+
+        boolean versionChanged = false;
+
+        XmlPullParser parser;
+        try {
+            parser = Xml.newPullParser();
+            parser.setInput(new BufferedInputStream(is), null);
+            parser.nextTag();
+            mState = readFromXml(parser, mContext);
+            versionChanged = mState.versionNumber < EXPECTED_STATE_VERSION;
+
+        } catch (IOException | XmlPullParserException e) {
+            Log.e(this, e, "Reading state from XML file");
+            mState = new State();
+        } finally {
+            try {
+                is.close();
+            } catch (IOException e) {
+                Log.e(this, e, "Closing InputStream");
+            }
+        }
+
+        // If an upgrade occurred, write out the changed data.
+        if (versionChanged) {
+            write();
+        }
+    }
+
+    private static void writeToXml(State state, XmlSerializer serializer)
+            throws IOException {
+        sStateXml.writeToXml(state, serializer);
+    }
+
+    private static State readFromXml(XmlPullParser parser, Context context)
+            throws IOException, XmlPullParserException {
+        State s = sStateXml.readFromXml(parser, 0, context);
+        return s != null ? s : new State();
+    }
+
+    ////////////////////////////////////////////////////////////////////////////////////////////////
+    //
+    // XML serialization
+    //
+
+    @VisibleForTesting
+    public abstract static class XmlSerialization<T> {
+        private static final String LENGTH_ATTRIBUTE = "length";
+        private static final String VALUE_TAG = "value";
+
+        /**
+         * Write the supplied object to XML
+         */
+        public abstract void writeToXml(T o, XmlSerializer serializer)
+                throws IOException;
+
+        /**
+         * Read from the supplied XML into a new object, returning null in case of an
+         * unrecoverable schema mismatch or other data error. 'parser' must be already
+         * positioned at the first tag that is expected to have been emitted by this
+         * object's writeToXml(). This object tries to fail early without modifying
+         * 'parser' if it does not recognize the data it sees.
+         */
+        public abstract T readFromXml(XmlPullParser parser, int version, Context context)
+                throws IOException, XmlPullParserException;
+
+        protected void writeTextSafely(String tagName, Object value, XmlSerializer serializer)
+                throws IOException {
+            if (value != null) {
+                serializer.startTag(null, tagName);
+                serializer.text(Objects.toString(value));
+                serializer.endTag(null, tagName);
+            }
+        }
+
+        /**
+         * Serializes a string array.
+         *
+         * @param tagName The tag name for the string array.
+         * @param values The string values to serialize.
+         * @param serializer The serializer.
+         * @throws IOException
+         */
+        protected void writeStringList(String tagName, List<String> values,
+                XmlSerializer serializer)
+                throws IOException {
+
+            serializer.startTag(null, tagName);
+            if (values != null) {
+                serializer.attribute(null, LENGTH_ATTRIBUTE, Objects.toString(values.size()));
+                for (String toSerialize : values) {
+                    serializer.startTag(null, VALUE_TAG);
+                    if (toSerialize != null ){
+                        serializer.text(toSerialize);
+                    }
+                    serializer.endTag(null, VALUE_TAG);
+                }
+            } else {
+                serializer.attribute(null, LENGTH_ATTRIBUTE, "0");
+            }
+            serializer.endTag(null, tagName);
+
+        }
+
+        /**
+         * Reads a string array from the XML parser.
+         *
+         * @param parser The XML parser.
+         * @return String array containing the parsed values.
+         * @throws IOException Exception related to IO.
+         * @throws XmlPullParserException Exception related to parsing.
+         */
+        protected List<String> readStringList(XmlPullParser parser)
+                throws IOException, XmlPullParserException {
+
+            int length = Integer.parseInt(parser.getAttributeValue(null, LENGTH_ATTRIBUTE));
+            List<String> arrayEntries = new ArrayList<String>(length);
+            String value = null;
+
+            if (length == 0) {
+                return arrayEntries;
+            }
+
+            int outerDepth = parser.getDepth();
+            while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+                if (parser.getName().equals(VALUE_TAG)) {
+                    parser.next();
+                    value = parser.getText();
+                    arrayEntries.add(value);
+                }
+            }
+
+            return arrayEntries;
+        }
+    }
+
+    @VisibleForTesting
+    public static final XmlSerialization<State> sStateXml =
+            new XmlSerialization<State>() {
+        private static final String CLASS_STATE = "phone_account_registrar_state";
+        private static final String DEFAULT_OUTGOING = "default_outgoing";
+        private static final String SIM_CALL_MANAGER = "sim_call_manager";
+        private static final String ACCOUNTS = "accounts";
+        private static final String VERSION = "version";
+
+        @Override
+        public void writeToXml(State o, XmlSerializer serializer)
+                throws IOException {
+            if (o != null) {
+                serializer.startTag(null, CLASS_STATE);
+                serializer.attribute(null, VERSION, Objects.toString(EXPECTED_STATE_VERSION));
+
+                if (o.defaultOutgoing != null) {
+                    serializer.startTag(null, DEFAULT_OUTGOING);
+                    sPhoneAccountHandleXml.writeToXml(o.defaultOutgoing, serializer);
+                    serializer.endTag(null, DEFAULT_OUTGOING);
+                }
+
+                if (o.simCallManager != null) {
+                    serializer.startTag(null, SIM_CALL_MANAGER);
+                    sPhoneAccountHandleXml.writeToXml(o.simCallManager, serializer);
+                    serializer.endTag(null, SIM_CALL_MANAGER);
+                }
+
+                serializer.startTag(null, ACCOUNTS);
+                for (PhoneAccount m : o.accounts) {
+                    sPhoneAccountXml.writeToXml(m, serializer);
+                }
+                serializer.endTag(null, ACCOUNTS);
+
+                serializer.endTag(null, CLASS_STATE);
+            }
+        }
+
+        @Override
+        public State readFromXml(XmlPullParser parser, int version, Context context)
+                throws IOException, XmlPullParserException {
+            if (parser.getName().equals(CLASS_STATE)) {
+                State s = new State();
+
+                String rawVersion = parser.getAttributeValue(null, VERSION);
+                s.versionNumber = TextUtils.isEmpty(rawVersion) ? 1 :
+                        Integer.parseInt(rawVersion);
+
+                int outerDepth = parser.getDepth();
+                while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+                    if (parser.getName().equals(DEFAULT_OUTGOING)) {
+                        parser.nextTag();
+                        s.defaultOutgoing = sPhoneAccountHandleXml.readFromXml(parser,
+                                s.versionNumber, context);
+                    } else if (parser.getName().equals(SIM_CALL_MANAGER)) {
+                        parser.nextTag();
+                        s.simCallManager = sPhoneAccountHandleXml.readFromXml(parser,
+                                s.versionNumber, context);
+                    } else if (parser.getName().equals(ACCOUNTS)) {
+                        int accountsDepth = parser.getDepth();
+                        while (XmlUtils.nextElementWithin(parser, accountsDepth)) {
+                            PhoneAccount account = sPhoneAccountXml.readFromXml(parser,
+                                    s.versionNumber, context);
+
+                            if (account != null && s.accounts != null) {
+                                s.accounts.add(account);
+                            }
+                        }
+                    }
+                }
+                return s;
+            }
+            return null;
+        }
+    };
+
+    @VisibleForTesting
+    public static final XmlSerialization<PhoneAccount> sPhoneAccountXml =
+            new XmlSerialization<PhoneAccount>() {
+        private static final String CLASS_PHONE_ACCOUNT = "phone_account";
+        private static final String ACCOUNT_HANDLE = "account_handle";
+        private static final String ADDRESS = "handle";
+        private static final String SUBSCRIPTION_ADDRESS = "subscription_number";
+        private static final String CAPABILITIES = "capabilities";
+        private static final String ICON_RES_ID = "icon_res_id";
+        private static final String LABEL = "label";
+        private static final String SHORT_DESCRIPTION = "short_description";
+        private static final String SUPPORTED_URI_SCHEMES = "supported_uri_schemes";
+        private static final String ENABLED = "enabled";
+        private static final String TRUE = "true";
+        private static final String FALSE = "false";
+
+        @Override
+        public void writeToXml(PhoneAccount o, XmlSerializer serializer)
+                throws IOException {
+            if (o != null) {
+                serializer.startTag(null, CLASS_PHONE_ACCOUNT);
+
+                if (o.getAccountHandle() != null) {
+                    serializer.startTag(null, ACCOUNT_HANDLE);
+                    sPhoneAccountHandleXml.writeToXml(o.getAccountHandle(), serializer);
+                    serializer.endTag(null, ACCOUNT_HANDLE);
+                }
+
+                writeTextSafely(ADDRESS, o.getAddress(), serializer);
+                writeTextSafely(SUBSCRIPTION_ADDRESS, o.getSubscriptionAddress(), serializer);
+                writeTextSafely(CAPABILITIES, Integer.toString(o.getCapabilities()), serializer);
+                writeTextSafely(ICON_RES_ID, Integer.toString(o.getIconResId()), serializer);
+                writeTextSafely(LABEL, o.getLabel(), serializer);
+                writeTextSafely(SHORT_DESCRIPTION, o.getShortDescription(), serializer);
+                writeStringList(SUPPORTED_URI_SCHEMES, o.getSupportedUriSchemes(), serializer);
+                writeTextSafely(ENABLED, o.isEnabled() ? TRUE : FALSE, serializer);
+
+                serializer.endTag(null, CLASS_PHONE_ACCOUNT);
+            }
+        }
+
+        public PhoneAccount readFromXml(XmlPullParser parser, int version, Context context)
+                throws IOException, XmlPullParserException {
+            if (parser.getName().equals(CLASS_PHONE_ACCOUNT)) {
+                int outerDepth = parser.getDepth();
+                PhoneAccountHandle accountHandle = null;
+                Uri address = null;
+                Uri subscriptionAddress = null;
+                int capabilities = 0;
+                int iconResId = 0;
+                String label = null;
+                String shortDescription = null;
+                List<String> supportedUriSchemes = null;
+                boolean enabled = false;
+
+                while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+                    if (parser.getName().equals(ACCOUNT_HANDLE)) {
+                        parser.nextTag();
+                        accountHandle = sPhoneAccountHandleXml.readFromXml(parser, version,
+                                context);
+                    } else if (parser.getName().equals(ADDRESS)) {
+                        parser.next();
+                        address = Uri.parse(parser.getText());
+                    } else if (parser.getName().equals(SUBSCRIPTION_ADDRESS)) {
+                        parser.next();
+                        String nextText = parser.getText();
+                        subscriptionAddress = nextText == null ? null : Uri.parse(nextText);
+                    } else if (parser.getName().equals(CAPABILITIES)) {
+                        parser.next();
+                        capabilities = Integer.parseInt(parser.getText());
+                    } else if (parser.getName().equals(ICON_RES_ID)) {
+                        parser.next();
+                        iconResId = Integer.parseInt(parser.getText());
+                    } else if (parser.getName().equals(LABEL)) {
+                        parser.next();
+                        label = parser.getText();
+                    } else if (parser.getName().equals(SHORT_DESCRIPTION)) {
+                        parser.next();
+                        shortDescription = parser.getText();
+                    } else if (parser.getName().equals(SUPPORTED_URI_SCHEMES)) {
+                        supportedUriSchemes = readStringList(parser);
+                    } else if (parser.getName().equals(ENABLED)) {
+                        parser.next();
+                        enabled = parser.getText().equals(TRUE);
+                    }
+                }
+
+                // Upgrade older phone accounts to specify the supported URI schemes.
+                if (version < 2) {
+                    ComponentName sipComponentName = new ComponentName("com.android.phone",
+                            "com.android.services.telephony.sip.SipConnectionService");
+
+                    supportedUriSchemes = new ArrayList<>();
+
+                    // Handle the SIP connection service.
+                    // Check the system settings to see if it also should handle "tel" calls.
+                    if (accountHandle.getComponentName().equals(sipComponentName)) {
+                        boolean useSipForPstn = useSipForPstnCalls(context);
+                        supportedUriSchemes.add(PhoneAccount.SCHEME_SIP);
+                        if (useSipForPstn) {
+                            supportedUriSchemes.add(PhoneAccount.SCHEME_TEL);
+                        }
+                    } else {
+                        supportedUriSchemes.add(PhoneAccount.SCHEME_TEL);
+                        supportedUriSchemes.add(PhoneAccount.SCHEME_VOICEMAIL);
+                    }
+                }
+
+                // Prior to version 3, PhoneAccounts didn't include the enabled option.  Enable
+                // all TelephonyConnectionService phone accounts by default.
+                if (version < 3) {
+                    ComponentName telephonyComponentName = new ComponentName("com.android.phone",
+                            "com.android.services.telephony.TelephonyConnectionService");
+
+                    if (accountHandle.getComponentName().equals(telephonyComponentName)) {
+                        enabled = true;
+                    }
+                }
+
+                return PhoneAccount.builder(accountHandle, label)
+                        .setAddress(address)
+                        .setSubscriptionAddress(subscriptionAddress)
+                        .setCapabilities(capabilities)
+                        .setIconResId(iconResId)
+                        .setShortDescription(shortDescription)
+                        .setSupportedUriSchemes(supportedUriSchemes)
+                        .setEnabled(enabled)
+                        .build();
+            }
+            return null;
+        }
+
+        /**
+         * Determines if the SIP call settings specify to use SIP for all calls, including PSTN calls.
+         *
+         * @param context The context.
+         * @return {@code True} if SIP should be used for all calls.
+         */
+        private boolean useSipForPstnCalls(Context context) {
+            String option = Settings.System.getString(context.getContentResolver(),
+                    Settings.System.SIP_CALL_OPTIONS);
+            option = (option != null) ? option : Settings.System.SIP_ADDRESS_ONLY;
+            return option.equals(Settings.System.SIP_ALWAYS);
+        }
+    };
+
+    @VisibleForTesting
+    public static final XmlSerialization<PhoneAccountHandle> sPhoneAccountHandleXml =
+            new XmlSerialization<PhoneAccountHandle>() {
+        private static final String CLASS_PHONE_ACCOUNT_HANDLE = "phone_account_handle";
+        private static final String COMPONENT_NAME = "component_name";
+        private static final String ID = "id";
+
+        @Override
+        public void writeToXml(PhoneAccountHandle o, XmlSerializer serializer)
+                throws IOException {
+            if (o != null) {
+                serializer.startTag(null, CLASS_PHONE_ACCOUNT_HANDLE);
+
+                if (o.getComponentName() != null) {
+                    writeTextSafely(
+                            COMPONENT_NAME, o.getComponentName().flattenToString(), serializer);
+                }
+
+                writeTextSafely(ID, o.getId(), serializer);
+
+                serializer.endTag(null, CLASS_PHONE_ACCOUNT_HANDLE);
+            }
+        }
+
+        @Override
+        public PhoneAccountHandle readFromXml(XmlPullParser parser, int version, Context context)
+                throws IOException, XmlPullParserException {
+            if (parser.getName().equals(CLASS_PHONE_ACCOUNT_HANDLE)) {
+                String componentNameString = null;
+                String idString = null;
+                int outerDepth = parser.getDepth();
+                while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+                    if (parser.getName().equals(COMPONENT_NAME)) {
+                        parser.next();
+                        componentNameString = parser.getText();
+                    } else if (parser.getName().equals(ID)) {
+                        parser.next();
+                        idString = parser.getText();
+                    }
+                }
+                if (componentNameString != null) {
+                    return new PhoneAccountHandle(
+                            ComponentName.unflattenFromString(componentNameString),
+                            idString);
+                }
+            }
+            return null;
+        }
+    };
+}
diff --git a/src/com/android/server/telecom/PhoneStateBroadcaster.java b/src/com/android/server/telecom/PhoneStateBroadcaster.java
new file mode 100644
index 0000000..d0d0b68
--- /dev/null
+++ b/src/com/android/server/telecom/PhoneStateBroadcaster.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 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;
+
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.telecom.CallState;
+import android.telephony.TelephonyManager;
+
+import com.android.internal.telephony.ITelephonyRegistry;
+
+/**
+ * Send a {@link TelephonyManager#ACTION_PHONE_STATE_CHANGED} broadcast when the call state
+ * changes.
+ */
+final class PhoneStateBroadcaster extends CallsManagerListenerBase {
+
+    private final ITelephonyRegistry mRegistry;
+    private int mCurrentState = TelephonyManager.CALL_STATE_IDLE;
+
+    public PhoneStateBroadcaster() {
+        mRegistry = ITelephonyRegistry.Stub.asInterface(ServiceManager.getService(
+                "telephony.registry"));
+        if (mRegistry == null) {
+            Log.w(this, "TelephonyRegistry is null");
+        }
+    }
+
+    @Override
+    public void onCallStateChanged(Call call, int oldState, int newState) {
+        if ((newState == CallState.DIALING || newState == CallState.ACTIVE
+                || newState == CallState.ON_HOLD) && !CallsManager.getInstance().hasRingingCall()) {
+            /*
+             * EXTRA_STATE_RINGING takes precedence over EXTRA_STATE_OFFHOOK, so if there is
+             * already a ringing call, don't broadcast EXTRA_STATE_OFFHOOK.
+             */
+            sendPhoneStateChangedBroadcast(call, TelephonyManager.CALL_STATE_OFFHOOK);
+        }
+    }
+
+    @Override
+    public void onCallAdded(Call call) {
+        if (call.getState() == CallState.RINGING) {
+            sendPhoneStateChangedBroadcast(call, TelephonyManager.CALL_STATE_RINGING);
+        }
+    };
+
+    @Override
+    public void onCallRemoved(Call call) {
+        if (!CallsManager.getInstance().hasAnyCalls()) {
+            sendPhoneStateChangedBroadcast(call, TelephonyManager.CALL_STATE_IDLE);
+        }
+    }
+
+    private void sendPhoneStateChangedBroadcast(Call call, int phoneState) {
+        if (phoneState == mCurrentState) {
+            return;
+        }
+
+        mCurrentState = phoneState;
+
+        String callHandle = null;
+        if (call.getHandle() != null) {
+            callHandle = call.getHandle().getSchemeSpecificPart();
+        }
+
+        try {
+            if (mRegistry != null) {
+                mRegistry.notifyCallState(phoneState, callHandle);
+                Log.i(this, "Broadcasted state change: %s", mCurrentState);
+            }
+        } catch (RemoteException e) {
+            Log.w(this, "RemoteException when notifying TelephonyRegistry of call state change.");
+        }
+    }
+}
diff --git a/src/com/android/server/telecom/ProximitySensorManager.java b/src/com/android/server/telecom/ProximitySensorManager.java
new file mode 100644
index 0000000..289366f
--- /dev/null
+++ b/src/com/android/server/telecom/ProximitySensorManager.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 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;
+
+import android.content.Context;
+import android.os.PowerManager;
+
+/**
+ * This class manages the proximity sensor and allows callers to turn it on and off.
+ */
+public class ProximitySensorManager extends CallsManagerListenerBase {
+    private static final String TAG = ProximitySensorManager.class.getSimpleName();
+
+    private final PowerManager.WakeLock mProximityWakeLock;
+
+    public ProximitySensorManager(Context context) {
+        PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+
+        if (pm.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
+            mProximityWakeLock = pm.newWakeLock(
+                    PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG);
+        } else {
+            mProximityWakeLock = null;
+        }
+        Log.d(this, "onCreate: mProximityWakeLock: ", mProximityWakeLock);
+    }
+
+    @Override
+    public void onCallRemoved(Call call) {
+        if (CallsManager.getInstance().getCalls().isEmpty()) {
+            Log.v(this, "all calls removed, resetting proximity sensor to default state");
+            turnOff(true);
+        }
+        super.onCallRemoved(call);
+    }
+
+    /**
+     * Turn the proximity sensor on.
+     */
+    void turnOn() {
+        if (CallsManager.getInstance().getCalls().isEmpty()) {
+            Log.w(this, "Asking to turn on prox sensor without a call? I don't think so.");
+            return;
+        }
+
+        if (mProximityWakeLock == null) {
+            return;
+        }
+        if (!mProximityWakeLock.isHeld()) {
+            Log.i(this, "Acquiring proximity wake lock");
+            mProximityWakeLock.acquire();
+        } else {
+            Log.i(this, "Proximity wake lock already acquired");
+        }
+    }
+
+    /**
+     * Turn the proximity sensor off.
+     * @param screenOnImmediately
+     */
+    void turnOff(boolean screenOnImmediately) {
+        if (mProximityWakeLock == null) {
+            return;
+        }
+        if (mProximityWakeLock.isHeld()) {
+            Log.i(this, "Releasing proximity wake lock");
+            int flags =
+                (screenOnImmediately ? 0 : PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY);
+            mProximityWakeLock.release(flags);
+        } else {
+            Log.i(this, "Proximity wake lock already released");
+        }
+    }
+}
diff --git a/src/com/android/server/telecom/QuickResponseUtils.java b/src/com/android/server/telecom/QuickResponseUtils.java
new file mode 100644
index 0000000..dad2907
--- /dev/null
+++ b/src/com/android/server/telecom/QuickResponseUtils.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 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;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+
+
+/**
+ * Utils class that exposes some helper routines to used to manage the QuickResponses
+ */
+public class QuickResponseUtils {
+    public static final String LOG_TAG = "QuickResponseUtils";
+
+    // SharedPreferences file name for our persistent settings.
+    public static final String SHARED_PREFERENCES_NAME = "respond_via_sms_prefs";
+    private static final String PACKAGE_NAME_TELEPHONY = "com.android.phone";
+
+    // Preference keys for the 4 "canned responses"; see RespondViaSmsManager$Settings.
+    // Since (for now at least) the number of messages is fixed at 4, and since
+    // SharedPreferences can't deal with arrays anyway, just store the messages
+    // as 4 separate strings.
+    public static final int NUM_CANNED_RESPONSES = 4;
+    public static final String KEY_CANNED_RESPONSE_PREF_1 = "canned_response_pref_1";
+    public static final String KEY_CANNED_RESPONSE_PREF_2 = "canned_response_pref_2";
+    public static final String KEY_CANNED_RESPONSE_PREF_3 = "canned_response_pref_3";
+    public static final String KEY_CANNED_RESPONSE_PREF_4 = "canned_response_pref_4";
+
+    /**
+     * As of L, QuickResponses were moved from Telephony to Telecom. Because of
+     * this, we need to make sure that we migrate any old QuickResponses to our
+     * current SharedPreferences.  This is a lazy migration as it happens only when
+     * the QuickResponse settings are viewed or if they are queried via RespondViaSmsManager.
+     */
+    public static void maybeMigrateLegacyQuickResponses() {
+        // The algorithm will go as such:
+        // If Telecom QuickResponses exist, we will skip migration because this implies
+        // that a user has already specified their desired QuickResponses and have abandoned any
+        // older QuickResponses.
+        // Then, if Telephony QuickResponses exist, we will move those to Telecom.
+        // If neither exist, we'll populate Telecom with the default QuickResponses.
+        // This guarantees the caller that QuickResponses exist in SharedPreferences after this
+        // function is called.
+
+        Log.d(LOG_TAG, "maybeMigrateLegacyQuickResponses() - Starting");
+
+        final Context telecomContext = TelecomApp.getInstance();
+        final SharedPreferences prefs = telecomContext.getSharedPreferences(
+                SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
+        final Resources res = telecomContext.getResources();
+
+        final boolean responsesExist = prefs.contains(KEY_CANNED_RESPONSE_PREF_1);
+        if (responsesExist) {
+            // If one QuickResponse exists, they all exist.
+            Log.d(LOG_TAG, "maybeMigrateLegacyQuickResponses() - Telecom QuickResponses exist");
+            return;
+        }
+
+        // Grab the all the default QuickResponses from our resources.
+        String cannedResponse1 = res.getString(R.string.respond_via_sms_canned_response_1);
+        String cannedResponse2 = res.getString(R.string.respond_via_sms_canned_response_2);
+        String cannedResponse3 = res.getString(R.string.respond_via_sms_canned_response_3);
+        String cannedResponse4 = res.getString(R.string.respond_via_sms_canned_response_4);
+
+        Log.d(LOG_TAG, "maybeMigrateLegacyQuickResponses() - No local QuickResponses");
+
+        // We don't have local QuickResponses, let's see if they live in
+        // the Telephony package and we'll fall back on using our default values.
+        Context telephonyContext = null;
+        try {
+            telephonyContext = telecomContext.createPackageContext(PACKAGE_NAME_TELEPHONY, 0);
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.e(LOG_TAG, e, "maybeMigrateLegacyQuickResponses() - Can't find Telephony package.");
+        }
+
+        // Read the old canned responses from the Telephony SharedPreference if possible.
+        if (telephonyContext != null) {
+            // Note that if any one QuickResponse does not exist, we'll use the default
+            // value to populate it.
+            Log.d(LOG_TAG, "maybeMigrateLegacyQuickResponses() - Using Telephony QuickResponses.");
+            final SharedPreferences oldPrefs = telephonyContext.getSharedPreferences(
+                    SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
+            cannedResponse1 = oldPrefs.getString(KEY_CANNED_RESPONSE_PREF_1, cannedResponse1);
+            cannedResponse2 = oldPrefs.getString(KEY_CANNED_RESPONSE_PREF_2, cannedResponse2);
+            cannedResponse3 = oldPrefs.getString(KEY_CANNED_RESPONSE_PREF_3, cannedResponse3);
+            cannedResponse4 = oldPrefs.getString(KEY_CANNED_RESPONSE_PREF_4, cannedResponse4);
+        }
+
+        // Either way, write them back into Telecom SharedPreferences.
+        final SharedPreferences.Editor editor = prefs.edit();
+        editor.putString(KEY_CANNED_RESPONSE_PREF_1, cannedResponse1);
+        editor.putString(KEY_CANNED_RESPONSE_PREF_2, cannedResponse2);
+        editor.putString(KEY_CANNED_RESPONSE_PREF_3, cannedResponse3);
+        editor.putString(KEY_CANNED_RESPONSE_PREF_4, cannedResponse4);
+        editor.commit();
+
+        Log.d(LOG_TAG, "maybeMigrateLegacyQuickResponses() - Done.");
+        return;
+    }
+}
diff --git a/src/com/android/server/telecom/README b/src/com/android/server/telecom/README
new file mode 100644
index 0000000..be202fe
--- /dev/null
+++ b/src/com/android/server/telecom/README
@@ -0,0 +1,2 @@
+Code to manage and handle phone calls etc, approximately migrated from
+/telephony etc.
\ No newline at end of file
diff --git a/src/com/android/server/telecom/RespondViaSmsManager.java b/src/com/android/server/telecom/RespondViaSmsManager.java
new file mode 100644
index 0000000..6c054e6
--- /dev/null
+++ b/src/com/android/server/telecom/RespondViaSmsManager.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2011 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 com.android.internal.os.SomeArgs;
+import com.android.internal.telephony.SmsApplication;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+import android.telecom.Response;
+import android.telephony.TelephonyManager;
+import android.widget.Toast;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Helper class to manage the "Respond via Message" feature for incoming calls.
+ */
+public class RespondViaSmsManager extends CallsManagerListenerBase {
+    private static final int MSG_CANNED_TEXT_MESSAGES_READY = 1;
+    private static final int MSG_SHOW_SENT_TOAST = 2;
+
+    private static final RespondViaSmsManager sInstance = new RespondViaSmsManager();
+
+    private final Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_CANNED_TEXT_MESSAGES_READY:
+                    SomeArgs args = (SomeArgs) msg.obj;
+                    try {
+                        Response<Void, List<String>> response =
+                                (Response<Void, List<String>>) args.arg1;
+                        List<String> textMessages =
+                                (List<String>) args.arg2;
+                        if (textMessages != null) {
+                            response.onResult(null, textMessages);
+                        } else {
+                            response.onError(null, 0, null);
+                        }
+                    } finally {
+                        args.recycle();
+                    }
+                    break;
+                case MSG_SHOW_SENT_TOAST:
+                    showMessageSentToast((String) msg.obj);
+                    break;
+            }
+        }
+    };
+
+    public static RespondViaSmsManager getInstance() { return sInstance; }
+
+    private RespondViaSmsManager() {}
+
+    /**
+     * Read the (customizable) canned responses from SharedPreferences,
+     * or from defaults if the user has never actually brought up
+     * the Settings UI.
+     *
+     * The interface of this method is asynchronous since it does disk I/O.
+     *
+     * @param response An object to receive an async reply, which will be called from
+     *                 the main thread.
+     */
+    public void loadCannedTextMessages(final Response<Void, List<String>> response) {
+        new Thread() {
+            @Override
+            public void run() {
+                Log.d(RespondViaSmsManager.this, "loadCannedResponses() starting");
+
+                // This function guarantees that QuickResponses will be in our
+                // SharedPreferences with the proper values considering there may be
+                // old QuickResponses in Telephony pre L.
+                QuickResponseUtils.maybeMigrateLegacyQuickResponses();
+
+                final SharedPreferences prefs = TelecomApp.getInstance().getSharedPreferences(
+                        QuickResponseUtils.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
+                final Resources res = TelecomApp.getInstance().getInstance().getResources();
+
+                final ArrayList<String> textMessages = new ArrayList<>(
+                        QuickResponseUtils.NUM_CANNED_RESPONSES);
+
+                // Note the default values here must agree with the corresponding
+                // android:defaultValue attributes in respond_via_sms_settings.xml.
+                textMessages.add(0, prefs.getString(QuickResponseUtils.KEY_CANNED_RESPONSE_PREF_1,
+                        res.getString(R.string.respond_via_sms_canned_response_1)));
+                textMessages.add(1, prefs.getString(QuickResponseUtils.KEY_CANNED_RESPONSE_PREF_2,
+                        res.getString(R.string.respond_via_sms_canned_response_2)));
+                textMessages.add(2, prefs.getString(QuickResponseUtils.KEY_CANNED_RESPONSE_PREF_3,
+                        res.getString(R.string.respond_via_sms_canned_response_3)));
+                textMessages.add(3, prefs.getString(QuickResponseUtils.KEY_CANNED_RESPONSE_PREF_4,
+                        res.getString(R.string.respond_via_sms_canned_response_4)));
+
+                Log.d(RespondViaSmsManager.this,
+                        "loadCannedResponses() completed, found responses: %s",
+                        textMessages.toString());
+
+                SomeArgs args = SomeArgs.obtain();
+                args.arg1 = response;
+                args.arg2 = textMessages;
+                mHandler.obtainMessage(MSG_CANNED_TEXT_MESSAGES_READY, args).sendToTarget();
+            }
+        }.start();
+    }
+
+    @Override
+    public void onIncomingCallRejected(Call call, boolean rejectWithMessage, String textMessage) {
+        if (rejectWithMessage) {
+            rejectCallWithMessage(call.getHandle().getSchemeSpecificPart(), textMessage);
+        }
+    }
+
+    private void showMessageSentToast(final String phoneNumber) {
+        // ...and show a brief confirmation to the user (since
+        // otherwise it's hard to be sure that anything actually
+        // happened.)
+        final Resources res = TelecomApp.getInstance().getResources();
+        final String formatString = res.getString(
+                R.string.respond_via_sms_confirmation_format);
+        final String confirmationMsg = String.format(formatString, phoneNumber);
+        Toast.makeText(TelecomApp.getInstance(), confirmationMsg,
+                Toast.LENGTH_LONG).show();
+
+        // TODO: If the device is locked, this toast won't actually ever
+        // be visible!  (That's because we're about to dismiss the call
+        // screen, which means that the device will return to the
+        // keyguard.  But toasts aren't visible on top of the keyguard.)
+        // Possible fixes:
+        // (1) Is it possible to allow a specific Toast to be visible
+        //     on top of the keyguard?
+        // (2) Artificially delay the dismissCallScreen() call by 3
+        //     seconds to allow the toast to be seen?
+        // (3) Don't use a toast at all; instead use a transient state
+        //     of the InCallScreen (perhaps via the InCallUiState
+        //     progressIndication feature), and have that state be
+        //     visible for 3 seconds before calling dismissCallScreen().
+    }
+
+    /**
+     * Reject the call with the specified message. If message is null this call is ignored.
+     */
+    private void rejectCallWithMessage(String phoneNumber, String textMessage) {
+        if (textMessage != null) {
+            final ComponentName component =
+                    SmsApplication.getDefaultRespondViaMessageApplication(
+                            TelecomApp.getInstance(), true /*updateIfNeeded*/);
+            if (component != null) {
+                // Build and send the intent
+                final Uri uri = Uri.fromParts(Constants.SCHEME_SMSTO, phoneNumber, null);
+                final Intent intent = new Intent(TelephonyManager.ACTION_RESPOND_VIA_MESSAGE, uri);
+                intent.putExtra(Intent.EXTRA_TEXT, textMessage);
+                mHandler.obtainMessage(MSG_SHOW_SENT_TOAST, phoneNumber).sendToTarget();
+                intent.setComponent(component);
+                TelecomApp.getInstance().startService(intent);
+            }
+        }
+    }
+}
diff --git a/src/com/android/server/telecom/RespondViaSmsSettings.java b/src/com/android/server/telecom/RespondViaSmsSettings.java
new file mode 100644
index 0000000..23bf7b2
--- /dev/null
+++ b/src/com/android/server/telecom/RespondViaSmsSettings.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2011 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.app.ActionBar;
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.EditTextPreference;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.view.Menu;
+import android.view.MenuItem;
+
+/**
+ * Helper class to manage the "Respond via SMS Message" feature for incoming calls.
+ */
+public class RespondViaSmsSettings {
+    private static final String KEY_PREFERRED_PACKAGE = "preferred_package_pref";
+    private static final String KEY_INSTANT_TEXT_DEFAULT_COMPONENT = "instant_text_def_component";
+
+    // TODO: This class is newly copied into Telecom (com.android.server.telecom) from it previous
+    // location in Telephony (com.android.phone). User's preferences stored in the old location
+    // will be lost. We need code here to migrate KLP -> LMP settings values.
+
+    /**
+     * Settings activity under "Call settings" to let you manage the
+     * canned responses; see respond_via_sms_settings.xml
+     */
+    public static class Settings extends PreferenceActivity
+            implements Preference.OnPreferenceChangeListener {
+        @Override
+        protected void onCreate(Bundle icicle) {
+            super.onCreate(icicle);
+            Log.d(this, "Settings: onCreate()...");
+
+            // This function guarantees that QuickResponses will be in our
+            // SharedPreferences with the proper values considering there may be
+            // old QuickResponses in Telephony pre L.
+            QuickResponseUtils.maybeMigrateLegacyQuickResponses();
+
+            getPreferenceManager().setSharedPreferencesName(
+                    QuickResponseUtils.SHARED_PREFERENCES_NAME);
+
+            // This preference screen is ultra-simple; it's just 4 plain
+            // <EditTextPreference>s, one for each of the 4 "canned responses".
+            //
+            // The only nontrivial thing we do here is copy the text value of
+            // each of those EditTextPreferences and use it as the preference's
+            // "title" as well, so that the user will immediately see all 4
+            // strings when they arrive here.
+            //
+            // Also, listen for change events (since we'll need to update the
+            // title any time the user edits one of the strings.)
+
+            addPreferencesFromResource(R.xml.respond_via_sms_settings);
+
+            EditTextPreference pref;
+            pref = (EditTextPreference) findPreference(
+                    QuickResponseUtils.KEY_CANNED_RESPONSE_PREF_1);
+            pref.setTitle(pref.getText());
+            pref.setOnPreferenceChangeListener(this);
+
+            pref = (EditTextPreference) findPreference(
+                    QuickResponseUtils.KEY_CANNED_RESPONSE_PREF_2);
+            pref.setTitle(pref.getText());
+            pref.setOnPreferenceChangeListener(this);
+
+            pref = (EditTextPreference) findPreference(
+                    QuickResponseUtils.KEY_CANNED_RESPONSE_PREF_3);
+            pref.setTitle(pref.getText());
+            pref.setOnPreferenceChangeListener(this);
+
+            pref = (EditTextPreference) findPreference(
+                    QuickResponseUtils.KEY_CANNED_RESPONSE_PREF_4);
+            pref.setTitle(pref.getText());
+            pref.setOnPreferenceChangeListener(this);
+
+            ActionBar actionBar = getActionBar();
+            if (actionBar != null) {
+                // android.R.id.home will be triggered in onOptionsItemSelected()
+                actionBar.setDisplayHomeAsUpEnabled(true);
+            }
+        }
+
+        // Preference.OnPreferenceChangeListener implementation
+        @Override
+        public boolean onPreferenceChange(Preference preference, Object newValue) {
+            Log.d(this, "onPreferenceChange: key = %s", preference.getKey());
+            Log.d(this, "  preference = '%s'", preference);
+            Log.d(this, "  newValue = '%s'", newValue);
+
+            EditTextPreference pref = (EditTextPreference) preference;
+
+            // Copy the new text over to the title, just like in onCreate().
+            // (Watch out: onPreferenceChange() is called *before* the
+            // Preference itself gets updated, so we need to use newValue here
+            // rather than pref.getText().)
+            pref.setTitle((String) newValue);
+
+            return true;  // means it's OK to update the state of the Preference with the new value
+        }
+
+        @Override
+        public boolean onOptionsItemSelected(MenuItem item) {
+            final int itemId = item.getItemId();
+            switch (itemId) {
+                case android.R.id.home:
+                    goUpToTopLevelSetting(this);
+                    return true;
+                case R.id.respond_via_message_reset:
+                    // Reset the preferences settings
+                    SharedPreferences prefs = getSharedPreferences(
+                            QuickResponseUtils.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
+                    SharedPreferences.Editor editor = prefs.edit();
+                    editor.remove(KEY_INSTANT_TEXT_DEFAULT_COMPONENT);
+                    editor.apply();
+
+                    return true;
+                default:
+            }
+            return super.onOptionsItemSelected(item);
+        }
+
+        @Override
+        public boolean onCreateOptionsMenu(Menu menu) {
+            getMenuInflater().inflate(R.menu.respond_via_message_settings_menu, menu);
+            return super.onCreateOptionsMenu(menu);
+        }
+    }
+
+    /**
+     * Finish current Activity and go up to the top level Settings.
+     */
+    public static void goUpToTopLevelSetting(Activity activity) {
+        activity.finish();
+     }
+}
diff --git a/src/com/android/server/telecom/RingbackPlayer.java b/src/com/android/server/telecom/RingbackPlayer.java
new file mode 100644
index 0000000..dc06c95
--- /dev/null
+++ b/src/com/android/server/telecom/RingbackPlayer.java
@@ -0,0 +1,142 @@
+/*
+ * 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;
+
+import android.telecom.CallState;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * Plays ringback tones. Ringback is different from other tones because it operates as the current
+ * audio for a call, whereas most tones play as simple timed events. This means ringback must be
+ * able to turn off and on as the user switches between calls. This is why it is implemented as its
+ * own class.
+ */
+class RingbackPlayer extends CallsManagerListenerBase {
+
+    private final CallsManager mCallsManager;
+
+    private final InCallTonePlayer.Factory mPlayerFactory;
+
+    /**
+     * The current call for which the ringback tone is being played.
+     */
+    private Call mCall;
+
+    /**
+     * The currently active player.
+     */
+    private InCallTonePlayer mTonePlayer;
+
+    RingbackPlayer(CallsManager callsManager, InCallTonePlayer.Factory playerFactory) {
+        mCallsManager = callsManager;
+        mPlayerFactory = playerFactory;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall) {
+        if (oldForegroundCall != null) {
+            stopRingbackForCall(oldForegroundCall);
+        }
+
+        if (shouldStartRinging(newForegroundCall)) {
+            startRingbackForCall(newForegroundCall);
+        }
+    }
+
+    @Override
+    public void onConnectionServiceChanged(
+            Call call,
+            ConnectionServiceWrapper oldService,
+            ConnectionServiceWrapper newService) {
+
+        // Treat as ending or begining dialing based on the state transition.
+        if (shouldStartRinging(call)) {
+            startRingbackForCall(call);
+        } else if (newService == null) {
+            stopRingbackForCall(call);
+        }
+    }
+
+    @Override
+    public void onRingbackRequested(Call call, boolean ignored) {
+        if (shouldStartRinging(call)) {
+            startRingbackForCall(call);
+        } else {
+            stopRingbackForCall(call);
+        }
+    }
+
+    /**
+     * Starts ringback for the specified dialing call as needed.
+     *
+     * @param call The call for which to ringback.
+     */
+    private void startRingbackForCall(Call call) {
+        Preconditions.checkState(call.getState() == CallState.DIALING);
+        ThreadUtil.checkOnMainThread();
+
+        if (mCall == call) {
+            Log.w(this, "Ignoring duplicate requests to ring for %s.", call);
+            return;
+        }
+
+        if (mCall != null) {
+            // We only get here for the foreground call so, there's no reason why there should
+            // exist a current dialing call.
+            Log.wtf(this, "Ringback player thinks there are two foreground-dialing calls.");
+        }
+
+        mCall = call;
+        if (mTonePlayer == null) {
+            Log.d(this, "Playing the ringback tone for %s.", call);
+            mTonePlayer = mPlayerFactory.createPlayer(InCallTonePlayer.TONE_RING_BACK);
+            mTonePlayer.startTone();
+        }
+    }
+
+    /**
+     * Stops the ringback for the specified dialing call as needed.
+     *
+     * @param call The call for which to stop ringback.
+     */
+    private void stopRingbackForCall(Call call) {
+        ThreadUtil.checkOnMainThread();
+
+        if (mCall == call) {
+            // The foreground call is no longer dialing or is no longer the foreground call. In
+            // either case, stop the ringback tone.
+            mCall = null;
+
+            if (mTonePlayer == null) {
+                Log.w(this, "No player found to stop.");
+            } else {
+                Log.i(this, "Stopping the ringback tone for %s.", call);
+                mTonePlayer.stopTone();
+                mTonePlayer = null;
+            }
+        }
+    }
+
+    private boolean shouldStartRinging(Call call) {
+        return call != null
+                && mCallsManager.getForegroundCall() == call
+                && call.getState() == CallState.DIALING
+                && call.isRingbackRequested();
+    }
+}
diff --git a/src/com/android/server/telecom/Ringer.java b/src/com/android/server/telecom/Ringer.java
new file mode 100644
index 0000000..34f6829
--- /dev/null
+++ b/src/com/android/server/telecom/Ringer.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 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;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemVibrator;
+import android.os.Vibrator;
+import android.provider.Settings;
+import android.telecom.CallState;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Controls the ringtone player.
+ */
+final class Ringer extends CallsManagerListenerBase {
+    private static final long[] VIBRATION_PATTERN = new long[] {
+        0, // No delay before starting
+        1000, // How long to vibrate
+        1000, // How long to wait before vibrating again
+    };
+
+    private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder()
+            .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+            .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+            .build();
+
+    /** Indicate that we want the pattern to repeat at the step which turns on vibration. */
+    private static final int VIBRATION_PATTERN_REPEAT = 1;
+
+    private final AsyncRingtonePlayer mRingtonePlayer = new AsyncRingtonePlayer();
+
+    /**
+     * Used to keep ordering of unanswered incoming calls. There can easily exist multiple incoming
+     * calls and explicit ordering is useful for maintaining the proper state of the ringer.
+     */
+    private final List<Call> mRingingCalls = new LinkedList<>();
+
+    private final CallAudioManager mCallAudioManager;
+    private final CallsManager mCallsManager;
+    private final InCallTonePlayer.Factory mPlayerFactory;
+    private final Context mContext;
+    private final Vibrator mVibrator;
+
+    private InCallTonePlayer mCallWaitingPlayer;
+
+    /**
+     * Used to track the status of {@link #mVibrator} in the case of simultaneous incoming calls.
+     */
+    private boolean mIsVibrating = false;
+
+    /** Initializes the Ringer. */
+    Ringer(
+            CallAudioManager callAudioManager,
+            CallsManager callsManager,
+            InCallTonePlayer.Factory playerFactory,
+            Context context) {
+
+        mCallAudioManager = callAudioManager;
+        mCallsManager = callsManager;
+        mPlayerFactory = playerFactory;
+        mContext = context;
+        // We don't rely on getSystemService(Context.VIBRATOR_SERVICE) to make sure this
+        // vibrator object will be isolated from others.
+        mVibrator = new SystemVibrator(TelecomApp.getInstance());
+    }
+
+    @Override
+    public void onCallAdded(final Call call) {
+        if (call.isIncoming() && call.getState() == CallState.RINGING) {
+            if (mRingingCalls.contains(call)) {
+                Log.wtf(this, "New ringing call is already in list of unanswered calls");
+            }
+            mRingingCalls.add(call);
+            updateRinging();
+        }
+    }
+
+    @Override
+    public void onCallRemoved(Call call) {
+        removeFromUnansweredCall(call);
+    }
+
+    @Override
+    public void onCallStateChanged(Call call, int oldState, int newState) {
+        if (newState != CallState.RINGING) {
+            removeFromUnansweredCall(call);
+        }
+    }
+
+    @Override
+    public void onIncomingCallAnswered(Call call) {
+        onRespondedToIncomingCall(call);
+    }
+
+    @Override
+    public void onIncomingCallRejected(Call call, boolean rejectWithMessage, String textMessage) {
+        onRespondedToIncomingCall(call);
+    }
+
+    @Override
+    public void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall) {
+        if (mRingingCalls.contains(oldForegroundCall) ||
+                mRingingCalls.contains(newForegroundCall)) {
+            updateRinging();
+        }
+    }
+
+    /**
+     * Silences the ringer for any actively ringing calls.
+     */
+    void silence() {
+        // Remove all calls from the "ringing" set and then update the ringer.
+        mRingingCalls.clear();
+        updateRinging();
+    }
+
+    private void onRespondedToIncomingCall(Call call) {
+        // Only stop the ringer if this call is the top-most incoming call.
+        if (getTopMostUnansweredCall() == call) {
+            stopRinging();
+            stopCallWaiting();
+        }
+
+        // We do not remove the call from mRingingCalls until the call state changes from
+        // STATE_RINGING or the call is removed. see onCallStateChanged or onCallRemoved.
+    }
+
+    private Call getTopMostUnansweredCall() {
+        return mRingingCalls.isEmpty() ? null : mRingingCalls.get(0);
+    }
+
+    /**
+     * Removes the specified call from the list of unanswered incoming calls and updates the ringer
+     * based on the new state of {@link #mRingingCalls}. Safe to call with a call that is not
+     * present in the list of incoming calls.
+     */
+    private void removeFromUnansweredCall(Call call) {
+        mRingingCalls.remove(call);
+        updateRinging();
+    }
+
+    private void updateRinging() {
+        if (mRingingCalls.isEmpty()) {
+            stopRinging();
+            stopCallWaiting();
+        } else {
+            startRingingOrCallWaiting();
+        }
+    }
+
+    private void startRingingOrCallWaiting() {
+        Call foregroundCall = mCallsManager.getForegroundCall();
+        Log.v(this, "startRingingOrCallWaiting, foregroundCall: %s.", foregroundCall);
+
+        if (mRingingCalls.contains(foregroundCall)) {
+            // The foreground call is one of incoming calls so play the ringer out loud.
+            stopCallWaiting();
+
+            if (!shouldRingForContact(foregroundCall.getContactUri())) {
+                return;
+            }
+
+            AudioManager audioManager =
+                    (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+            if (audioManager.getStreamVolume(AudioManager.STREAM_RING) > 0) {
+                Log.v(this, "startRingingOrCallWaiting");
+                mCallAudioManager.setIsRinging(true);
+
+                // Because we wait until a contact info query to complete before processing a
+                // call (for the purposes of direct-to-voicemail), the information about custom
+                // ringtones should be available by the time this code executes. We can safely
+                // request the custom ringtone from the call and expect it to be current.
+                mRingtonePlayer.play(foregroundCall.getRingtone());
+            } else {
+                Log.v(this, "startRingingOrCallWaiting, skipping because volume is 0");
+            }
+
+            if (shouldVibrate(TelecomApp.getInstance()) && !mIsVibrating) {
+                mVibrator.vibrate(VIBRATION_PATTERN, VIBRATION_PATTERN_REPEAT,
+                        VIBRATION_ATTRIBUTES);
+                mIsVibrating = true;
+            }
+        } else {
+            Log.v(this, "Playing call-waiting tone.");
+
+            // All incoming calls are in background so play call waiting.
+            stopRinging();
+
+            if (mCallWaitingPlayer == null) {
+                mCallWaitingPlayer =
+                        mPlayerFactory.createPlayer(InCallTonePlayer.TONE_CALL_WAITING);
+                mCallWaitingPlayer.startTone();
+            }
+        }
+    }
+
+    private boolean shouldRingForContact(Uri contactUri) {
+        final NotificationManager manager =
+                (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+        final Bundle extras = new Bundle();
+        if (contactUri != null) {
+            extras.putStringArray(Notification.EXTRA_PEOPLE, new String[] {contactUri.toString()});
+        }
+        return manager.matchesCallFilter(extras);
+    }
+
+    private void stopRinging() {
+        Log.v(this, "stopRinging");
+
+        mRingtonePlayer.stop();
+
+        if (mIsVibrating) {
+            mVibrator.cancel();
+            mIsVibrating = false;
+        }
+
+        // Even though stop is asynchronous it's ok to update the audio manager. Things like audio
+        // focus are voluntary so releasing focus too early is not detrimental.
+        mCallAudioManager.setIsRinging(false);
+    }
+
+    private void stopCallWaiting() {
+        Log.v(this, "stop call waiting.");
+        if (mCallWaitingPlayer != null) {
+            mCallWaitingPlayer.stopTone();
+            mCallWaitingPlayer = null;
+        }
+    }
+
+    private boolean shouldVibrate(Context context) {
+        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        int ringerMode = audioManager.getRingerMode();
+        if (getVibrateWhenRinging(context)) {
+            return ringerMode != AudioManager.RINGER_MODE_SILENT;
+        } else {
+            return ringerMode == AudioManager.RINGER_MODE_VIBRATE;
+        }
+    }
+
+    private boolean getVibrateWhenRinging(Context context) {
+        if (!mVibrator.hasVibrator()) {
+            return false;
+        }
+        return Settings.System.getInt(context.getContentResolver(),
+                Settings.System.VIBRATE_WHEN_RINGING, 0) != 0;
+    }
+}
diff --git a/src/com/android/server/telecom/ServiceBinder.java b/src/com/android/server/telecom/ServiceBinder.java
new file mode 100644
index 0000000..98e7bd0
--- /dev/null
+++ b/src/com/android/server/telecom/ServiceBinder.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 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;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.os.IInterface;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+
+import com.google.common.collect.Sets;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Abstract class to perform the work of binding and unbinding to the specified service interface.
+ * Subclasses supply the service intent and component name and this class will invoke protected
+ * methods when the class is bound, unbound, or upon failure.
+ */
+abstract class ServiceBinder<ServiceInterface extends IInterface> {
+
+    /**
+     * Callback to notify after a binding succeeds or fails.
+     */
+    interface BindCallback {
+        void onSuccess();
+        void onFailure();
+    }
+
+    /**
+     * Listener for bind events on ServiceBinder.
+     */
+    interface Listener<ServiceBinderClass extends ServiceBinder<?>> {
+        void onUnbind(ServiceBinderClass serviceBinder);
+    }
+
+    /**
+     * Helper class to perform on-demand binding.
+     */
+    final class Binder {
+        /**
+         * Performs an asynchronous bind to the service (only if not already bound) and executes the
+         * specified callback.
+         *
+         * @param callback The callback to notify of the binding's success or failure.
+         */
+        void bind(BindCallback callback) {
+            ThreadUtil.checkOnMainThread();
+            Log.d(ServiceBinder.this, "bind()");
+
+            // Reset any abort request if we're asked to bind again.
+            clearAbort();
+
+            if (!mCallbacks.isEmpty()) {
+                // Binding already in progress, append to the list of callbacks and bail out.
+                mCallbacks.add(callback);
+                return;
+            }
+
+            mCallbacks.add(callback);
+            if (mServiceConnection == null) {
+                Intent serviceIntent = new Intent(mServiceAction).setComponent(mComponentName);
+                ServiceConnection connection = new ServiceBinderConnection();
+
+                Log.d(ServiceBinder.this, "Binding to service with intent: %s", serviceIntent);
+                if (!mContext.bindService(serviceIntent, connection, Context.BIND_AUTO_CREATE)) {
+                    handleFailedConnection();
+                    return;
+                }
+            } else {
+                Log.d(ServiceBinder.this, "Service is already bound.");
+                Preconditions.checkNotNull(mBinder);
+                handleSuccessfulConnection();
+            }
+        }
+    }
+
+    private final class ServiceBinderConnection implements ServiceConnection {
+        @Override
+        public void onServiceConnected(ComponentName componentName, IBinder binder) {
+            ThreadUtil.checkOnMainThread();
+            Log.i(this, "Service bound %s", componentName);
+
+            // Unbind request was queued so unbind immediately.
+            if (mIsBindingAborted) {
+                clearAbort();
+                logServiceDisconnected("onServiceConnected");
+                mContext.unbindService(this);
+                handleFailedConnection();
+                return;
+            }
+
+            mServiceConnection = this;
+            setBinder(binder);
+            handleSuccessfulConnection();
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName componentName) {
+            logServiceDisconnected("onServiceDisconnected");
+
+            mServiceConnection = null;
+            clearAbort();
+
+            handleServiceDisconnected();
+        }
+    }
+
+    /** The application context. */
+    private final Context mContext;
+
+    /** The intent action to use when binding through {@link Context#bindService}. */
+    private final String mServiceAction;
+
+    /** The component name of the service to bind to. */
+    private final ComponentName mComponentName;
+
+    /** The set of callbacks waiting for notification of the binding's success or failure. */
+    private final Set<BindCallback> mCallbacks = Sets.newHashSet();
+
+    /** Used to bind and unbind from the service. */
+    private ServiceConnection mServiceConnection;
+
+    /** The binder provided by {@link ServiceConnection#onServiceConnected} */
+    private IBinder mBinder;
+
+    private int mAssociatedCallCount = 0;
+
+    /**
+     * Indicates that an unbind request was made when the service was not yet bound. If the service
+     * successfully connects when this is true, it should be unbound immediately.
+     */
+    private boolean mIsBindingAborted;
+
+    /**
+     * Set of currently registered listeners.
+     * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
+     * load factor before resizing, 1 means we only expect a single thread to
+     * access the map so make only a single shard
+     */
+    private final Set<Listener> mListeners = Collections.newSetFromMap(
+            new ConcurrentHashMap<Listener, Boolean>(8, 0.9f, 1));
+
+    /**
+     * Persists the specified parameters and initializes the new instance.
+     *
+     * @param serviceAction The intent-action used with {@link Context#bindService}.
+     * @param componentName The component name of the service with which to bind.
+     */
+    protected ServiceBinder(String serviceAction, ComponentName componentName) {
+        Preconditions.checkState(!Strings.isNullOrEmpty(serviceAction));
+        Preconditions.checkNotNull(componentName);
+
+        mContext = TelecomApp.getInstance();
+        mServiceAction = serviceAction;
+        mComponentName = componentName;
+    }
+
+    final void incrementAssociatedCallCount() {
+        mAssociatedCallCount++;
+        Log.v(this, "Call count increment %d, %s", mAssociatedCallCount,
+                mComponentName.flattenToShortString());
+    }
+
+    final void decrementAssociatedCallCount() {
+        if (mAssociatedCallCount > 0) {
+            mAssociatedCallCount--;
+            Log.v(this, "Call count decrement %d, %s", mAssociatedCallCount,
+                    mComponentName.flattenToShortString());
+
+            if (mAssociatedCallCount == 0) {
+                unbind();
+            }
+        } else {
+            Log.wtf(this, "%s: ignoring a request to decrement mAssociatedCallCount below zero",
+                    mComponentName.getClassName());
+        }
+    }
+
+    final int getAssociatedCallCount() {
+        return mAssociatedCallCount;
+    }
+
+    /**
+     * Unbinds from the service if already bound, no-op otherwise.
+     */
+    final void unbind() {
+        ThreadUtil.checkOnMainThread();
+
+        if (mServiceConnection == null) {
+            // We're not yet bound, so queue up an abort request.
+            mIsBindingAborted = true;
+        } else {
+            logServiceDisconnected("unbind");
+            mContext.unbindService(mServiceConnection);
+            mServiceConnection = null;
+            setBinder(null);
+        }
+    }
+
+    final ComponentName getComponentName() {
+        return mComponentName;
+    }
+
+    final boolean isServiceValid(String actionName) {
+        if (mBinder == null) {
+            Log.w(this, "%s invoked while service is unbound", actionName);
+            return false;
+        }
+
+        return true;
+    }
+
+    final void addListener(Listener listener) {
+        mListeners.add(listener);
+    }
+
+    final void removeListener(Listener listener) {
+        if (listener != null) {
+            mListeners.remove(listener);
+        }
+    }
+
+    /**
+     * Logs a standard message upon service disconnection. This method exists because there is no
+     * single method called whenever the service unbinds and we want to log the same string in all
+     * instances where that occurs.  (Context.unbindService() does not cause onServiceDisconnected
+     * to execute).
+     *
+     * @param sourceTag Tag to disambiguate
+     */
+    private void logServiceDisconnected(String sourceTag) {
+        Log.i(this, "Service unbound %s, from %s.", mComponentName, sourceTag);
+    }
+
+    /**
+     * Notifies all the outstanding callbacks that the service is successfully bound. The list of
+     * outstanding callbacks is cleared afterwards.
+     */
+    private void handleSuccessfulConnection() {
+        for (BindCallback callback : mCallbacks) {
+            callback.onSuccess();
+        }
+        mCallbacks.clear();
+    }
+
+    /**
+     * Notifies all the outstanding callbacks that the service failed to bind. The list of
+     * outstanding callbacks is cleared afterwards.
+     */
+    private void handleFailedConnection() {
+        for (BindCallback callback : mCallbacks) {
+            callback.onFailure();
+        }
+        mCallbacks.clear();
+    }
+
+    /**
+     * Handles a service disconnection.
+     */
+    private void handleServiceDisconnected() {
+        setBinder(null);
+    }
+
+    private void clearAbort() {
+        mIsBindingAborted = false;
+    }
+
+    /**
+     * Sets the (private) binder and updates the child class.
+     *
+     * @param binder The new binder value.
+     */
+    private void setBinder(IBinder binder) {
+        if (mBinder != binder) {
+            mBinder = binder;
+
+            setServiceInterface(binder);
+
+            if (binder == null) {
+                for (Listener l : mListeners) {
+                    l.onUnbind(this);
+                }
+            }
+        }
+    }
+
+    /**
+     * Sets the service interface after the service is bound or unbound.
+     *
+     * @param binder The actual bound service implementation.
+     */
+    protected abstract void setServiceInterface(IBinder binder);
+}
diff --git a/src/com/android/server/telecom/StatusBarNotifier.java b/src/com/android/server/telecom/StatusBarNotifier.java
new file mode 100644
index 0000000..14253a0
--- /dev/null
+++ b/src/com/android/server/telecom/StatusBarNotifier.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 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;
+
+import android.app.StatusBarManager;
+import android.content.Context;
+
+/**
+ * Manages the special status bar notifications used by the phone app.
+ */
+final class StatusBarNotifier extends CallsManagerListenerBase {
+    private static final String SLOT_MUTE = "mute";
+    private static final String SLOT_SPEAKERPHONE = "speakerphone";
+
+    private final Context mContext;
+    private final CallsManager mCallsManager;
+    private final StatusBarManager mStatusBarManager;
+
+    private boolean mIsShowingMute;
+    private boolean mIsShowingSpeakerphone;
+
+    StatusBarNotifier(Context context, CallsManager callsManager) {
+        mContext = context;
+        mCallsManager = callsManager;
+        mStatusBarManager = (StatusBarManager) context.getSystemService(Context.STATUS_BAR_SERVICE);
+    }
+
+    /** ${inheritDoc} */
+    @Override
+    public void onCallRemoved(Call call) {
+        if (!mCallsManager.hasAnyCalls()) {
+            notifyMute(false);
+            notifySpeakerphone(false);
+        }
+    }
+
+    void notifyMute(boolean isMuted) {
+        // Never display anything if there are no calls.
+        if (!mCallsManager.hasAnyCalls()) {
+            isMuted = false;
+        }
+
+        if (mIsShowingMute == isMuted) {
+            return;
+        }
+
+        Log.d(this, "Mute status bar icon being set to %b", isMuted);
+
+        if (isMuted) {
+            mStatusBarManager.setIcon(
+                    SLOT_MUTE,
+                    android.R.drawable.stat_notify_call_mute,
+                    0,  /* iconLevel */
+                    mContext.getString(R.string.accessibility_call_muted));
+        } else {
+            mStatusBarManager.removeIcon(SLOT_MUTE);
+        }
+        mIsShowingMute = isMuted;
+    }
+
+    void notifySpeakerphone(boolean isSpeakerphone) {
+        // Never display anything if there are no calls.
+        if (!mCallsManager.hasAnyCalls()) {
+            isSpeakerphone = false;
+        }
+
+        if (mIsShowingSpeakerphone == isSpeakerphone) {
+            return;
+        }
+
+        Log.d(this, "Speakerphone status bar icon being set to %b", isSpeakerphone);
+
+        if (isSpeakerphone) {
+            mStatusBarManager.setIcon(
+                    SLOT_SPEAKERPHONE,
+                    android.R.drawable.stat_sys_speakerphone,
+                    0,  /* iconLevel */
+                    mContext.getString(R.string.accessibility_speakerphone_enabled));
+        } else {
+            mStatusBarManager.removeIcon(SLOT_SPEAKERPHONE);
+        }
+        mIsShowingSpeakerphone = isSpeakerphone;
+    }
+}
diff --git a/src/com/android/server/telecom/TelecomApp.java b/src/com/android/server/telecom/TelecomApp.java
new file mode 100644
index 0000000..c2e79eb
--- /dev/null
+++ b/src/com/android/server/telecom/TelecomApp.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 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;
+
+import android.app.Application;
+import android.os.UserHandle;
+
+/**
+ * Top-level Application class for Telecom.
+ */
+public final class TelecomApp extends Application {
+
+    /** Singleton instance of TelecomApp. */
+    private static TelecomApp sInstance;
+
+    /**
+     * Missed call notifier. Exists here so that the instance can be shared with
+     * {@link TelecomBroadcastReceiver}.
+     */
+    private MissedCallNotifier mMissedCallNotifier;
+
+    /**
+     * Maintains the list of registered {@link android.telecom.PhoneAccountHandle}s.
+     */
+    private PhoneAccountRegistrar mPhoneAccountRegistrar;
+
+    /** {@inheritDoc} */
+    @Override public void onCreate() {
+        super.onCreate();
+        sInstance = this;
+
+        mMissedCallNotifier = new MissedCallNotifier(this);
+        mPhoneAccountRegistrar = new PhoneAccountRegistrar(this);
+
+        if (UserHandle.myUserId() == UserHandle.USER_OWNER) {
+            TelecomServiceImpl.init(mMissedCallNotifier, mPhoneAccountRegistrar);
+        }
+    }
+
+    public static TelecomApp getInstance() {
+        if (null == sInstance) {
+            throw new IllegalStateException("No TelecomApp running.");
+        }
+        return sInstance;
+    }
+
+    MissedCallNotifier getMissedCallNotifier() {
+        return mMissedCallNotifier;
+    }
+
+    PhoneAccountRegistrar getPhoneAccountRegistrar() {
+        return mPhoneAccountRegistrar;
+    }
+}
diff --git a/src/com/android/server/telecom/TelecomBroadcastReceiver.java b/src/com/android/server/telecom/TelecomBroadcastReceiver.java
new file mode 100644
index 0000000..36c23fc
--- /dev/null
+++ b/src/com/android/server/telecom/TelecomBroadcastReceiver.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 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;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+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 TelecomBroadcastReceiver extends BroadcastReceiver {
+    /** The action used to send SMS response for the missed call notification. */
+    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 =
+            "com.android.server.telecom.ACTION_CALL_BACK_FROM_NOTIFICATION";
+
+    /** The action used to clear missed calls. */
+    static final String ACTION_CLEAR_MISSED_CALLS =
+            "com.android.server.telecom.ACTION_CLEAR_MISSED_CALLS";
+
+
+    /** {@inheritDoc} */
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        String action = intent.getAction();
+
+        Log.v(this, "Action received: %s.", action);
+
+        MissedCallNotifier missedCallNotifier = TelecomApp.getInstance().getMissedCallNotifier();
+
+        // Send an SMS from the missed call notification.
+        if (ACTION_SEND_SMS_FROM_NOTIFICATION.equals(action)) {
+            // Close the notification shade and the notification itself.
+            closeSystemDialogs(context);
+            missedCallNotifier.clearMissedCalls();
+
+            Intent callIntent = new Intent(Intent.ACTION_SENDTO, intent.getData());
+            callIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            context.startActivity(callIntent);
+
+        // Call back recent caller from the missed call notification.
+        } else if (ACTION_CALL_BACK_FROM_NOTIFICATION.equals(action)) {
+            // Close the notification shade and the notification itself.
+            closeSystemDialogs(context);
+            missedCallNotifier.clearMissedCalls();
+
+            Intent callIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, intent.getData());
+            callIntent.setFlags(
+                    Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+            context.startActivity(callIntent);
+
+        // Clear the missed call notification and call log entries.
+        } else if (ACTION_CLEAR_MISSED_CALLS.equals(action)) {
+            missedCallNotifier.clearMissedCalls();
+        }
+    }
+
+    /**
+     * Closes open system dialogs and the notification shade.
+     */
+    private void closeSystemDialogs(Context context) {
+        Intent intent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
+        context.sendBroadcastAsUser(intent, UserHandle.ALL);
+    }
+}
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
new file mode 100644
index 0000000..5bce513
--- /dev/null
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -0,0 +1,624 @@
+/*
+ * Copyright (C) 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;
+
+import android.app.AppOpsManager;
+import android.content.ComponentName;
+import android.content.Context;
+
+import android.content.Intent;
+import android.content.res.Resources;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.telecom.CallState;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telephony.TelephonyManager;
+
+import com.android.internal.telecom.ITelecomService;
+
+import java.util.List;
+
+/**
+ * Implementation of the ITelecom interface.
+ */
+public class TelecomServiceImpl extends ITelecomService.Stub {
+    private static final String REGISTER_PROVIDER_OR_SUBSCRIPTION =
+            "com.android.server.telecom.permission.REGISTER_PROVIDER_OR_SUBSCRIPTION";
+
+    /** ${inheritDoc} */
+    @Override
+    public IBinder asBinder() {
+        return super.asBinder();
+    }
+
+ /**
+     * A request object for use with {@link MainThreadHandler}. Requesters should wait() on the
+     * request after sending. The main thread will notify the request when it is complete.
+     */
+    private static final class MainThreadRequest {
+        /** The result of the request that is run on the main thread */
+        public Object result;
+    }
+
+    /**
+     * A handler that processes messages on the main thread in the phone process. Since many
+     * of the Phone calls are not thread safe this is needed to shuttle the requests from the
+     * inbound binder threads to the main thread in the phone process.
+     */
+    private final class MainThreadHandler extends Handler {
+        @Override
+        public void handleMessage(Message msg) {
+            if (msg.obj instanceof MainThreadRequest) {
+                MainThreadRequest request = (MainThreadRequest) msg.obj;
+                Object result = null;
+                switch (msg.what) {
+                    case MSG_SILENCE_RINGER:
+                        mCallsManager.getRinger().silence();
+                        break;
+                    case MSG_SHOW_CALL_SCREEN:
+                        mCallsManager.getInCallController().bringToForeground(msg.arg1 == 1);
+                        break;
+                    case MSG_END_CALL:
+                        result = endCallInternal();
+                        break;
+                    case MSG_ACCEPT_RINGING_CALL:
+                        acceptRingingCallInternal();
+                        break;
+                    case MSG_CANCEL_MISSED_CALLS_NOTIFICATION:
+                        mMissedCallNotifier.clearMissedCalls();
+                        break;
+                    case MSG_IS_TTY_SUPPORTED:
+                        result = mCallsManager.isTtySupported();
+                        break;
+                    case MSG_GET_CURRENT_TTY_MODE:
+                        result = mCallsManager.getCurrentTtyMode();
+                        break;
+                }
+
+                if (result != null) {
+                    request.result = result;
+                    synchronized(request) {
+                        request.notifyAll();
+                    }
+                }
+            }
+        }
+    }
+
+    /** Private constructor; @see init() */
+    private static final String TAG = TelecomServiceImpl.class.getSimpleName();
+
+    private static final String SERVICE_NAME = "telecom";
+
+    private static final int MSG_SILENCE_RINGER = 1;
+    private static final int MSG_SHOW_CALL_SCREEN = 2;
+    private static final int MSG_END_CALL = 3;
+    private static final int MSG_ACCEPT_RINGING_CALL = 4;
+    private static final int MSG_CANCEL_MISSED_CALLS_NOTIFICATION = 5;
+    private static final int MSG_IS_TTY_SUPPORTED = 6;
+    private static final int MSG_GET_CURRENT_TTY_MODE = 7;
+
+    /** The singleton instance. */
+    private static TelecomServiceImpl sInstance;
+
+    private final MainThreadHandler mMainThreadHandler = new MainThreadHandler();
+    private final CallsManager mCallsManager = CallsManager.getInstance();
+    private final MissedCallNotifier mMissedCallNotifier;
+    private final PhoneAccountRegistrar mPhoneAccountRegistrar;
+    private final AppOpsManager mAppOpsManager;
+
+    private TelecomServiceImpl(
+            MissedCallNotifier missedCallNotifier, PhoneAccountRegistrar phoneAccountRegistrar) {
+        mMissedCallNotifier = missedCallNotifier;
+        mPhoneAccountRegistrar = phoneAccountRegistrar;
+        mAppOpsManager =
+                (AppOpsManager) TelecomApp.getInstance().getSystemService(Context.APP_OPS_SERVICE);
+
+        publish();
+    }
+
+    /**
+     * Initialize the singleton TelecommServiceImpl instance.
+     * This is only done once, at startup, from TelecommApp.onCreate().
+     */
+    static TelecomServiceImpl init(
+            MissedCallNotifier missedCallNotifier, PhoneAccountRegistrar phoneAccountRegistrar) {
+        synchronized (TelecomServiceImpl.class) {
+            if (sInstance == null) {
+                sInstance = new TelecomServiceImpl(missedCallNotifier, phoneAccountRegistrar);
+            } else {
+                Log.wtf(TAG, "init() called multiple times!  sInstance %s", sInstance);
+            }
+            return sInstance;
+        }
+    }
+
+    //
+    // Implementation of the ITelecomService interface.
+    //
+
+    @Override
+    public PhoneAccountHandle getDefaultOutgoingPhoneAccount(String uriScheme) {
+        try {
+            return mPhoneAccountRegistrar.getDefaultOutgoingPhoneAccount(uriScheme);
+        } catch (Exception e) {
+            Log.e(this, e, "getDefaultOutgoingPhoneAccount");
+            throw e;
+        }
+    }
+
+    @Override
+    public PhoneAccountHandle getUserSelectedOutgoingPhoneAccount() {
+        try {
+            return mPhoneAccountRegistrar.getUserSelectedOutgoingPhoneAccount();
+        } catch (Exception e) {
+            Log.e(this, e, "getUserSelectedOutgoingPhoneAccount");
+            throw e;
+        }
+    }
+
+    @Override
+    public void setUserSelectedOutgoingPhoneAccount(PhoneAccountHandle accountHandle) {
+        enforceModifyPermission();
+
+        try {
+            mPhoneAccountRegistrar.setUserSelectedOutgoingPhoneAccount(accountHandle);
+        } catch (Exception e) {
+            Log.e(this, e, "setUserSelectedOutgoingPhoneAccount");
+            throw e;
+        }
+    }
+
+    @Override
+    public List<PhoneAccountHandle> getEnabledPhoneAccounts() {
+        try {
+            return mPhoneAccountRegistrar.getEnabledPhoneAccounts();
+        } catch (Exception e) {
+            Log.e(this, e, "getEnabledPhoneAccounts");
+            throw e;
+        }
+    }
+
+    @Override
+    public List<PhoneAccountHandle> getPhoneAccountsSupportingScheme(String uriScheme) {
+        try {
+            return mPhoneAccountRegistrar.getEnabledPhoneAccounts(uriScheme);
+        } catch (Exception e) {
+            Log.e(this, e, "getPhoneAccountsSupportingScheme");
+            throw e;
+        }
+    }
+
+    @Override
+    public PhoneAccount getPhoneAccount(PhoneAccountHandle accountHandle) {
+        try {
+            return mPhoneAccountRegistrar.getPhoneAccount(accountHandle);
+        } catch (Exception e) {
+            Log.e(this, e, "getPhoneAccount %s", accountHandle);
+            throw e;
+        }
+    }
+
+    @Override
+    public int getAllPhoneAccountsCount() {
+        try {
+            return mPhoneAccountRegistrar.getAllPhoneAccountsCount();
+        } catch (Exception e) {
+            Log.e(this, e, "getAllPhoneAccountsCount");
+            throw e;
+        }
+    }
+
+    @Override
+    public List<PhoneAccount> getAllPhoneAccounts() {
+        try {
+            return mPhoneAccountRegistrar.getAllPhoneAccounts();
+        } catch (Exception e) {
+            Log.e(this, e, "getAllPhoneAccounts");
+            throw e;
+        }
+    }
+
+    @Override
+    public List<PhoneAccountHandle> getAllPhoneAccountHandles() {
+        try {
+            return mPhoneAccountRegistrar.getAllPhoneAccountHandles();
+        } catch (Exception e) {
+            Log.e(this, e, "getAllPhoneAccounts");
+            throw e;
+        }
+    }
+
+    @Override
+    public PhoneAccountHandle getSimCallManager() {
+        try {
+            return mPhoneAccountRegistrar.getSimCallManager();
+        } catch (Exception e) {
+            Log.e(this, e, "getSimCallManager");
+            throw e;
+        }
+    }
+
+    @Override
+    public void setSimCallManager(PhoneAccountHandle accountHandle) {
+        enforceModifyPermission();
+
+        try {
+            mPhoneAccountRegistrar.setSimCallManager(accountHandle);
+        } catch (Exception e) {
+            Log.e(this, e, "setSimCallManager");
+            throw e;
+        }
+    }
+
+    @Override
+    public List<PhoneAccountHandle> getSimCallManagers() {
+        try {
+            return mPhoneAccountRegistrar.getConnectionManagerPhoneAccounts();
+        } catch (Exception e) {
+            Log.e(this, e, "getSimCallManagers");
+            throw e;
+        }
+    }
+
+    @Override
+    public void registerPhoneAccount(PhoneAccount account) {
+        try {
+            enforceModifyPermissionOrCallingPackage(
+                    account.getAccountHandle().getComponentName().getPackageName());
+            if (account.hasCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER) ||
+                account.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) {
+                enforceRegisterProviderOrSubscriptionPermission();
+            }
+
+            // If the account is marked as enabled or has CAPABILITY_ALWAYS_ENABLED set, check to
+            // ensure the caller has modify permission.  If they do not, set the account to be
+            // disabled and remove CAPABILITY_ALWAYS_ENABLED.
+            if (account.isEnabled() ||
+                    account.hasCapabilities(PhoneAccount.CAPABILITY_ALWAYS_ENABLED)) {
+                try {
+                    enforceModifyPermission();
+                } catch (SecurityException e) {
+                    // Caller does not have modify permission, so change account to disabled by
+                    // default and remove the CAPABILITY_ALWAYS_ENABLED capability.
+                    int capabilities = account.getCapabilities() &
+                            ~PhoneAccount.CAPABILITY_ALWAYS_ENABLED;
+                    account = account.toBuilder()
+                            .setEnabled(false)
+                            .setCapabilities(capabilities)
+                            .build();
+                }
+            }
+
+            mPhoneAccountRegistrar.registerPhoneAccount(account);
+        } catch (Exception e) {
+            Log.e(this, e, "registerPhoneAccount %s", account);
+            throw e;
+        }
+    }
+
+    @Override
+    public void setPhoneAccountEnabled(PhoneAccountHandle account, boolean isEnabled) {
+        try {
+            enforceModifyPermission();
+            mPhoneAccountRegistrar.setPhoneAccountEnabled(account, isEnabled);
+        } catch (Exception e) {
+            Log.e(this, e, "setPhoneAccountEnabled %s %d", account, isEnabled ? 1 : 0);
+            throw e;
+        }
+    }
+
+    @Override
+    public void unregisterPhoneAccount(PhoneAccountHandle accountHandle) {
+        try {
+            enforceModifyPermissionOrCallingPackage(
+                    accountHandle.getComponentName().getPackageName());
+            mPhoneAccountRegistrar.unregisterPhoneAccount(accountHandle);
+        } catch (Exception e) {
+            Log.e(this, e, "unregisterPhoneAccount %s", accountHandle);
+            throw e;
+        }
+    }
+
+    @Override
+    public void clearAccounts(String packageName) {
+        try {
+            enforceModifyPermissionOrCallingPackage(packageName);
+            mPhoneAccountRegistrar.clearAccounts(packageName);
+        } catch (Exception e) {
+            Log.e(this, e, "clearAccounts %s", packageName);
+            throw e;
+        }
+    }
+
+    /**
+     * @see android.telecom.TelecomManager#silenceRinger
+     */
+    @Override
+    public void silenceRinger() {
+        Log.d(this, "silenceRinger");
+        enforceModifyPermission();
+        sendRequestAsync(MSG_SILENCE_RINGER, 0);
+    }
+
+    /**
+     * @see android.telecom.TelecomManager#getDefaultPhoneApp
+     */
+    @Override
+    public ComponentName getDefaultPhoneApp() {
+        Resources resources = TelecomApp.getInstance().getResources();
+        return new ComponentName(
+                resources.getString(R.string.ui_default_package),
+                resources.getString(R.string.dialer_default_class));
+    }
+
+    /**
+     * @see android.telecom.TelecomManager#isInCall
+     */
+    @Override
+    public boolean isInCall() {
+        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().
+        return mCallsManager.hasAnyCalls();
+    }
+
+    /**
+     * @see android.telecom.TelecomManager#isRinging
+     */
+    @Override
+    public boolean isRinging() {
+        enforceReadPermission();
+        return mCallsManager.hasRingingCall();
+    }
+
+    /**
+     * @see android.telecom.TelecomManager#endCall
+     */
+    @Override
+    public boolean endCall() {
+        enforceModifyPermission();
+        return (boolean) sendRequest(MSG_END_CALL);
+    }
+
+    /**
+     * @see android.telecom.TelecomManager#acceptRingingCall
+     */
+    @Override
+    public void acceptRingingCall() {
+        enforceModifyPermission();
+        sendRequestAsync(MSG_ACCEPT_RINGING_CALL, 0);
+    }
+
+    /**
+     * @see android.telecom.TelecomManager#showInCallScreen
+     */
+    @Override
+    public void showInCallScreen(boolean showDialpad) {
+        enforceReadPermissionOrDefaultDialer();
+        sendRequestAsync(MSG_SHOW_CALL_SCREEN, showDialpad ? 1 : 0);
+    }
+
+    /**
+     * @see android.telecom.TelecomManager#cancelMissedCallsNotification
+     */
+    @Override
+    public void cancelMissedCallsNotification() {
+        enforceModifyPermissionOrDefaultDialer();
+        sendRequestAsync(MSG_CANCEL_MISSED_CALLS_NOTIFICATION, 0);
+    }
+
+    /**
+     * @see android.telecom.TelecomManager#handleMmi
+     */
+    @Override
+    public boolean handlePinMmi(String dialString) {
+        enforceModifyPermissionOrDefaultDialer();
+
+        // Switch identity so that TelephonyManager checks Telecom's permissions instead.
+        long token = Binder.clearCallingIdentity();
+        boolean retval = getTelephonyManager().handlePinMmi(dialString);
+        Binder.restoreCallingIdentity(token);
+
+        return retval;
+    }
+
+    /**
+     * @see android.telecom.TelecomManager#isTtySupported
+     */
+    @Override
+    public boolean isTtySupported() {
+        enforceReadPermission();
+        return (boolean) sendRequest(MSG_IS_TTY_SUPPORTED);
+    }
+
+    /**
+     * @see android.telecom.TelecomManager#getCurrentTtyMode
+     */
+    @Override
+    public int getCurrentTtyMode() {
+        enforceReadPermission();
+        return (int) sendRequest(MSG_GET_CURRENT_TTY_MODE);
+    }
+
+    /**
+     * @see android.telecom.TelecomManager#addNewIncomingCall
+     */
+    @Override
+    public void addNewIncomingCall(PhoneAccountHandle phoneAccountHandle, Bundle extras) {
+        if (phoneAccountHandle != null && phoneAccountHandle.getComponentName() != null) {
+            mAppOpsManager.checkPackage(
+                    Binder.getCallingUid(), phoneAccountHandle.getComponentName().getPackageName());
+
+            Intent intent = new Intent(TelecomManager.ACTION_INCOMING_CALL);
+            intent.setPackage(TelecomApp.getInstance().getPackageName());
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle);
+            if (extras != null) {
+                intent.putExtra(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS, extras);
+            }
+
+            long token = Binder.clearCallingIdentity();
+            TelecomApp.getInstance().startActivityAsUser(intent, UserHandle.CURRENT);
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    //
+    // Supporting methods for the ITelecomService interface implementation.
+    //
+
+    private void acceptRingingCallInternal() {
+        Call call = mCallsManager.getFirstCallWithState(CallState.RINGING);
+        if (call != null) {
+            call.answer(call.getVideoState());
+        }
+    }
+
+    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 = mCallsManager.getForegroundCall();
+        if (call == null) {
+            call = mCallsManager.getFirstCallWithState(
+                    CallState.ACTIVE,
+                    CallState.DIALING,
+                    CallState.RINGING,
+                    CallState.ON_HOLD);
+        }
+
+        if (call != null) {
+            if (call.getState() == CallState.RINGING) {
+                call.reject(false /* rejectWithMessage */, null);
+            } else {
+                call.disconnect();
+            }
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Make sure the caller has the MODIFY_PHONE_STATE permission.
+     *
+     * @throws SecurityException if the caller does not have the required permission
+     */
+    private void enforceModifyPermission() {
+        TelecomApp.getInstance().enforceCallingOrSelfPermission(
+                android.Manifest.permission.MODIFY_PHONE_STATE, null);
+    }
+
+    private void enforceModifyPermissionOrDefaultDialer() {
+        if (!isDefaultDialerCalling()) {
+            enforceModifyPermission();
+        }
+    }
+
+    private void enforceRegisterProviderOrSubscriptionPermission() {
+        TelecomApp.getInstance().enforceCallingOrSelfPermission(
+                REGISTER_PROVIDER_OR_SUBSCRIPTION, null);
+    }
+
+    private void enforceModifyPermissionOrCallingPackage(String packageName) {
+        // TODO: Use a new telecom permission for this instead of reusing modify.
+        try {
+            enforceModifyPermission();
+        } catch (SecurityException e) {
+            enforceCallingPackage(packageName);
+        }
+    }
+
+    private void enforceReadPermission() {
+        TelecomApp.getInstance().enforceCallingOrSelfPermission(
+                android.Manifest.permission.READ_PHONE_STATE, null);
+    }
+
+    private void enforceReadPermissionOrDefaultDialer() {
+        if (!isDefaultDialerCalling()) {
+            enforceReadPermission();
+        }
+    }
+
+    private void enforceCallingPackage(String packageName) {
+        mAppOpsManager.checkPackage(Binder.getCallingUid(), packageName);
+    }
+
+    private boolean isDefaultDialerCalling() {
+        ComponentName defaultDialerComponent = getDefaultPhoneApp();
+        if (defaultDialerComponent != null) {
+            try {
+                mAppOpsManager.checkPackage(
+                        Binder.getCallingUid(), defaultDialerComponent.getPackageName());
+                return true;
+            } catch (SecurityException e) {
+                Log.e(TAG, e, "Could not get default dialer.");
+            }
+        }
+        return false;
+    }
+
+    private TelephonyManager getTelephonyManager() {
+        return (TelephonyManager)
+                TelecomApp.getInstance().getSystemService(Context.TELEPHONY_SERVICE);
+    }
+
+    private void publish() {
+        Log.d(this, "publish: %s", this);
+        ServiceManager.addService(SERVICE_NAME, this);
+    }
+
+    private MainThreadRequest sendRequestAsync(int command, int arg1) {
+        MainThreadRequest request = new MainThreadRequest();
+        mMainThreadHandler.obtainMessage(command, arg1, 0, request).sendToTarget();
+        return request;
+    }
+
+    /**
+     * Posts the specified command to be executed on the main thread, waits for the request to
+     * complete, and returns the result.
+     */
+    private Object sendRequest(int command) {
+        if (Looper.myLooper() == mMainThreadHandler.getLooper()) {
+            MainThreadRequest request = new MainThreadRequest();
+            mMainThreadHandler.handleMessage(mMainThreadHandler.obtainMessage(command, request));
+            return request.result;
+        } else {
+            MainThreadRequest request = sendRequestAsync(command, 0);
+
+            // Wait for the request to complete
+            synchronized (request) {
+                while (request.result == null) {
+                    try {
+                        request.wait();
+                    } catch (InterruptedException e) {
+                        // Do nothing, go back and wait until the request is complete
+                    }
+                }
+            }
+            return request.result;
+        }
+    }
+}
diff --git a/src/com/android/server/telecom/TelephonyUtil.java b/src/com/android/server/telecom/TelephonyUtil.java
new file mode 100644
index 0000000..29b6f89
--- /dev/null
+++ b/src/com/android/server/telecom/TelephonyUtil.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.net.Uri;
+import android.telephony.PhoneNumberUtils;
+
+/**
+ * Utilities to deal with the system telephony services. The system telephony services are treated
+ * differently from 3rd party services in some situations (emergency calls, audio focus, etc...).
+ */
+public final class TelephonyUtil {
+    private static final String TAG = TelephonyUtil.class.getSimpleName();
+
+    private static final String TELEPHONY_PACKAGE_NAME = "com.android.phone";
+
+    private static final String PSTN_CALL_SERVICE_CLASS_NAME =
+            "com.android.services.telephony.TelephonyConnectionService";
+
+    private TelephonyUtil() {}
+
+    static boolean isPstnComponentName(ComponentName componentName) {
+        final ComponentName pstnComponentName = new ComponentName(
+                TELEPHONY_PACKAGE_NAME, PSTN_CALL_SERVICE_CLASS_NAME);
+        return pstnComponentName.equals(componentName);
+    }
+
+    static boolean shouldProcessAsEmergency(Context context, Uri handle) {
+        return handle != null && PhoneNumberUtils.isPotentialLocalEmergencyNumber(
+                context, handle.getSchemeSpecificPart());
+    }
+}
diff --git a/src/com/android/server/telecom/ThreadUtil.java b/src/com/android/server/telecom/ThreadUtil.java
new file mode 100644
index 0000000..650e73f
--- /dev/null
+++ b/src/com/android/server/telecom/ThreadUtil.java
@@ -0,0 +1,53 @@
+/*
+ * 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;
+
+import android.os.Looper;
+
+/**
+ * Helper methods to deal with threading related tasks.
+ */
+public final class ThreadUtil {
+    private static final String TAG = ThreadUtil.class.getSimpleName();
+
+    private ThreadUtil() {}
+
+    /** @return whether this method is being called from the main (UI) thread. */
+    public static boolean isOnMainThread() {
+        return Looper.getMainLooper() == Looper.myLooper();
+    }
+
+    /**
+     * Checks that this is being executed on the main thread. If not, a message is logged at
+     * WTF-level priority.
+     */
+    public static void checkOnMainThread() {
+        if (!isOnMainThread()) {
+            Log.wtf(TAG, new IllegalStateException(), "Must be on the main thread!");
+        }
+    }
+
+    /**
+     * Checks that this is not being executed on the main thread. If so, a message is logged at
+     * WTF-level priority.
+     */
+    public static void checkNotOnMainThread() {
+        if (isOnMainThread()) {
+            Log.wtf(TAG, new IllegalStateException(), "Must not be on the main thread!");
+        }
+    }
+}
diff --git a/src/com/android/server/telecom/Timeouts.java b/src/com/android/server/telecom/Timeouts.java
new file mode 100644
index 0000000..4434db4
--- /dev/null
+++ b/src/com/android/server/telecom/Timeouts.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 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;
+
+import android.provider.Settings;
+
+/**
+ * A helper class which serves only to make it easier to lookup timeout values. This class should
+ * never be instantiated, and only accessed through the {@link #get(String, long)} method.
+ *
+ * These methods are safe to call from any thread, including the UI thread.
+ */
+public final class Timeouts {
+    /** A prefix to use for all keys so to not clobber the global namespace. */
+    private static final String PREFIX = "telecom.";
+
+    private Timeouts() {}
+
+    /**
+     * Returns the timeout value from Settings or the default value if it hasn't been changed. This
+     * method is safe to call from any thread, including the UI thread.
+     *
+     * @param key Settings key to retrieve.
+     * @param defaultValue Default value, in milliseconds.
+     * @return The timeout value from Settings or the default value if it hasn't been changed.
+     */
+    private static long get(String key, long defaultValue) {
+        return Settings.Secure.getLong(
+                TelecomApp.getInstance().getContentResolver(), PREFIX + key, defaultValue);
+    }
+
+    /**
+     * Returns the longest period, in milliseconds, to wait for the query for direct-to-voicemail
+     * to complete. If the query goes beyond this timeout, the incoming call screen is shown to the
+     * user.
+     */
+    public static long getDirectToVoicemailMillis() {
+        return get("direct_to_voicemail_ms", 500L);
+    }
+}
diff --git a/src/com/android/server/telecom/TtyManager.java b/src/com/android/server/telecom/TtyManager.java
new file mode 100644
index 0000000..945da5e
--- /dev/null
+++ b/src/com/android/server/telecom/TtyManager.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 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;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.telecom.TelecomManager;
+
+final class TtyManager implements WiredHeadsetManager.Listener {
+    private final TtyBroadcastReceiver mReceiver = new TtyBroadcastReceiver();
+    private final Context mContext;
+    private final WiredHeadsetManager mWiredHeadsetManager;
+    private int mPreferredTtyMode = TelecomManager.TTY_MODE_OFF;
+    private int mCurrentTtyMode = TelecomManager.TTY_MODE_OFF;
+
+    TtyManager(Context context, WiredHeadsetManager wiredHeadsetManager) {
+        mContext = context;
+        mWiredHeadsetManager = wiredHeadsetManager;
+        mWiredHeadsetManager.addListener(this);
+
+        mPreferredTtyMode = Settings.Secure.getInt(
+                mContext.getContentResolver(),
+                Settings.Secure.PREFERRED_TTY_MODE,
+                TelecomManager.TTY_MODE_OFF);
+
+        IntentFilter intentFilter = new IntentFilter(
+                TelecomManager.ACTION_TTY_PREFERRED_MODE_CHANGED);
+        mContext.registerReceiver(mReceiver, intentFilter);
+
+        updateCurrentTtyMode();
+    }
+
+    boolean isTtySupported() {
+        boolean isEnabled = mContext.getResources().getBoolean(R.bool.tty_enabled);
+        Log.v(this, "isTtySupported: %b", isEnabled);
+        return isEnabled;
+    }
+
+    int getCurrentTtyMode() {
+        return mCurrentTtyMode;
+    }
+
+    @Override
+    public void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn) {
+        Log.v(this, "onWiredHeadsetPluggedInChanged");
+        updateCurrentTtyMode();
+    }
+
+    private void updateCurrentTtyMode() {
+        int newTtyMode = TelecomManager.TTY_MODE_OFF;
+        if (isTtySupported() && mWiredHeadsetManager.isPluggedIn()) {
+            newTtyMode = mPreferredTtyMode;
+        }
+        Log.v(this, "updateCurrentTtyMode, %d -> %d", mCurrentTtyMode, newTtyMode);
+
+        if (mCurrentTtyMode != newTtyMode) {
+            mCurrentTtyMode = newTtyMode;
+            Intent ttyModeChanged = new Intent(TelecomManager.ACTION_CURRENT_TTY_MODE_CHANGED);
+            ttyModeChanged.putExtra(TelecomManager.EXTRA_CURRENT_TTY_MODE, mCurrentTtyMode);
+            mContext.sendBroadcastAsUser(ttyModeChanged, UserHandle.ALL);
+
+            updateAudioTtyMode();
+        }
+    }
+
+    private void updateAudioTtyMode() {
+        String audioTtyMode;
+        switch (mCurrentTtyMode) {
+            case TelecomManager.TTY_MODE_FULL:
+                audioTtyMode = "tty_full";
+                break;
+            case TelecomManager.TTY_MODE_VCO:
+                audioTtyMode = "tty_vco";
+                break;
+            case TelecomManager.TTY_MODE_HCO:
+                audioTtyMode = "tty_hco";
+                break;
+            case TelecomManager.TTY_MODE_OFF:
+            default:
+                audioTtyMode = "tty_off";
+                break;
+        }
+        Log.v(this, "updateAudioTtyMode, %s", audioTtyMode);
+
+        AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+        audioManager.setParameters("tty_mode=" + audioTtyMode);
+    }
+
+    private final class TtyBroadcastReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+            Log.v(TtyManager.this, "onReceive, action: %s", action);
+            if (action.equals(TelecomManager.ACTION_TTY_PREFERRED_MODE_CHANGED)) {
+                int newPreferredTtyMode = intent.getIntExtra(
+                        TelecomManager.EXTRA_TTY_PREFERRED_MODE, TelecomManager.TTY_MODE_OFF);
+                if (mPreferredTtyMode != newPreferredTtyMode) {
+                    mPreferredTtyMode = newPreferredTtyMode;
+                    updateCurrentTtyMode();
+                }
+            }
+        }
+    }
+}
diff --git a/src/com/android/server/telecom/WiredHeadsetManager.java b/src/com/android/server/telecom/WiredHeadsetManager.java
new file mode 100644
index 0000000..8ce7b61
--- /dev/null
+++ b/src/com/android/server/telecom/WiredHeadsetManager.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 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;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/** Listens for and caches headset state. */
+class WiredHeadsetManager {
+    interface Listener {
+        void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn);
+    }
+
+    /** Receiver for wired headset plugged and unplugged events. */
+    private class WiredHeadsetBroadcastReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (intent.getAction().equals(Intent.ACTION_HEADSET_PLUG)) {
+                boolean isPluggedIn = intent.getIntExtra("state", 0) == 1;
+                Log.v(WiredHeadsetManager.this, "ACTION_HEADSET_PLUG event, plugged in: %b",
+                        isPluggedIn);
+                onHeadsetPluggedInChanged(isPluggedIn);
+            }
+        }
+    }
+
+    private final WiredHeadsetBroadcastReceiver mReceiver;
+    private boolean mIsPluggedIn;
+    /**
+     * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
+     * load factor before resizing, 1 means we only expect a single thread to
+     * access the map so make only a single shard
+     */
+    private final Set<Listener> mListeners = Collections.newSetFromMap(
+            new ConcurrentHashMap<Listener, Boolean>(8, 0.9f, 1));
+
+    WiredHeadsetManager(Context context) {
+        mReceiver = new WiredHeadsetBroadcastReceiver();
+
+        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        mIsPluggedIn = audioManager.isWiredHeadsetOn();
+
+        // Register for misc other intent broadcasts.
+        IntentFilter intentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
+        context.registerReceiver(mReceiver, intentFilter);
+    }
+
+    void addListener(Listener listener) {
+        mListeners.add(listener);
+    }
+
+    void removeListener(Listener listener) {
+        if (listener != null) {
+            mListeners.remove(listener);
+        }
+    }
+
+    boolean isPluggedIn() {
+        return mIsPluggedIn;
+    }
+
+    private void onHeadsetPluggedInChanged(boolean isPluggedIn) {
+        if (mIsPluggedIn != isPluggedIn) {
+            Log.v(this, "onHeadsetPluggedInChanged, mIsPluggedIn: %b -> %b", mIsPluggedIn,
+                    isPluggedIn);
+            boolean oldIsPluggedIn = mIsPluggedIn;
+            mIsPluggedIn = isPluggedIn;
+            for (Listener listener : mListeners) {
+                listener.onWiredHeadsetPluggedInChanged(oldIsPluggedIn, mIsPluggedIn);
+            }
+        }
+    }
+}