Add audio mode and audio focus support to Telecomm

This CL adds support for:
  - audio focus
  - switching to speaker phone
  - mute
  - wired headset

Bluetooth support is coming in a different CL.

Change-Id: Iaf4013ea747548451cff45b48bcbb5b1dd1b8498
diff --git a/src/com/android/telecomm/Call.java b/src/com/android/telecomm/Call.java
index 568d7c6..0789135 100644
--- a/src/com/android/telecomm/Call.java
+++ b/src/com/android/telecomm/Call.java
@@ -20,6 +20,7 @@
 import android.telecomm.CallInfo;
 import android.telecomm.CallState;
 import android.telecomm.GatewayInfo;
+import android.telephony.PhoneNumberUtils;
 
 import com.google.android.collect.Sets;
 import com.google.common.base.Preconditions;
@@ -80,6 +81,8 @@
      */
     private Set<CallServiceWrapper> mIncompatibleCallServices;
 
+    private boolean mIsEmergencyCall;
+
     /**
      * Creates an empty call object with a unique call ID.
      *
@@ -100,7 +103,7 @@
     Call(Uri handle, ContactInfo contactInfo, GatewayInfo gatewayInfo, boolean isIncoming) {
         mId = UUID.randomUUID().toString();  // UUIDs should provide sufficient uniqueness.
         mState = CallState.NEW;
-        mHandle = handle;
+        setHandle(handle);
         mContactInfo = contactInfo;
         mGatewayInfo = gatewayInfo;
         mIsIncoming = isIncoming;
@@ -141,6 +144,12 @@
 
     void setHandle(Uri handle) {
         mHandle = handle;
+        mIsEmergencyCall = mHandle != null && PhoneNumberUtils.isLocalEmergencyNumber(
+                mHandle.getSchemeSpecificPart(), TelecommApp.getInstance());
+    }
+
+    boolean isEmergencyCall() {
+        return mIsEmergencyCall;
     }
 
     /**
@@ -332,6 +341,19 @@
         return new CallInfo(mId, mState, mHandle, mGatewayInfo);
     }
 
+    /** Checks if this is a live call or not. */
+    boolean isAlive() {
+        switch (mState) {
+            case NEW:
+            case RINGING:
+            case DISCONNECTED:
+            case ABORTED:
+                return false;
+            default:
+                return true;
+        }
+    }
+
     /**
      * @return True if the call is ringing, else logs the action name.
      */
diff --git a/src/com/android/telecomm/CallAudioManager.java b/src/com/android/telecomm/CallAudioManager.java
index 26987ba..b8d980b 100644
--- a/src/com/android/telecomm/CallAudioManager.java
+++ b/src/com/android/telecomm/CallAudioManager.java
@@ -18,176 +18,270 @@
 
 import android.content.Context;
 import android.media.AudioManager;
+import android.telecomm.CallAudioState;
 import android.telecomm.CallState;
 
-import com.google.common.collect.Lists;
-
-import java.util.List;
+import com.google.common.base.Preconditions;
 
 /**
  * This class manages audio modes, streams and other properties.
  */
 final class CallAudioManager extends CallsManagerListenerBase {
-    private AsyncRingtonePlayer mRinger = new AsyncRingtonePlayer();
+    private static final int STREAM_NONE = -1;
 
-    private boolean mHasAudioFocus = false;
+    private final AudioManager mAudioManager;
+    private final WiredHeadsetManager mWiredHeadsetManager;
+    private CallAudioState mAudioState;
+    private int mAudioFocusStreamType;
+    private boolean mIsRinging;
+    private boolean mWasSpeakerOn;
 
-    /**
-     * 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();
+    CallAudioManager() {
+        Context context = TelecommApp.getInstance();
+        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+        mWiredHeadsetManager = new WiredHeadsetManager(this);
+        mAudioState = getInitialAudioState();
+        mAudioFocusStreamType = STREAM_NONE;
+    }
 
-    /**
-     * Denotes when the ringer is disabled. This is useful in temporarily disabling the ringer when
-     * the a call is answered/rejected by the user, but the call hasn't actually moved out of the
-     * ringing state.
-     */
-    private boolean mIsRingingDisabled = false;
+    CallAudioState getAudioState() {
+        return mAudioState;
+    }
 
     @Override
     public void onCallAdded(Call call) {
-        if (call.getState() == CallState.RINGING) {
-            mUnansweredCallIds.add(call.getId());
+        updateAudioStreamAndMode();
+        if (CallsManager.getInstance().getCalls().size() == 1) {
+            Log.v(this, "first call added, reseting system audio to default state");
+            setInitialAudioState();
+        } else if (!call.isIncoming()) {
+            // Unmute new outgoing call.
+            setSystemAudioState(false, mAudioState.route, mAudioState.supportedRouteMask);
         }
-        updateAudio();
     }
 
     @Override
     public void onCallRemoved(Call call) {
-        removeFromUnansweredCallIds(call.getId());
-        updateAudio();
+        if (CallsManager.getInstance().getCalls().isEmpty()) {
+            Log.v(this, "all calls removed, reseting system audio to default state");
+            setInitialAudioState();
+        }
+        updateAudioStreamAndMode();
     }
 
     @Override
     public void onCallStateChanged(Call call, CallState oldState, CallState newState) {
-        if (oldState == CallState.RINGING) {
-            removeFromUnansweredCallIds(call.getId());
-        }
-
-        updateAudio();
+        updateAudioStreamAndMode();
     }
 
     @Override
     public void onIncomingCallAnswered(Call call) {
-        mIsRingingDisabled = true;
-        updateAudio();
+        // Unmute new incoming call.
+        setSystemAudioState(false, mAudioState.route, mAudioState.supportedRouteMask);
     }
 
     @Override
-    public void onIncomingCallRejected(Call call) {
-        mIsRingingDisabled = true;
-        updateAudio();
+    public void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall) {
+        updateAudioStreamAndMode();
+        // Ensure that the foreground call knows about the latest audio state.
+        updateAudioForForegroundCall();
     }
 
-    /**
-     * Reads the current state of all calls from CallsManager and sets the appropriate audio modes
-     * as well as triggers the start/stop of the ringer.
-     */
-    private void updateAudio() {
-        CallsManager callsManager = CallsManager.getInstance();
+    void mute(boolean shouldMute) {
+        Log.v(this, "mute, shouldMute: %b", shouldMute);
 
-        boolean hasRingingCall = !mIsRingingDisabled && !mUnansweredCallIds.isEmpty();
-        boolean hasLiveCall = callsManager.hasCallWithState(CallState.ACTIVE, CallState.DIALING);
-
-        int mode = hasRingingCall ? AudioManager.MODE_RINGTONE :
-               hasLiveCall ? AudioManager.MODE_IN_CALL :
-               AudioManager.MODE_NORMAL;
-
-        boolean needsFocus = (mode != AudioManager.MODE_NORMAL);
-
-        // Acquiring focus needs to be first, unlike releasing focus, which happens at the end.
-        if (needsFocus) {
-            acquireFocus(hasRingingCall);
-            setMode(mode);
+        // Don't mute if there are any emergency calls.
+        if (CallsManager.getInstance().hasEmergencyCall()) {
+            shouldMute = false;
+            Log.v(this, "ignoring mute for emergency call");
         }
 
-        if (hasRingingCall) {
-            mRinger.play();
-        } else {
-            mRinger.stop();
-        }
-
-        if (!needsFocus && mHasAudioFocus) {
-            setMode(AudioManager.MODE_NORMAL);
-            releaseFocus();
+        if (mAudioState.isMuted != shouldMute) {
+            setSystemAudioState(shouldMute, mAudioState.route, mAudioState.supportedRouteMask);
         }
     }
 
     /**
-     * Acquires audio focus.
+     * Changed the audio route, for example from earpiece to speaker phone.
      *
-     * @param isForRinging True if this focus is for playing the ringer.
+     * @param route The new audio route to use. See {@link CallAudioState}.
      */
-    private void acquireFocus(boolean isForRinging) {
-        if (!mHasAudioFocus) {
-            int stream = isForRinging ? AudioManager.STREAM_RING : AudioManager.STREAM_VOICE_CALL;
+    void setAudioRoute(int route) {
+        Log.v(this, "setAudioRoute, route: %s", CallAudioState.audioRouteToString(route));
 
-            AudioManager audioManager = getAudioManager();
-            audioManager.requestAudioFocusForCall(stream, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
-            audioManager.setMicrophoneMute(false);
-            audioManager.setSpeakerphoneOn(false);
-            mHasAudioFocus = true;
+        // 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 == CallAudioState.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();
         }
     }
 
     /**
-     * Releases focus.
-     */
-    void releaseFocus() {
-        if (mHasAudioFocus) {
-            AudioManager audioManager = getAudioManager();
+      * 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.
+      */
+    void onHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn) {
+        int newRoute = CallAudioState.ROUTE_EARPIECE;
+        if (newIsPluggedIn) {
+            newRoute = CallAudioState.ROUTE_WIRED_HEADSET;
+        } else if (mWasSpeakerOn) {
+            Call call = CallsManager.getInstance().getForegroundCall();
+            if (call != null && call.isAlive()) {
+                // Restore the speaker state.
+                newRoute = CallAudioState.ROUTE_SPEAKER;
+            }
+        }
+        setSystemAudioState(mAudioState.isMuted, newRoute, calculateSupportedRoutes());
+    }
 
-            // Reset speakerphone and mute in case they were changed by telecomm.
-            audioManager.setMicrophoneMute(false);
-            audioManager.setSpeakerphoneOn(false);
-            audioManager.abandonAudioFocusForCall();
+    private void setSystemAudioState(boolean isMuted, int route, int supportedRouteMask) {
+        CallAudioState oldAudioState = mAudioState;
+        mAudioState = new CallAudioState(isMuted, route, supportedRouteMask);
+        Log.i(this, "changing audio state from %s to %s", oldAudioState, mAudioState);
 
-            mHasAudioFocus = false;
-            Log.v(this, "Focus released");
+        // 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 == CallAudioState.ROUTE_SPEAKER) {
+            if (!mAudioManager.isSpeakerphoneOn()) {
+                Log.i(this, "turning speaker phone on");
+                mAudioManager.setSpeakerphoneOn(true);
+            }
+        } else if (mAudioState.route == CallAudioState.ROUTE_EARPIECE ||
+                mAudioState.route == CallAudioState.ROUTE_WIRED_HEADSET) {
+            // Wired headset and earpiece work the same way
+            if (mAudioManager.isSpeakerphoneOn()) {
+                Log.i(this, "turning speaker phone off");
+                mAudioManager.setSpeakerphoneOn(false);
+            }
+        }
+
+        if (!oldAudioState.equals(mAudioState)) {
+            CallsManager.getInstance().onAudioStateChanged(oldAudioState, mAudioState);
+            updateAudioForForegroundCall();
+        }
+    }
+
+    private void updateAudioStreamAndMode() {
+        Log.v(this, "updateAudioStreamAndMode, mIsRinging: %b", mIsRinging);
+        if (mIsRinging) {
+            requestAudioFocusAndSetMode(AudioManager.STREAM_RING, AudioManager.MODE_RINGTONE);
+        } else {
+            Call call = CallsManager.getInstance().getForegroundCall();
+            if (call != null) {
+                int mode = TelephonyUtil.isCurrentlyPSTNCall(call) ?
+                        AudioManager.MODE_IN_CALL : AudioManager.MODE_IN_COMMUNICATION;
+                requestAudioFocusAndSetMode(AudioManager.STREAM_VOICE_CALL, mode);
+            } else {
+                abandonAudioFocus();
+            }
+        }
+    }
+
+    private void requestAudioFocusAndSetMode(int stream, int mode) {
+        Log.v(this, "setSystemAudioStreamAndMode, stream: %d -> %d", mAudioFocusStreamType, stream);
+        Preconditions.checkState(stream != STREAM_NONE);
+
+        // Only request audio focus once. If the stream type changes there's no need to abandon
+        // and re-request audio focus.  The system doesn't really care about the stream we requested
+        // focus for so just silently switch.
+        if (mAudioFocusStreamType == STREAM_NONE) {
+            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 (mAudioFocusStreamType != STREAM_NONE) {
+            setMode(AudioManager.MODE_NORMAL);
+            Log.v(this, "abandoning audio focus");
+            mAudioManager.abandonAudioFocusForCall();
+            mAudioFocusStreamType = STREAM_NONE;
+        }
     }
 
     /**
      * Sets the audio mode.
      *
-     * @param mode Mode constant from AudioManager.MODE_*.
+     * @param newMode Mode constant from AudioManager.MODE_*.
      */
-    void setMode(int mode) {
-        if (mHasAudioFocus) {
-            AudioManager audioManager = getAudioManager();
-            if (mode != audioManager.getMode()) {
-                Log.v(this, "Audio mode set to %d.", mode);
-                audioManager.setMode(mode);
-                Log.v(this, "Audio mode actually set to %d.", audioManager.getMode());
+    private void setMode(int newMode) {
+        Preconditions.checkState(mAudioFocusStreamType != STREAM_NONE);
+        int oldMode = mAudioManager.getMode();
+        Log.v(this, "Request to change audio mode from %d to %d", oldMode, newMode);
+        if (oldMode != newMode) {
+            mAudioManager.setMode(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 == CallAudioState.ROUTE_WIRED_OR_EARPIECE) {
+            route = CallAudioState.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 = CallAudioState.ROUTE_EARPIECE;
             }
+        }
+        return route;
+    }
+
+    private int calculateSupportedRoutes() {
+        int routeMask = CallAudioState.ROUTE_SPEAKER;
+
+        if (mWiredHeadsetManager.isPluggedIn()) {
+            routeMask |= CallAudioState.ROUTE_WIRED_HEADSET;
         } else {
-            Log.wtf(this, "Trying to set audio mode to %d without focus.", mode);
+            routeMask |= CallAudioState.ROUTE_EARPIECE;
         }
+
+        return routeMask;
     }
 
-    /**
-     * Removes the specified call from the list of unanswered incoming calls.
-     *
-     * @param callId The ID of the call.
-     */
-    private void removeFromUnansweredCallIds(String callId) {
-        if (!mUnansweredCallIds.isEmpty()) {
-            // If the call is the top-most call, then no longer disable the ringer.
-            if (callId.equals(mUnansweredCallIds.get(0))) {
-                mIsRingingDisabled = false;
-            }
-
-            mUnansweredCallIds.remove(callId);
-        }
+    private CallAudioState getInitialAudioState() {
+        int supportedRouteMask = calculateSupportedRoutes();
+        return new CallAudioState(false,
+                selectWiredOrEarpiece(CallAudioState.ROUTE_WIRED_OR_EARPIECE, supportedRouteMask),
+                supportedRouteMask);
     }
 
-    /**
-     * Returns the system audio manager.
-     */
-    private AudioManager getAudioManager() {
-        return (AudioManager) TelecommApp.getInstance().getSystemService(Context.AUDIO_SERVICE);
+    private void setInitialAudioState() {
+        CallAudioState audioState = getInitialAudioState();
+        setSystemAudioState(audioState.isMuted, audioState.route, audioState.supportedRouteMask);
+    }
+
+    private void updateAudioForForegroundCall() {
+        Call call = CallsManager.getInstance().getForegroundCall();
+        if (call != null && call.getCallService() != null) {
+            call.getCallService().onAudioStateChanged(call.getId(), mAudioState);
+        }
     }
 }
diff --git a/src/com/android/telecomm/CallServiceWrapper.java b/src/com/android/telecomm/CallServiceWrapper.java
index 46e99cb..0722ec6 100644
--- a/src/com/android/telecomm/CallServiceWrapper.java
+++ b/src/com/android/telecomm/CallServiceWrapper.java
@@ -19,6 +19,7 @@
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.telecomm.CallAudioState;
 import android.telecomm.CallInfo;
 import android.telecomm.CallService;
 import android.telecomm.CallServiceDescriptor;
@@ -147,7 +148,7 @@
     }
 
     /** See {@link ICallService#hold}. */
-    public void hold(String callId) {
+    void hold(String callId) {
         if (isServiceValid("hold")) {
             try {
                 mServiceInterface.hold(callId);
@@ -158,7 +159,7 @@
     }
 
     /** See {@link ICallService#unhold}. */
-    public void unhold(String callId) {
+    void unhold(String callId) {
         if (isServiceValid("unhold")) {
             try {
                 mServiceInterface.unhold(callId);
@@ -168,6 +169,17 @@
         }
     }
 
+    /** See {@link ICallService#onAudioStateChanged}. */
+    void onAudioStateChanged(String activeCallId, CallAudioState audioState) {
+        if (isServiceValid("onAudioStateChanged")) {
+            try {
+                mServiceInterface.onAudioStateChanged(activeCallId, audioState);
+            } catch (RemoteException e) {
+                Log.e(this, e, "Failed to update audio state for call %s", activeCallId);
+            }
+        }
+    }
+
     /**
      * Starts retrieval of details for an incoming call. Details are returned through the
      * call-service adapter using the specified call ID. Upon failure, the specified error callback
diff --git a/src/com/android/telecomm/CallsManager.java b/src/com/android/telecomm/CallsManager.java
index 22d9ce6..114dd67 100644
--- a/src/com/android/telecomm/CallsManager.java
+++ b/src/com/android/telecomm/CallsManager.java
@@ -18,6 +18,7 @@
 
 import android.net.Uri;
 import android.os.Bundle;
+import android.telecomm.CallAudioState;
 import android.telecomm.CallInfo;
 import android.telecomm.CallServiceDescriptor;
 import android.telecomm.CallState;
@@ -49,6 +50,7 @@
         void onIncomingCallAnswered(Call call);
         void onIncomingCallRejected(Call call);
         void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall);
+        void onAudioStateChanged(CallAudioState oldAudioState, CallAudioState newAudioState);
     }
 
     private static final CallsManager INSTANCE = new CallsManager();
@@ -73,10 +75,12 @@
 
     private final CallsManagerListener mPhoneStateBroadcaster;
 
-    private final CallsManagerListener mCallAudioManager;
+    private final CallAudioManager mCallAudioManager;
 
     private final CallsManagerListener mInCallController;
 
+    private final Ringer mRinger;
+
     private final List<OutgoingCallValidator> mOutgoingCallValidators = Lists.newArrayList();
 
     private final List<IncomingCallValidator> mIncomingCallValidators = Lists.newArrayList();
@@ -90,6 +94,7 @@
         mPhoneStateBroadcaster = new PhoneStateBroadcaster();
         mCallAudioManager = new CallAudioManager();
         mInCallController = new InCallController();
+        mRinger = new Ringer(mCallAudioManager);
     }
 
     static CallsManager getInstance() {
@@ -104,6 +109,19 @@
         return mForegroundCall;
     }
 
+    boolean hasEmergencyCall() {
+        for (Call call : mCalls.values()) {
+            if (call.isEmergencyCall()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    CallAudioState getAudioState() {
+        return mCallAudioManager.getAudioState();
+    }
+
     /**
      * 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
@@ -232,6 +250,7 @@
             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,
@@ -257,6 +276,7 @@
             mPhoneStateBroadcaster.onIncomingCallRejected(call);
             mCallAudioManager.onIncomingCallRejected(call);
             mInCallController.onIncomingCallRejected(call);
+            mRinger.onIncomingCallRejected(call);
 
             call.reject();
         }
@@ -312,6 +332,29 @@
         }
     }
 
+    /** 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 when the audio state changes. */
+    void onAudioStateChanged(CallAudioState oldAudioState, CallAudioState newAudioState) {
+        Log.v(this, "onAudioStateChanged, audioState: %s -> %s", oldAudioState, newAudioState);
+        mCallLogManager.onAudioStateChanged(oldAudioState, newAudioState);
+        mPhoneStateBroadcaster.onAudioStateChanged(oldAudioState, newAudioState);
+        mCallAudioManager.onAudioStateChanged(oldAudioState, newAudioState);
+        mInCallController.onAudioStateChanged(oldAudioState, newAudioState);
+        mRinger.onAudioStateChanged(oldAudioState, newAudioState);
+    }
+
     void markCallAsRinging(String callId) {
         setCallState(callId, CallState.RINGING);
     }
@@ -340,21 +383,6 @@
     }
 
     /**
-     * @return True if there exists a call with the specific state.
-     */
-    boolean hasCallWithState(CallState... states) {
-        for (Call call : mCalls.values()) {
-            for (CallState state : states) {
-                if (call.getState() == state) {
-                    return true;
-                }
-            }
-        }
-
-        return false;
-    }
-
-    /**
      * Cleans up any calls currently associated with the specified call service when the
      * call-service binder disconnects unexpectedly.
      *
@@ -381,6 +409,7 @@
         mPhoneStateBroadcaster.onCallAdded(call);
         mCallAudioManager.onCallAdded(call);
         mInCallController.onCallAdded(call);
+        mRinger.onCallAdded(call);
         updateForegroundCall();
     }
 
@@ -391,6 +420,7 @@
         mPhoneStateBroadcaster.onCallRemoved(call);
         mCallAudioManager.onCallRemoved(call);
         mInCallController.onCallRemoved(call);
+        mRinger.onCallRemoved(call);
         updateForegroundCall();
     }
 
@@ -437,6 +467,7 @@
                 mPhoneStateBroadcaster.onCallStateChanged(call, oldState, newState);
                 mCallAudioManager.onCallStateChanged(call, oldState, newState);
                 mInCallController.onCallStateChanged(call, oldState, newState);
+                mRinger.onCallStateChanged(call, oldState, newState);
                 updateForegroundCall();
             }
         }
@@ -453,7 +484,7 @@
                 newForegroundCall = call;
                 break;
             }
-            if (call.getState() == CallState.ACTIVE) {
+            if (call.isAlive()) {
                 newForegroundCall = call;
                 // Don't break in case there's a ringing call that has priority.
             }
@@ -467,6 +498,7 @@
             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
index c81067f..04b78fe 100644
--- a/src/com/android/telecomm/CallsManagerListenerBase.java
+++ b/src/com/android/telecomm/CallsManagerListenerBase.java
@@ -16,10 +16,11 @@
 
 package com.android.telecomm;
 
+import android.telecomm.CallAudioState;
 import android.telecomm.CallState;
 
 /**
- * Provides a default implementation for listeners for CallsManager.
+ * Provides a default implementation for listeners of CallsManager.
  */
 class CallsManagerListenerBase implements CallsManager.CallsManagerListener {
     @Override
@@ -45,4 +46,8 @@
     @Override
     public void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall) {
     }
+
+    @Override
+    public void onAudioStateChanged(CallAudioState oldAudioState, CallAudioState newAudioState) {
+    }
 }
diff --git a/src/com/android/telecomm/InCallAdapter.java b/src/com/android/telecomm/InCallAdapter.java
index 37dc3b9..e61c919 100644
--- a/src/com/android/telecomm/InCallAdapter.java
+++ b/src/com/android/telecomm/InCallAdapter.java
@@ -88,4 +88,24 @@
             }
         });
     }
+
+    /** {@inheritDoc} */
+    @Override
+    public void mute(final boolean shouldMute) {
+        mHandler.post(new Runnable() {
+            @Override public void run() {
+                mCallsManager.mute(shouldMute);
+            }
+        });
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setAudioRoute(final int route) {
+        mHandler.post(new Runnable() {
+            @Override public void run() {
+                mCallsManager.setAudioRoute(route);
+            }
+        });
+    }
 }
diff --git a/src/com/android/telecomm/InCallController.java b/src/com/android/telecomm/InCallController.java
index 0bfe30d..5c3e3e6 100644
--- a/src/com/android/telecomm/InCallController.java
+++ b/src/com/android/telecomm/InCallController.java
@@ -22,6 +22,7 @@
 import android.content.ServiceConnection;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.telecomm.CallAudioState;
 import android.telecomm.CallInfo;
 import android.telecomm.CallState;
 
@@ -138,6 +139,19 @@
         }
     }
 
+    @Override
+    public void onAudioStateChanged(CallAudioState oldAudioState, CallAudioState newAudioState) {
+        if (mInCallService != null) {
+            Log.i(this, "Calling onAudioStateChanged, audioState: %s -> %s", oldAudioState,
+                    newAudioState);
+            try {
+                mInCallService.onAudioStateChanged(newAudioState);
+            } catch (RemoteException e) {
+                Log.e(this, e, "Exception attempting to update audio state.");
+            }
+        }
+    }
+
     /**
      * Unbinds an existing bound connection to the in-call app.
      */
@@ -198,6 +212,7 @@
             for (Call call : calls) {
                 onCallAdded(call);
             }
+            onAudioStateChanged(null, CallsManager.getInstance().getAudioState());
         } else {
             unbind();
         }
diff --git a/src/com/android/telecomm/Ringer.java b/src/com/android/telecomm/Ringer.java
new file mode 100644
index 0000000..dbf76d5
--- /dev/null
+++ b/src/com/android/telecomm/Ringer.java
@@ -0,0 +1,125 @@
+/*
+ * 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;
+
+import com.google.common.collect.Lists;
+
+import java.util.List;
+
+/**
+ * Controls the ringtone player.
+ */
+final class Ringer extends CallsManagerListenerBase {
+    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<String> mUnansweredCallIds = Lists.newLinkedList();
+
+    private final CallAudioManager mCallAudioManager;
+
+    Ringer(CallAudioManager callAudioManager) {
+        mCallAudioManager = callAudioManager;
+    }
+
+    @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();
+            }
+        }
+    }
+
+    private void startRinging() {
+        AudioManager audioManager = (AudioManager) TelecommApp.getInstance().getSystemService(
+                Context.AUDIO_SERVICE);
+        if (audioManager.getStreamVolume(AudioManager.STREAM_RING) > 0) {
+            Log.v(this, "startRinging");
+            mCallAudioManager.setIsRinging(true);
+            mRingtonePlayer.play();
+        } else {
+            Log.v(this, "startRinging, skipping because volume is 0");
+        }
+    }
+
+    private void stopRinging() {
+        Log.v(this, "stopRinging");
+        mRingtonePlayer.stop();
+        // 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);
+    }
+}
diff --git a/src/com/android/telecomm/WiredHeadsetManager.java b/src/com/android/telecomm/WiredHeadsetManager.java
new file mode 100644
index 0000000..329df71
--- /dev/null
+++ b/src/com/android/telecomm/WiredHeadsetManager.java
@@ -0,0 +1,74 @@
+/*
+ * 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.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+
+/**
+ * Listens for and caches headset state.  Used By the CallAudioManger for maintaining
+ * overall audio state for use in the UI layer. Also provides method for connecting the bluetooth
+ * headset to the phone call.
+ */
+class WiredHeadsetManager {
+    /** 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 CallAudioManager mCallAudioManager;
+    private final WiredHeadsetBroadcastReceiver mReceiver;
+    private boolean mIsPluggedIn;
+
+    WiredHeadsetManager(CallAudioManager callAudioManager) {
+        mCallAudioManager = callAudioManager;
+        mReceiver = new WiredHeadsetBroadcastReceiver();
+
+        Context context = TelecommApp.getInstance();
+        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);
+    }
+
+    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;
+            mCallAudioManager.onHeadsetPluggedInChanged(oldIsPluggedIn, mIsPluggedIn);
+        }
+    }
+}