Audio routing part 2

- Added WIRED_HEADSET and SPEAKER to AudioModes
- Added WiredHeadsetManager class
- AudioRouter now listens to WiredHeadsetManager and PhoneUtils for
  speakerphone
- AudioRouter maintains state across earpiece,headset,bluetooth,speaker
   - Most code copied from InCallScreen for speaker logic
- CallHandlerService listens to audioRouter for audio states
- Moved Wired headset logic from phoneglobals to wiredheadsetmanager
- Better toString for logging Call objects

Change-Id: Iebc8c83934ce5eff6f1918b0610804fadb163b43
diff --git a/common/Android.mk b/common/Android.mk
index 972ed39..2667e45 100644
--- a/common/Android.mk
+++ b/common/Android.mk
@@ -16,6 +16,8 @@
 
 include $(CLEAR_VARS)
 
+LOCAL_STATIC_JAVA_LIBRARIES := guava
+
 LOCAL_SRC_FILES := $(call all-java-files-under, src) \
         $(call all-Iaidl-files-under, src)
 
diff --git a/common/src/com/android/services/telephony/common/AudioMode.java b/common/src/com/android/services/telephony/common/AudioMode.java
index f296a93..b0043ef 100644
--- a/common/src/com/android/services/telephony/common/AudioMode.java
+++ b/common/src/com/android/services/telephony/common/AudioMode.java
@@ -21,10 +21,13 @@
  */
 public class AudioMode {
     // These can be used as a bit mask
-    public static int EARPIECE = 0x00000001;
-    public static int BLUETOOTH = 0x00000002;
+    public static int EARPIECE      = 0x00000001;
+    public static int BLUETOOTH     = 0x00000002;
+    public static int WIRED_HEADSET = 0x00000004;
+    public static int SPEAKER       = 0x00000008;
 
-    public static int ALL_MODES = EARPIECE | BLUETOOTH;
+    public static int WIRED_OR_EARPIECE = EARPIECE | WIRED_HEADSET;
+    public static int ALL_MODES = EARPIECE | BLUETOOTH | WIRED_HEADSET | SPEAKER;
 
     public static String toString(int mode) {
         if ((mode & ~ALL_MODES) != 0x0) {
@@ -38,6 +41,12 @@
         if ((mode & BLUETOOTH) == BLUETOOTH) {
             listAppend(buffer, "BLUETOOTH");
         }
+        if ((mode & WIRED_HEADSET) == WIRED_HEADSET) {
+            listAppend(buffer, "WIRED_HEADSET");
+        }
+        if ((mode & SPEAKER) == SPEAKER) {
+            listAppend(buffer, "SPEAKER");
+        }
 
         return buffer.toString();
     }
diff --git a/common/src/com/android/services/telephony/common/Call.java b/common/src/com/android/services/telephony/common/Call.java
index 68b493f..a3ba003 100644
--- a/common/src/com/android/services/telephony/common/Call.java
+++ b/common/src/com/android/services/telephony/common/Call.java
@@ -16,9 +16,14 @@
 
 package com.android.services.telephony.common;
 
+import com.google.common.collect.ImmutableMap;
+
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import java.nio.Buffer;
+import java.util.Map;
+
 import com.android.internal.telephony.PhoneConstants;
 
 /**
@@ -52,6 +57,16 @@
         public static final int ONHOLD = 6;
     }
 
+    private static final Map<Integer, String> STATE_MAP = ImmutableMap.<Integer, String>builder()
+            .put(Call.State.ACTIVE, "ACTIVE")
+            .put(Call.State.CALL_WAITING, "CALL_WAITING")
+            .put(Call.State.DIALING, "DIALING")
+            .put(Call.State.IDLE, "IDLE")
+            .put(Call.State.INCOMING, "INCOMING")
+            .put(Call.State.ONHOLD, "ONHOLD")
+            .put(Call.State.INVALID, "INVALID")
+            .build();
+
     // Number presentation type for caller id display
     // normal
     public static int PRESENTATION_ALLOWED = PhoneConstants.PRESENTATION_ALLOWED;
@@ -157,4 +172,13 @@
         mCnapName = in.readString();
     }
 
+    @Override
+    public String toString() {
+        StringBuffer buffer = new StringBuffer();
+        buffer.append("callId: ");
+        buffer.append(mCallId);
+        buffer.append(", state: ");
+        buffer.append(STATE_MAP.get(mState));
+        return buffer.toString();
+    }
 }
diff --git a/common/src/com/android/services/telephony/common/ICallHandlerService.aidl b/common/src/com/android/services/telephony/common/ICallHandlerService.aidl
index 33055c5..4b6f913 100644
--- a/common/src/com/android/services/telephony/common/ICallHandlerService.aidl
+++ b/common/src/com/android/services/telephony/common/ICallHandlerService.aidl
@@ -61,5 +61,5 @@
      * Called when the supported audio modes change.
      * {@see AudioMode}
      */
-    void onAudioModeSupportChange(in int modeMask);
+    void onSupportedAudioModeChange(in int modeMask);
 }
diff --git a/src/com/android/phone/AudioRouter.java b/src/com/android/phone/AudioRouter.java
index 2b8201c..812d92c 100644
--- a/src/com/android/phone/AudioRouter.java
+++ b/src/com/android/phone/AudioRouter.java
@@ -19,11 +19,14 @@
 import com.google.common.collect.Lists;
 
 import android.content.Context;
-import android.media.AudioManager;
 import android.os.SystemProperties;
+import android.provider.MediaStore.Audio;
 import android.util.Log;
 
+import com.android.internal.telephony.CallManager;
+import com.android.internal.telephony.PhoneConstants;
 import com.android.phone.BluetoothManager.BluetoothIndicatorListener;
+import com.android.phone.WiredHeadsetManager.WiredHeadsetListener;
 import com.android.services.telephony.common.AudioMode;
 
 import java.util.List;
@@ -31,7 +34,7 @@
 /**
  * Responsible for Routing in-call audio and maintaining routing state.
  */
-/* package */ class AudioRouter implements BluetoothIndicatorListener {
+/* package */ class AudioRouter implements BluetoothIndicatorListener, WiredHeadsetListener {
 
     private static String LOG_TAG = AudioRouter.class.getSimpleName();
     private static final boolean DBG =
@@ -40,13 +43,19 @@
 
     private final Context mContext;
     private final BluetoothManager mBluetoothManager;
+    private final WiredHeadsetManager mWiredHeadsetManager;
+    private final CallManager mCallManager;
     private final List<AudioModeListener> mListeners = Lists.newArrayList();
     private int mAudioMode = AudioMode.EARPIECE;
     private int mPreviousMode = AudioMode.EARPIECE;
+    private int mSupportedModes = AudioMode.ALL_MODES;
 
-    public AudioRouter(Context context, BluetoothManager bluetoothManager) {
+    public AudioRouter(Context context, BluetoothManager bluetoothManager,
+            WiredHeadsetManager wiredHeadsetManager, CallManager callManager) {
         mContext = context;
         mBluetoothManager = bluetoothManager;
+        mWiredHeadsetManager = wiredHeadsetManager;
+        mCallManager = callManager;
 
         init();
     }
@@ -59,11 +68,22 @@
     }
 
     /**
+     * Returns the currently supported audio modes.
+     */
+    public int getSupportedAudioModes() {
+        return mSupportedModes;
+    }
+
+    /**
      * Add a listener to audio mode changes.
      */
     public void addAudioModeListener(AudioModeListener listener) {
         if (!mListeners.contains(listener)) {
             mListeners.add(listener);
+
+            // For first notification, mPreviousAudioMode doesn't make sense.
+            listener.onAudioModeChange(mAudioMode, mAudioMode);
+            listener.onSupportedAudioModeChange(mSupportedModes);
         }
     }
 
@@ -80,13 +100,90 @@
      * Sets the audio mode to the mode that is passed in.
      */
     public void setAudioMode(int mode) {
-        if (AudioMode.BLUETOOTH == mode) {
+        logD("setAudioMode " + AudioMode.toString(mode));
 
-            // dont set mAudioMode because we will get a notificaiton through
-            // onBluetoothIndicationChange if successful
-            toggleBluetooth(true);
-        } else if (AudioMode.EARPIECE == mode) {
-            toggleBluetooth(false);
+        // Since they are mutually exclusive and one is ALWAYS valid, we allow a special input of
+        // WIRED_OR_EARPIECE so that callers dont have to make a call to check which is supported
+        // before calling setAudioMode.
+        if (mode == AudioMode.WIRED_OR_EARPIECE) {
+            mode = AudioMode.WIRED_OR_EARPIECE & mSupportedModes;
+
+            if (mode == 0) {
+                Log.wtf(LOG_TAG, "One of wired headset or earpiece should always be valid.");
+                // assume earpiece in this case.
+                mode = AudioMode.EARPIECE;
+            }
+        }
+
+        if ((calculateSupportedModes() | mode) == 0) {
+            Log.wtf(LOG_TAG, "Asking to set to a mode that is unsupported: " + mode);
+            return;
+        }
+
+        if (AudioMode.SPEAKER == mode) {
+
+            if (!PhoneUtils.isSpeakerOn(mContext)) {
+                // Switch away from Bluetooth, if it was active.
+                if (mBluetoothManager.isBluetoothAvailable() &&
+                        mBluetoothManager.isBluetoothAudioConnected()) {
+
+                    mBluetoothManager.disconnectBluetoothAudio();
+                }
+                PhoneUtils.turnOnSpeaker(mContext, true, true);
+            }
+
+        } else if (AudioMode.BLUETOOTH == mode) {
+
+            // If already connected to BT, there's nothing to do here.
+            if (mBluetoothManager.isBluetoothAvailable() &&
+                    !mBluetoothManager.isBluetoothAudioConnected()) {
+                // Manually turn the speaker phone off, instead of allowing the
+                // Bluetooth audio routing to handle it, since there's other
+                // important state-updating that needs to happen in the
+                // PhoneUtils.turnOnSpeaker() method.
+                // (Similarly, whenever the user turns *on* the speaker, we
+                // manually disconnect the active bluetooth headset;
+                // see toggleSpeaker() and/or switchInCallAudio().)
+                if (PhoneUtils.isSpeakerOn(mContext)) {
+                    PhoneUtils.turnOnSpeaker(mContext, false, true);
+                }
+                mBluetoothManager.connectBluetoothAudio();
+            }
+
+        // Wired headset and earpiece work the same way
+        } else if (AudioMode.EARPIECE == mode || AudioMode.WIRED_HEADSET == mode) {
+
+            // Switch to either the handset earpiece, or the wired headset (if connected.)
+            // (Do this by simply making sure both speaker and bluetooth are off.)
+            if (mBluetoothManager.isBluetoothAvailable() &&
+                    mBluetoothManager.isBluetoothAudioConnected()) {
+                mBluetoothManager.disconnectBluetoothAudio();
+            }
+            if (PhoneUtils.isSpeakerOn(mContext)) {
+                PhoneUtils.turnOnSpeaker(mContext, false, true);
+            }
+
+        } else {
+            Log.wtf(LOG_TAG, "Asking us to set to an unsupported mode " +
+                    AudioMode.toString(mode) + " (" + mode + ")");
+
+            // set it to the current audio mode
+            mode = mAudioMode;
+        }
+
+        updateAudioModeTo(mode);
+    }
+
+    /**
+     * Turns on speaker.
+     */
+    public void setSpeaker(boolean on) {
+        logD("setSpeaker " + on);
+
+        if (on) {
+            setAudioMode(AudioMode.SPEAKER);
+        } else {
+            setAudioMode(AudioMode.WIRED_OR_EARPIECE);
         }
     }
 
@@ -96,47 +193,150 @@
      */
     @Override
     public void onBluetoothIndicationChange(boolean isConnected, BluetoothManager btManager) {
-        int newMode = mAudioMode;
+        logD("onBluetoothIndicationChange " + isConnected);
 
-        if (isConnected) {
-            newMode = AudioMode.BLUETOOTH;
-        } else {
-            newMode = AudioMode.EARPIECE;
-        }
-
-        changeAudioModeTo(newMode);
+        // this will read the new bluetooth mode appropriately
+        updateAudioModeTo(calculateModeFromCurrentState());
     }
 
-    private void toggleBluetooth(boolean on) {
-        if (on) {
-            mBluetoothManager.connectBluetoothAudio();
-        } else {
-            mBluetoothManager.disconnectBluetoothAudio();
+    /**
+     * Called when the state of the wired headset changes.
+     */
+    @Override
+    public void onWiredHeadsetConnection(boolean pluggedIn) {
+        logD("onWireHeadsetConnection " + pluggedIn);
+
+        // Since the presence of a wired headset or bluetooth affects the
+        // speakerphone, update the "speaker" state.  We ONLY want to do
+        // this on the wired headset connect / disconnect events for now
+        // though.
+        PhoneConstants.State phoneState = mCallManager.getState();
+
+        int newMode = mAudioMode;
+
+        // TODO: Consider using our stored states instead
+
+        // Do not change speaker state if phone is not off hook
+        if (phoneState == PhoneConstants.State.OFFHOOK &&
+                !mBluetoothManager.isBluetoothHeadsetAudioOn()) {
+            if (!pluggedIn) {
+                // if the state is "not connected", restore the speaker state.
+                PhoneUtils.restoreSpeakerMode(mContext);
+
+                if (PhoneUtils.isSpeakerOn(mContext)) {
+                    newMode = AudioMode.SPEAKER;
+                } else {
+                    newMode = AudioMode.EARPIECE;
+                }
+            } else {
+                // if the state is "connected", force the speaker off without
+                // storing the state.
+                PhoneUtils.turnOnSpeaker(mContext, false, false);
+
+                newMode = AudioMode.WIRED_HEADSET;
+            }
         }
+
+        updateAudioModeTo(newMode);
     }
 
     private void init() {
         mBluetoothManager.addBluetoothIndicatorListener(this);
+        mWiredHeadsetManager.addWiredHeadsetListener(this);
     }
 
-    private void changeAudioModeTo(int mode) {
+    /**
+     * Reads the state of the world to determine Audio mode.
+     */
+    private int calculateModeFromCurrentState() {
+
+        int mode = AudioMode.EARPIECE;
+
+        // There is a very specific ordering here
+        if (mBluetoothManager.isBluetoothAudioConnectedOrPending()) {
+            mode = AudioMode.BLUETOOTH;
+        } else if (PhoneUtils.isSpeakerOn(mContext)) {
+            mode = AudioMode.SPEAKER;
+        } else if (mWiredHeadsetManager.isHeadsetPlugged()) {
+            mode = AudioMode.WIRED_HEADSET;
+        }
+
+        logD("calculateModeFromCurrentState " + AudioMode.toString(mode));
+
+        return mode;
+    }
+
+    /**
+     * Changes the audio mode to the mode in the parameter.
+     */
+    private void updateAudioModeTo(int mode) {
+        int oldSupportedModes = mSupportedModes;
+
+        mSupportedModes = calculateSupportedModes();
+
+        // This is a weird state that shouldn't happen, but we get called here
+        // once we've changed the audio layers so lets log the error, but assume
+        // that it went through. If it happens it is likely it is a race condition
+        // that will resolve itself when we get updates on the mode change.
+        if ((mSupportedModes & mode) == 0) {
+            Log.e(LOG_TAG, "Setting audio mode to an unsupported mode!");
+        }
+
+        boolean doNotify = oldSupportedModes != mSupportedModes;
+
+        // only update if it really changed.
         if (mAudioMode != mode) {
             Log.i(LOG_TAG, "Audio mode changing to " + AudioMode.toString(mode));
 
             mPreviousMode = mAudioMode;
             mAudioMode = mode;
 
+            doNotify = true;
+        }
+
+        if (doNotify) {
             notifyListeners();
         }
     }
 
+    /**
+     * Gets the updates supported modes from the state of the audio systems.
+     */
+    private int calculateSupportedModes() {
+        // speaker phone always a supported state
+        int supportedModes = AudioMode.SPEAKER;
+
+        if (mWiredHeadsetManager.isHeadsetPlugged()) {
+            supportedModes |= AudioMode.WIRED_HEADSET;
+        } else {
+            supportedModes |= AudioMode.EARPIECE;
+        }
+
+        if (mBluetoothManager.isBluetoothAvailable()) {
+            supportedModes |= AudioMode.BLUETOOTH;
+        }
+
+        return supportedModes;
+    }
+
     private void notifyListeners() {
+        logD("AudioMode: " + AudioMode.toString(mAudioMode));
+        logD("Supported AudioMode: " + AudioMode.toString(mSupportedModes));
+
         for (int i = 0; i < mListeners.size(); i++) {
             mListeners.get(i).onAudioModeChange(mPreviousMode, mAudioMode);
+            mListeners.get(i).onSupportedAudioModeChange(mSupportedModes);
         }
     }
 
     public interface AudioModeListener {
         void onAudioModeChange(int previousMode, int newMode);
+        void onSupportedAudioModeChange(int modeMask);
+    }
+
+    private void logD(String msg) {
+        if (DBG) {
+            Log.d(LOG_TAG, msg);
+        }
     }
 }
diff --git a/src/com/android/phone/BluetoothManager.java b/src/com/android/phone/BluetoothManager.java
index 8d20c25..6f26b95 100644
--- a/src/com/android/phone/BluetoothManager.java
+++ b/src/com/android/phone/BluetoothManager.java
@@ -38,13 +38,12 @@
 import java.util.List;
 
 /**
- * Listens to and caches bluetooth headset state.  Used By the CallHandlerServiceProxy
- * for sending this state to the UI layer.  Also provides method for connection the bluetooth
- * headset to the phone call.  This is used by the CallCommandService to allow the UI to use the
- * bluetooth headset if connected.
+ * Listens to and caches bluetooth headset state.  Used By the AudioRouter for maintaining
+ * overall audio state for use in the UI layer. Also provides method for connecting the bluetooth
+ * headset to the phone call.
  */
 public class BluetoothManager {
-    private static final String LOG_TAG = "InCallScreen";
+    private static final String LOG_TAG = BluetoothManager.class.getSimpleName();
     private static final boolean DBG =
             (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
     private static final boolean VDBG = (PhoneGlobals.DBG_LEVEL >= 2);
diff --git a/src/com/android/phone/CallHandlerServiceProxy.java b/src/com/android/phone/CallHandlerServiceProxy.java
index 165e693..ad69d80 100644
--- a/src/com/android/phone/CallHandlerServiceProxy.java
+++ b/src/com/android/phone/CallHandlerServiceProxy.java
@@ -48,19 +48,22 @@
             (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
 
 
-    private Context mContext;
-    private CallModeler mCallModeler;
-    private ServiceConnection mConnection;
-    private ICallHandlerService mCallHandlerService;
+    private AudioRouter mAudioRouter;
     private CallCommandService mCallCommandService;
+    private CallModeler mCallModeler;
+    private Context mContext;
+    private ICallHandlerService mCallHandlerService;
+    private ServiceConnection mConnection;
 
     public CallHandlerServiceProxy(Context context, CallModeler callModeler,
-            CallCommandService callCommandService) {
+            CallCommandService callCommandService, AudioRouter audioRouter) {
         mContext = context;
         mCallCommandService = callCommandService;
         mCallModeler = callModeler;
+        mAudioRouter = audioRouter;
 
         setupServiceConnection();
+        mAudioRouter.addAudioModeListener(this);
         mCallModeler.addListener(this);
 
         // start the whole process
@@ -108,6 +111,8 @@
 
         if (mCallHandlerService != null) {
             try {
+                if (DBG) Log.d(TAG, "onSupportAudioModeChange");
+
                 mCallHandlerService.onAudioModeChange(newMode);
             } catch (RemoteException e) {
                 Log.e(TAG, "Remote exception handling onAudioModeChange", e);
@@ -115,6 +120,19 @@
         }
     }
 
+    @Override
+    public void onSupportedAudioModeChange(int modeMask) {
+        if (mCallHandlerService != null) {
+            try {
+                if (DBG) Log.d(TAG, "onSupportAudioModeChange: " + AudioMode.toString(modeMask));
+
+                mCallHandlerService.onSupportedAudioModeChange(modeMask);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Remote exception handling onAudioModeChange", e);
+            }
+        }
+    }
+
     /**
      * Sets up the connection with ICallHandlerService
      */
diff --git a/src/com/android/phone/InCallScreen.java b/src/com/android/phone/InCallScreen.java
index f20406b..8f52f29 100644
--- a/src/com/android/phone/InCallScreen.java
+++ b/src/com/android/phone/InCallScreen.java
@@ -2766,119 +2766,14 @@
         PhoneUtils.setMute(newMuteState);
     }
 
-    /**
-     * Toggles whether or not to route in-call audio to the bluetooth
-     * headset, or do nothing (but log a warning) if no bluetooth device
-     * is actually connected.
-     *
-     * TODO: this method is currently unused, but the "audio mode" UI
-     * design is still in flux so let's keep it around for now.
-     * (But if we ultimately end up *not* providing any way for the UI to
-     * simply "toggle bluetooth", we can get rid of this method.)
-     */
     public void toggleBluetooth() {
-        if (VDBG) log("toggleBluetooth()...");
-
-        // TODO(klp): Copy to new AudioManager for new incall
-
-        /*
-        if (isBluetoothAvailable()) {
-            // Toggle the bluetooth audio connection state:
-            if (isBluetoothAudioConnected()) {
-                disconnectBluetoothAudio();
-            } else {
-                // Manually turn the speaker phone off, instead of allowing the
-                // Bluetooth audio routing to handle it, since there's other
-                // important state-updating that needs to happen in the
-                // PhoneUtils.turnOnSpeaker() method.
-                // (Similarly, whenever the user turns *on* the speaker, we
-                // manually disconnect the active bluetooth headset;
-                // see toggleSpeaker() and/or switchInCallAudio().)
-                if (PhoneUtils.isSpeakerOn(this)) {
-                    PhoneUtils.turnOnSpeaker(this, false, true);
-                }
-
-                connectBluetoothAudio();
-            }
-        } else {
-            // Bluetooth isn't available; the onscreen UI shouldn't have
-            // allowed this request in the first place!
-            Log.w(LOG_TAG, "toggleBluetooth(): bluetooth is unavailable");
-        }
-
-        // And update the InCallTouchUi widget (since the "audio mode"
-        // button might need to change its appearance based on the new
-        // audio state.)
-        updateInCallTouchUi();
-        */
+        // TODO(klp) this still here to avoid compile errors until remove
+        // the UI from services/Telephony completely.
     }
 
-    /**
-     * Switches the current routing of in-call audio between speaker,
-     * bluetooth, and the built-in earpiece (or wired headset.)
-     *
-     * This method is used on devices that provide a single 3-way switch
-     * for audio routing.  For devices that provide separate toggles for
-     * Speaker and Bluetooth, see toggleBluetooth() and toggleSpeaker().
-     *
-     * TODO: UI design is still in flux.  If we end up totally
-     * eliminating the concept of Speaker and Bluetooth toggle buttons,
-     * we can get rid of toggleBluetooth() and toggleSpeaker().
-     */
     public void switchInCallAudio(InCallAudioMode newMode) {
-        // TODO(klp): Copy to new AudioManager for new incall
-        /*
-
-        log("switchInCallAudio: new mode = " + newMode);
-        switch (newMode) {
-            case SPEAKER:
-                if (!PhoneUtils.isSpeakerOn(this)) {
-                    // Switch away from Bluetooth, if it was active.
-                    if (isBluetoothAvailable() && isBluetoothAudioConnected()) {
-                        disconnectBluetoothAudio();
-                    }
-                    PhoneUtils.turnOnSpeaker(this, true, true);
-                }
-                break;
-
-            case BLUETOOTH:
-                // If already connected to BT, there's nothing to do here.
-                if (isBluetoothAvailable() && !isBluetoothAudioConnected()) {
-                    // Manually turn the speaker phone off, instead of allowing the
-                    // Bluetooth audio routing to handle it, since there's other
-                    // important state-updating that needs to happen in the
-                    // PhoneUtils.turnOnSpeaker() method.
-                    // (Similarly, whenever the user turns *on* the speaker, we
-                    // manually disconnect the active bluetooth headset;
-                    // see toggleSpeaker() and/or switchInCallAudio().)
-                    if (PhoneUtils.isSpeakerOn(this)) {
-                        PhoneUtils.turnOnSpeaker(this, false, true);
-                    }
-                    connectBluetoothAudio();
-                }
-                break;
-
-            case EARPIECE:
-                // Switch to either the handset earpiece, or the wired headset (if connected.)
-                // (Do this by simply making sure both speaker and bluetooth are off.)
-                if (isBluetoothAvailable() && isBluetoothAudioConnected()) {
-                    disconnectBluetoothAudio();
-                }
-                if (PhoneUtils.isSpeakerOn(this)) {
-                    PhoneUtils.turnOnSpeaker(this, false, true);
-                }
-                break;
-
-            default:
-                Log.wtf(LOG_TAG, "switchInCallAudio: unexpected mode " + newMode);
-                break;
-        }
-
-        // And finally, update the InCallTouchUi widget (since the "audio
-        // mode" button might need to change its appearance based on the
-        // new audio state.)
-        updateInCallTouchUi();
-        */
+        // TODO(klp) this still here to avoid compile errors until remove
+        // the UI from services/Telephony completely.
     }
 
     /**
diff --git a/src/com/android/phone/InCallTouchUi.java b/src/com/android/phone/InCallTouchUi.java
index 993717e..59f2f32 100644
--- a/src/com/android/phone/InCallTouchUi.java
+++ b/src/com/android/phone/InCallTouchUi.java
@@ -867,7 +867,10 @@
         // depending on whether a wired headset is physically plugged in.
         MenuItem earpieceItem = menu.findItem(R.id.audio_mode_earpiece);
         MenuItem wiredHeadsetItem = menu.findItem(R.id.audio_mode_wired_headset);
-        final boolean usingHeadset = mApp.isHeadsetPlugged();
+
+        // TODO(klp): This is a compile stop-gap.  This will all be deleted
+        final boolean usingHeadset = false; //mApp.isHeadsetPlugged();
+
         earpieceItem.setVisible(!usingHeadset);
         earpieceItem.setEnabled(!usingHeadset);
         wiredHeadsetItem.setVisible(usingHeadset);
diff --git a/src/com/android/phone/PhoneGlobals.java b/src/com/android/phone/PhoneGlobals.java
index 3e81175..b09789a 100644
--- a/src/com/android/phone/PhoneGlobals.java
+++ b/src/com/android/phone/PhoneGlobals.java
@@ -69,14 +69,18 @@
 import com.android.phone.common.CallLogAsync;
 import com.android.phone.BluetoothManager.BluetoothIndicatorListener;
 import com.android.phone.OtaUtils.CdmaOtaScreenState;
+import com.android.phone.WiredHeadsetManager.WiredHeadsetListener;
 import com.android.server.sip.SipService;
+import com.android.services.telephony.common.AudioMode;
 
 /**
  * Global state for the telephony subsystem when running in the primary
  * phone process.
  */
 public class PhoneGlobals extends ContextWrapper
-        implements AccelerometerListener.OrientationListener, BluetoothIndicatorListener {
+        implements AccelerometerListener.OrientationListener,
+                   BluetoothIndicatorListener,
+                   WiredHeadsetListener {
     /* package */ static final String LOG_TAG = "PhoneApp";
 
     /**
@@ -103,7 +107,6 @@
 
     // Message codes; see mHandler below.
     private static final int EVENT_SIM_NETWORK_LOCKED = 3;
-    private static final int EVENT_WIRED_HEADSET_PLUG = 7;
     private static final int EVENT_SIM_STATE_CHANGED = 8;
     private static final int EVENT_UPDATE_INCALL_NOTIFICATION = 9;
     private static final int EVENT_DATA_ROAMING_DISCONNECTED = 10;
@@ -179,6 +182,7 @@
     private RejectWithTextMessageManager rejectWithTextMessageManager;
     private IBluetoothHeadsetPhone mBluetoothPhone;
     private Ringer ringer;
+    private WiredHeadsetManager wiredHeadsetManager;
 
     static int mDockState = Intent.EXTRA_DOCK_STATE_UNDOCKED;
     static boolean sVoiceCapable = true;
@@ -200,11 +204,6 @@
     private boolean mIsSimPinEnabled;
     private String mCachedSimPin;
 
-    // True if a wired headset is currently plugged in, based on the state
-    // from the latest Intent.ACTION_HEADSET_PLUG broadcast we received in
-    // mReceiver.onReceive().
-    private boolean mIsHeadsetPlugged;
-
     // True if the keyboard is currently *not* hidden
     // Gets updated whenever there is a Configuration change
     private boolean mIsHardKeyboardOpen;
@@ -337,34 +336,6 @@
                     PhoneUtils.cancelMmiCode(phone);
                     break;
 
-                case EVENT_WIRED_HEADSET_PLUG:
-                    // Since the presence of a wired headset or bluetooth affects the
-                    // speakerphone, update the "speaker" state.  We ONLY want to do
-                    // this on the wired headset connect / disconnect events for now
-                    // though, so we're only triggering on EVENT_WIRED_HEADSET_PLUG.
-
-                    phoneState = mCM.getState();
-                    // Do not change speaker state if phone is not off hook
-                    if (phoneState == PhoneConstants.State.OFFHOOK &&
-                            !bluetoothManager.isBluetoothHeadsetAudioOn()) {
-                        if (!isHeadsetPlugged()) {
-                            // if the state is "not connected", restore the speaker state.
-                            PhoneUtils.restoreSpeakerMode(getApplicationContext());
-                        } else {
-                            // if the state is "connected", force the speaker off without
-                            // storing the state.
-                            PhoneUtils.turnOnSpeaker(getApplicationContext(), false, false);
-                        }
-                    }
-                    // Update the Proximity sensor based on headset state
-                    updateProximitySensorMode(phoneState);
-
-                    // Force TTY state update according to new headset state
-                    if (mTtyEnabled) {
-                        sendMessage(obtainMessage(EVENT_TTY_PREFERRED_MODE_CHANGED, 0));
-                    }
-                    break;
-
                 case EVENT_SIM_STATE_CHANGED:
                     // Marks the event where the SIM goes into ready state.
                     // Right now, this is only used for the PUK-unlocking
@@ -400,7 +371,10 @@
 
                     phoneState = mCM.getState();
                     if (phoneState == PhoneConstants.State.OFFHOOK &&
-                        !isHeadsetPlugged() && !bluetoothManager.isBluetoothHeadsetAudioOn()) {
+                            !wiredHeadsetManager.isHeadsetPlugged() &&
+                            !bluetoothManager.isBluetoothHeadsetAudioOn()) {
+                        audioRouter.setSpeaker(inDockMode);
+
                         PhoneUtils.turnOnSpeaker(getApplicationContext(), inDockMode, true);
                         updateInCallScreen();  // Has no effect if the InCallScreen isn't visible
                     }
@@ -409,7 +383,7 @@
                 case EVENT_TTY_PREFERRED_MODE_CHANGED:
                     // TTY mode is only applied if a headset is connected
                     int ttyMode;
-                    if (isHeadsetPlugged()) {
+                    if (wiredHeadsetManager.isHeadsetPlugged()) {
                         ttyMode = mPreferredTtyMode;
                     } else {
                         ttyMode = Phone.TTY_MODE_OFF;
@@ -555,8 +529,12 @@
             // Plays DTMF Tones
             dtmfTonePlayer = new DTMFTonePlayer(mCM, callModeler);
 
+            // Manages wired headset state
+            wiredHeadsetManager = new WiredHeadsetManager(this);
+            wiredHeadsetManager.addWiredHeadsetListener(this);
+
             // Audio router
-            audioRouter = new AudioRouter(this, bluetoothManager);
+            audioRouter = new AudioRouter(this, bluetoothManager, wiredHeadsetManager, mCM);
 
             // Service used by in-call UI to control calls
             callCommandService = new CallCommandService(this, mCM, callModeler, dtmfTonePlayer,
@@ -564,7 +542,7 @@
 
             // Sends call state to the UI
             callHandlerServiceProxy = new CallHandlerServiceProxy(this, callModeler,
-                    callCommandService);
+                    callCommandService, audioRouter);
 
             // Create the CallNotifer singleton, which handles
             // asynchronous events from the telephony layer (like
@@ -593,7 +571,6 @@
             IntentFilter intentFilter =
                     new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED);
             intentFilter.addAction(TelephonyIntents.ACTION_ANY_DATA_CONNECTION_STATE_CHANGED);
-            intentFilter.addAction(Intent.ACTION_HEADSET_PLUG);
             intentFilter.addAction(Intent.ACTION_DOCK_EVENT);
             intentFilter.addAction(TelephonyIntents.ACTION_SIM_STATE_CHANGED);
             intentFilter.addAction(TelephonyIntents.ACTION_RADIO_TECHNOLOGY_CHANGED);
@@ -721,6 +698,14 @@
         return bluetoothManager;
     }
 
+    /* package */ WiredHeadsetManager getWiredHeadsetManager() {
+        return wiredHeadsetManager;
+    }
+
+    /* package */ AudioRouter getAudioRouter() {
+        return audioRouter;
+    }
+
     /**
      * Returns an Intent that can be used to go to the "Call log"
      * UI (aka CallLogActivity) in the Contacts app.
@@ -1176,7 +1161,7 @@
                 // turn proximity sensor off and turn screen on immediately if
                 // we are using a headset, the keyboard is open, or the device
                 // is being held in a horizontal position.
-                boolean screenOnImmediately = (isHeadsetPlugged()
+                boolean screenOnImmediately = (wiredHeadsetManager.isHeadsetPlugged()
                                                || PhoneUtils.isSpeakerOn(this)
                                                || bluetoothManager.isBluetoothHeadsetAudioOn()
                                                || mIsHardKeyboardOpen);
@@ -1371,15 +1356,6 @@
 
 
     /**
-     * @return true if a wired headset is currently plugged in.
-     *
-     * @see Intent.ACTION_HEADSET_PLUG (which we listen for in mReceiver.onReceive())
-     */
-    boolean isHeadsetPlugged() {
-        return mIsHeadsetPlugged;
-    }
-
-    /**
      * This needs to be called any time the bluetooth headset state or the
      * telephony state changes.
      * TODO(klp): See about a ProximityManager-type class listening to bluetooth
@@ -1393,6 +1369,22 @@
     }
 
     /**
+     * This is called when the wired headset state changes.
+     */
+    @Override
+    public void onWiredHeadsetConnection(boolean pluggedIn) {
+        PhoneConstants.State phoneState = mCM.getState();
+
+        // Update the Proximity sensor based on headset state
+        updateProximitySensorMode(phoneState);
+
+        // Force TTY state update according to new headset state
+        if (mTtyEnabled) {
+            mHandler.sendMessage(mHandler.obtainMessage(EVENT_TTY_PREFERRED_MODE_CHANGED, 0));
+        }
+    }
+
+    /**
      * Receiver for misc intent broadcasts the Phone app cares about.
      */
     private class PhoneAppBroadcastReceiver extends BroadcastReceiver {
@@ -1420,12 +1412,6 @@
                 mHandler.sendEmptyMessage(disconnectedDueToRoaming
                                           ? EVENT_DATA_ROAMING_DISCONNECTED
                                           : EVENT_DATA_ROAMING_OK);
-            } else if (action.equals(Intent.ACTION_HEADSET_PLUG)) {
-                if (VDBG) Log.d(LOG_TAG, "mReceiver: ACTION_HEADSET_PLUG");
-                if (VDBG) Log.d(LOG_TAG, "    state: " + intent.getIntExtra("state", 0));
-                if (VDBG) Log.d(LOG_TAG, "    name: " + intent.getStringExtra("name"));
-                mIsHeadsetPlugged = (intent.getIntExtra("state", 0) == 1);
-                mHandler.sendMessage(mHandler.obtainMessage(EVENT_WIRED_HEADSET_PLUG, 0));
             } else if ((action.equals(TelephonyIntents.ACTION_SIM_STATE_CHANGED)) &&
                     (mPUKEntryActivity != null)) {
                 // if an attempt to un-PUK-lock the device was made, while we're
diff --git a/src/com/android/phone/PhoneUtils.java b/src/com/android/phone/PhoneUtils.java
index f243123..93f3002 100644
--- a/src/com/android/phone/PhoneUtils.java
+++ b/src/com/android/phone/PhoneUtils.java
@@ -2460,11 +2460,15 @@
         boolean activated = false;
         if (PhoneGlobals.mDockState != Intent.EXTRA_DOCK_STATE_UNDOCKED) {
             if (DBG) log("activateSpeakerIfDocked(): In a dock -> may need to turn on speaker.");
-            PhoneGlobals app = PhoneGlobals.getInstance();
-            final BluetoothManager btManager = app.getBluetoothManager();
+            final PhoneGlobals app = PhoneGlobals.getInstance();
 
-            if (!app.isHeadsetPlugged() && !btManager.isBluetoothHeadsetAudioOn()) {
-                turnOnSpeaker(phone.getContext(), true, true);
+            // TODO(klp): This function should move to AudioRouter
+            final BluetoothManager btManager = app.getBluetoothManager();
+            final WiredHeadsetManager wiredHeadset = app.getWiredHeadsetManager();
+            final AudioRouter audioRouter = app.getAudioRouter();
+
+            if (!wiredHeadset.isHeadsetPlugged() && !btManager.isBluetoothHeadsetAudioOn()) {
+                audioRouter.setSpeaker(true);
                 activated = true;
             }
         }
diff --git a/src/com/android/phone/WiredHeadsetManager.java b/src/com/android/phone/WiredHeadsetManager.java
new file mode 100644
index 0000000..4f09db6
--- /dev/null
+++ b/src/com/android/phone/WiredHeadsetManager.java
@@ -0,0 +1,113 @@
+/*
+ * 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.phone;
+
+import com.google.android.collect.Lists;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.SystemProperties;
+import android.util.Log;
+
+import java.util.List;
+
+/**
+ * Listens for and caches headset state.  Used By the AudioRouter for maintaining
+ * overall audio state for use in the UI layer. Also provides method for connecting the bluetooth
+ * headset to the phone call.
+ */
+public class WiredHeadsetManager {
+    private static final String LOG_TAG = WiredHeadsetManager.class.getSimpleName();
+    private static final boolean DBG =
+            (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
+    private static final boolean VDBG = (PhoneGlobals.DBG_LEVEL >= 2);
+
+    // True if a wired headset is currently plugged in, based on the state
+    // from the latest Intent.ACTION_HEADSET_PLUG broadcast we received in
+    // mReceiver.onReceive().
+    private boolean mIsHeadsetPlugged = false;
+    private final WiredHeadsetBroadcastReceiver mReceiver;
+    private final List<WiredHeadsetListener> mListeners = Lists.newArrayList();
+
+    public WiredHeadsetManager(Context context) {
+        mReceiver = new WiredHeadsetBroadcastReceiver();
+
+        // Register for misc other intent broadcasts.
+        IntentFilter intentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
+        context.registerReceiver(mReceiver, intentFilter);
+    }
+
+    /**
+     * Returns connection state of the wires headset.
+     */
+    public boolean isHeadsetPlugged() {
+        return mIsHeadsetPlugged;
+    }
+
+    /**
+     * Add a listener for wired headset connection status.
+     */
+    public void addWiredHeadsetListener(WiredHeadsetListener listener) {
+        if (!mListeners.contains(listener)) {
+            mListeners.add(listener);
+        }
+    }
+
+    /**
+     * Called when we get an event from the system for the headset connection state.
+     */
+    private void onHeadsetConnection(boolean pluggedIn) {
+        if (DBG) {
+            Log.d(LOG_TAG, "Wired headset connected: " +  pluggedIn);
+        }
+        mIsHeadsetPlugged = pluggedIn;
+
+        notifyListeners();
+    }
+
+    private void notifyListeners() {
+        for (int i = 0; i < mListeners.size(); i++) {
+            mListeners.get(i).onWiredHeadsetConnection(mIsHeadsetPlugged);
+        }
+    }
+
+    /**
+     * Receiver for misc intent broadcasts the BluetoothManager cares about.
+     */
+    private class WiredHeadsetBroadcastReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            String action = intent.getAction();
+
+            if (action.equals(Intent.ACTION_HEADSET_PLUG)) {
+                if (VDBG) Log.d(LOG_TAG, "mReceiver: ACTION_HEADSET_PLUG");
+                if (VDBG) Log.d(LOG_TAG, "    state: " + intent.getIntExtra("state", 0));
+                if (VDBG) Log.d(LOG_TAG, "    name: " + intent.getStringExtra("name"));
+                onHeadsetConnection(intent.getIntExtra("state", 0) == 1);
+            }
+        }
+    }
+
+    /**
+     * Listeners for those that want to know about the headset state.
+     */
+    public interface WiredHeadsetListener {
+        void onWiredHeadsetConnection(boolean pluggedIn);
+    }
+}