Audio Routing support in UI

Changes:
- AudioModeProvider - receives audio mode changes from
  CallHandlerService
- CallButtonPresenter listens to AudioModeProvider so that it can use
  those changes in the UI.
- CallButtonFragment uses the audio mode from Presenter() to display:
   - The correct layers for the supported modes
   - The popup menu when bluetooth is enabled

Change-Id: I8ec4024f7bbb5e3b1cfdaae20a4dd2f85ae802cf
diff --git a/InCallUI/res/drawable-hdpi/ic_ab_dialer_holo_dark.png b/InCallUI/res/drawable-hdpi/ic_ab_dialer_holo_dark.png
new file mode 100644
index 0000000..ecfeb2d
--- /dev/null
+++ b/InCallUI/res/drawable-hdpi/ic_ab_dialer_holo_dark.png
Binary files differ
diff --git a/InCallUI/res/drawable-hdpi/ic_bluetooth_holo_dark.png b/InCallUI/res/drawable-hdpi/ic_bluetooth_holo_dark.png
new file mode 100644
index 0000000..ba22b0f
--- /dev/null
+++ b/InCallUI/res/drawable-hdpi/ic_bluetooth_holo_dark.png
Binary files differ
diff --git a/InCallUI/res/drawable-ldrtl-hdpi/ic_ab_dialer_holo_dark.png b/InCallUI/res/drawable-ldrtl-hdpi/ic_ab_dialer_holo_dark.png
new file mode 100644
index 0000000..7ec3709
--- /dev/null
+++ b/InCallUI/res/drawable-ldrtl-hdpi/ic_ab_dialer_holo_dark.png
Binary files differ
diff --git a/InCallUI/res/drawable-ldrtl-mdpi/ic_ab_dialer_holo_dark.png b/InCallUI/res/drawable-ldrtl-mdpi/ic_ab_dialer_holo_dark.png
new file mode 100644
index 0000000..6020b3d
--- /dev/null
+++ b/InCallUI/res/drawable-ldrtl-mdpi/ic_ab_dialer_holo_dark.png
Binary files differ
diff --git a/InCallUI/res/drawable-ldrtl-xhdpi/ic_ab_dialer_holo_dark.png b/InCallUI/res/drawable-ldrtl-xhdpi/ic_ab_dialer_holo_dark.png
new file mode 100644
index 0000000..c42e7e3
--- /dev/null
+++ b/InCallUI/res/drawable-ldrtl-xhdpi/ic_ab_dialer_holo_dark.png
Binary files differ
diff --git a/InCallUI/res/drawable-mdpi/ic_ab_dialer_holo_dark.png b/InCallUI/res/drawable-mdpi/ic_ab_dialer_holo_dark.png
new file mode 100644
index 0000000..51ad9e3
--- /dev/null
+++ b/InCallUI/res/drawable-mdpi/ic_ab_dialer_holo_dark.png
Binary files differ
diff --git a/InCallUI/res/drawable-mdpi/ic_bluetooth_holo_dark.png b/InCallUI/res/drawable-mdpi/ic_bluetooth_holo_dark.png
new file mode 100644
index 0000000..fb69031
--- /dev/null
+++ b/InCallUI/res/drawable-mdpi/ic_bluetooth_holo_dark.png
Binary files differ
diff --git a/InCallUI/res/drawable-xhdpi/ic_ab_dialer_holo_dark.png b/InCallUI/res/drawable-xhdpi/ic_ab_dialer_holo_dark.png
new file mode 100644
index 0000000..3f43a82
--- /dev/null
+++ b/InCallUI/res/drawable-xhdpi/ic_ab_dialer_holo_dark.png
Binary files differ
diff --git a/InCallUI/res/drawable-xhdpi/ic_bluetooth_holo_dark.png b/InCallUI/res/drawable-xhdpi/ic_bluetooth_holo_dark.png
new file mode 100644
index 0000000..24cb893
--- /dev/null
+++ b/InCallUI/res/drawable-xhdpi/ic_bluetooth_holo_dark.png
Binary files differ
diff --git a/InCallUI/res/menu/incall_audio_mode_menu.xml b/InCallUI/res/menu/incall_audio_mode_menu.xml
new file mode 100644
index 0000000..0139895
--- /dev/null
+++ b/InCallUI/res/menu/incall_audio_mode_menu.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 Google Inc.
+
+     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.
+-->
+
+<!-- "Audio mode" popup menu for the in-call UI. -->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- TODO: Need final icon assets.  Also, PopupMenu currently ignores the
+         android:icon attribute anyway(!) -->
+    <item android:id="@+id/audio_mode_speaker"
+          android:icon="@drawable/ic_sound_holo_dark"
+          android:title="@string/audio_mode_speaker" />
+
+    <!-- We display *either* "earpiece" or "wired headset", never both,
+         depending on whether a wired headset is physically plugged in
+         (see InCallTouchUi.showAudioModePopup().) -->
+    <item android:id="@+id/audio_mode_earpiece"
+          android:icon="@drawable/ic_ab_dialer_holo_dark"
+          android:title="@string/audio_mode_earpiece" />
+    <item android:id="@+id/audio_mode_wired_headset"
+          android:icon="@drawable/ic_ab_dialer_holo_dark"
+          android:title="@string/audio_mode_wired_headset" />
+
+    <item android:id="@+id/audio_mode_bluetooth"
+          android:icon="@drawable/ic_bluetooth_holo_dark"
+          android:title="@string/audio_mode_bluetooth" />
+</menu>
diff --git a/InCallUI/src/com/android/incallui/AnswerFragment.java b/InCallUI/src/com/android/incallui/AnswerFragment.java
index 196a66b..79247c5 100644
--- a/InCallUI/src/com/android/incallui/AnswerFragment.java
+++ b/InCallUI/src/com/android/incallui/AnswerFragment.java
@@ -69,6 +69,11 @@
     }
 
     @Override
+    public void onDestroyView() {
+        getPresenter().onUiUnready(this);
+    }
+
+    @Override
     public void showAnswerUi(boolean show) {
         getView().setVisibility(show ? View.VISIBLE : View.GONE);
     }
diff --git a/InCallUI/src/com/android/incallui/AudioModeProvider.java b/InCallUI/src/com/android/incallui/AudioModeProvider.java
new file mode 100644
index 0000000..abbbfb6
--- /dev/null
+++ b/InCallUI/src/com/android/incallui/AudioModeProvider.java
@@ -0,0 +1,89 @@
+/*
+ * 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.incallui;
+
+import com.google.android.collect.Lists;
+
+import com.android.services.telephony.common.AudioMode;
+
+import java.util.List;
+
+
+/**
+ * Proxy class for getting and setting the audio mode.
+ */
+/* package */ class AudioModeProvider {
+
+    private static AudioModeProvider sAudioModeProvider;
+    private int mAudioMode = AudioMode.EARPIECE;
+    private int mSupportedModes = AudioMode.ALL_MODES;
+    private final List<AudioModeListener> mListeners = Lists.newArrayList();
+
+    public static synchronized AudioModeProvider getInstance() {
+        if (sAudioModeProvider == null) {
+            sAudioModeProvider = new AudioModeProvider();
+        }
+        return sAudioModeProvider;
+    }
+
+    /**
+     * Access only through getInstance()
+     */
+    private AudioModeProvider() {
+    }
+
+    public void onAudioModeChange(int newMode) {
+        mAudioMode = newMode;
+
+        for (AudioModeListener l : mListeners) {
+            l.onAudioMode(mAudioMode);
+        }
+    }
+
+    public void onSupportedAudioModeChange(int newModeMask) {
+        mSupportedModes = newModeMask;
+
+        for (AudioModeListener l : mListeners) {
+            l.onSupportedAudioMode(mSupportedModes);
+        }
+    }
+
+    public void addListener(AudioModeListener listener) {
+        if (!mListeners.contains(listener)) {
+            mListeners.add(listener);
+        }
+    }
+
+    public void removeListener(AudioModeListener listener) {
+        if (mListeners.contains(listener)) {
+            mListeners.remove(listener);
+        }
+    }
+
+    public int getSupportedModes() {
+        return mSupportedModes;
+    }
+
+    public int getAudioMode() {
+        return mAudioMode;
+    }
+
+    /* package */ interface AudioModeListener {
+        void onAudioMode(int newMode);
+        void onSupportedAudioMode(int modeMask);
+    }
+}
diff --git a/InCallUI/src/com/android/incallui/CallButtonFragment.java b/InCallUI/src/com/android/incallui/CallButtonFragment.java
index 772251d..175fe4d 100644
--- a/InCallUI/src/com/android/incallui/CallButtonFragment.java
+++ b/InCallUI/src/com/android/incallui/CallButtonFragment.java
@@ -17,39 +17,48 @@
 package com.android.incallui;
 
 import android.content.Context;
+import android.graphics.drawable.LayerDrawable;
 import android.media.AudioManager;
 import android.os.Bundle;
 import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.CompoundButton;
+import android.widget.PopupMenu;
+import android.widget.PopupMenu.OnDismissListener;
+import android.widget.PopupMenu.OnMenuItemClickListener;
 import android.widget.ToggleButton;
 
+import com.android.services.telephony.common.AudioMode;
+
 /**
  * Fragment for call control buttons
  */
 public class CallButtonFragment extends BaseFragment<CallButtonPresenter>
-        implements CallButtonPresenter.CallButtonUi {
+        implements CallButtonPresenter.CallButtonUi, OnMenuItemClickListener,
+                OnDismissListener {
 
     private ToggleButton mMuteButton;
     private ToggleButton mAudioButton;
     private ToggleButton mHoldButton;
     private ToggleButton mShowDialpadButton;
 
+    private PopupMenu mAudioModePopup;
+    private boolean mAudioModePopupVisible;
     private View mEndCallButton;
 
     @Override
     CallButtonPresenter createPresenter() {
-        return new CallButtonPresenter();
+        // TODO: find a cleaner way to include audio mode provider than
+        // having a singleton instance.
+        return new CallButtonPresenter(AudioModeProvider.getInstance());
     }
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
-
-        final AudioManager audioManager = (AudioManager) getActivity().getSystemService(
-                Context.AUDIO_SERVICE);
-        getPresenter().init(audioManager);
     }
 
     @Override
@@ -74,10 +83,10 @@
         });
 
         mAudioButton = (ToggleButton) parent.findViewById(R.id.audioButton);
-        mAudioButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+        mAudioButton.setOnClickListener(new View.OnClickListener() {
             @Override
-            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
-                getPresenter().speakerClicked(isChecked);
+            public void onClick(View view) {
+                onAudioButtonClicked();
             }
         });
 
@@ -103,6 +112,14 @@
     @Override
     public void onViewCreated(View view, Bundle savedInstanceState) {
         getPresenter().onUiReady(this);
+
+        // set the buttons
+        updateAudioButtons(getPresenter().getSupportedAudio());
+    }
+
+    @Override
+    public void onDestroyView() {
+        getPresenter().onUiUnready(this);
     }
 
     @Override
@@ -119,20 +136,259 @@
         mMuteButton.setChecked(value);
     }
 
-    /**
-     * TODO(klp): Rename this from setSpeaker() to setAudio() once it does more than speakerphone.
-     */
-    @Override
-    public void setSpeaker(boolean value) {
-        mAudioButton.setChecked(value);
-    }
-
     @Override
     public void setHold(boolean value) {
         mHoldButton.setChecked(value);
     }
 
     @Override
+    public void setAudio(int mode) {
+    }
+
+    @Override
+    public void setSupportedAudio(int modeMask) {
+        updateAudioButtons(modeMask);
+        refreshAudioModePopup();
+    }
+
+    @Override
+    public boolean onMenuItemClick(MenuItem item) {
+        Logger.d(this, "- onMenuItemClick: " + item);
+        Logger.d(this, "  id: " + item.getItemId());
+        Logger.d(this, "  title: '" + item.getTitle() + "'");
+
+        int mode = AudioMode.WIRED_OR_EARPIECE;
+
+        switch (item.getItemId()) {
+            case R.id.audio_mode_speaker:
+                mode = AudioMode.SPEAKER;
+                break;
+            case R.id.audio_mode_earpiece:
+            case R.id.audio_mode_wired_headset:
+                // InCallAudioMode.EARPIECE means either the handset earpiece,
+                // or the wired headset (if connected.)
+                mode = AudioMode.WIRED_OR_EARPIECE;
+                break;
+            case R.id.audio_mode_bluetooth:
+                mode = AudioMode.BLUETOOTH;
+                break;
+            default:
+                Logger.e(this, "onMenuItemClick:  unexpected View ID " + item.getItemId()
+                        + " (MenuItem = '" + item + "')");
+                break;
+        }
+
+        getPresenter().setAudioMode(mode);
+
+        return true;
+    }
+
+    // PopupMenu.OnDismissListener implementation; see showAudioModePopup().
+    // This gets called when the PopupMenu gets dismissed for *any* reason, like
+    // the user tapping outside its bounds, or pressing Back, or selecting one
+    // of the menu items.
+    @Override
+    public void onDismiss(PopupMenu menu) {
+        Logger.d(this, "- onDismiss: " + menu);
+        mAudioModePopupVisible = false;
+    }
+
+    /**
+     * Checks for supporting modes.  If bluetooth is supported, it uses the audio
+     * pop up menu.  Otherwise, it toggles the speakerphone.
+     */
+    private void onAudioButtonClicked() {
+        Logger.d(this, "onAudioButtonClicked: " +
+                AudioMode.toString(getPresenter().getSupportedAudio()));
+
+        if (isSupported(AudioMode.BLUETOOTH)) {
+            showAudioModePopup();
+        } else {
+            getPresenter().toggleSpeakerphone();
+        }
+    }
+
+    /**
+     * Refreshes the "Audio mode" popup if it's visible.  This is useful
+     * (for example) when a wired headset is plugged or unplugged,
+     * since we need to switch back and forth between the "earpiece"
+     * and "wired headset" items.
+     *
+     * This is safe to call even if the popup is already dismissed, or even if
+     * you never called showAudioModePopup() in the first place.
+     */
+    public void refreshAudioModePopup() {
+        if (mAudioModePopup != null && mAudioModePopupVisible) {
+            // Dismiss the previous one
+            mAudioModePopup.dismiss();  // safe even if already dismissed
+            // And bring up a fresh PopupMenu
+            showAudioModePopup();
+        }
+    }
+
+    /**
+     * Updates the audio button so that the appriopriate visual layers
+     * are visible based on the supported audio formats.
+     */
+    private void updateAudioButtons(int supportedModes) {
+        final boolean bluetoothSupported = isSupported(AudioMode.BLUETOOTH);
+        final boolean speakerSupported = isSupported(AudioMode.SPEAKER);
+
+        boolean audioButtonEnabled = false;
+        boolean audioButtonChecked = false;
+        boolean showMoreIndicator = false;
+
+        boolean showBluetoothIcon = false;
+        boolean showSpeakerphoneOnIcon = false;
+        boolean showSpeakerphoneOffIcon = false;
+        boolean showHandsetIcon = false;
+
+        boolean showToggleIndicator = false;
+
+        if (bluetoothSupported) {
+            Logger.d(this, "updateAudioButtons - popup menu mode");
+
+            audioButtonEnabled = true;
+            showMoreIndicator = true;
+            // The audio button is NOT a toggle in this state.  (And its
+            // setChecked() state is irrelevant since we completely hide the
+            // btn_compound_background layer anyway.)
+
+            // Update desired layers:
+            if (isAudio(AudioMode.BLUETOOTH)) {
+                showBluetoothIcon = true;
+            } else if (isAudio(AudioMode.SPEAKER)) {
+                showSpeakerphoneOnIcon = true;
+            } else {
+                showHandsetIcon = true;
+                // TODO: if a wired headset is plugged in, that takes precedence
+                // over the handset earpiece.  If so, maybe we should show some
+                // sort of "wired headset" icon here instead of the "handset
+                // earpiece" icon.  (Still need an asset for that, though.)
+            }
+        } else if (speakerSupported) {
+            Logger.d(this, "updateAudioButtons - speaker toggle mode");
+
+            audioButtonEnabled = true;
+
+            // The audio button *is* a toggle in this state, and indicated the
+            // current state of the speakerphone.
+            audioButtonChecked = isAudio(AudioMode.SPEAKER);
+
+            // update desired layers:
+            showToggleIndicator = true;
+
+            showSpeakerphoneOnIcon = isAudio(AudioMode.SPEAKER);
+            showSpeakerphoneOffIcon = !showSpeakerphoneOnIcon;
+        } else {
+            Logger.d(this, "updateAudioButtons - disabled...");
+
+            // The audio button is a toggle in this state, but that's mostly
+            // irrelevant since it's always disabled and unchecked.
+            audioButtonEnabled = false;
+            audioButtonChecked = false;
+
+            // update desired layers:
+            showToggleIndicator = true;
+            showSpeakerphoneOffIcon = true;
+        }
+
+        // Finally, update it all!
+
+        Logger.v(this, "audioButtonEnabled: " + audioButtonEnabled);
+        Logger.v(this, "audioButtonChecked: " + audioButtonChecked);
+        Logger.v(this, "showMoreIndicator: " + showMoreIndicator);
+        Logger.v(this, "showBluetoothIcon: " + showBluetoothIcon);
+        Logger.v(this, "showSpeakerphoneOnIcon: " + showSpeakerphoneOnIcon);
+        Logger.v(this, "showSpeakerphoneOffIcon: " + showSpeakerphoneOffIcon);
+        Logger.v(this, "showHandsetIcon: " + showHandsetIcon);
+
+        // Constants for Drawable.setAlpha()
+        final int HIDDEN = 0;
+        final int VISIBLE = 255;
+
+        mAudioButton.setEnabled(audioButtonEnabled);
+        mAudioButton.setChecked(audioButtonChecked);
+
+        final LayerDrawable layers = (LayerDrawable) mAudioButton.getBackground();
+        Logger.d(this, "'layers' drawable: " + layers);
+
+        layers.findDrawableByLayerId(R.id.compoundBackgroundItem)
+                .setAlpha(showToggleIndicator ? VISIBLE : HIDDEN);
+
+        layers.findDrawableByLayerId(R.id.moreIndicatorItem)
+                .setAlpha(showMoreIndicator ? VISIBLE : HIDDEN);
+
+        layers.findDrawableByLayerId(R.id.bluetoothItem)
+                .setAlpha(showBluetoothIcon ? VISIBLE : HIDDEN);
+
+        layers.findDrawableByLayerId(R.id.handsetItem)
+                .setAlpha(showHandsetIcon ? VISIBLE : HIDDEN);
+
+        layers.findDrawableByLayerId(R.id.speakerphoneOnItem)
+                .setAlpha(showSpeakerphoneOnIcon ? VISIBLE : HIDDEN);
+
+        layers.findDrawableByLayerId(R.id.speakerphoneOffItem)
+                .setAlpha(showSpeakerphoneOffIcon ? VISIBLE : HIDDEN);
+    }
+
+    private void showAudioModePopup() {
+        Logger.d(this, "showAudioPopup()...");
+
+        mAudioModePopup = new PopupMenu(getView().getContext(), mAudioButton /* anchorView */);
+        mAudioModePopup.getMenuInflater().inflate(R.menu.incall_audio_mode_menu,
+                mAudioModePopup.getMenu());
+        mAudioModePopup.setOnMenuItemClickListener(this);
+        mAudioModePopup.setOnDismissListener(this);
+
+        final Menu menu = mAudioModePopup.getMenu();
+
+        // TODO: Still need to have the "currently active" audio mode come
+        // up pre-selected (or focused?) with a blue highlight.  Still
+        // need exact visual design, and possibly framework support for this.
+        // See comments below for the exact logic.
+
+        final MenuItem speakerItem = menu.findItem(R.id.audio_mode_speaker);
+        speakerItem.setEnabled(isSupported(AudioMode.SPEAKER));
+        // TODO: Show speakerItem as initially "selected" if
+        // speaker is on.
+
+        // We display *either* "earpiece" or "wired headset", never both,
+        // depending on whether a wired headset is physically plugged in.
+        final MenuItem earpieceItem = menu.findItem(R.id.audio_mode_earpiece);
+        final MenuItem wiredHeadsetItem = menu.findItem(R.id.audio_mode_wired_headset);
+
+        final boolean usingHeadset = isSupported(AudioMode.WIRED_HEADSET);
+        earpieceItem.setVisible(!usingHeadset);
+        earpieceItem.setEnabled(!usingHeadset);
+        wiredHeadsetItem.setVisible(usingHeadset);
+        wiredHeadsetItem.setEnabled(usingHeadset);
+        // TODO: Show the above item (either earpieceItem or wiredHeadsetItem)
+        // as initially "selected" if speakerOn and
+        // bluetoothIndicatorOn are both false.
+
+        final MenuItem bluetoothItem = menu.findItem(R.id.audio_mode_bluetooth);
+        bluetoothItem.setEnabled(isSupported(AudioMode.BLUETOOTH));
+        // TODO: Show bluetoothItem as initially "selected" if
+        // bluetoothIndicatorOn is true.
+
+        mAudioModePopup.show();
+
+        // Unfortunately we need to manually keep track of the popup menu's
+        // visiblity, since PopupMenu doesn't have an isShowing() method like
+        // Dialogs do.
+        mAudioModePopupVisible = true;
+    }
+
+    private boolean isSupported(int mode) {
+        return (mode == (getPresenter().getSupportedAudio() & mode));
+    }
+
+    private boolean isAudio(int mode) {
+        return (mode == getPresenter().getAudioMode());
+    }
+
+    @Override
     public void displayDialpad(boolean value) {
         mShowDialpadButton.setChecked(value);
         if (getActivity() != null && getActivity() instanceof InCallActivity) {
diff --git a/InCallUI/src/com/android/incallui/CallButtonPresenter.java b/InCallUI/src/com/android/incallui/CallButtonPresenter.java
index 35e6ab7..d4b2cf2 100644
--- a/InCallUI/src/com/android/incallui/CallButtonPresenter.java
+++ b/InCallUI/src/com/android/incallui/CallButtonPresenter.java
@@ -20,28 +20,38 @@
 
 import android.media.AudioManager;
 
+import com.android.incallui.AudioModeProvider.AudioModeListener;
 import com.android.incallui.InCallPresenter.InCallState;
 import com.android.incallui.InCallPresenter.InCallStateListener;
+import com.android.services.telephony.common.AudioMode;
 import com.android.services.telephony.common.Call;
 
 /**
  * Logic for call buttons.
  */
 public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButtonUi>
-        implements InCallStateListener {
+        implements InCallStateListener, AudioModeListener {
 
-    private AudioManager mAudioManager;
     private Call mCall;
+    private final AudioModeProvider mAudioModeProvider;
 
-    public void init(AudioManager audioManager) {
-        mAudioManager = audioManager;
+    public CallButtonPresenter(AudioModeProvider audioModeProvider) {
+
+        // AudioModeProvider works effectively as a pass through. However, if we
+        // had this presenter listen for changes directly, it would have to live forever
+        // or risk missing important updates.
+        mAudioModeProvider = audioModeProvider;
+        mAudioModeProvider.addListener(this);
     }
 
     @Override
     public void onUiReady(CallButtonUi ui) {
         super.onUiReady(ui);
-        getUi().setMute(mAudioManager.isMicrophoneMute());
-        getUi().setSpeaker(mAudioManager.isSpeakerphoneOn());
+    }
+
+    @Override
+    public void onUiUnready(CallButtonUi ui) {
+        mAudioModeProvider.removeListener(this);
     }
 
     @Override
@@ -55,23 +65,62 @@
         }
     }
 
+    @Override
+    public void onAudioMode(int mode) {
+        getUi().setAudio(mode);
+    }
+
+    @Override
+    public void onSupportedAudioMode(int mask) {
+        getUi().setSupportedAudio(mask);
+    }
+
+    public int getAudioMode() {
+        return mAudioModeProvider.getAudioMode();
+    }
+
+    public int getSupportedAudio() {
+        return mAudioModeProvider.getSupportedModes();
+    }
+
+    public void setAudioMode(int mode) {
+
+        // TODO: Set a intermediate state in this presenter until we get
+        // an update for onAudioMode().  This will make UI response immediate
+        // if it turns out to be slow
+
+        Logger.d(this, "Sending new Audio Mode: " + AudioMode.toString(mode));
+        CallCommandClient.getInstance().setAudioMode(mode);
+    }
+
+    /**
+     * Function assumes that bluetooth is not supported.
+     */
+    public void toggleSpeakerphone() {
+        // this function should not be called if bluetooth is available
+        if (0 != (AudioMode.BLUETOOTH & mAudioModeProvider.getSupportedModes())) {
+
+            // It's clear the UI is off, so update the supported mode once again.
+            Logger.e(this, "toggling speakerphone not allowed when bluetooth supported.");
+            getUi().setSupportedAudio(mAudioModeProvider.getSupportedModes());
+            return;
+        }
+
+        int newMode = AudioMode.SPEAKER;
+
+        // if speakerphone is already on, change to wired/earpiece
+        if (mAudioModeProvider.getAudioMode() == AudioMode.SPEAKER) {
+            newMode = AudioMode.WIRED_OR_EARPIECE;
+        }
+
+        setAudioMode(newMode);
+    }
+
     public void endCallClicked() {
         Preconditions.checkNotNull(mCall);
 
         // TODO(klp): hook up call id.
         CallCommandClient.getInstance().disconnectCall(mCall.getCallId());
-
-        // TODO(klp): Remove once all state is gathered from CallList.
-        //            This will be wrong when you disconnect from a call if
-        //            the user has another call on hold.
-        reset();
-    }
-
-    private void reset() {
-        getUi().setVisible(false);
-        getUi().setMute(false);
-        getUi().setSpeaker(false);
-        getUi().setHold(false);
     }
 
     public void muteClicked(boolean checked) {
@@ -81,13 +130,6 @@
         getUi().setMute(checked);
     }
 
-    public void speakerClicked(boolean checked) {
-        Logger.d(this, "turning on speaker: " + checked);
-
-        CallCommandClient.getInstance().turnSpeakerOn(checked);
-        getUi().setSpeaker(checked);
-    }
-
     public void holdClicked(boolean checked) {
         Preconditions.checkNotNull(mCall);
 
@@ -106,8 +148,9 @@
     public interface CallButtonUi extends Ui {
         void setVisible(boolean on);
         void setMute(boolean on);
-        void setSpeaker(boolean on);
         void setHold(boolean on);
         void displayDialpad(boolean on);
+        void setAudio(int mode);
+        void setSupportedAudio(int mask);
     }
 }
diff --git a/InCallUI/src/com/android/incallui/CallCardFragment.java b/InCallUI/src/com/android/incallui/CallCardFragment.java
index b584d5e..d4782c5 100644
--- a/InCallUI/src/com/android/incallui/CallCardFragment.java
+++ b/InCallUI/src/com/android/incallui/CallCardFragment.java
@@ -55,6 +55,7 @@
         return inflater.inflate(R.layout.call_card, container, false);
     }
 
+
     @Override
     public void onViewCreated(View view, Bundle savedInstanceState) {
         mPhoneNumber = (TextView) view.findViewById(R.id.phoneNumber);
@@ -69,6 +70,11 @@
     }
 
     @Override
+    public void onDestroyView() {
+        getPresenter().onUiUnready(this);
+    }
+
+    @Override
     public void setVisible(boolean on) {
         if (on) {
             getView().setVisibility(View.VISIBLE);
diff --git a/InCallUI/src/com/android/incallui/CallCommandClient.java b/InCallUI/src/com/android/incallui/CallCommandClient.java
index 80a9e9e..8e0a16c 100644
--- a/InCallUI/src/com/android/incallui/CallCommandClient.java
+++ b/InCallUI/src/com/android/incallui/CallCommandClient.java
@@ -88,9 +88,9 @@
         }
     }
 
-    public void turnSpeakerOn(boolean onOff) {
+    public void setAudioMode(int mode) {
         try {
-            mCommandService.speaker(onOff);
+            mCommandService.setAudioMode(mode);
         } catch (RemoteException e) {
             Logger.e(this, "Error setting speaker.", e);
         }
diff --git a/InCallUI/src/com/android/incallui/CallHandlerService.java b/InCallUI/src/com/android/incallui/CallHandlerService.java
index 173f6a3..8431fc9 100644
--- a/InCallUI/src/com/android/incallui/CallHandlerService.java
+++ b/InCallUI/src/com/android/incallui/CallHandlerService.java
@@ -22,6 +22,7 @@
 import android.os.IBinder;
 import android.os.Message;
 
+import com.android.services.telephony.common.AudioMode;
 import com.android.services.telephony.common.Call;
 import com.android.services.telephony.common.ICallCommandService;
 import com.android.services.telephony.common.ICallHandlerService;
@@ -39,10 +40,14 @@
     private static final int ON_UPDATE_CALL = 1;
     private static final int ON_UPDATE_MULTI_CALL = 2;
     private static final int ON_UPDATE_CALL_WITH_TEXT_RESPONSES = 3;
+    private static final int ON_AUDIO_MODE = 4;
+    private static final int ON_SUPPORTED_AUDIO_MODE = 5;
+
 
     private CallList mCallList;
     private Handler mMainHandler;
     private InCallPresenter mInCallPresenter;
+    private AudioModeProvider mAudioModeProvider;
 
     @Override
     public void onCreate() {
@@ -51,6 +56,7 @@
         mCallList = CallList.getInstance();
         mMainHandler = new MainHandler();
         mInCallPresenter = InCallPresenter.init(this);
+        mAudioModeProvider = AudioModeProvider.getInstance();
     }
 
     @Override
@@ -66,12 +72,13 @@
 
         @Override
         public void setCallCommandService(ICallCommandService service) {
-            Logger.d(this, "onConnected: " + service.toString());
+            Logger.d(CallHandlerService.this, "onConnected: " + service.toString());
             CallCommandClient.init(service);
         }
 
         @Override
         public void onDisconnect(Call call) {
+            Logger.d(CallHandlerService.this, "onDisconnected");
             mMainHandler.sendMessage(mMainHandler.obtainMessage(ON_UPDATE_CALL, 0, 0, call));
         }
 
@@ -86,16 +93,24 @@
 
         @Override
         public void onUpdate(List<Call> calls, boolean fullUpdate) {
+            Logger.d(CallHandlerService.this, "onUpdate ");
             // TODO(klp): Add use of fullUpdate to message
             mMainHandler.sendMessage(mMainHandler.obtainMessage(ON_UPDATE_MULTI_CALL, 0, 0, calls));
         }
 
         @Override
         public void onAudioModeChange(int mode) {
+            Logger.d(CallHandlerService.this, "onAudioModeChange : " + AudioMode.toString(mode));
+            mMainHandler.sendMessage(mMainHandler.obtainMessage(ON_AUDIO_MODE, mode, 0, null));
         }
 
         @Override
-        public void onAudioModeSupportChange(int modeMask) {
+        public void onSupportedAudioModeChange(int modeMask) {
+            Logger.d(CallHandlerService.this, "onSupportedAudioModeChange : " +
+                    AudioMode.toString(modeMask));
+
+            mMainHandler.sendMessage(
+                    mMainHandler.obtainMessage(ON_SUPPORTED_AUDIO_MODE, modeMask, 0, null));
         }
     };
 
@@ -115,6 +130,8 @@
     }
 
     private void executeMessage(Message msg) {
+        Logger.d(this, "executeMessage " + msg.what);
+
         switch (msg.what) {
             case ON_UPDATE_CALL:
                 mCallList.onUpdate((Call) msg.obj);
@@ -125,6 +142,12 @@
             case ON_UPDATE_CALL_WITH_TEXT_RESPONSES:
                 mCallList.onUpdate((AbstractMap.SimpleEntry<Call, List<String> >) msg.obj);
                 break;
+            case ON_AUDIO_MODE:
+                mAudioModeProvider.onAudioModeChange(msg.arg1);
+                break;
+            case ON_SUPPORTED_AUDIO_MODE:
+                mAudioModeProvider.onSupportedAudioModeChange(msg.arg1);
+                break;
             default:
                 break;
         }
diff --git a/InCallUI/src/com/android/incallui/Presenter.java b/InCallUI/src/com/android/incallui/Presenter.java
index d4024d5..b4962ae 100644
--- a/InCallUI/src/com/android/incallui/Presenter.java
+++ b/InCallUI/src/com/android/incallui/Presenter.java
@@ -32,6 +32,13 @@
         mUi = ui;
     }
 
+    /**
+     * Called when the UI view is destroyed in Fragment.onDestroyView().
+     */
+    public void onUiUnready(U ui) {
+        mUi = null;
+    }
+
     public U getUi() {
         return mUi;
     }