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/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);
+        }
     }
 }