Add CallsManagerListener

Also contains some minor bug fixes:
- add an incoming field to Call
- correcty log failed outgoing calls (previously these were mostly dropped)
- log missed incoming calls

Change-Id: I72dc39efd519302c1f765f4f9c9d04c5095e45a6
diff --git a/Android.mk b/Android.mk
index 76a190e..48eece0 100644
--- a/Android.mk
+++ b/Android.mk
@@ -15,6 +15,9 @@
 
 LOCAL_PROGUARD_ENABLED := optimization
 
+# Workaround for "local variable type mismatch" error.
+LOCAL_DX_FLAGS += --no-locals
+
 include $(BUILD_PACKAGE)
 
 # Build the test package.
diff --git a/src/com/android/telecomm/Call.java b/src/com/android/telecomm/Call.java
index 849c876..82bde9e 100644
--- a/src/com/android/telecomm/Call.java
+++ b/src/com/android/telecomm/Call.java
@@ -41,6 +41,9 @@
     /** Additional contact information beyond handle above, optional. */
     private final ContactInfo mContactInfo;
 
+    /** True if this is an incoming call. */
+    private final boolean mIsIncoming;
+
     /**
      * The time this call was created, typically also the time this call was added to the set
      * of pending outgoing calls (mPendingOutgoingCalls) that's maintained by the switchboard.
@@ -77,9 +80,11 @@
 
     /**
      * Creates an empty call object with a unique call ID.
+     *
+     * @param isIncoming True if this is an incoming call.
      */
-    Call() {
-        this(null, null);
+    Call(boolean isIncoming) {
+        this(null, null, isIncoming);
     }
 
     /**
@@ -87,19 +92,22 @@
      *
      * @param handle The handle to dial.
      * @param contactInfo Information about the entity being called.
+     * @param isIncoming True if this is an incoming call.
      */
-    Call(Uri handle, ContactInfo contactInfo) {
+    Call(Uri handle, ContactInfo contactInfo, boolean isIncoming) {
         mId = UUID.randomUUID().toString();  // UUIDs should provide sufficient uniqueness.
         mState = CallState.NEW;
         mHandle = handle;
         mContactInfo = contactInfo;
+        mIsIncoming = isIncoming;
         mCreationTime = new Date();
     }
 
     /** {@inheritDoc} */
     @Override public String toString() {
         return String.format(Locale.US, "[%s, %s, %s, %s]", mId, mState,
-                mCallService.getComponentName(), Log.pii(mHandle));
+                mCallService == null ? "<null>" : mCallService.getComponentName(),
+                Log.pii(mHandle));
     }
 
     String getId() {
@@ -116,9 +124,12 @@
      * 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(CallState state) {
-        mState = state;
-        clearCallInfo();
+    void setState(CallState newState) {
+        if (mState != newState) {
+            Log.v(this, "setState %s -> %s", mState, newState);
+            mState = newState;
+            clearCallInfo();
+        }
     }
 
     Uri getHandle() {
@@ -133,6 +144,10 @@
         return mContactInfo;
     }
 
+    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 mCreationTime.
@@ -222,7 +237,6 @@
             }
             clearCallService();
             clearCallServiceSelector();
-            mState = CallState.ABORTED;
         }
     }
 
diff --git a/src/com/android/telecomm/CallAudioManager.java b/src/com/android/telecomm/CallAudioManager.java
new file mode 100644
index 0000000..07d5dbe
--- /dev/null
+++ b/src/com/android/telecomm/CallAudioManager.java
@@ -0,0 +1,59 @@
+/*
+ * 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.telecomm;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.telecomm.CallState;
+
+/**
+ * This class manages audio modes, streams and other properties.
+ */
+final class CallAudioManager extends CallsManagerListenerBase {
+    @Override
+    public void onCallStateChanged(Call call, CallState oldState, CallState newState) {
+        switch (newState) {
+            case ACTIVE:
+                updateAudioFocusForActiveCall(call);
+                break;
+            case DISCONNECTED:
+                updateAudioFocusForNoCall();
+                break;
+            default:
+                break;
+        }
+    }
+
+    private void updateAudioFocusForActiveCall(Call call) {
+        Log.v(this, "onForegroundCallChanged, requesting audio focus");
+        Context context = TelecommApp.getInstance();
+        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        audioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
+                    AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+        audioManager.setMode(AudioManager.MODE_IN_CALL);
+        audioManager.setMicrophoneMute(false);
+        audioManager.setSpeakerphoneOn(false);
+    }
+
+    private void updateAudioFocusForNoCall() {
+        Log.v(this, "updateAudioFocusForNoCall, abandoning audio focus");
+        Context context = TelecommApp.getInstance().getApplicationContext();
+        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        audioManager.setMode(AudioManager.MODE_NORMAL);
+        audioManager.abandonAudioFocusForCall();
+    }
+}
diff --git a/src/com/android/telecomm/CallLogManager.java b/src/com/android/telecomm/CallLogManager.java
index 718c685..faaa12d 100644
--- a/src/com/android/telecomm/CallLogManager.java
+++ b/src/com/android/telecomm/CallLogManager.java
@@ -20,6 +20,7 @@
 import android.net.Uri;
 import android.os.AsyncTask;
 import android.provider.CallLog.Calls;
+import android.telecomm.CallState;
 import android.telephony.PhoneNumberUtils;
 
 import com.android.internal.telephony.PhoneConstants;
@@ -29,7 +30,7 @@
  * caller details to the call log. All logging activity will be performed asynchronously in a
  * background thread to avoid blocking on the main thread.
  */
-class CallLogManager {
+final class CallLogManager extends CallsManagerListenerBase {
     /**
      * Parameter object to hold the arguments to add a call in the call log DB.
      */
@@ -72,19 +73,19 @@
         mContext = context;
     }
 
-    void logDisconnectedCall(Call call) {
-        // TODO: Until we add more state to the Call object to track whether this disconnected
-        // call orginated as an incoming or outgoing call, always log it as an incoming call.
-        // See b/13420887.
-        logCall(call, Calls.INCOMING_TYPE);
-    }
-
-    void logFailedOutgoingCall(Call call) {
-        logCall(call, Calls.OUTGOING_TYPE);
-    }
-
-    void logMissedCall(Call call) {
-        logCall(call, Calls.MISSED_TYPE);
+    @Override
+    public void onCallStateChanged(Call call, CallState oldState, CallState newState) {
+        if (newState == CallState.DISCONNECTED || newState == CallState.ABORTED) {
+            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);
+        }
     }
 
     /**
diff --git a/src/com/android/telecomm/CallsManager.java b/src/com/android/telecomm/CallsManager.java
index 26f7ac1..452f8e1 100644
--- a/src/com/android/telecomm/CallsManager.java
+++ b/src/com/android/telecomm/CallsManager.java
@@ -16,21 +16,18 @@
 
 package com.android.telecomm;
 
-import android.Manifest;
 import android.content.Context;
-import android.content.Intent;
-import android.media.AudioManager;
 import android.net.Uri;
 import android.os.Bundle;
-import android.telecomm.CallService;
+import android.telecomm.CallInfo;
 import android.telecomm.CallServiceDescriptor;
 import android.telecomm.CallState;
-import android.telecomm.TelecommConstants;
-import android.telephony.TelephonyManager;
 
 import com.android.internal.telecomm.ICallService;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 
@@ -46,15 +43,19 @@
  */
 public final class CallsManager {
 
+    interface CallsManagerListener {
+        void onCallAdded(Call call);
+        void onCallRemoved(Call call);
+        void onCallStateChanged(Call call, CallState oldState, CallState newState);
+        void onIncomingCallAnswered(Call call);
+        void onIncomingCallRejected(Call call);
+        void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall);
+    }
+
     private static final CallsManager INSTANCE = new CallsManager();
 
     private final Switchboard mSwitchboard;
 
-    /** Used to control the in-call app. */
-    private final InCallController mInCallController;
-
-    private final Ringer mRinger;
-
     /**
      * The main call repository. Keeps an instance of all live calls keyed by call ID. New incoming
      * and outgoing calls are added to the map and removed when the calls move to the disconnected
@@ -64,24 +65,20 @@
     private final Map<String, Call> mCalls = Maps.newHashMap();
 
     /**
-     * Used to keep ordering of unanswered incoming calls. The existence of multiple call services
-     * means that there can easily exist multiple incoming calls and explicit ordering is useful for
-     * maintaining the proper state of the ringer.
-     * TODO(santoscordon): May want to add comments about ITelephony.answerCall() method since
-     * ordering may also apply to that case.
+     * 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 final List<Call> mUnansweredIncomingCalls = Lists.newLinkedList();
+    private Call mForegroundCall;
 
-    /**
-     * May be unnecessary per off-line discussions (between santoscordon and gilad) since the set
-     * of CallsManager APIs that need to be exposed to the dialer (or any application firing call
-     * intents) may be empty.
-     */
-    private DialerAdapter mDialerAdapter;
+    private final CallsManagerListener mCallLogManager;
 
-    private InCallAdapter mInCallAdapter;
+    private final CallsManagerListener mPhoneStateBroadcaster;
 
-    private CallLogManager mCallLogManager;
+    private final CallsManagerListener mCallAudioManager;
+
+    private final CallsManagerListener mInCallController;
+
+    private final CallsManagerListener mRinger;
 
     private VoicemailManager mVoicemailManager;
 
@@ -94,15 +91,25 @@
      */
     private CallsManager() {
         mSwitchboard = new Switchboard(this);
-        mInCallController = new InCallController(this);
-        mRinger = new Ringer();
         mCallLogManager = new CallLogManager(TelecommApp.getInstance());
+        mPhoneStateBroadcaster = new PhoneStateBroadcaster();
+        mCallAudioManager = new CallAudioManager();
+        mInCallController = new InCallController();
+        mRinger = new Ringer();
     }
 
     static CallsManager getInstance() {
         return INSTANCE;
     }
 
+    ImmutableCollection<Call> getCalls() {
+        return ImmutableList.copyOf(mCalls.values());
+    }
+
+    Call getForegroundCall() {
+        return mForegroundCall;
+    }
+
     /**
      * Starts the incoming call sequence by having switchboard gather more information about the
      * specified call; using the specified call service descriptor. Upon success, execution returns
@@ -116,7 +123,7 @@
         // Create a call with no handle. Eventually, switchboard will update the call with
         // additional information from the call service, but for now we just need one to pass around
         // with a unique call ID.
-        Call call = new Call();
+        Call call = new Call(true /* isIncoming */);
 
         mSwitchboard.retrieveIncomingCall(call, descriptor, extras);
     }
@@ -126,10 +133,11 @@
      * list of live calls. Also notifies the in-call app so the user can answer or reject the call.
      *
      * @param call The new incoming call.
+     * @param callInfo The details of the call.
      */
-    void handleSuccessfulIncomingCall(Call call) {
+    void handleSuccessfulIncomingCall(Call call, CallInfo callInfo) {
         Log.d(this, "handleSuccessfulIncomingCall");
-        Preconditions.checkState(call.getState() == CallState.RINGING);
+        Preconditions.checkState(callInfo.getState() == CallState.RINGING);
 
         Uri handle = call.getHandle();
         ContactInfo contactInfo = call.getContactInfo();
@@ -143,13 +151,18 @@
 
         // No objection to accept the incoming call, proceed with potentially connecting it (based
         // on the user's action, or lack thereof).
+        call.setHandle(callInfo.getHandle());
+        setCallState(call, callInfo.getState());
         addCall(call);
+    }
 
-        mUnansweredIncomingCalls.add(call);
-        if (mUnansweredIncomingCalls.size() == 1) {
-            // Start the ringer if we are the top-most incoming call (the only one in this case).
-            mRinger.startRinging();
-        }
+    /**
+     * Called when an incoming call was not connected.
+     *
+     * @param call The incoming call.
+     */
+    void handleUnsuccessfulIncomingCall(Call call) {
+        setCallState(call, CallState.DISCONNECTED);
     }
 
     /**
@@ -171,30 +184,36 @@
         }
 
         // No objection to issue the call, proceed with trying to put it through.
-        Call call = new Call(handle, contactInfo);
+        Call call = new Call(handle, contactInfo, false /* isIncoming */);
+        setCallState(call, CallState.DIALING);
+        addCall(call);
         mSwitchboard.placeOutgoingCall(call);
     }
 
     /**
-     * Adds a new outgoing call to the list of live calls and notifies the in-call app.
+     * Called when a call service acknowledges that it can place a call.
      *
      * @param call The new outgoing call.
      */
     void handleSuccessfulOutgoingCall(Call call) {
-        // OutgoingCallProcessor sets the call state to DIALING when it receives confirmation of the
-        // placed call from the call service so there is no need to set it here. Instead, check that
-        // the state is appropriate.
-        Preconditions.checkState(call.getState() == CallState.DIALING);
-        addCall(call);
+        Log.v(this, "handleSuccessfulOutgoingCall, %s", call);
     }
 
     /**
-     * Informs mCallLogManager about the outgoing call that failed, so that it can be logged.
+     * Called when an outgoing call was not placed.
      *
-     * @param call The failed outgoing call.
+     * @param call The outgoing call.
+     * @param isAborted True if the call was unsuccessful because it was aborted.
      */
-    void handleFailedOutgoingCall(Call call) {
-        mCallLogManager.logFailedOutgoingCall(call);
+    void handleUnsuccessfulOutgoingCall(Call call, boolean isAborted) {
+        Log.v(this, "handleAbortedOutgoingCall, call: %s, isAborted: %b", call, isAborted);
+        if (isAborted) {
+            call.abort();
+            setCallState(call, CallState.ABORTED);
+        } else {
+            setCallState(call, CallState.DISCONNECTED);
+        }
+        removeCall(call);
     }
 
     /**
@@ -209,7 +228,11 @@
         if (call == null) {
             Log.i(this, "Request to answer a non-existent call %s", callId);
         } else {
-            stopRinging(call);
+            mCallLogManager.onIncomingCallAnswered(call);
+            mPhoneStateBroadcaster.onIncomingCallAnswered(call);
+            mCallAudioManager.onIncomingCallAnswered(call);
+            mInCallController.onIncomingCallAnswered(call);
+            mRinger.onIncomingCallAnswered(call);
 
             // We do not update the UI until we get confirmation of the answer() through
             // {@link #markCallAsActive}. However, if we ever change that to look more responsive,
@@ -231,7 +254,12 @@
         if (call == null) {
             Log.i(this, "Request to reject a non-existent call %s", callId);
         } else {
-            stopRinging(call);
+            mCallLogManager.onIncomingCallRejected(call);
+            mPhoneStateBroadcaster.onIncomingCallRejected(call);
+            mCallAudioManager.onIncomingCallRejected(call);
+            mInCallController.onIncomingCallRejected(call);
+            mRinger.onIncomingCallRejected(call);
+
             call.reject();
         }
     }
@@ -250,13 +278,6 @@
         } else {
             call.disconnect();
         }
-
-        // TODO(sail): Replace with CallAudioManager.
-        Log.v(this, "disconnectCall, abandoning audio focus");
-        Context context = TelecommApp.getInstance().getApplicationContext();
-        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
-        audioManager.setMode(AudioManager.MODE_NORMAL);
-        audioManager.abandonAudioFocusForCall();
     }
 
     /**
@@ -303,25 +324,10 @@
 
     void markCallAsActive(String callId) {
         setCallState(callId, CallState.ACTIVE);
-        removeFromUnansweredCalls(callId);
-        mInCallController.markCallAsActive(callId);
-
-        // TODO(sail): Replace with CallAudioManager.
-        Log.v(this, "markCallAsActive, requesting audio focus");
-        Context context = TelecommApp.getInstance().getApplicationContext();
-        AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
-        audioManager.requestAudioFocusForCall(AudioManager.STREAM_VOICE_CALL,
-                    AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
-        audioManager.setMode(AudioManager.MODE_IN_CALL);
-        audioManager.setMicrophoneMute(false);
-        audioManager.setSpeakerphoneOn(false);
     }
 
     void markCallAsOnHold(String callId) {
         setCallState(callId, CallState.ON_HOLD);
-
-        // Notify the in-call UI
-        mInCallController.markCallAsOnHold(callId);
     }
 
     /**
@@ -332,35 +338,7 @@
      */
     void markCallAsDisconnected(String callId) {
         setCallState(callId, CallState.DISCONNECTED);
-        removeFromUnansweredCalls(callId);
-
-        Call call = mCalls.remove(callId);
-        // At this point the call service has confirmed that the call is disconnected to it is
-        // safe to disassociate the call from its call service.
-        call.clearCallService();
-
-        // Notify the in-call UI
-        mInCallController.markCallAsDisconnected(callId);
-        if (mCalls.isEmpty()) {
-            mInCallController.unbind();
-        }
-
-        // Log the call in the call log.
-        mCallLogManager.logDisconnectedCall(call);
-    }
-
-    /**
-     * Sends all the live calls to the in-call app if any exist. If there are no live calls, then
-     * tells the in-call controller to unbind since it is not needed.
-     */
-    void updateInCall() {
-        if (mCalls.isEmpty()) {
-            mInCallController.unbind();
-        } else {
-            for (Call call : mCalls.values()) {
-                addInCallEntry(call);
-            }
-        }
+        removeCall(mCalls.remove(callId));
     }
 
     /**
@@ -370,14 +348,24 @@
      */
     private void addCall(Call call) {
         mCalls.put(call.getId(), call);
-        addInCallEntry(call);
+
+        mCallLogManager.onCallAdded(call);
+        mPhoneStateBroadcaster.onCallAdded(call);
+        mCallAudioManager.onCallAdded(call);
+        mInCallController.onCallAdded(call);
+        mRinger.onCallAdded(call);
+        updateForegroundCall();
     }
 
-    /**
-     * Notifies the in-call app of the specified (new) call.
-     */
-    private void addInCallEntry(Call call) {
-        mInCallController.addCall(call.toCallInfo());
+    private void removeCall(Call call) {
+        call.clearCallService();
+
+        mCallLogManager.onCallRemoved(call);
+        mPhoneStateBroadcaster.onCallRemoved(call);
+        mCallAudioManager.onCallRemoved(call);
+        mInCallController.onCallRemoved(call);
+        mRinger.onCallRemoved(call);
+        updateForegroundCall();
     }
 
     /**
@@ -396,102 +384,67 @@
             Log.w(this, "Call %s was not found while attempting to update the state to %s.",
                     callId, newState );
         } else {
-            if (newState != call.getState()) {
-                // 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., ACTIVE -> RINGING).
-                // TODO(santoscordon): Consider putting a stop to the above and turning CallState
-                // into a well-defined state machine.
-                // TODO(santoscordon): Define expected state transitions here, and log when an
-                // unexpected transition occurs.
-                call.setState(newState);
-                // TODO(santoscordon): Notify the in-call app whenever a call changes state.
+            setCallState(call, newState);
+        }
+    }
 
-                broadcastState(call);
+    /**
+     * Sets the specified state on the specified call. Updates the ringer if the call is exiting
+     * the RINGING state.
+     *
+     * @param call The call.
+     * @param newState The new state of the call.
+     */
+    private void setCallState(Call call, CallState newState) {
+        CallState oldState = call.getState();
+        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., ACTIVE -> RINGING).
+            // TODO(santoscordon): Consider putting a stop to the above and turning CallState
+            // into a well-defined state machine.
+            // TODO(santoscordon): 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.containsKey(call.getId())) {
+                mCallLogManager.onCallStateChanged(call, oldState, newState);
+                mPhoneStateBroadcaster.onCallStateChanged(call, oldState, newState);
+                mCallAudioManager.onCallStateChanged(call, oldState, newState);
+                mInCallController.onCallStateChanged(call, oldState, newState);
+                mRinger.onCallStateChanged(call, oldState, newState);
+                updateForegroundCall();
             }
         }
     }
 
     /**
-     * Send a {@link TelephonyManager#ACTION_PHONE_STATE_CHANGED} broadcast when the call state
-     * changes. TODO: Split this out into a separate class and do it properly; this is just a first
-     * pass over the functionality.
-     *
-     * @param call The {@link Call} being updated.
+     * Checks which call should be visible to the user and have audio focus.
      */
-    private void broadcastState(Call call) {
-        final String callState;
-        switch (call.getState()) {
-            case DIALING:
-            case ACTIVE:
-            case ON_HOLD:
-                callState = TelephonyManager.EXTRA_STATE_OFFHOOK;
+    private void updateForegroundCall() {
+        Call newForegroundCall = null;
+        for (Call call : mCalls.values()) {
+            // Incoming ringing calls have priority.
+            if (call.getState() == CallState.RINGING) {
+                newForegroundCall = call;
                 break;
-
-            case RINGING:
-                callState = TelephonyManager.EXTRA_STATE_RINGING;
-                break;
-
-            case DISCONNECTED:
-            case ABORTED:
-                callState = TelephonyManager.EXTRA_STATE_IDLE;
-                break;
-
-            default:
-                Log.w(this, "Call is in an unknown state (%s), not broadcasting: %s", call.getState(),
-                        call.getId());
-                return;
-        }
-
-        Intent updateIntent = new Intent(TelephonyManager.ACTION_PHONE_STATE_CHANGED);
-        updateIntent.putExtra(TelephonyManager.EXTRA_STATE, callState);
-        // TODO: See if we can add this (the current API doesn't have a callId).
-        updateIntent.putExtra(TelecommConstants.EXTRA_CALL_ID, call.getId());
-
-        // Populate both, since the original API was needlessly complicated.
-        updateIntent.putExtra(TelephonyManager.EXTRA_INCOMING_NUMBER, call.getHandle());
-        // TODO: See if we can add this (the current API only sets this on NEW_OUTGOING_CALL).
-        updateIntent.putExtra(Intent.EXTRA_PHONE_NUMBER, call.getHandle());
-
-        // TODO: Replace these with real constants once this API has been vetted.
-        CallServiceWrapper callService = call.getCallService();
-        if (callService != null) {
-            updateIntent.putExtra(CallService.class.getName(), callService.getComponentName());
-        }
-        TelecommApp.getInstance().sendBroadcast(updateIntent, Manifest.permission.READ_PHONE_STATE);
-        Log.i(this, "Broadcasted state change: %s", callState);
-    }
-
-    /**
-     * Removes the specified call from the list of unanswered incoming calls and updates the ringer
-     * based on the new state of {@link #mUnansweredIncomingCalls}. Safe to call with a call ID that
-     * is not present in the list of incoming calls.
-     *
-     * @param callId The ID of the call.
-     */
-    private void removeFromUnansweredCalls(String callId) {
-        Call call = mCalls.get(callId);
-        if (call != null && mUnansweredIncomingCalls.remove(call)) {
-            if (mUnansweredIncomingCalls.isEmpty()) {
-                mRinger.stopRinging();
-            } else {
-                mRinger.startRinging();
+            }
+            if (call.getState() == CallState.ACTIVE) {
+                newForegroundCall = call;
+                // Don't break in case there's a ringing call that has priority.
             }
         }
-    }
 
-    /**
-     * Stops playing the ringer if the specified call is the top-most incoming call. This exists
-     * separately from {@link #removeFromUnansweredCalls} to allow stopping the ringer for calls
-     * that should remain in {@link #mUnansweredIncomingCalls}, invoked from {@link #answerCall}
-     * and {@link #rejectCall}.
-     *
-     * @param call The call for which we should stop ringing.
-     */
-    private void stopRinging(Call call) {
-        // Only stop the ringer if this call is the top-most incoming call.
-        if (!mUnansweredIncomingCalls.isEmpty() && mUnansweredIncomingCalls.get(0) == call) {
-            mRinger.stopRinging();
+        if (newForegroundCall != mForegroundCall) {
+            Call oldForegroundCall = mForegroundCall;
+            mForegroundCall = newForegroundCall;
+
+            mCallLogManager.onForegroundCallChanged(oldForegroundCall, mForegroundCall);
+            mPhoneStateBroadcaster.onForegroundCallChanged(oldForegroundCall, mForegroundCall);
+            mCallAudioManager.onForegroundCallChanged(oldForegroundCall, mForegroundCall);
+            mInCallController.onForegroundCallChanged(oldForegroundCall, mForegroundCall);
+            mRinger.onForegroundCallChanged(oldForegroundCall, mForegroundCall);
         }
     }
 }
diff --git a/src/com/android/telecomm/CallsManagerListenerBase.java b/src/com/android/telecomm/CallsManagerListenerBase.java
new file mode 100644
index 0000000..c81067f
--- /dev/null
+++ b/src/com/android/telecomm/CallsManagerListenerBase.java
@@ -0,0 +1,48 @@
+/*
+ * 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.telecomm;
+
+import android.telecomm.CallState;
+
+/**
+ * Provides a default implementation for listeners for CallsManager.
+ */
+class CallsManagerListenerBase implements CallsManager.CallsManagerListener {
+    @Override
+    public void onCallAdded(Call call) {
+    }
+
+    @Override
+    public void onCallRemoved(Call call) {
+    }
+
+    @Override
+    public void onCallStateChanged(Call call, CallState oldState, CallState newState) {
+    }
+
+    @Override
+    public void onIncomingCallAnswered(Call call) {
+    }
+
+    @Override
+    public void onIncomingCallRejected(Call call) {
+    }
+
+    @Override
+    public void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall) {
+    }
+}
diff --git a/src/com/android/telecomm/InCallController.java b/src/com/android/telecomm/InCallController.java
index 4f863c5..0bfe30d 100644
--- a/src/com/android/telecomm/InCallController.java
+++ b/src/com/android/telecomm/InCallController.java
@@ -23,8 +23,10 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.telecomm.CallInfo;
+import android.telecomm.CallState;
 
 import com.android.internal.telecomm.IInCallService;
+import com.google.common.collect.ImmutableCollection;
 
 /**
  * Binds to {@link IInCallService} and provides the service to {@link CallsManager} through which it
@@ -35,7 +37,7 @@
  * CallsManager will invoke {@link #disconnect} to sever the binding until the in-call UI is needed
  * again.
  */
-public final class InCallController {
+public final class InCallController extends CallsManagerListenerBase {
     /**
      * Used to bind to the in-call app and triggers the start of communication between
      * CallsManager and in-call app.
@@ -69,93 +71,77 @@
     /** Maintains a binding connection to the in-call app. */
     private final InCallServiceConnection mConnection = new InCallServiceConnection();
 
-    private final CallsManager mCallsManager;
-
     /** The in-call app implementation, see {@link IInCallService}. */
     private IInCallService mInCallService;
 
-    /**
-     * Persists the specified parameters.
-     */
-    InCallController(CallsManager callsManager) {
-        mCallsManager = callsManager;
-    }
-
     // TODO(santoscordon): May be better to expose the IInCallService methods directly from this
     // class as its own method to make the CallsManager code easier to read.
     IInCallService getService() {
         return mInCallService;
     }
 
-    /**
-     * Indicates to the in-call app that a new call has been created and an appropriate
-     * user-interface should be built and shown to notify the user.  Information about the call
-     * including its current state is passed in through the callInfo object.
-     *
-     * @param callInfo Details about the new call.
-     */
-    void addCall(CallInfo callInfo) {
-        try {
-            if (mInCallService == null) {
-                bind();
-            } else {
-                // TODO(santoscordon): Protect against logging phone number.
-                Log.i(this, "Adding call: %s", callInfo);
-                mInCallService.addCall(callInfo);
+    @Override
+    public void onCallAdded(Call call) {
+        if (mInCallService == null) {
+            bind();
+        } else {
+            Log.i(this, "Adding call: %s", call);
+            try {
+                mInCallService.addCall(call.toCallInfo());
+            } catch (RemoteException e) {
+                Log.e(this, e, "Exception attempting to addCall.");
             }
-        } catch (RemoteException e) {
-            Log.e(this, e, "Exception attempting to addCall.");
         }
     }
 
-    /**
-     * Indicates to the in-call app that a call has moved to the active state.
-     *
-     * @param callId The identifier of the call that became active.
-     */
-    void markCallAsActive(String callId) {
-        try {
-            if (mInCallService != null) {
-                Log.i(this, "Mark call as ACTIVE: %s", callId);
-                mInCallService.setActive(callId);
-            }
-        } catch (RemoteException e) {
-            Log.e(this, e, "Exception attempting to markCallAsActive.");
+    @Override
+    public void onCallRemoved(Call call) {
+        if (CallsManager.getInstance().getCalls().isEmpty()) {
+            // TODO(sail): Wait for all messages to be delivered to the service before unbinding.
+            unbind();
         }
     }
 
-    /**
-     * Indicates to the in-call app that a call has been disconnected and the user should be
-     * notified.
-     *
-     * @param callId The identifier of the call that was disconnected.
-     */
-    void markCallAsDisconnected(String callId) {
-        try {
-            if (mInCallService != null) {
-                Log.i(this, "Mark call as DISCONNECTED: %s", callId);
-                mInCallService.setDisconnected(callId);
-            }
-        } catch (RemoteException e) {
-            Log.e(this, e, "Exception attempting to markCallAsDisconnected.");
+    @Override
+    public void onCallStateChanged(Call call, CallState oldState, CallState newState) {
+        if (mInCallService == null) {
+            return;
         }
-    }
 
-    void markCallAsOnHold(String callId) {
-        try {
-            if (mInCallService != null) {
-                Log.i(this, "Mark call as HOLD: %s", callId);
-                mInCallService.setOnHold(callId);
-            }
-        } catch (RemoteException e) {
-            Log.e(this, e, "Exception attempting to markCallAsHeld.");
+        switch (newState) {
+            case ACTIVE:
+                Log.i(this, "Mark call as ACTIVE: %s", call.getId());
+                try {
+                    mInCallService.setActive(call.getId());
+                } catch (RemoteException e) {
+                    Log.e(this, e, "Exception attempting to call setActive.");
+                }
+                break;
+            case ON_HOLD:
+                Log.i(this, "Mark call as HOLD: %s", call.getId());
+                try {
+                    mInCallService.setOnHold(call.getId());
+                } catch (RemoteException e) {
+                    Log.e(this, e, "Exception attempting to call setOnHold.");
+                }
+                break;
+            case DISCONNECTED:
+                Log.i(this, "Mark call as DISCONNECTED: %s", call.getId());
+                try {
+                    mInCallService.setDisconnected(call.getId());
+                } catch (RemoteException e) {
+                    Log.e(this, e, "Exception attempting to call setDisconnected.");
+                }
+                break;
+            default:
+                break;
         }
     }
 
     /**
      * Unbinds an existing bound connection to the in-call app.
      */
-    void unbind() {
+    private void unbind() {
         ThreadUtil.checkOnMainThread();
         if (mInCallService != null) {
             Log.i(this, "Unbinding from InCallService");
@@ -199,17 +185,22 @@
         mInCallService = IInCallService.Stub.asInterface(service);
 
         try {
-            mInCallService.setInCallAdapter(new InCallAdapter(mCallsManager));
+            mInCallService.setInCallAdapter(new InCallAdapter(CallsManager.getInstance()));
         } catch (RemoteException e) {
             Log.e(this, e, "Failed to set the in-call adapter.");
             mInCallService = null;
+            return;
         }
 
         // Upon successful connection, send the state of the world to the in-call app.
-        if (mInCallService != null) {
-            mCallsManager.updateInCall();
+        ImmutableCollection<Call> calls = CallsManager.getInstance().getCalls();
+        if (!calls.isEmpty()) {
+            for (Call call : calls) {
+                onCallAdded(call);
+            }
+        } else {
+            unbind();
         }
-
     }
 
     /**
diff --git a/src/com/android/telecomm/IncomingCallsManager.java b/src/com/android/telecomm/IncomingCallsManager.java
index 0493df8..6edbbbf 100644
--- a/src/com/android/telecomm/IncomingCallsManager.java
+++ b/src/com/android/telecomm/IncomingCallsManager.java
@@ -82,10 +82,7 @@
         Call call = mPendingIncomingCalls.remove(callInfo.getId());
         if (call != null) {
             Log.d(this, "Incoming call %s found.", call.getId());
-            call.setHandle(callInfo.getHandle());
-            call.setState(callInfo.getState());
-
-            mSwitchboard.handleSuccessfulIncomingCall(call);
+            mSwitchboard.handleSuccessfulIncomingCall(call, callInfo);
         }
     }
 
diff --git a/src/com/android/telecomm/OutgoingCallProcessor.java b/src/com/android/telecomm/OutgoingCallProcessor.java
index bd49b66..bbcdb17 100644
--- a/src/com/android/telecomm/OutgoingCallProcessor.java
+++ b/src/com/android/telecomm/OutgoingCallProcessor.java
@@ -144,6 +144,7 @@
      * Initiates the attempt to place the call.  No-op beyond the first invocation.
      */
     void process() {
+        Log.v(this, "process, mIsAborted: %b", mIsAborted);
         if (!mIsAborted) {
             // Only process un-aborted calls.
             ThreadUtil.checkOnMainThread();
@@ -165,23 +166,24 @@
      *     false otherwise.
      */
     void setIsCompatibleWith(String callId, boolean isCompatible) {
-      if (!callId.equals(mCall.getId())) {
-          Log.wtf(this, "setIsCompatibleWith invoked with unexpected call ID: %s - expected call"
+        Log.v(this, "setIsCompatibleWith, callId: %s, isCompatible: %b", callId, isCompatible);
+        if (!callId.equals(mCall.getId())) {
+            Log.wtf(this, "setIsCompatibleWith invoked with unexpected call ID: %s - expected call"
                   + " ID: %s", callId, mCall.getId());
-          return;
-      }
+            return;
+        }
 
-      if (!mIsAborted) {
-          CallServiceWrapper callService = mCall.getCallService();
-          if (callService != null) {
-              if (isCompatible) {
-                  callService.call(mCall.toCallInfo(), mNextCallServiceCallback);
-                  return;
-              }
-              mIncompatibleCallServices.add(callService);
-          }
-          attemptNextCallService();
-      }
+        if (!mIsAborted) {
+            CallServiceWrapper callService = mCall.getCallService();
+            if (callService != null) {
+                if (isCompatible) {
+                    callService.call(mCall.toCallInfo(), mNextCallServiceCallback);
+                    return;
+                }
+                mIncompatibleCallServices.add(callService);
+            }
+            attemptNextCallService();
+        }
     }
 
     /**
@@ -189,10 +191,11 @@
      * switchboard through the outgoing-calls manager.
      */
     void abort() {
+        Log.v(this, "abort");
         ThreadUtil.checkOnMainThread();
         if (!mIsAborted) {
-            mCall.abort();
             mIsAborted = true;
+            mOutgoingCallsManager.handleFailedOutgoingCall(mCall, true /* isAborted */);
         }
     }
 
@@ -202,6 +205,7 @@
      * placed the call.
      */
     void handleSuccessfulCallAttempt() {
+        Log.v(this, "handleSuccessfulCallAttempt");
         ThreadUtil.checkOnMainThread();
 
         if (mIsAborted) {
@@ -209,7 +213,6 @@
             return;
         }
 
-        mCall.setState(CallState.DIALING);
         for (CallServiceWrapper callService : mIncompatibleCallServices) {
             mCall.addIncompatibleCallService(callService);
         }
@@ -223,6 +226,7 @@
      * @param reason The call-service supplied reason for the failed call attempt.
      */
     void handleFailedCallAttempt(String reason) {
+        Log.v(this, "handleFailedCallAttempt");
         if (!mIsAborted) {
             ThreadUtil.checkOnMainThread();
 
@@ -237,6 +241,7 @@
      * are available.
      */
     private void attemptNextSelector() {
+        Log.v(this, "attemptNextSelector, mIsAborted: %b", mIsAborted);
         if (mIsAborted) {
             return;
         }
@@ -253,7 +258,8 @@
             }
 
         } else {
-            mOutgoingCallsManager.handleFailedOutgoingCall(mCall);
+            Log.v(this, "attemptNextSelector, no more selectors, failing");
+            mOutgoingCallsManager.handleFailedOutgoingCall(mCall, false /* isAborted */);
         }
     }
 
@@ -286,6 +292,7 @@
     private void processSelectedCallServiceDescriptors(
             List<CallServiceDescriptor> selectedCallServiceDescriptors) {
 
+        Log.v(this, "processSelectedCallServiceDescriptors");
         if (selectedCallServiceDescriptors == null || selectedCallServiceDescriptors.isEmpty()) {
             attemptNextSelector();
         } else if (mCallServiceDescriptorIterator == null) {
@@ -301,6 +308,7 @@
      * {@link #attemptNextSelector}.
      */
     private void attemptNextCallService() {
+        Log.v(this, "attemptNextCallService, mIsAborted: %b", mIsAborted);
         if (mIsAborted) {
             return;
         }
diff --git a/src/com/android/telecomm/OutgoingCallsManager.java b/src/com/android/telecomm/OutgoingCallsManager.java
index 7c2a501..37f7eb1 100644
--- a/src/com/android/telecomm/OutgoingCallsManager.java
+++ b/src/com/android/telecomm/OutgoingCallsManager.java
@@ -77,6 +77,7 @@
      *     false otherwise.
      */
     void setIsCompatibleWith(String callId, boolean isCompatible) {
+        Log.v(this, "setIsCompatibleWith, callId %s, isCompatible: %b", callId, isCompatible);
         OutgoingCallProcessor processor = mOutgoingCallProcessors.get(callId);
         if (processor == null) {
             // Shouldn't happen, so log a wtf if it does.
@@ -94,6 +95,7 @@
      * @param callId The ID of the call.
      */
     void handleSuccessfulCallAttempt(String callId) {
+        Log.v(this, "handleSuccessfulCallAttempt, callId: %s", callId);
         OutgoingCallProcessor processor = mOutgoingCallProcessors.remove(callId);
 
         if (processor == null) {
@@ -114,6 +116,7 @@
      * @param reason The call-service supplied reason for the failed call attempt.
      */
     void handleFailedCallAttempt(String callId, String reason) {
+        Log.v(this, "handleFailedCallAttempt, callId: %s, reason: %s", callId, reason);
         OutgoingCallProcessor processor = mOutgoingCallProcessors.get(callId);
 
         // TODO(santoscordon): Consider combining the check here and in handleSuccessfulCallAttempt.
@@ -133,10 +136,12 @@
      * should cleanup and notify Switchboard.
      *
      * @param call The failed outgoing call.
+     * @param isAborted True if the call timedout and is aborted.
      */
-    void handleFailedOutgoingCall(Call call) {
+    void handleFailedOutgoingCall(Call call, boolean isAborted) {
+        Log.v(this, "handleFailedOutgoingCall, call: %s", call);
         mOutgoingCallProcessors.remove(call.getId());
-        mSwitchboard.handleFailedOutgoingCall(call);
+        mSwitchboard.handleFailedOutgoingCall(call, isAborted);
     }
 
     /**
@@ -145,6 +150,7 @@
      * @param call The call to be aborted.
      */
     void abort(Call call) {
+        Log.v(this, "abort, call: %s", call);
         OutgoingCallProcessor processor = mOutgoingCallProcessors.remove(call.getId());
         if (processor != null) {
             processor.abort();
diff --git a/src/com/android/telecomm/PhoneStateBroadcaster.java b/src/com/android/telecomm/PhoneStateBroadcaster.java
new file mode 100644
index 0000000..4bb77d3
--- /dev/null
+++ b/src/com/android/telecomm/PhoneStateBroadcaster.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.telecomm;
+
+import android.Manifest;
+import android.content.Intent;
+import android.telecomm.CallService;
+import android.telecomm.CallState;
+import android.telecomm.TelecommConstants;
+import android.telephony.TelephonyManager;
+
+/**
+ * Send a {@link TelephonyManager#ACTION_PHONE_STATE_CHANGED} broadcast when the call state
+ * changes.
+ */
+final class PhoneStateBroadcaster extends CallsManagerListenerBase {
+    @Override
+    public void onCallStateChanged(Call call, CallState oldState, CallState newState) {
+        final String phoneState;
+        switch (newState) {
+            case DIALING:
+            case ACTIVE:
+            case ON_HOLD:
+                phoneState = TelephonyManager.EXTRA_STATE_OFFHOOK;
+                break;
+            case RINGING:
+                phoneState = TelephonyManager.EXTRA_STATE_RINGING;
+                break;
+            case ABORTED:
+            case DISCONNECTED:
+                phoneState = TelephonyManager.EXTRA_STATE_IDLE;
+                break;
+            default:
+                Log.w(this, "Call is in an unknown state (%s), not broadcasting: %s",
+                        newState, call.getId());
+                return;
+        }
+        sendPhoneStateChangedBroadcast(call, phoneState);
+    }
+
+    private void sendPhoneStateChangedBroadcast(Call call, String phoneState) {
+        Log.v(this, "sendPhoneStateChangedBroadcast, call %s, phoneState: %s", call, phoneState);
+
+        Intent intent = new Intent(TelephonyManager.ACTION_PHONE_STATE_CHANGED);
+        intent.putExtra(TelephonyManager.EXTRA_STATE, phoneState);
+        // TODO: See if we can add this (the current API doesn't have a callId).
+        intent.putExtra(TelecommConstants.EXTRA_CALL_ID, call.getId());
+
+        // Populate both, since the original API was needlessly complicated.
+        intent.putExtra(TelephonyManager.EXTRA_INCOMING_NUMBER, call.getHandle());
+        // TODO: See if we can add this (the current API only sets this on NEW_OUTGOING_CALL).
+        intent.putExtra(Intent.EXTRA_PHONE_NUMBER, call.getHandle());
+
+        // TODO: Replace these with real constants once this API has been vetted.
+        CallServiceWrapper callService = call.getCallService();
+        if (callService != null) {
+            intent.putExtra(CallService.class.getName(), callService.getComponentName());
+        }
+        TelecommApp.getInstance().sendBroadcast(intent, Manifest.permission.READ_PHONE_STATE);
+        Log.i(this, "Broadcasted state change: %s", phoneState);
+    }
+}
diff --git a/src/com/android/telecomm/Ringer.java b/src/com/android/telecomm/Ringer.java
index bca0d15..bc24560 100644
--- a/src/com/android/telecomm/Ringer.java
+++ b/src/com/android/telecomm/Ringer.java
@@ -23,18 +23,18 @@
 import android.media.MediaPlayer.OnPreparedListener;
 import android.media.RingtoneManager;
 import android.net.Uri;
+import android.telecomm.CallState;
 
 import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
 
 import java.io.IOException;
+import java.util.List;
 
 /**
  * Controls ringing and vibration for incoming calls.
- *
- * TODO(santoscordon): Consider moving all ringing responsibility to InCall app as an implementation
- * within InCallServiceBase.
  */
-final class Ringer implements OnErrorListener, OnPreparedListener {
+final class Ringer extends CallsManagerListenerBase implements OnErrorListener, OnPreparedListener {
     // States for the Ringer.
     /** Actively playing the ringer. */
     private static final int RINGING = 1;
@@ -56,11 +56,79 @@
     /** The active media player for the ringer. */
     private MediaPlayer mMediaPlayer;
 
+    /*
+     * Used to keep ordering of unanswered incoming calls. The existence of multiple call services
+     * means that there can easily exist multiple incoming calls and explicit ordering is useful for
+     * maintaining the proper state of the ringer.
+     */
+    private final List<String> mUnansweredCallIds = Lists.newLinkedList();
+
+    @Override
+    public void onCallAdded(Call call) {
+        if (call.isIncoming() && call.getState() == CallState.RINGING) {
+            if (mUnansweredCallIds.contains(call.getId())) {
+                Log.wtf(this, "New ringing call is already in list of unanswered calls");
+            }
+            mUnansweredCallIds.add(call.getId());
+            if (mUnansweredCallIds.size() == 1) {
+                // Start the ringer if we are the top-most incoming call (the only one in this
+                // case).
+                startRinging();
+            }
+        }
+    }
+
+    @Override
+    public void onCallRemoved(Call call) {
+        removeFromUnansweredCallIds(call.getId());
+    }
+
+    @Override
+    public void onCallStateChanged(Call call, CallState oldState, CallState newState) {
+        if (newState != CallState.RINGING) {
+            removeFromUnansweredCallIds(call.getId());
+        }
+    }
+
+    @Override
+    public void onIncomingCallAnswered(Call call) {
+        onRespondedToIncomingCall(call);
+    }
+
+    @Override
+    public void onIncomingCallRejected(Call call) {
+        onRespondedToIncomingCall(call);
+    }
+
+    private void onRespondedToIncomingCall(Call call) {
+        // Only stop the ringer if this call is the top-most incoming call.
+        if (!mUnansweredCallIds.isEmpty() && mUnansweredCallIds.get(0).equals(call.getId())) {
+            stopRinging();
+        }
+    }
+
+    /**
+     * Removes the specified call from the list of unanswered incoming calls and updates the ringer
+     * based on the new state of {@link #mUnansweredCallIds}. Safe to call with a call ID that
+     * is not present in the list of incoming calls.
+     *
+     * @param callId The ID of the call.
+     */
+    private void removeFromUnansweredCallIds(String callId) {
+        if (mUnansweredCallIds.remove(callId)) {
+            if (mUnansweredCallIds.isEmpty()) {
+                stopRinging();
+            } else {
+                startRinging();
+            }
+        }
+    }
+
     /**
      * Starts the vibration, ringer, and/or call-waiting tone.
      * TODO(santoscordon): vibration and call-waiting tone.
      */
-    void startRinging() {
+    private void startRinging() {
         // Check if we are muted before playing the ringer.
         if (getAudioManager().getStreamVolume(AudioManager.STREAM_RING) > 0) {
             moveToState(RINGING);
@@ -72,7 +140,7 @@
     /**
      * Stops the vibration, ringer, and/or call-waiting tone.
      */
-    void stopRinging() {
+    private void stopRinging() {
         moveToState(STOPPED);
     }
 
diff --git a/src/com/android/telecomm/Switchboard.java b/src/com/android/telecomm/Switchboard.java
index af8d380..8a3fc81 100644
--- a/src/com/android/telecomm/Switchboard.java
+++ b/src/com/android/telecomm/Switchboard.java
@@ -22,6 +22,7 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.Looper;
+import android.telecomm.CallInfo;
 import android.telecomm.CallServiceDescriptor;
 import android.telecomm.TelecommConstants;
 
@@ -208,9 +209,10 @@
      * Handles the case where an outgoing call could not be proceed by any of the
      * selector/call-service implementations, see {@link OutgoingCallProcessor}.
      */
-    void handleFailedOutgoingCall(Call call) {
+    void handleFailedOutgoingCall(Call call, boolean isAborted) {
         Log.d(this, "handleFailedOutgoingCall");
 
+        mCallsManager.handleUnsuccessfulOutgoingCall(call, isAborted);
         finalizeOutgoingCall(call);
     }
 
@@ -219,10 +221,10 @@
      * resulting call to {@link CallsManager} as the final step in the incoming sequence. At that
      * point, {@link CallsManager} should bring up the incoming-call UI.
      */
-    void handleSuccessfulIncomingCall(Call call) {
+    void handleSuccessfulIncomingCall(Call call, CallInfo callInfo) {
         Log.d(this, "handleSuccessfulIncomingCall");
 
-        mCallsManager.handleSuccessfulIncomingCall(call);
+        mCallsManager.handleSuccessfulIncomingCall(call, callInfo);
         mBinderDeallocator.releaseUsePermit();
     }
 
@@ -241,6 +243,8 @@
 
         mBinderDeallocator.releaseUsePermit();
 
+        mCallsManager.handleUnsuccessfulIncomingCall(call);
+
         // At the moment there is nothing more to do if an incoming call is not retrieved. We may at
         // a future date bind to the in-call app optimistically during the incoming-call sequence
         // and this method could tell {@link CallsManager} to unbind from the in-call app if the