Merge "Refactor CallAudioManager's audio routing"
diff --git a/src/com/android/server/telecom/BluetoothManager.java b/src/com/android/server/telecom/BluetoothManager.java
index 4d7bdb6..389d0f6 100644
--- a/src/com/android/server/telecom/BluetoothManager.java
+++ b/src/com/android/server/telecom/BluetoothManager.java
@@ -28,6 +28,7 @@
 import android.os.Looper;
 import android.os.SystemClock;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
 
 import java.util.List;
@@ -37,6 +38,9 @@
  * overall audio state. Also provides method for connecting the bluetooth headset to the phone call.
  */
 public class BluetoothManager {
+    public interface BluetoothStateListener {
+        void onBluetoothStateChange(BluetoothManager bluetoothManager);
+    }
 
     private final BluetoothProfile.ServiceListener mBluetoothProfileServiceListener =
             new BluetoothProfile.ServiceListener() {
@@ -83,7 +87,7 @@
     private final Handler mHandler = new Handler(Looper.getMainLooper());
 
     private final BluetoothAdapter mBluetoothAdapter;
-    private final CallAudioManager mCallAudioManager;
+    private BluetoothStateListener mBluetoothStateListener;
 
     private BluetoothHeadset mBluetoothHeadset;
     private boolean mBluetoothConnectionPending = false;
@@ -102,9 +106,8 @@
     private final Context mContext;
 
 
-    public BluetoothManager(Context context, CallAudioManager callAudioManager) {
+    public BluetoothManager(Context context) {
         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
-        mCallAudioManager = callAudioManager;
         mContext = context;
 
         if (mBluetoothAdapter != null) {
@@ -119,6 +122,10 @@
         context.registerReceiver(mReceiver, intentFilter);
     }
 
+    public void setBluetoothStateListener(BluetoothStateListener bluetoothStateListener) {
+        mBluetoothStateListener = bluetoothStateListener;
+    }
+
     //
     // Bluetooth helper methods.
     //
@@ -138,7 +145,8 @@
      *         available to the user (i.e. if the device is BT-capable
      *         and a headset is connected.)
      */
-    boolean isBluetoothAvailable() {
+    @VisibleForTesting
+    public boolean isBluetoothAvailable() {
         Log.v(this, "isBluetoothAvailable()...");
 
         // There's no need to ask the Bluetooth system service if BT is enabled:
@@ -208,7 +216,8 @@
      *              that the BT audio connection is currently being set
      *              up, and will be connected soon.)
      */
-    /* package */ boolean isBluetoothAudioConnectedOrPending() {
+    @VisibleForTesting
+    public boolean isBluetoothAudioConnectedOrPending() {
         if (isBluetoothAudioConnected()) {
             Log.v(this, "isBluetoothAudioConnectedOrPending: ==> TRUE (really connected)");
             return true;
@@ -233,10 +242,11 @@
      * Notified audio manager of a change to the bluetooth state.
      */
     void updateBluetoothState() {
-        mCallAudioManager.onBluetoothStateChange(this);
+        mBluetoothStateListener.onBluetoothStateChange(this);
     }
 
-    void connectBluetoothAudio() {
+    @VisibleForTesting
+    public void connectBluetoothAudio() {
         Log.v(this, "connectBluetoothAudio()...");
         if (mBluetoothHeadset != null) {
             mBluetoothHeadset.connectAudio();
@@ -254,7 +264,8 @@
                 Timeouts.getBluetoothPendingTimeoutMillis(mContext.getContentResolver()));
     }
 
-    void disconnectBluetoothAudio() {
+    @VisibleForTesting
+    public void disconnectBluetoothAudio() {
         Log.v(this, "disconnectBluetoothAudio()...");
         if (mBluetoothHeadset != null) {
             mBluetoothHeadset.disconnectAudio();
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index 233ca1a..c36761a 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -376,7 +376,7 @@
         mCallerInfoAsyncQueryFactory = callerInfoAsyncQueryFactory;
         setHandle(handle);
         mPostDialDigits = handle != null
-                ? PhoneNumberUtils.extractPostDialPortion(handle.getSchemeSpecificPart()) : null;
+                ? PhoneNumberUtils.extractPostDialPortion(handle.getSchemeSpecificPart()) : "";
         mGatewayInfo = gatewayInfo;
         setConnectionManagerPhoneAccount(connectionManagerPhoneAccountHandle);
         setTargetPhoneAccount(targetPhoneAccountHandle);
@@ -858,7 +858,8 @@
         return mConferenceLevelActiveCall;
     }
 
-    ConnectionServiceWrapper getConnectionService() {
+    @VisibleForTesting
+    public ConnectionServiceWrapper getConnectionService() {
         return mConnectionService;
     }
 
@@ -1155,7 +1156,8 @@
     }
 
     /** Checks if this is a live call or not. */
-    boolean isAlive() {
+    @VisibleForTesting
+    public boolean isAlive() {
         switch (mState) {
             case CallState.NEW:
             case CallState.RINGING:
diff --git a/src/com/android/server/telecom/CallAudioManager.java b/src/com/android/server/telecom/CallAudioManager.java
index 3a9d0fe..32a5120 100644
--- a/src/com/android/server/telecom/CallAudioManager.java
+++ b/src/com/android/server/telecom/CallAudioManager.java
@@ -16,34 +16,23 @@
 
 package com.android.server.telecom;
 
-import android.app.ActivityManagerNative;
 import android.content.Context;
-import android.content.pm.UserInfo;
 import android.media.AudioManager;
 import android.media.IAudioService;
-import android.os.Binder;
 import android.os.Handler;
-import android.os.IBinder;
 import android.os.Looper;
 import android.os.Message;
-import android.os.RemoteException;
-import android.os.ServiceManager;
-import android.os.UserHandle;
-import android.provider.MediaStore;
 import android.telecom.CallAudioState;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.Preconditions;
 
-import java.util.Objects;
-
 /**
  * This class manages audio modes, streams and other properties.
  */
 @VisibleForTesting
-public class CallAudioManager extends CallsManagerListenerBase
-        implements WiredHeadsetManager.Listener, DockManager.Listener {
+public class CallAudioManager extends CallsManagerListenerBase {
     private static final int STREAM_NONE = -1;
 
     private static final String STREAM_DESCRIPTION_NONE = "STEAM_NONE";
@@ -64,9 +53,7 @@
     private static final String MODE_DESCRIPTION_IN_COMMUNICATION = "MODE_IN_COMMUNICATION";
 
     private static final int MSG_AUDIO_MANAGER_INITIALIZE = 0;
-    private static final int MSG_AUDIO_MANAGER_TURN_ON_SPEAKER = 1;
     private static final int MSG_AUDIO_MANAGER_ABANDON_AUDIO_FOCUS_FOR_CALL = 2;
-    private static final int MSG_AUDIO_MANAGER_SET_MICROPHONE_MUTE = 3;
     private static final int MSG_AUDIO_MANAGER_REQUEST_AUDIO_FOCUS_FOR_CALL = 4;
     private static final int MSG_AUDIO_MANAGER_SET_MODE = 5;
 
@@ -81,45 +68,10 @@
                     mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
                     break;
                 }
-                case MSG_AUDIO_MANAGER_TURN_ON_SPEAKER: {
-                    boolean on = (msg.arg1 != 0);
-                    // Wired headset and earpiece work the same way
-                    if (mAudioManager.isSpeakerphoneOn() != on) {
-                        Log.i(this, "turning speaker phone %s", on);
-                        mAudioManager.setSpeakerphoneOn(on);
-                    }
-                    break;
-                }
                 case MSG_AUDIO_MANAGER_ABANDON_AUDIO_FOCUS_FOR_CALL: {
                     mAudioManager.abandonAudioFocusForCall();
                     break;
                 }
-                case MSG_AUDIO_MANAGER_SET_MICROPHONE_MUTE: {
-                    boolean mute = (msg.arg1 != 0);
-                    if (mute != mAudioManager.isMicrophoneMute()) {
-                        IAudioService audio = mAudioServiceFactory.getAudioService();
-                        Log.i(this, "changing microphone mute state to: %b [serviceIsNull=%b]",
-                                mute, audio == null);
-                        if (audio != null) {
-                            try {
-                                // We use the audio service directly here so that we can specify
-                                // the current user. Telecom runs in the system_server process which
-                                // may run as a separate user from the foreground user. If we
-                                // used AudioManager directly, we would change mute for the system's
-                                // user and not the current foreground, which we want to avoid.
-                                audio.setMicrophoneMute(
-                                        mute, mContext.getOpPackageName(), getCurrentUserId());
-
-                            } catch (RemoteException e) {
-                                Log.e(this, e, "Remote exception while toggling mute.");
-                            }
-                            // TODO: Check microphone state after attempting to set to ensure that
-                            // our state corroborates AudioManager's state.
-                        }
-                    }
-
-                    break;
-                }
                 case MSG_AUDIO_MANAGER_REQUEST_AUDIO_FOCUS_FOR_CALL: {
                     int stream = msg.arg1;
                     mAudioManager.requestAudioFocusForCall(
@@ -159,48 +111,32 @@
 
     private final Context mContext;
     private final TelecomSystem.SyncRoot mLock;
-    private final StatusBarNotifier mStatusBarNotifier;
-    private final BluetoothManager mBluetoothManager;
-    private final WiredHeadsetManager mWiredHeadsetManager;
-    private final DockManager mDockManager;
     private final CallsManager mCallsManager;
-    private final AudioServiceFactory mAudioServiceFactory;
+    private final CallAudioRouteStateMachine mCallAudioRouteStateMachine;
 
-    private CallAudioState mCallAudioState;
     private int mAudioFocusStreamType;
     private boolean mIsRinging;
     private boolean mIsTonePlaying;
-    private boolean mWasSpeakerOn;
     private int mMostRecentlyUsedMode = AudioManager.MODE_IN_CALL;
     private Call mCallToSpeedUpMTAudio = null;
 
-    CallAudioManager(
+    public CallAudioManager(
             Context context,
             TelecomSystem.SyncRoot lock,
-            StatusBarNotifier statusBarNotifier,
-            WiredHeadsetManager wiredHeadsetManager,
-            DockManager dockManager,
             CallsManager callsManager,
-            AudioServiceFactory audioServiceFactory) {
+            CallAudioRouteStateMachine callAudioRouteStateMachine) {
         mContext = context;
         mLock = lock;
         mAudioManagerHandler.obtainMessage(MSG_AUDIO_MANAGER_INITIALIZE, 0, 0).sendToTarget();
-        mStatusBarNotifier = statusBarNotifier;
-        mBluetoothManager = new BluetoothManager(context, this);
-        mWiredHeadsetManager = wiredHeadsetManager;
         mCallsManager = callsManager;
-
-        mWiredHeadsetManager.addListener(this);
-        mDockManager = dockManager;
-        mDockManager.addListener(this);
-
-        saveAudioState(getInitialAudioState(null));
         mAudioFocusStreamType = STREAM_NONE;
-        mAudioServiceFactory = audioServiceFactory;
+
+        mCallAudioRouteStateMachine = callAudioRouteStateMachine;
     }
 
-    CallAudioState getCallAudioState() {
-        return mCallAudioState;
+    @VisibleForTesting
+    public CallAudioState getCallAudioState() {
+        return mCallAudioRouteStateMachine.getCurrentCallAudioState();
     }
 
     @Override
@@ -211,8 +147,7 @@
         if (hasFocus() && getForegroundCall() == call) {
             if (!call.isIncoming()) {
                 // Unmute new outgoing call.
-                setSystemAudioState(false, mCallAudioState.getRoute(),
-                        mCallAudioState.getSupportedRouteMask());
+                mCallAudioRouteStateMachine.sendMessage(CallAudioRouteStateMachine.MUTE_OFF);
             }
         }
     }
@@ -220,13 +155,13 @@
     @Override
     public void onCallRemoved(Call call) {
         Log.v(this, "onCallRemoved");
+        if (mCallsManager.getCalls().isEmpty()) {
+            Log.v(this, "all calls removed, resetting system audio to default state");
+            mCallAudioRouteStateMachine.sendMessage(CallAudioRouteStateMachine.REINITIALIZE);
+        }
+
         // If we didn't already have focus, there's nothing to do.
         if (hasFocus()) {
-            if (mCallsManager.getCalls().isEmpty()) {
-                Log.v(this, "all calls removed, resetting system audio to default state");
-                setInitialAudioState(null, false /* force */);
-                mWasSpeakerOn = false;
-            }
             updateAudioStreamAndMode(call);
         }
     }
@@ -240,19 +175,12 @@
     @Override
     public void onIncomingCallAnswered(Call call) {
         Log.v(this, "onIncomingCallAnswered");
-        int route = mCallAudioState.getRoute();
 
-        // We do two things:
-        // (1) If this is the first call, then we can to turn on bluetooth if available.
-        // (2) Unmute the audio for the new incoming call.
-        boolean isOnlyCall = mCallsManager.getCalls().size() == 1;
-        if (isOnlyCall && mBluetoothManager.isBluetoothAvailable()) {
-            mBluetoothManager.connectBluetoothAudio();
-            route = CallAudioState.ROUTE_BLUETOOTH;
+        if (mCallsManager.getCalls().size() == 1) {
+            mCallAudioRouteStateMachine.sendMessage(CallAudioRouteStateMachine.SWITCH_FOCUS,
+                    CallAudioRouteStateMachine.HAS_FOCUS);
         }
 
-        setSystemAudioState(false /* isMute */, route, mCallAudioState.getSupportedRouteMask());
-
         if (call.can(android.telecom.Call.Details.CAPABILITY_SPEED_UP_MT_AUDIO)) {
             Log.v(this, "Speed up audio setup for IMS MT call.");
             mCallToSpeedUpMTAudio = call;
@@ -272,73 +200,8 @@
         updateAudioStreamAndMode(call);
     }
 
-    /**
-      * 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.
-      */
-    @Override
-    public void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn) {
-        // This can happen even when there are no calls and we don't have focus.
-        if (!hasFocus()) {
-            return;
-        }
-
-        boolean isCurrentlyWiredHeadset = mCallAudioState.getRoute()
-                == CallAudioState.ROUTE_WIRED_HEADSET;
-
-        int newRoute = mCallAudioState.getRoute();  // start out with existing route
-        if (newIsPluggedIn) {
-            newRoute = CallAudioState.ROUTE_WIRED_HEADSET;
-        } else if (isCurrentlyWiredHeadset) {
-            Call call = getForegroundCall();
-            boolean hasLiveCall = call != null && call.isAlive();
-
-            if (hasLiveCall) {
-                // In order of preference when a wireless headset is unplugged.
-                if (mWasSpeakerOn) {
-                    newRoute = CallAudioState.ROUTE_SPEAKER;
-                } else {
-                    newRoute = CallAudioState.ROUTE_EARPIECE;
-                }
-
-                // We don't automatically connect to bluetooth when user unplugs their wired headset
-                // and they were previously using the wired. Wired and earpiece are effectively the
-                // same choice in that they replace each other as an option when wired headsets
-                // are plugged in and out. This means that keeping it earpiece is a bit more
-                // consistent with the status quo.  Bluetooth also has more danger associated with
-                // choosing it in the wrong curcumstance because bluetooth devices can be
-                // semi-public (like in a very-occupied car) where earpiece doesn't carry that risk.
-            }
-        }
-
-        // We need to call this every time even if we do not change the route because the supported
-        // routes changed either to include or not include WIRED_HEADSET.
-        setSystemAudioState(mCallAudioState.isMuted(), newRoute, calculateSupportedRoutes());
-    }
-
-    @Override
-    public void onDockChanged(boolean isDocked) {
-        // This can happen even when there are no calls and we don't have focus.
-        if (!hasFocus()) {
-            return;
-        }
-
-        if (isDocked) {
-            // Device just docked, turn to speakerphone. Only do so if the route is currently
-            // earpiece so that we dont switch out of a BT headset or a wired headset.
-            if (mCallAudioState.getRoute() == CallAudioState.ROUTE_EARPIECE) {
-                setAudioRoute(CallAudioState.ROUTE_SPEAKER);
-            }
-        } else {
-            // Device just undocked, remove from speakerphone if possible.
-            if (mCallAudioState.getRoute() == CallAudioState.ROUTE_SPEAKER) {
-                setAudioRoute(CallAudioState.ROUTE_WIRED_OR_EARPIECE);
-            }
-        }
-    }
-
     void toggleMute() {
-        mute(!mCallAudioState.isMuted());
+        mCallAudioRouteStateMachine.sendMessage(CallAudioRouteStateMachine.TOGGLE_MUTE);
     }
 
     void mute(boolean shouldMute) {
@@ -354,15 +217,8 @@
             Log.v(this, "ignoring mute for emergency call");
         }
 
-        if (mCallAudioState.isMuted() != shouldMute) {
-            // We user CallsManager's foreground call so that we dont ignore ringing calls
-            // for logging purposes
-            Log.event(mCallsManager.getForegroundCall(), Log.Events.MUTE,
-                    shouldMute ? "on" : "off");
-
-            setSystemAudioState(shouldMute, mCallAudioState.getRoute(),
-                    mCallAudioState.getSupportedRouteMask());
-        }
+        mCallAudioRouteStateMachine.sendMessage(shouldMute
+                ? CallAudioRouteStateMachine.MUTE_ON : CallAudioRouteStateMachine.MUTE_OFF);
     }
 
     /**
@@ -371,28 +227,30 @@
      * @param route The new audio route to use. See {@link CallAudioState}.
      */
     void setAudioRoute(int route) {
-        // This can happen even when there are no calls and we don't have focus.
-        if (!hasFocus()) {
-            return;
-        }
-
         Log.v(this, "setAudioRoute, route: %s", CallAudioState.audioRouteToString(route));
-
-        // Change ROUTE_WIRED_OR_EARPIECE to a single entry.
-        int newRoute = selectWiredOrEarpiece(route, mCallAudioState.getSupportedRouteMask());
-
-        // If route is unsupported, do nothing.
-        if ((mCallAudioState.getSupportedRouteMask() | newRoute) == 0) {
-            Log.wtf(this, "Asking to set to a route that is unsupported: %d", newRoute);
-            return;
-        }
-
-        if (mCallAudioState.getRoute() != 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(mCallAudioState.isMuted(), newRoute,
-                    mCallAudioState.getSupportedRouteMask());
+        switch (route) {
+            case CallAudioState.ROUTE_BLUETOOTH:
+                mCallAudioRouteStateMachine.sendMessage(
+                        CallAudioRouteStateMachine.SWITCH_BLUETOOTH);
+                return;
+            case CallAudioState.ROUTE_SPEAKER:
+                mCallAudioRouteStateMachine.sendMessage(
+                        CallAudioRouteStateMachine.SWITCH_SPEAKER);
+                return;
+            case CallAudioState.ROUTE_WIRED_HEADSET:
+                mCallAudioRouteStateMachine.sendMessage(
+                        CallAudioRouteStateMachine.SWITCH_HEADSET);
+                return;
+            case CallAudioState.ROUTE_EARPIECE:
+                mCallAudioRouteStateMachine.sendMessage(
+                        CallAudioRouteStateMachine.SWITCH_EARPIECE);
+                return;
+            case CallAudioState.ROUTE_WIRED_OR_EARPIECE:
+                mCallAudioRouteStateMachine.sendMessage(
+                        CallAudioRouteStateMachine.SWITCH_WIRED_OR_EARPIECE);
+                return;
+            default:
+                Log.wtf(this, "Invalid route specified: %d", route);
         }
     }
 
@@ -426,44 +284,6 @@
         }
     }
 
-    /**
-     * Updates the audio routing according to the bluetooth state.
-     */
-    void onBluetoothStateChange(BluetoothManager bluetoothManager) {
-        // This can happen even when there are no calls and we don't have focus.
-        if (!hasFocus()) {
-            return;
-        }
-
-        int supportedRoutes = calculateSupportedRoutes();
-        int newRoute = mCallAudioState.getRoute();
-        if (bluetoothManager.isBluetoothAudioConnectedOrPending()) {
-            newRoute = CallAudioState.ROUTE_BLUETOOTH;
-        } else if (mCallAudioState.getRoute() == CallAudioState.ROUTE_BLUETOOTH) {
-            newRoute = selectWiredOrEarpiece(CallAudioState.ROUTE_WIRED_OR_EARPIECE,
-                    supportedRoutes);
-            // Do not switch to speaker when bluetooth disconnects.
-            mWasSpeakerOn = false;
-        }
-
-        setSystemAudioState(mCallAudioState.isMuted(), newRoute, supportedRoutes);
-    }
-
-    boolean isBluetoothAudioOn() {
-        return mBluetoothManager.isBluetoothAudioConnected();
-    }
-
-    boolean isBluetoothDeviceAvailable() {
-        return mBluetoothManager.isBluetoothAvailable();
-    }
-
-    private void saveAudioState(CallAudioState callAudioState) {
-        mCallAudioState = callAudioState;
-        mStatusBarNotifier.notifyMute(mCallAudioState.isMuted());
-        mStatusBarNotifier.notifySpeakerphone(mCallAudioState.getRoute()
-                == CallAudioState.ROUTE_SPEAKER);
-    }
-
     private void onCallUpdated(Call call) {
         updateAudioStreamAndMode(call);
         if (call != null && call.getState() == CallState.ACTIVE &&
@@ -472,70 +292,6 @@
         }
     }
 
-    private void setSystemAudioState(boolean isMuted, int route, int supportedRouteMask) {
-        setSystemAudioState(false /* force */, isMuted, route, supportedRouteMask);
-    }
-
-    private void setSystemAudioState(
-            boolean force, boolean isMuted, int route, int supportedRouteMask) {
-        if (!hasFocus()) {
-            return;
-        }
-
-        CallAudioState oldAudioState = mCallAudioState;
-        saveAudioState(new CallAudioState(isMuted, route, supportedRouteMask));
-        if (!force && Objects.equals(oldAudioState, mCallAudioState)) {
-            return;
-        }
-
-        Log.i(this, "setSystemAudioState: changing from %s to %s", oldAudioState, mCallAudioState);
-        Log.event(mCallsManager.getForegroundCall(), Log.Events.AUDIO_ROUTE,
-                CallAudioState.audioRouteToString(mCallAudioState.getRoute()));
-
-        mAudioManagerHandler.obtainMessage(
-                MSG_AUDIO_MANAGER_SET_MICROPHONE_MUTE,
-                mCallAudioState.isMuted() ? 1 : 0,
-                0)
-                .sendToTarget();
-
-        // Audio route.
-        if (mCallAudioState.getRoute() == CallAudioState.ROUTE_BLUETOOTH) {
-            turnOnSpeaker(false);
-            turnOnBluetooth(true);
-        } else if (mCallAudioState.getRoute() == CallAudioState.ROUTE_SPEAKER) {
-            turnOnBluetooth(false);
-            turnOnSpeaker(true);
-        } else if (mCallAudioState.getRoute() == CallAudioState.ROUTE_EARPIECE ||
-                mCallAudioState.getRoute() == CallAudioState.ROUTE_WIRED_HEADSET) {
-            turnOnBluetooth(false);
-            turnOnSpeaker(false);
-        }
-
-        if (!oldAudioState.equals(mCallAudioState)) {
-            mCallsManager.onCallAudioStateChanged(oldAudioState, mCallAudioState);
-            updateAudioForForegroundCall();
-        }
-    }
-
-    private void turnOnSpeaker(boolean on) {
-        mAudioManagerHandler.obtainMessage(MSG_AUDIO_MANAGER_TURN_ON_SPEAKER, on ? 1 : 0, 0)
-                .sendToTarget();
-    }
-
-    private void turnOnBluetooth(boolean on) {
-        if (mBluetoothManager.isBluetoothAvailable()) {
-            boolean isAlreadyOn = mBluetoothManager.isBluetoothAudioConnectedOrPending();
-            if (on != isAlreadyOn) {
-                Log.i(this, "connecting bluetooth %s", on);
-                if (on) {
-                    mBluetoothManager.connectBluetoothAudio();
-                } else {
-                    mBluetoothManager.disconnectBluetoothAudio();
-                }
-            }
-        }
-    }
-
     private void updateAudioStreamAndMode() {
         updateAudioStreamAndMode(null /* call */);
     }
@@ -544,7 +300,6 @@
         Log.i(this, "updateAudioStreamAndMode :  mIsRinging: %b, mIsTonePlaying: %b, call: %s",
                 mIsRinging, mIsTonePlaying, callToUpdate);
 
-        boolean wasVoiceCall = mAudioFocusStreamType == AudioManager.STREAM_VOICE_CALL;
         if (mIsRinging) {
             Log.i(this, "updateAudioStreamAndMode : ringing");
             requestAudioFocusAndSetMode(AudioManager.STREAM_RING, AudioManager.MODE_RINGTONE);
@@ -587,14 +342,6 @@
                 // focus will be correctly abandoned by the if clause above.
             }
         }
-
-        boolean isVoiceCall = mAudioFocusStreamType == AudioManager.STREAM_VOICE_CALL;
-
-        // If we transition from not a voice call to a voice call, we need to set an initial audio
-        // state for the call.
-        if (!wasVoiceCall && isVoiceCall) {
-            setInitialAudioState(callToUpdate, true /* force */);
-        }
     }
 
     private void requestAudioFocusAndSetMode(int stream, int mode) {
@@ -615,6 +362,8 @@
                     .sendToTarget();
         }
         mAudioFocusStreamType = stream;
+        mCallAudioRouteStateMachine.sendMessage(CallAudioRouteStateMachine.SWITCH_FOCUS,
+                CallAudioRouteStateMachine.HAS_FOCUS);
 
         setMode(mode);
     }
@@ -628,6 +377,8 @@
             mAudioFocusStreamType = STREAM_NONE;
             mCallToSpeedUpMTAudio = null;
         }
+        mCallAudioRouteStateMachine.sendMessage(CallAudioRouteStateMachine.SWITCH_FOCUS,
+                CallAudioRouteStateMachine.NO_FOCUS);
     }
 
     /**
@@ -640,77 +391,11 @@
         mAudioManagerHandler.obtainMessage(MSG_AUDIO_MANAGER_SET_MODE, newMode, 0).sendToTarget();
     }
 
-    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 {
-            routeMask |= CallAudioState.ROUTE_EARPIECE;
-        }
-
-        if (mBluetoothManager.isBluetoothAvailable()) {
-            routeMask |=  CallAudioState.ROUTE_BLUETOOTH;
-        }
-
-        return routeMask;
-    }
-
-    private CallAudioState getInitialAudioState(Call call) {
-        int supportedRouteMask = calculateSupportedRoutes();
-        int route = selectWiredOrEarpiece(
-                CallAudioState.ROUTE_WIRED_OR_EARPIECE, supportedRouteMask);
-
-        // We want the UI to indicate that "bluetooth is in use" in two slightly different cases:
-        // (a) The obvious case: if a bluetooth headset is currently in use for an ongoing call.
-        // (b) The not-so-obvious case: if an incoming call is ringing, and we expect that audio
-        //     *will* be routed to a bluetooth headset once the call is answered. In this case, just
-        //     check if the headset is available. Note this only applies when we are dealing with
-        //     the first call.
-        if (call != null && mBluetoothManager.isBluetoothAvailable()) {
-            switch(call.getState()) {
-                case CallState.ACTIVE:
-                case CallState.ON_HOLD:
-                case CallState.DIALING:
-                case CallState.CONNECTING:
-                case CallState.RINGING:
-                    route = CallAudioState.ROUTE_BLUETOOTH;
-                    break;
-                default:
-                    break;
-            }
-        }
-
-        return new CallAudioState(false, route, supportedRouteMask);
-    }
-
-    private void setInitialAudioState(Call call, boolean force) {
-        CallAudioState audioState = getInitialAudioState(call);
-        Log.i(this, "setInitialAudioState : audioState = %s, call = %s", audioState, call);
-        setSystemAudioState(
-                force, audioState.isMuted(), audioState.getRoute(),
-                audioState.getSupportedRouteMask());
-    }
-
     private void updateAudioForForegroundCall() {
         Call call = mCallsManager.getForegroundCall();
         if (call != null && call.getConnectionService() != null) {
-            call.getConnectionService().onCallAudioStateChanged(call, mCallAudioState);
+            call.getConnectionService().onCallAudioStateChanged(call,
+                    mCallAudioRouteStateMachine.getCurrentCallAudioState());
         }
     }
 
@@ -738,19 +423,6 @@
         return mAudioFocusStreamType != STREAM_NONE;
     }
 
-    private int getCurrentUserId() {
-        final long ident = Binder.clearCallingIdentity();
-        try {
-            UserInfo currentUser = ActivityManagerNative.getDefault().getCurrentUser();
-            return currentUser.id;
-        } catch (RemoteException e) {
-            // Activity manager not running, nothing we can do assume user 0.
-        } finally {
-            Binder.restoreCallingIdentity(ident);
-        }
-        return UserHandle.USER_OWNER;
-    }
-
     /**
      * Translates an {@link AudioManager} stream type to a human-readable string description.
      *
@@ -813,23 +485,10 @@
      * @param pw The {@code IndentingPrintWriter} to write the state to.
      */
     public void dump(IndentingPrintWriter pw) {
-        pw.println("mAudioState: " + mCallAudioState);
-        pw.println("mBluetoothManager:");
-        pw.increaseIndent();
-        mBluetoothManager.dump(pw);
-        pw.decreaseIndent();
-        if (mWiredHeadsetManager != null) {
-            pw.println("mWiredHeadsetManager:");
-            pw.increaseIndent();
-            mWiredHeadsetManager.dump(pw);
-            pw.decreaseIndent();
-        } else {
-            pw.println("mWiredHeadsetManager: null");
-        }
+        pw.println("mAudioState: " + mCallAudioRouteStateMachine.getCurrentCallAudioState());
         pw.println("mAudioFocusStreamType: " + streamTypeToString(mAudioFocusStreamType));
         pw.println("mIsRinging: " + mIsRinging);
         pw.println("mIsTonePlaying: " + mIsTonePlaying);
-        pw.println("mWasSpeakerOn: " + mWasSpeakerOn);
         pw.println("mMostRecentlyUsedMode: " + modeToString(mMostRecentlyUsedMode));
     }
-}
+}
\ No newline at end of file
diff --git a/src/com/android/server/telecom/CallAudioRoutePeripheralAdapter.java b/src/com/android/server/telecom/CallAudioRoutePeripheralAdapter.java
new file mode 100644
index 0000000..26a4cb8
--- /dev/null
+++ b/src/com/android/server/telecom/CallAudioRoutePeripheralAdapter.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2015 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.server.telecom;
+
+/**
+ * A class that acts as a listener to things that could change call audio routing, namely
+ * bluetooth status, wired headset status, and dock status.
+ */
+public class CallAudioRoutePeripheralAdapter implements BluetoothManager.BluetoothStateListener,
+        WiredHeadsetManager.Listener, DockManager.Listener {
+    private final CallAudioRouteStateMachine mCallAudioRouteStateMachine;
+    private final BluetoothManager mBluetoothManager;
+
+    public CallAudioRoutePeripheralAdapter(
+            CallAudioRouteStateMachine callAudioRouteStateMachine,
+            BluetoothManager bluetoothManager,
+            WiredHeadsetManager wiredHeadsetManager,
+            DockManager dockManager) {
+        mCallAudioRouteStateMachine = callAudioRouteStateMachine;
+        mBluetoothManager = bluetoothManager;
+
+        mBluetoothManager.setBluetoothStateListener(this);
+        wiredHeadsetManager.addListener(this);
+        dockManager.addListener(this);
+    }
+
+    @Override
+    public void onBluetoothStateChange(BluetoothManager bluetoothManager) {
+        if (bluetoothManager.isBluetoothAvailable()) {
+            mCallAudioRouteStateMachine.sendMessage(CallAudioRouteStateMachine.CONNECT_BLUETOOTH);
+        } else {
+            mCallAudioRouteStateMachine.sendMessage(
+                    CallAudioRouteStateMachine.DISCONNECT_BLUETOOTH);
+        }
+    }
+
+    public boolean isBluetoothAudioOn() {
+        return mBluetoothManager.isBluetoothAudioConnected();
+    }
+
+    /**
+      * 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.
+      */
+    @Override
+    public void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn) {
+        if (!oldIsPluggedIn && newIsPluggedIn) {
+            mCallAudioRouteStateMachine.sendMessage(
+                    CallAudioRouteStateMachine.CONNECT_WIRED_HEADSET);
+        } else if (oldIsPluggedIn && !newIsPluggedIn){
+            mCallAudioRouteStateMachine.sendMessage(
+                    CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET);
+        }
+    }
+
+    @Override
+    public void onDockChanged(boolean isDocked) {
+        mCallAudioRouteStateMachine.sendMessage(
+                isDocked ? CallAudioRouteStateMachine.CONNECT_DOCK
+                        : CallAudioRouteStateMachine.DISCONNECT_DOCK
+        );
+    }
+}
diff --git a/src/com/android/server/telecom/CallAudioRouteStateMachine.java b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
new file mode 100644
index 0000000..e6ef8f4
--- /dev/null
+++ b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
@@ -0,0 +1,1132 @@
+/*
+ * Copyright (C) 2015 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.server.telecom;
+
+
+import android.app.ActivityManagerNative;
+import android.content.Context;
+import android.content.pm.UserInfo;
+import android.media.AudioManager;
+import android.media.IAudioService;
+import android.os.Binder;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.telecom.CallAudioState;
+
+import com.android.internal.util.IState;
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+
+import java.util.HashMap;
+
+/**
+ * This class describes the available routes of a call as a state machine.
+ * Transitions are caused solely by the commands sent as messages. Possible values for msg.what
+ * are defined as event constants in this file.
+ *
+ * The eight states are all instances of the abstract base class, {@link AudioState}. Each state
+ * is a combination of one of the four audio routes (earpiece, wired headset, bluetooth, and
+ * speakerphone) and audio focus status (active or quiescent).
+ *
+ * Messages are processed first by the processMessage method in the base class, AudioState.
+ * Any messages not completely handled by AudioState are further processed by the same method in
+ * the route-specific abstract classes: {@link EarpieceRoute}, {@link HeadsetRoute},
+ * {@link BluetoothRoute}, and {@link SpeakerRoute}. Finally, messages that are not handled at
+ * this level are then processed by the classes corresponding to the state instances themselves.
+ *
+ * There are several variables carrying additional state. These include:
+ * mAvailableRoutes: A bitmask describing which audio routes are available
+ * mWasOnSpeaker: A boolean indicating whether we should switch to speakerphone after disconnecting
+ *     from a wired headset
+ * mIsMuted: a boolean indicating whether the audio is muted
+ */
+public class CallAudioRouteStateMachine extends StateMachine {
+    /** Direct the audio stream through the device's earpiece. */
+    public static final int ROUTE_EARPIECE      = CallAudioState.ROUTE_EARPIECE;
+
+    /** Direct the audio stream through Bluetooth. */
+    public static final int ROUTE_BLUETOOTH     = CallAudioState.ROUTE_BLUETOOTH;
+
+    /** Direct the audio stream through a wired headset. */
+    public static final int ROUTE_WIRED_HEADSET = CallAudioState.ROUTE_WIRED_HEADSET;
+
+    /** Direct the audio stream through the device's speakerphone. */
+    public static final int ROUTE_SPEAKER       = CallAudioState.ROUTE_SPEAKER;
+
+    /** Valid values for msg.what */
+    public static final int CONNECT_WIRED_HEADSET = 1;
+    public static final int DISCONNECT_WIRED_HEADSET = 2;
+    public static final int CONNECT_BLUETOOTH = 3;
+    public static final int DISCONNECT_BLUETOOTH = 4;
+    public static final int CONNECT_DOCK = 5;
+    public static final int DISCONNECT_DOCK = 6;
+
+    public static final int SWITCH_EARPIECE = 1001;
+    public static final int SWITCH_BLUETOOTH = 1002;
+    public static final int SWITCH_HEADSET = 1003;
+    public static final int SWITCH_SPEAKER = 1004;
+    public static final int SWITCH_WIRED_OR_EARPIECE = 1005;
+
+    public static final int REINITIALIZE = 2001;
+
+    public static final int MUTE_ON = 3001;
+    public static final int MUTE_OFF = 3002;
+    public static final int TOGGLE_MUTE = 3003;
+
+    public static final int SWITCH_FOCUS = 4001;
+
+    /** Valid values for mAudioFocusType */
+    public static final int NO_FOCUS = 1;
+    public static final int HAS_FOCUS = 2;
+
+    private static final String ACTIVE_EARPIECE_ROUTE_NAME = "ActiveEarpieceRoute";
+    private static final String ACTIVE_BLUETOOTH_ROUTE_NAME = "ActiveBluetoothRoute";
+    private static final String ACTIVE_SPEAKER_ROUTE_NAME = "ActiveSpeakerRoute";
+    private static final String ACTIVE_HEADSET_ROUTE_NAME = "ActiveHeadsetRoute";
+    private static final String QUIESCENT_EARPIECE_ROUTE_NAME = "QuiescentEarpieceRoute";
+    private static final String QUIESCENT_BLUETOOTH_ROUTE_NAME = "QuiescentBluetoothRoute";
+    private static final String QUIESCENT_SPEAKER_ROUTE_NAME = "QuiescentSpeakerRoute";
+    private static final String QUIESCENT_HEADSET_ROUTE_NAME = "QuiescentHeadsetRoute";
+
+    public static final String NAME = CallAudioRouteStateMachine.class.getName();
+
+    abstract class AudioState extends State {
+        @Override
+        public void enter() {
+            super.enter();
+            Log.event(mCallsManager.getForegroundCall(), Log.Events.AUDIO_ROUTE,
+                    "Entering state " + getName());
+        }
+
+        @Override
+        public void exit() {
+            Log.event(mCallsManager.getForegroundCall(), Log.Events.AUDIO_ROUTE,
+                    "Leaving state " + getName());
+            super.exit();
+        }
+
+        @Override
+        public boolean processMessage(Message msg) {
+            Log.d(this, "Message received: %s", msg);
+            switch (msg.what) {
+                case CONNECT_WIRED_HEADSET:
+                    Log.event(mCallsManager.getForegroundCall(), Log.Events.AUDIO_ROUTE,
+                            "Wired headset connected");
+                    mAvailableRoutes &= ~ROUTE_EARPIECE;
+                    mAvailableRoutes |= ROUTE_WIRED_HEADSET;
+                    return NOT_HANDLED;
+                case CONNECT_BLUETOOTH:
+                    // This case is here because the bluetooth manager sends out a lot of spurious
+                    // state changes, and no layers above this one can tell which are actual changes
+                    // in connection/disconnection status. This filters it out.
+                    if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) {
+                        return HANDLED; // Do nothing if we already have bluetooth as enabled.
+                    } else {
+                        Log.event(mCallsManager.getForegroundCall(), Log.Events.AUDIO_ROUTE,
+                                "Bluetooth connected");
+                        mAvailableRoutes |= ROUTE_BLUETOOTH;
+                        return NOT_HANDLED;
+                    }
+                case DISCONNECT_WIRED_HEADSET:
+                    Log.event(mCallsManager.getForegroundCall(), Log.Events.AUDIO_ROUTE,
+                            "Wired headset disconnected");
+                    mAvailableRoutes &= ~ROUTE_WIRED_HEADSET;
+                    mAvailableRoutes |= ROUTE_EARPIECE;
+                    return NOT_HANDLED;
+                case DISCONNECT_BLUETOOTH:
+                    if ((mAvailableRoutes & ROUTE_BLUETOOTH) == 0) {
+                        return HANDLED;
+                    } else {
+                        Log.event(mCallsManager.getForegroundCall(), Log.Events.AUDIO_ROUTE,
+                                "Bluetooth disconnected");
+                        mAvailableRoutes &= ~ROUTE_BLUETOOTH;
+                        return NOT_HANDLED;
+                    }
+                case SWITCH_WIRED_OR_EARPIECE:
+                    if ((mAvailableRoutes & ROUTE_EARPIECE) != 0) {
+                        sendInternalMessage(SWITCH_EARPIECE);
+                    } else if ((mAvailableRoutes & ROUTE_WIRED_HEADSET) != 0) {
+                        sendInternalMessage(SWITCH_HEADSET);
+                    } else {
+                        Log.e(this, new IllegalStateException(),
+                                "Neither headset nor earpiece are available. Defaulting to " +
+                                        "earpiece.");
+                        sendInternalMessage(SWITCH_EARPIECE);
+                    }
+                    return HANDLED;
+                case REINITIALIZE:
+                    CallAudioState initState = getInitialAudioState();
+                    mAvailableRoutes = initState.getSupportedRouteMask();
+                    mIsMuted = initState.isMuted();
+                    mWasOnSpeaker = initState.getRoute() == ROUTE_SPEAKER;
+                    transitionTo(mRouteCodeToQuiescentState.get(initState.getRoute()));
+                    return HANDLED;
+                default:
+                    return NOT_HANDLED;
+            }
+        }
+
+        // Behavior will depend on whether the state is an active one or a quiescent one.
+        abstract public void updateSystemAudioState();
+        abstract public boolean isActive();
+    }
+
+    class ActiveEarpieceRoute extends EarpieceRoute {
+        @Override
+        public String getName() {
+            return ACTIVE_EARPIECE_ROUTE_NAME;
+        }
+
+        @Override
+        public boolean isActive() {
+            return true;
+        }
+
+        @Override
+        public void enter() {
+            super.enter();
+            setSpeakerphoneOn(false);
+            setBluetoothOn(false);
+            CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_EARPIECE,
+                    mAvailableRoutes);
+            setSystemAudioState(mCurrentCallAudioState, newState);
+            updateInternalCallAudioState();
+        }
+
+        @Override
+        public void updateSystemAudioState() {
+            CallAudioState oldAudioState = mCurrentCallAudioState;
+            updateInternalCallAudioState();
+            setSystemAudioState(oldAudioState, mCurrentCallAudioState);
+        }
+
+        @Override
+        public boolean processMessage(Message msg) {
+            if (super.processMessage(msg) == HANDLED) {
+                return HANDLED;
+            }
+            switch (msg.what) {
+                case SWITCH_EARPIECE:
+                    // Nothing to do here
+                    return HANDLED;
+                case SWITCH_BLUETOOTH:
+                    if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) {
+                        transitionTo(mActiveBluetoothRoute);
+                    } else {
+                        Log.w(this, "Ignoring switch to bluetooth command. Not available.");
+                    }
+                    return HANDLED;
+                case SWITCH_HEADSET:
+                    if ((mAvailableRoutes & ROUTE_WIRED_HEADSET) != 0) {
+                        transitionTo(mActiveHeadsetRoute);
+                    } else {
+                        Log.w(this, "Ignoring switch to headset command. Not available.");
+                    }
+                    return HANDLED;
+                case SWITCH_SPEAKER:
+                    transitionTo(mActiveSpeakerRoute);
+                    return HANDLED;
+                case SWITCH_FOCUS:
+                    if (msg.arg1 == NO_FOCUS) {
+                        transitionTo(mQuiescentEarpieceRoute);
+                    }
+                    return HANDLED;
+                default:
+                    return NOT_HANDLED;
+            }
+        }
+    }
+
+    class QuiescentEarpieceRoute extends EarpieceRoute {
+        @Override
+        public String getName() {
+            return QUIESCENT_EARPIECE_ROUTE_NAME;
+        }
+
+        @Override
+        public boolean isActive() {
+            return false;
+        }
+
+        @Override
+        public void enter() {
+            super.enter();
+            updateInternalCallAudioState();
+        }
+
+        @Override
+        public void updateSystemAudioState() {
+            updateInternalCallAudioState();
+        }
+
+        @Override
+        public boolean processMessage(Message msg) {
+            if (super.processMessage(msg) == HANDLED) {
+                return HANDLED;
+            }
+            switch (msg.what) {
+                case SWITCH_EARPIECE:
+                    // Nothing to do here
+                    return HANDLED;
+                case SWITCH_BLUETOOTH:
+                    if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) {
+                        transitionTo(mQuiescentBluetoothRoute);
+                    } else {
+                        Log.w(this, "Ignoring switch to bluetooth command. Not available.");
+                    }
+                    return HANDLED;
+                case SWITCH_HEADSET:
+                    if ((mAvailableRoutes & ROUTE_WIRED_HEADSET) != 0) {
+                        transitionTo(mQuiescentHeadsetRoute);
+                    } else {
+                        Log.w(this, "Ignoring switch to headset command. Not available.");
+                    }
+                    return HANDLED;
+                case SWITCH_SPEAKER:
+                    transitionTo(mQuiescentSpeakerRoute);
+                    return HANDLED;
+                case SWITCH_FOCUS:
+                    if (msg.arg1 == HAS_FOCUS) {
+                        transitionTo(mActiveEarpieceRoute);
+                    }
+                    return HANDLED;
+                default:
+                    return NOT_HANDLED;
+            }
+        }
+    }
+
+    abstract class EarpieceRoute extends AudioState {
+        @Override
+        public boolean processMessage(Message msg) {
+            if (super.processMessage(msg) == HANDLED) {
+                return HANDLED;
+            }
+            switch (msg.what) {
+                case CONNECT_WIRED_HEADSET:
+                    sendInternalMessage(SWITCH_HEADSET);
+                    return HANDLED;
+                case CONNECT_BLUETOOTH:
+                    sendInternalMessage(SWITCH_BLUETOOTH);
+                    return HANDLED;
+                case DISCONNECT_BLUETOOTH:
+                    updateSystemAudioState();
+                    // No change in audio route required
+                    return HANDLED;
+                case DISCONNECT_WIRED_HEADSET:
+                    Log.e(this, new IllegalStateException(),
+                            "Wired headset should not go from connected to not when on " +
+                            "earpiece");
+                    updateSystemAudioState();
+                    return HANDLED;
+                case CONNECT_DOCK:
+                    sendInternalMessage(SWITCH_SPEAKER);
+                    return HANDLED;
+                case DISCONNECT_DOCK:
+                    // Nothing to do here
+                    return HANDLED;
+                default:
+                    return NOT_HANDLED;
+            }
+        }
+    }
+
+    class ActiveHeadsetRoute extends HeadsetRoute {
+        @Override
+        public String getName() {
+            return ACTIVE_HEADSET_ROUTE_NAME;
+        }
+
+        @Override
+        public boolean isActive() {
+            return true;
+        }
+
+        @Override
+        public void enter() {
+            super.enter();
+            setSpeakerphoneOn(false);
+            setBluetoothOn(false);
+            CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_WIRED_HEADSET,
+                    mAvailableRoutes);
+            setSystemAudioState(mCurrentCallAudioState, newState);
+            updateInternalCallAudioState();
+        }
+
+        @Override
+        public void updateSystemAudioState() {
+            CallAudioState oldAudioState = mCurrentCallAudioState;
+            updateInternalCallAudioState();
+            setSystemAudioState(oldAudioState, mCurrentCallAudioState);
+        }
+
+        @Override
+        public boolean processMessage(Message msg) {
+            if (super.processMessage(msg) == HANDLED) {
+                return HANDLED;
+            }
+            switch (msg.what) {
+                case SWITCH_EARPIECE:
+                    if ((mAvailableRoutes & ROUTE_EARPIECE) != 0) {
+                        transitionTo(mActiveEarpieceRoute);
+                    } else {
+                        Log.w(this, "Ignoring switch to earpiece command. Not available.");
+                    }
+                    return HANDLED;
+                case SWITCH_BLUETOOTH:
+                    if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) {
+                        transitionTo(mActiveBluetoothRoute);
+                    } else {
+                        Log.w(this, "Ignoring switch to bluetooth command. Not available.");
+                    }
+                    return HANDLED;
+                case SWITCH_HEADSET:
+                    // Nothing to do
+                    return HANDLED;
+                case SWITCH_SPEAKER:
+                    transitionTo(mActiveSpeakerRoute);
+                    return HANDLED;
+                case SWITCH_FOCUS:
+                    if (msg.arg1 == NO_FOCUS) {
+                        transitionTo(mQuiescentHeadsetRoute);
+                    }
+                    return HANDLED;
+                default:
+                    return NOT_HANDLED;
+            }
+        }
+    }
+
+    class QuiescentHeadsetRoute extends HeadsetRoute {
+        @Override
+        public String getName() {
+            return QUIESCENT_HEADSET_ROUTE_NAME;
+        }
+
+        @Override
+        public boolean isActive() {
+            return false;
+        }
+
+        @Override
+        public void enter() {
+            super.enter();
+            updateInternalCallAudioState();
+        }
+
+        @Override
+        public void updateSystemAudioState() {
+            updateInternalCallAudioState();
+        }
+
+        @Override
+        public boolean processMessage(Message msg) {
+            if (super.processMessage(msg) == HANDLED) {
+                return HANDLED;
+            }
+            switch (msg.what) {
+                case SWITCH_EARPIECE:
+                    if ((mAvailableRoutes & ROUTE_EARPIECE) != 0) {
+                        transitionTo(mQuiescentEarpieceRoute);
+                    } else {
+                        Log.w(this, "Ignoring switch to earpiece command. Not available.");
+                    }
+                    return HANDLED;
+                case SWITCH_BLUETOOTH:
+                    if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) {
+                        transitionTo(mQuiescentBluetoothRoute);
+                    } else {
+                        Log.w(this, "Ignoring switch to bluetooth command. Not available.");
+                    }
+                    return HANDLED;
+                case SWITCH_HEADSET:
+                    // Nothing to do
+                    return HANDLED;
+                case SWITCH_SPEAKER:
+                    transitionTo(mQuiescentSpeakerRoute);
+                    return HANDLED;
+                case SWITCH_FOCUS:
+                    if (msg.arg1 == HAS_FOCUS) {
+                        transitionTo(mActiveHeadsetRoute);
+                    }
+                    return HANDLED;
+                default:
+                    return NOT_HANDLED;
+            }
+        }
+    }
+
+    abstract class HeadsetRoute extends AudioState {
+        @Override
+        public boolean processMessage(Message msg) {
+            if (super.processMessage(msg) == HANDLED) {
+                return HANDLED;
+            }
+            switch (msg.what) {
+                case CONNECT_WIRED_HEADSET:
+                    Log.e(this, new IllegalStateException(),
+                            "Wired headset should already be connected.");
+                    mAvailableRoutes |= ROUTE_WIRED_HEADSET;
+                    updateSystemAudioState();
+                    return HANDLED;
+                case CONNECT_BLUETOOTH:
+                    sendInternalMessage(SWITCH_BLUETOOTH);
+                    return HANDLED;
+                case DISCONNECT_BLUETOOTH:
+                    updateSystemAudioState();
+                    // No change in audio route required
+                    return HANDLED;
+                case DISCONNECT_WIRED_HEADSET:
+                    if (mWasOnSpeaker) {
+                        sendInternalMessage(SWITCH_SPEAKER);
+                    } else {
+                        sendInternalMessage(SWITCH_EARPIECE);
+                    }
+                    return HANDLED;
+                case CONNECT_DOCK:
+                    // Nothing to do here
+                    return HANDLED;
+                case DISCONNECT_DOCK:
+                    // Nothing to do here
+                    return HANDLED;
+                default:
+                    return NOT_HANDLED;
+            }
+        }
+    }
+
+    class ActiveBluetoothRoute extends BluetoothRoute {
+        @Override
+        public String getName() {
+            return ACTIVE_BLUETOOTH_ROUTE_NAME;
+        }
+
+        @Override
+        public boolean isActive() {
+            return true;
+        }
+
+        @Override
+        public void enter() {
+            super.enter();
+            setSpeakerphoneOn(false);
+            setBluetoothOn(true);
+            CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_BLUETOOTH,
+                    mAvailableRoutes);
+            setSystemAudioState(mCurrentCallAudioState, newState);
+            updateInternalCallAudioState();
+        }
+
+        @Override
+        public void updateSystemAudioState() {
+            CallAudioState oldAudioState = mCurrentCallAudioState;
+            updateInternalCallAudioState();
+            setSystemAudioState(oldAudioState, mCurrentCallAudioState);
+        }
+
+        @Override
+        public boolean processMessage(Message msg) {
+            if (super.processMessage(msg) == HANDLED) {
+                return HANDLED;
+            }
+            switch (msg.what) {
+                case SWITCH_EARPIECE:
+                    if ((mAvailableRoutes & ROUTE_EARPIECE) != 0) {
+                        transitionTo(mActiveEarpieceRoute);
+                    } else {
+                        Log.w(this, "Ignoring switch to earpiece command. Not available.");
+                    }
+                    return HANDLED;
+                case SWITCH_BLUETOOTH:
+                    // Nothing to do
+                    return HANDLED;
+                case SWITCH_HEADSET:
+                    if ((mAvailableRoutes & ROUTE_WIRED_HEADSET) != 0) {
+                        transitionTo(mActiveHeadsetRoute);
+                    } else {
+                        Log.w(this, "Ignoring switch to headset command. Not available.");
+                    }
+                    return HANDLED;
+                case SWITCH_SPEAKER:
+                    transitionTo(mActiveSpeakerRoute);
+                    return HANDLED;
+                case SWITCH_FOCUS:
+                    if (msg.arg1 == NO_FOCUS) {
+                        transitionTo(mQuiescentBluetoothRoute);
+                    }
+                    return HANDLED;
+                default:
+                    return NOT_HANDLED;
+            }
+        }
+    }
+
+    class QuiescentBluetoothRoute extends BluetoothRoute {
+        @Override
+        public String getName() {
+            return QUIESCENT_BLUETOOTH_ROUTE_NAME;
+        }
+
+        @Override
+        public boolean isActive() {
+            return false;
+        }
+
+        @Override
+        public void enter() {
+            super.enter();
+            updateInternalCallAudioState();
+        }
+
+        @Override
+        public void updateSystemAudioState() {
+            updateInternalCallAudioState();
+        }
+
+        @Override
+        public boolean processMessage(Message msg) {
+            if (super.processMessage(msg) == HANDLED) {
+                return HANDLED;
+            }
+            switch (msg.what) {
+                case SWITCH_EARPIECE:
+                    if ((mAvailableRoutes & ROUTE_EARPIECE) != 0) {
+                        transitionTo(mQuiescentEarpieceRoute);
+                    } else {
+                        Log.w(this, "Ignoring switch to earpiece command. Not available.");
+                    }
+                    return HANDLED;
+                case SWITCH_BLUETOOTH:
+                    // Nothing to do
+                    return HANDLED;
+                case SWITCH_HEADSET:
+                    if ((mAvailableRoutes & ROUTE_WIRED_HEADSET) != 0) {
+                        transitionTo(mQuiescentHeadsetRoute);
+                    } else {
+                        Log.w(this, "Ignoring switch to headset command. Not available.");
+                    }
+                    return HANDLED;
+                case SWITCH_SPEAKER:
+                    transitionTo(mQuiescentSpeakerRoute);
+                    return HANDLED;
+                case SWITCH_FOCUS:
+                    if (msg.arg1 == HAS_FOCUS) {
+                        transitionTo(mActiveBluetoothRoute);
+                    }
+                    return HANDLED;
+                default:
+                    return NOT_HANDLED;
+            }
+        }
+    }
+
+    abstract class BluetoothRoute extends AudioState {
+        @Override
+        public boolean processMessage(Message msg) {
+            if (super.processMessage(msg) == HANDLED) {
+                return HANDLED;
+            }
+            switch (msg.what) {
+                case CONNECT_WIRED_HEADSET:
+                    sendInternalMessage(SWITCH_HEADSET);
+                    return HANDLED;
+                case CONNECT_BLUETOOTH:
+                    // We can't tell when a change in bluetooth state corresponds to an
+                    // actual connection or disconnection, so we'll just ignore it if we're already
+                    // in the bluetooth route.
+                    return HANDLED;
+                case DISCONNECT_BLUETOOTH:
+                    sendInternalMessage(SWITCH_WIRED_OR_EARPIECE);
+                    mWasOnSpeaker = false;
+                    return HANDLED;
+                case DISCONNECT_WIRED_HEADSET:
+                    updateSystemAudioState();
+                    // No change in audio route required
+                    return HANDLED;
+                case CONNECT_DOCK:
+                    // Nothing to do here
+                    return HANDLED;
+                case DISCONNECT_DOCK:
+                    // Nothing to do here
+                    return HANDLED;
+                default:
+                    return NOT_HANDLED;
+            }
+        }
+    }
+
+    class ActiveSpeakerRoute extends SpeakerRoute {
+        @Override
+        public String getName() {
+            return ACTIVE_SPEAKER_ROUTE_NAME;
+        }
+
+        @Override
+        public boolean isActive() {
+            return true;
+        }
+
+        @Override
+        public void enter() {
+            super.enter();
+            mWasOnSpeaker = true;
+            setSpeakerphoneOn(true);
+            setBluetoothOn(false);
+            CallAudioState newState = new CallAudioState(mIsMuted, ROUTE_SPEAKER,
+                    mAvailableRoutes);
+            setSystemAudioState(mCurrentCallAudioState, newState);
+            updateInternalCallAudioState();
+        }
+
+        @Override
+        public void updateSystemAudioState() {
+            CallAudioState oldAudioState = mCurrentCallAudioState;
+            updateInternalCallAudioState();
+            setSystemAudioState(oldAudioState, mCurrentCallAudioState);
+        }
+
+        @Override
+        public boolean processMessage(Message msg) {
+            if (super.processMessage(msg) == HANDLED) {
+                return HANDLED;
+            }
+            switch(msg.what) {
+                case SWITCH_EARPIECE:
+                    if ((mAvailableRoutes & ROUTE_EARPIECE) != 0) {
+                        transitionTo(mActiveEarpieceRoute);
+                    } else {
+                        Log.w(this, "Ignoring switch to earpiece command. Not available.");
+                    }
+                    return HANDLED;
+                case SWITCH_BLUETOOTH:
+                    if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) {
+                        transitionTo(mActiveBluetoothRoute);
+                    } else {
+                        Log.w(this, "Ignoring switch to bluetooth command. Not available.");
+                    }
+                    return HANDLED;
+                case SWITCH_HEADSET:
+                    if ((mAvailableRoutes & ROUTE_WIRED_HEADSET) != 0) {
+                        transitionTo(mActiveHeadsetRoute);
+                    } else {
+                        Log.w(this, "Ignoring switch to headset command. Not available.");
+                    }
+                    return HANDLED;
+                case SWITCH_SPEAKER:
+                    // Nothing to do
+                    return HANDLED;
+                case SWITCH_FOCUS:
+                    if (msg.arg1 == NO_FOCUS) {
+                        transitionTo(mQuiescentSpeakerRoute);
+                    }
+                    return HANDLED;
+                default:
+                    return NOT_HANDLED;
+            }
+        }
+    }
+
+    class QuiescentSpeakerRoute extends SpeakerRoute {
+        @Override
+        public String getName() {
+            return QUIESCENT_SPEAKER_ROUTE_NAME;
+        }
+
+        @Override
+        public boolean isActive() {
+            return false;
+        }
+
+        @Override
+        public void enter() {
+            super.enter();
+            // Omit setting mWasOnSpeaker to true here, since this does not reflect a call
+            // actually being on speakerphone.
+            updateInternalCallAudioState();
+        }
+
+        @Override
+        public void updateSystemAudioState() {
+            updateInternalCallAudioState();
+        }
+
+        @Override
+        public boolean processMessage(Message msg) {
+            if (super.processMessage(msg) == HANDLED) {
+                return HANDLED;
+            }
+            switch(msg.what) {
+                case SWITCH_EARPIECE:
+                    if ((mAvailableRoutes & ROUTE_EARPIECE) != 0) {
+                        transitionTo(mQuiescentEarpieceRoute);
+                    } else {
+                        Log.w(this, "Ignoring switch to earpiece command. Not available.");
+                    }
+                    return HANDLED;
+                case SWITCH_BLUETOOTH:
+                    if ((mAvailableRoutes & ROUTE_BLUETOOTH) != 0) {
+                        transitionTo(mQuiescentBluetoothRoute);
+                    } else {
+                        Log.w(this, "Ignoring switch to bluetooth command. Not available.");
+                    }
+                    return HANDLED;
+                case SWITCH_HEADSET:
+                    if ((mAvailableRoutes & ROUTE_WIRED_HEADSET) != 0) {
+                        transitionTo(mQuiescentHeadsetRoute);
+                    } else {
+                        Log.w(this, "Ignoring switch to headset command. Not available.");
+                    }
+                    return HANDLED;
+                case SWITCH_SPEAKER:
+                    // Nothing to do
+                    return HANDLED;
+                case SWITCH_FOCUS:
+                    if (msg.arg1 == HAS_FOCUS) {
+                        transitionTo(mActiveSpeakerRoute);
+                    }
+                    return HANDLED;
+                default:
+                    return NOT_HANDLED;
+            }
+        }
+    }
+
+    abstract class SpeakerRoute extends AudioState {
+        @Override
+        public boolean processMessage(Message msg) {
+            if (super.processMessage(msg) == HANDLED) {
+                return HANDLED;
+            }
+            switch (msg.what) {
+                case CONNECT_WIRED_HEADSET:
+                    sendInternalMessage(SWITCH_HEADSET);
+                    return HANDLED;
+                case CONNECT_BLUETOOTH:
+                    sendInternalMessage(SWITCH_BLUETOOTH);
+                    return HANDLED;
+                case DISCONNECT_BLUETOOTH:
+                    updateSystemAudioState();
+                    // No change in audio route required
+                    return HANDLED;
+                case DISCONNECT_WIRED_HEADSET:
+                    updateSystemAudioState();
+                    // No change in audio route required
+                    return HANDLED;
+                case CONNECT_DOCK:
+                    // Nothing to do here
+                    return HANDLED;
+                case DISCONNECT_DOCK:
+                    sendInternalMessage(SWITCH_WIRED_OR_EARPIECE);
+                    return HANDLED;
+               default:
+                    return NOT_HANDLED;
+            }
+        }
+    }
+
+    private final ActiveEarpieceRoute mActiveEarpieceRoute = new ActiveEarpieceRoute();
+    private final ActiveHeadsetRoute mActiveHeadsetRoute = new ActiveHeadsetRoute();
+    private final ActiveBluetoothRoute mActiveBluetoothRoute = new ActiveBluetoothRoute();
+    private final ActiveSpeakerRoute mActiveSpeakerRoute = new ActiveSpeakerRoute();
+    private final QuiescentEarpieceRoute mQuiescentEarpieceRoute = new QuiescentEarpieceRoute();
+    private final QuiescentHeadsetRoute mQuiescentHeadsetRoute = new QuiescentHeadsetRoute();
+    private final QuiescentBluetoothRoute mQuiescentBluetoothRoute = new QuiescentBluetoothRoute();
+    private final QuiescentSpeakerRoute mQuiescentSpeakerRoute = new QuiescentSpeakerRoute();
+
+    /**
+     * A few pieces of hidden state. Used to avoid exponential explosion of number of explicit
+     * states
+     */
+    private int mAvailableRoutes;
+    private boolean mWasOnSpeaker;
+    private boolean mIsMuted;
+
+    private final Context mContext;
+    private final CallsManager mCallsManager;
+    private final AudioManager mAudioManager;
+    private final BluetoothManager mBluetoothManager;
+    private final WiredHeadsetManager mWiredHeadsetManager;
+    private final StatusBarNotifier mStatusBarNotifier;
+    private final CallAudioManager.AudioServiceFactory mAudioServiceFactory;
+
+    private HashMap<String, Integer> mStateNameToRouteCode;
+    private HashMap<Integer, AudioState> mRouteCodeToQuiescentState;
+
+    // CallAudioState is used as an interface to communicate with many other system components.
+    // No internal state transitions should depend on this variable.
+    private CallAudioState mCurrentCallAudioState;
+
+    public CallAudioRouteStateMachine(
+            Context context,
+            CallsManager callsManager,
+            BluetoothManager bluetoothManager,
+            WiredHeadsetManager wiredHeadsetManager,
+            StatusBarNotifier statusBarNotifier,
+            CallAudioManager.AudioServiceFactory audioServiceFactory) {
+        super(NAME);
+        addState(mActiveEarpieceRoute);
+        addState(mActiveHeadsetRoute);
+        addState(mActiveBluetoothRoute);
+        addState(mActiveSpeakerRoute);
+        addState(mQuiescentEarpieceRoute);
+        addState(mQuiescentHeadsetRoute);
+        addState(mQuiescentBluetoothRoute);
+        addState(mQuiescentSpeakerRoute);
+
+        mContext = context;
+        mCallsManager = callsManager;
+        mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+        mBluetoothManager = bluetoothManager;
+        mWiredHeadsetManager = wiredHeadsetManager;
+        mStatusBarNotifier = statusBarNotifier;
+        mAudioServiceFactory = audioServiceFactory;
+
+        mStateNameToRouteCode = new HashMap<>(8);
+        mStateNameToRouteCode.put(mQuiescentEarpieceRoute.getName(), ROUTE_EARPIECE);
+        mStateNameToRouteCode.put(mQuiescentBluetoothRoute.getName(), ROUTE_BLUETOOTH);
+        mStateNameToRouteCode.put(mQuiescentHeadsetRoute.getName(), ROUTE_WIRED_HEADSET);
+        mStateNameToRouteCode.put(mQuiescentSpeakerRoute.getName(), ROUTE_SPEAKER);
+        mStateNameToRouteCode.put(mActiveEarpieceRoute.getName(), ROUTE_EARPIECE);
+        mStateNameToRouteCode.put(mActiveBluetoothRoute.getName(), ROUTE_BLUETOOTH);
+        mStateNameToRouteCode.put(mActiveHeadsetRoute.getName(), ROUTE_WIRED_HEADSET);
+        mStateNameToRouteCode.put(mActiveSpeakerRoute.getName(), ROUTE_SPEAKER);
+
+        mRouteCodeToQuiescentState = new HashMap<>(4);
+        mRouteCodeToQuiescentState.put(ROUTE_EARPIECE, mQuiescentEarpieceRoute);
+        mRouteCodeToQuiescentState.put(ROUTE_BLUETOOTH, mQuiescentBluetoothRoute);
+        mRouteCodeToQuiescentState.put(ROUTE_SPEAKER, mQuiescentSpeakerRoute);
+        mRouteCodeToQuiescentState.put(ROUTE_WIRED_HEADSET, mQuiescentHeadsetRoute);
+        initialize();
+    }
+
+    /**
+     * Initializes the state machine with info on initial audio route, supported audio routes,
+     * and mute status.
+     */
+    public void initialize() {
+        CallAudioState initState = getInitialAudioState();
+        initialize(initState);
+    }
+
+    public void initialize(CallAudioState initState) {
+        mCurrentCallAudioState = initState;
+        mAvailableRoutes = initState.getSupportedRouteMask();
+        mIsMuted = initState.isMuted();
+        mWasOnSpeaker = initState.getRoute() == ROUTE_SPEAKER;
+
+        mStatusBarNotifier.notifyMute(initState.isMuted());
+        mStatusBarNotifier.notifySpeakerphone(initState.getRoute() == CallAudioState.ROUTE_SPEAKER);
+        setInitialState(mRouteCodeToQuiescentState.get(initState.getRoute()));
+        start();
+    }
+
+    /**
+     * Getter for the current CallAudioState object that the state machine is keeping track of.
+     * Used for compatibility purposes.
+     */
+    public CallAudioState getCurrentCallAudioState() {
+        return mCurrentCallAudioState;
+    }
+
+    /**
+     * This is for state-independent changes in audio route (i.e. muting)
+     * @param msg that couldn't be handled.
+     */
+    @Override
+    protected void unhandledMessage(Message msg) {
+        CallAudioState newCallAudioState;
+        switch (msg.what) {
+            case MUTE_ON:
+                setMuteOn(true);
+                newCallAudioState = new CallAudioState(mIsMuted,
+                        mCurrentCallAudioState.getRoute(),
+                        mAvailableRoutes);
+                setSystemAudioState(mCurrentCallAudioState, newCallAudioState);
+                updateInternalCallAudioState();
+                return;
+            case MUTE_OFF:
+                setMuteOn(false);
+                newCallAudioState = new CallAudioState(mIsMuted,
+                        mCurrentCallAudioState.getRoute(),
+                        mAvailableRoutes);
+                setSystemAudioState(mCurrentCallAudioState, newCallAudioState);
+                updateInternalCallAudioState();
+                return;
+            case TOGGLE_MUTE:
+                if (mIsMuted) {
+                    sendInternalMessage(MUTE_OFF);
+                } else {
+                    sendInternalMessage(MUTE_ON);
+                }
+                return;
+            default:
+                Log.e(this, new IllegalStateException(),
+                        "Unexpected message code");
+        }
+    }
+
+    private void setSpeakerphoneOn(boolean on) {
+        if (mAudioManager.isSpeakerphoneOn() != on) {
+            Log.i(this, "turning speaker phone %s", on);
+            mAudioManager.setSpeakerphoneOn(on);
+        }
+    }
+
+    private void setBluetoothOn(boolean on) {
+        if (mBluetoothManager.isBluetoothAvailable()) {
+            boolean isAlreadyOn = mBluetoothManager.isBluetoothAudioConnectedOrPending();
+            if (on != isAlreadyOn) {
+                Log.i(this, "connecting bluetooth %s", on);
+                if (on) {
+                    mBluetoothManager.connectBluetoothAudio();
+                } else {
+                    mBluetoothManager.disconnectBluetoothAudio();
+                }
+            }
+        }
+    }
+
+    private void setMuteOn(boolean mute) {
+        mIsMuted = mute;
+        Log.event(mCallsManager.getForegroundCall(), Log.Events.MUTE,
+                mute ? "on" : "off");
+        if (mute != mAudioManager.isMicrophoneMute() && isInActiveState()) {
+            IAudioService audio = mAudioServiceFactory.getAudioService();
+            Log.i(this, "changing microphone mute state to: %b [serviceIsNull=%b]",
+                    mute, audio == null);
+            if (audio != null) {
+                try {
+                    // We use the audio service directly here so that we can specify
+                    // the current user. Telecom runs in the system_server process which
+                    // may run as a separate user from the foreground user. If we
+                    // used AudioManager directly, we would change mute for the system's
+                    // user and not the current foreground, which we want to avoid.
+                    audio.setMicrophoneMute(
+                            mute, mContext.getOpPackageName(), getCurrentUserId());
+
+                } catch (RemoteException e) {
+                    Log.e(this, e, "Remote exception while toggling mute.");
+                }
+                // TODO: Check microphone state after attempting to set to ensure that
+                // our state corroborates AudioManager's state.
+            }
+        }
+    }
+
+    /**
+     * Updates the CallAudioState object from current internal state. The result is used for
+     * external communication only.
+     */
+    private void updateInternalCallAudioState() {
+        IState currentState = getCurrentState();
+        if (currentState == null) {
+            Log.e(this, new IllegalStateException(), "Current state should never be null" +
+                    " when updateInternalCallAudioState is called.");
+            mCurrentCallAudioState = new CallAudioState(
+                    mIsMuted, mCurrentCallAudioState.getRoute(), mAvailableRoutes);
+            return;
+        }
+        int currentRoute = mStateNameToRouteCode.get(currentState.getName());
+        mCurrentCallAudioState = new CallAudioState(mIsMuted, currentRoute, mAvailableRoutes);
+    }
+
+    private void setSystemAudioState(CallAudioState oldCallAudioState,
+            CallAudioState newCallAudioState) {
+        Log.i(this, "setSystemAudioState: changing from %s to %s", oldCallAudioState,
+                newCallAudioState);
+        Log.event(mCallsManager.getForegroundCall(), Log.Events.AUDIO_ROUTE,
+                CallAudioState.audioRouteToString(newCallAudioState.getRoute()));
+
+        if (!oldCallAudioState.equals(newCallAudioState)) {
+            mCallsManager.onCallAudioStateChanged(oldCallAudioState, newCallAudioState);
+            updateAudioForForegroundCall(newCallAudioState);
+        }
+    }
+
+    private void updateAudioForForegroundCall(CallAudioState newCallAudioState) {
+        Call call = mCallsManager.getForegroundCall();
+        if (call != null && call.getConnectionService() != null) {
+            call.getConnectionService().onCallAudioStateChanged(call, newCallAudioState);
+        }
+    }
+
+    private int calculateSupportedRoutes() {
+        int routeMask = CallAudioState.ROUTE_SPEAKER;
+
+        if (mWiredHeadsetManager.isPluggedIn()) {
+            routeMask |= CallAudioState.ROUTE_WIRED_HEADSET;
+        } else {
+            routeMask |= CallAudioState.ROUTE_EARPIECE;
+        }
+
+        if (mBluetoothManager.isBluetoothAvailable()) {
+            routeMask |=  CallAudioState.ROUTE_BLUETOOTH;
+        }
+
+        return routeMask;
+    }
+
+    private void sendInternalMessage(int messageCode) {
+        // Internal messages are messages which the state machine sends to itself in the
+        // course of processing externally-sourced messages. We want to send these messages at
+        // the front of the queue in order to make actions appear atomic to the user and to
+        // prevent scenarios such as these:
+        // 1. State machine handler thread is suspended for some reason.
+        // 2. Headset gets connected (sends CONNECT_HEADSET).
+        // 3. User switches to speakerphone in the UI (sends SWITCH_SPEAKER).
+        // 4. State machine handler is un-suspended.
+        // 5. State machine handler processes the CONNECT_HEADSET message and sends
+        //    SWITCH_HEADSET at end of queue.
+        // 6. State machine handler processes SWITCH_SPEAKER.
+        // 7. State machine handler processes SWITCH_HEADSET.
+        sendMessageAtFrontOfQueue(messageCode);
+    }
+
+    private CallAudioState getInitialAudioState() {
+        int supportedRouteMask = calculateSupportedRoutes();
+        int route = (supportedRouteMask & ROUTE_WIRED_HEADSET) != 0
+                ? ROUTE_WIRED_HEADSET : ROUTE_EARPIECE;
+        if ((supportedRouteMask & ROUTE_BLUETOOTH) != 0) {
+            route = ROUTE_BLUETOOTH;
+        }
+
+        return new CallAudioState(false, route, supportedRouteMask);
+    }
+
+    private int getCurrentUserId() {
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            UserInfo currentUser = ActivityManagerNative.getDefault().getCurrentUser();
+            return currentUser.id;
+        } catch (RemoteException e) {
+            // Activity manager not running, nothing we can do assume user 0.
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+        return UserHandle.USER_OWNER;
+    }
+
+    private boolean isInActiveState() {
+        AudioState currentState = (AudioState) getCurrentState();
+        if (currentState == null) {
+            Log.w(this, "Current state is null, assuming inactive state");
+            return false;
+        }
+        return currentState.isActive();
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 12ee98e..c6bd951 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -132,6 +132,7 @@
             new ConcurrentHashMap<CallsManagerListener, Boolean>(16, 0.9f, 1));
     private final HeadsetMediaButton mHeadsetMediaButton;
     private final WiredHeadsetManager mWiredHeadsetManager;
+    private final BluetoothManager mBluetoothManager;
     private final DockManager mDockManager;
     private final TtyManager mTtyManager;
     private final ProximitySensorManager mProximitySensorManager;
@@ -171,7 +172,9 @@
             HeadsetMediaButtonFactory headsetMediaButtonFactory,
             ProximitySensorManagerFactory proximitySensorManagerFactory,
             InCallWakeLockControllerFactory inCallWakeLockControllerFactory,
-            CallAudioManager.AudioServiceFactory audioServiceFactory) {
+            CallAudioManager.AudioServiceFactory audioServiceFactory,
+            BluetoothManager bluetoothManager,
+            WiredHeadsetManager wiredHeadsetManager) {
         mContext = context;
         mLock = lock;
         mContactsAsyncHelper = contactsAsyncHelper;
@@ -179,13 +182,29 @@
         mPhoneAccountRegistrar = phoneAccountRegistrar;
         mMissedCallNotifier = missedCallNotifier;
         StatusBarNotifier statusBarNotifier = new StatusBarNotifier(context, this);
-        mWiredHeadsetManager = new WiredHeadsetManager(context);
+        mWiredHeadsetManager = wiredHeadsetManager;
+        mBluetoothManager = bluetoothManager;
         mDockManager = new DockManager(context);
-        mCallAudioManager = new CallAudioManager(
-                context, mLock, statusBarNotifier,
-                mWiredHeadsetManager, mDockManager, this, audioServiceFactory);
-        InCallTonePlayer.Factory playerFactory = 
-	    new InCallTonePlayer.Factory(mCallAudioManager, lock);
+        CallAudioRouteStateMachine callAudioRouteStateMachine = new CallAudioRouteStateMachine(
+                context,
+                this,
+                bluetoothManager,
+                wiredHeadsetManager,
+                statusBarNotifier,
+                audioServiceFactory
+        );
+        CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter =
+                new CallAudioRoutePeripheralAdapter(
+                        callAudioRouteStateMachine,
+                        bluetoothManager,
+                        wiredHeadsetManager,
+                        mDockManager);
+
+        mCallAudioManager = new CallAudioManager(context, mLock, this, callAudioRouteStateMachine);
+
+        InCallTonePlayer.Factory playerFactory = new InCallTonePlayer.Factory(mCallAudioManager,
+                callAudioRoutePeripheralAdapter, lock);
+
         RingtoneFactory ringtoneFactory = new RingtoneFactory(context);
         SystemVibrator systemVibrator = new SystemVibrator(context);
         AsyncRingtonePlayer asyncRingtonePlayer = new AsyncRingtonePlayer();
@@ -193,7 +212,7 @@
         mRinger = new Ringer(
                 mCallAudioManager, this, playerFactory, context, systemSettingsUtil,
                 asyncRingtonePlayer, ringtoneFactory, systemVibrator);
-	mHeadsetMediaButton = headsetMediaButtonFactory.create(context, this, mLock);
+        mHeadsetMediaButton = headsetMediaButtonFactory.create(context, this, mLock);
         mTtyManager = new TtyManager(context, mWiredHeadsetManager);
         mProximitySensorManager = proximitySensorManagerFactory.create(context, this);
         mPhoneStateBroadcaster = new PhoneStateBroadcaster(this);
@@ -846,7 +865,7 @@
             call.answer(videoState);
             if (VideoProfile.isVideo(videoState) &&
                 !mWiredHeadsetManager.isPluggedIn() &&
-                !mCallAudioManager.isBluetoothDeviceAvailable() &&
+                !mBluetoothManager.isBluetoothAvailable() &&
                 isSpeakerEnabledForVideoCalls()) {
                 call.setStartWithSpeakerphoneOn(true);
             }
@@ -1034,7 +1053,9 @@
     }
 
     /** Called when the audio state changes. */
-    void onCallAudioStateChanged(CallAudioState oldAudioState, CallAudioState newAudioState) {
+    @VisibleForTesting
+    public void onCallAudioStateChanged(CallAudioState oldAudioState, CallAudioState
+            newAudioState) {
         Log.v(this, "onAudioStateChanged, audioState: %s -> %s", oldAudioState, newAudioState);
         for (CallsManagerListener listener : mListeners) {
             listener.onCallAudioStateChanged(oldAudioState, newAudioState);
diff --git a/src/com/android/server/telecom/ConnectionServiceWrapper.java b/src/com/android/server/telecom/ConnectionServiceWrapper.java
index 619fe01..f7046bc 100644
--- a/src/com/android/server/telecom/ConnectionServiceWrapper.java
+++ b/src/com/android/server/telecom/ConnectionServiceWrapper.java
@@ -37,6 +37,7 @@
 import android.telecom.TelecomManager;
 import android.telecom.VideoProfile;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.telecom.IConnectionService;
 import com.android.internal.telecom.IConnectionServiceAdapter;
 import com.android.internal.telecom.IVideoProvider;
@@ -57,7 +58,8 @@
  * {@link IConnectionService} directly and instead should use this class to invoke methods of
  * {@link IConnectionService}.
  */
-final class ConnectionServiceWrapper extends ServiceBinder {
+@VisibleForTesting
+public class ConnectionServiceWrapper extends ServiceBinder {
 
     private final class Adapter extends IConnectionServiceAdapter.Stub {
 
@@ -684,7 +686,8 @@
     }
 
     /** @see IConnectionService#onCallAudioStateChanged(String,CallAudioState) */
-    void onCallAudioStateChanged(Call activeCall, CallAudioState audioState) {
+    @VisibleForTesting
+    public void onCallAudioStateChanged(Call activeCall, CallAudioState audioState) {
         final String callId = mCallIdMapper.getCallId(activeCall);
         if (callId != null && isServiceValid("onCallAudioStateChanged")) {
             try {
diff --git a/src/com/android/server/telecom/DockManager.java b/src/com/android/server/telecom/DockManager.java
index e6ad446..1048e2b 100644
--- a/src/com/android/server/telecom/DockManager.java
+++ b/src/com/android/server/telecom/DockManager.java
@@ -21,6 +21,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
 
 import java.util.Collections;
@@ -28,8 +29,10 @@
 import java.util.concurrent.ConcurrentHashMap;
 
 /** Listens for and caches car dock state. */
-class DockManager {
-    interface Listener {
+@VisibleForTesting
+public class DockManager {
+    @VisibleForTesting
+    public interface Listener {
         void onDockChanged(boolean isDocked);
     }
 
@@ -65,7 +68,8 @@
         context.registerReceiver(mReceiver, intentFilter);
     }
 
-    void addListener(Listener listener) {
+    @VisibleForTesting
+    public void addListener(Listener listener) {
         mListeners.add(listener);
     }
 
diff --git a/src/com/android/server/telecom/InCallTonePlayer.java b/src/com/android/server/telecom/InCallTonePlayer.java
index bdebace..4fa4389 100644
--- a/src/com/android/server/telecom/InCallTonePlayer.java
+++ b/src/com/android/server/telecom/InCallTonePlayer.java
@@ -35,15 +35,20 @@
      */
     public static class Factory {
         private final CallAudioManager mCallAudioManager;
+        private final CallAudioRoutePeripheralAdapter mCallAudioRoutePeripheralAdapter;
         private final TelecomSystem.SyncRoot mLock;
 
-        Factory(CallAudioManager callAudioManager, TelecomSystem.SyncRoot lock) {
+        Factory(CallAudioManager callAudioManager,
+                CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter,
+                TelecomSystem.SyncRoot lock) {
             mCallAudioManager = callAudioManager;
+            mCallAudioRoutePeripheralAdapter = callAudioRoutePeripheralAdapter;
             mLock = lock;
         }
 
         public InCallTonePlayer createPlayer(int tone) {
-            return new InCallTonePlayer(tone, mCallAudioManager, mLock);
+            return new InCallTonePlayer(tone, mCallAudioManager,
+                    mCallAudioRoutePeripheralAdapter, mLock);
         }
     }
 
@@ -85,6 +90,7 @@
     private static int sTonesPlaying = 0;
 
     private final CallAudioManager mCallAudioManager;
+    private final CallAudioRoutePeripheralAdapter mCallAudioRoutePeripheralAdapter;
 
     private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
 
@@ -105,10 +111,12 @@
     private InCallTonePlayer(
             int toneId,
             CallAudioManager callAudioManager,
+            CallAudioRoutePeripheralAdapter callAudioRoutePeripheralAdapter,
             TelecomSystem.SyncRoot lock) {
         mState = STATE_OFF;
         mToneId = toneId;
         mCallAudioManager = callAudioManager;
+        mCallAudioRoutePeripheralAdapter = callAudioRoutePeripheralAdapter;
         mLock = lock;
     }
 
@@ -197,7 +205,7 @@
             }
 
             int stream = AudioManager.STREAM_VOICE_CALL;
-            if (mCallAudioManager.isBluetoothAudioOn()) {
+            if (mCallAudioRoutePeripheralAdapter.isBluetoothAudioOn()) {
                 stream = AudioManager.STREAM_BLUETOOTH_SCO;
             }
 
diff --git a/src/com/android/server/telecom/StatusBarNotifier.java b/src/com/android/server/telecom/StatusBarNotifier.java
index c8c3c18..d8ede59 100644
--- a/src/com/android/server/telecom/StatusBarNotifier.java
+++ b/src/com/android/server/telecom/StatusBarNotifier.java
@@ -19,12 +19,15 @@
 import android.app.StatusBarManager;
 import android.content.Context;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 // TODO: Needed for move to system service: import com.android.internal.R;
 
 /**
  * Manages the special status bar notifications used by the phone app.
  */
-final class StatusBarNotifier extends CallsManagerListenerBase {
+@VisibleForTesting
+public class StatusBarNotifier extends CallsManagerListenerBase {
     private static final String SLOT_MUTE = "mute";
     private static final String SLOT_SPEAKERPHONE = "speakerphone";
 
@@ -50,7 +53,8 @@
         }
     }
 
-    void notifyMute(boolean isMuted) {
+    @VisibleForTesting
+    public void notifyMute(boolean isMuted) {
         // Never display anything if there are no calls.
         if (!mCallsManager.hasAnyCalls()) {
             isMuted = false;
@@ -74,7 +78,8 @@
         mIsShowingMute = isMuted;
     }
 
-    void notifySpeakerphone(boolean isSpeakerphone) {
+    @VisibleForTesting
+    public void notifySpeakerphone(boolean isSpeakerphone) {
         // Never display anything if there are no calls.
         if (!mCallsManager.hasAnyCalls()) {
             isSpeakerphone = false;
diff --git a/src/com/android/server/telecom/TelecomSystem.java b/src/com/android/server/telecom/TelecomSystem.java
index 7ebcddc..e7acf52 100644
--- a/src/com/android/server/telecom/TelecomSystem.java
+++ b/src/com/android/server/telecom/TelecomSystem.java
@@ -125,6 +125,9 @@
         mMissedCallNotifier = missedCallNotifier;
         mPhoneAccountRegistrar = new PhoneAccountRegistrar(mContext);
         mContactsAsyncHelper = new ContactsAsyncHelper(mLock);
+        BluetoothManager bluetoothManager = new BluetoothManager(mContext);
+        WiredHeadsetManager wiredHeadsetManager = new WiredHeadsetManager(mContext);
+
 
         mCallsManager = new CallsManager(
                 mContext,
@@ -136,7 +139,9 @@
                 headsetMediaButtonFactory,
                 proximitySensorManagerFactory,
                 inCallWakeLockControllerFactory,
-                audioServiceFactory);
+                audioServiceFactory,
+                bluetoothManager,
+                wiredHeadsetManager);
 
         mRespondViaSmsManager = new RespondViaSmsManager(mCallsManager, mLock);
         mCallsManager.setRespondViaSmsManager(mRespondViaSmsManager);
diff --git a/src/com/android/server/telecom/WiredHeadsetManager.java b/src/com/android/server/telecom/WiredHeadsetManager.java
index ef5f38c..f25e928 100644
--- a/src/com/android/server/telecom/WiredHeadsetManager.java
+++ b/src/com/android/server/telecom/WiredHeadsetManager.java
@@ -22,6 +22,7 @@
 import android.content.IntentFilter;
 import android.media.AudioManager;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
 
 import java.util.Collections;
@@ -29,8 +30,10 @@
 import java.util.concurrent.ConcurrentHashMap;
 
 /** Listens for and caches headset state. */
-class WiredHeadsetManager {
-    interface Listener {
+@VisibleForTesting
+public class WiredHeadsetManager {
+    @VisibleForTesting
+    public interface Listener {
         void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn);
     }
 
@@ -58,7 +61,7 @@
     private final Set<Listener> mListeners = Collections.newSetFromMap(
             new ConcurrentHashMap<Listener, Boolean>(8, 0.9f, 1));
 
-    WiredHeadsetManager(Context context) {
+    public WiredHeadsetManager(Context context) {
         mReceiver = new WiredHeadsetBroadcastReceiver();
 
         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
@@ -69,7 +72,8 @@
         context.registerReceiver(mReceiver, intentFilter);
     }
 
-    void addListener(Listener listener) {
+    @VisibleForTesting
+    public void addListener(Listener listener) {
         mListeners.add(listener);
     }
 
@@ -79,7 +83,8 @@
         }
     }
 
-    boolean isPluggedIn() {
+    @VisibleForTesting
+    public boolean isPluggedIn() {
         return mIsPluggedIn;
     }
 
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
new file mode 100644
index 0000000..496ad7d
--- /dev/null
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
@@ -0,0 +1,652 @@
+/*
+ * Copyright (C) 2015 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.server.telecom.tests;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.IAudioService;
+import android.telecom.CallAudioState;
+
+import com.android.server.telecom.BluetoothManager;
+import com.android.server.telecom.Call;
+import com.android.server.telecom.CallAudioManager;
+import com.android.server.telecom.CallAudioRouteStateMachine;
+import com.android.server.telecom.CallsManager;
+import com.android.server.telecom.ConnectionServiceWrapper;
+import com.android.server.telecom.StatusBarNotifier;
+import com.android.server.telecom.WiredHeadsetManager;
+
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+
+public class CallAudioRouteStateMachineTest extends TelecomTestCase {
+    private static final int NONE = 0;
+    private static final int ON = 1;
+    private static final int OFF = 2;
+
+    private class TestParameters {
+        public String name;
+        public int initialRoute;
+        public int availableRoutes; // may excl. speakerphone, because that's always available
+        public int speakerInteraction; // one of NONE, ON, or OFF
+        public int bluetoothInteraction; // one of NONE, ON, or OFF
+        public int action;
+        public int expectedRoute;
+        public int expectedAvailableRoutes; // also may exclude the speakerphone.
+
+        public TestParameters(String name, int initialRoute, int availableRoutes, int
+                speakerInteraction, int bluetoothInteraction, int action, int expectedRoute, int
+                expectedAvailableRoutes) {
+            this.name = name;
+            this.initialRoute = initialRoute;
+            this.availableRoutes = availableRoutes;
+            this.speakerInteraction = speakerInteraction;
+            this.bluetoothInteraction = bluetoothInteraction;
+            this.action = action;
+            this.expectedRoute = expectedRoute;
+            this.expectedAvailableRoutes = expectedAvailableRoutes;
+        }
+
+        @Override
+        public String toString() {
+            return "TestParameters{" +
+                    "name='" + name + '\'' +
+                    ", initialRoute=" + initialRoute +
+                    ", availableRoutes=" + availableRoutes +
+                    ", speakerInteraction=" + speakerInteraction +
+                    ", bluetoothInteraction=" + bluetoothInteraction +
+                    ", action=" + action +
+                    ", expectedRoute=" + expectedRoute +
+                    ", expectedAvailableRoutes=" + expectedAvailableRoutes +
+                    '}';
+        }
+    }
+
+    @Mock CallsManager mockCallsManager;
+    @Mock BluetoothManager mockBluetoothManager;
+    @Mock IAudioService mockAudioService;
+    @Mock ConnectionServiceWrapper mockConnectionServiceWrapper;
+    @Mock WiredHeadsetManager mockWiredHeadsetManager;
+    @Mock StatusBarNotifier mockStatusBarNotifier;
+    @Mock Call fakeCall;
+
+    private CallAudioManager.AudioServiceFactory mAudioServiceFactory;
+    private static final int TEST_TIMEOUT = 200;
+    private AudioManager mockAudioManager;
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        MockitoAnnotations.initMocks(this);
+        mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
+        mockAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+
+        mAudioServiceFactory = new CallAudioManager.AudioServiceFactory() {
+            @Override
+            public IAudioService getAudioService() {
+                return mockAudioService;
+            }
+        };
+
+        when(mockCallsManager.getForegroundCall()).thenReturn(fakeCall);
+        when(fakeCall.getConnectionService()).thenReturn(mockConnectionServiceWrapper);
+        when(fakeCall.isAlive()).thenReturn(true);
+        doNothing().when(mockConnectionServiceWrapper).onCallAudioStateChanged(any(Call.class),
+                any(CallAudioState.class));
+    }
+
+    public void testStateMachineTransitionsWithFocus() throws Throwable {
+        List<TestParameters> paramList = generateTransitionTests();
+        for (TestParameters params : paramList) {
+            try {
+                runParametrizedTestCaseWithFocus(params);
+            } catch (Throwable e) {
+                String newMessage = "Failed at parameters: \n" + params.toString() + '\n'
+                        + e.getMessage();
+                throw(new Throwable(newMessage, e));
+            }
+        }
+    }
+
+    public void testStateMachineTransitionsWithoutFocus() throws Throwable {
+        List<TestParameters> paramList = generateTransitionTests();
+        for (TestParameters params : paramList) {
+            try {
+                runParametrizedTestCaseWithoutFocus(params);
+            } catch (Throwable e) {
+                String newMessage = "Failed at parameters: \n" + params.toString() + '\n'
+                        + e.getMessage();
+                throw(new Throwable(newMessage, e));
+            }
+        }
+    }
+
+    public void testSpeakerPersistence() {
+        CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+                mContext,
+                mockCallsManager,
+                mockBluetoothManager,
+                mockWiredHeadsetManager,
+                mockStatusBarNotifier,
+                mAudioServiceFactory);
+
+        when(mockBluetoothManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
+        when(mockBluetoothManager.isBluetoothAvailable()).thenReturn(true);
+        when(mockAudioManager.isSpeakerphoneOn()).thenReturn(true);
+        doAnswer(new Answer() {
+            @Override
+            public Object answer(InvocationOnMock invocation) throws Throwable {
+                Object[] args = invocation.getArguments();
+                when(mockAudioManager.isSpeakerphoneOn()).thenReturn((Boolean) args[0]);
+                return null;
+            }
+        }).when(mockAudioManager).setSpeakerphoneOn(any(Boolean.class));
+        CallAudioState initState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER);
+        stateMachine.initialize(initState);
+
+        stateMachine.sendMessage(CallAudioRouteStateMachine.SWITCH_FOCUS,
+                CallAudioRouteStateMachine.HAS_FOCUS);
+        stateMachine.sendMessage(CallAudioRouteStateMachine.CONNECT_WIRED_HEADSET);
+        CallAudioState expectedMiddleState = new CallAudioState(false,
+                CallAudioState.ROUTE_WIRED_HEADSET,
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER);
+        verifyNewSystemCallAudioState(initState, expectedMiddleState);
+        resetMocks();
+
+        stateMachine.sendMessage(CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET);
+        verifyNewSystemCallAudioState(expectedMiddleState, initState);
+    }
+
+    public void testInitializationWithNoHeadsetNoBluetooth() {
+        when(mockWiredHeadsetManager.isPluggedIn()).thenReturn(false);
+        when(mockBluetoothManager.isBluetoothAvailable()).thenReturn(false);
+
+        CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+                mContext,
+                mockCallsManager,
+                mockBluetoothManager,
+                mockWiredHeadsetManager,
+                mockStatusBarNotifier,
+                mAudioServiceFactory);
+        stateMachine.initialize();
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER);
+        assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+    }
+
+    public void testInitializationWithHeadsetNoBluetooth() {
+        when(mockWiredHeadsetManager.isPluggedIn()).thenReturn(true);
+        when(mockBluetoothManager.isBluetoothAvailable()).thenReturn(false);
+
+        CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+                mContext,
+                mockCallsManager,
+                mockBluetoothManager,
+                mockWiredHeadsetManager,
+                mockStatusBarNotifier,
+                mAudioServiceFactory);
+        stateMachine.initialize();
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER);
+        assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+    }
+
+    public void testInitializationWithHeadsetAndBluetooth() {
+        when(mockWiredHeadsetManager.isPluggedIn()).thenReturn(true);
+        when(mockBluetoothManager.isBluetoothAvailable()).thenReturn(true);
+
+        CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+                mContext,
+                mockCallsManager,
+                mockBluetoothManager,
+                mockWiredHeadsetManager,
+                mockStatusBarNotifier,
+                mAudioServiceFactory);
+        stateMachine.initialize();
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER
+                | CallAudioState.ROUTE_BLUETOOTH);
+        assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+    }
+
+    public void testInitializationWithBluetoothNoHeadset() {
+        when(mockWiredHeadsetManager.isPluggedIn()).thenReturn(false);
+        when(mockBluetoothManager.isBluetoothAvailable()).thenReturn(true);
+
+        CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+                mContext,
+                mockCallsManager,
+                mockBluetoothManager,
+                mockWiredHeadsetManager,
+                mockStatusBarNotifier,
+                mAudioServiceFactory);
+        stateMachine.initialize();
+        CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER
+                        | CallAudioState.ROUTE_BLUETOOTH);
+        assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+    }
+
+    private List<TestParameters> generateTransitionTests() {
+        List<TestParameters> params = new ArrayList<>();
+        params.add(new TestParameters(
+                "Connect headset during earpiece", // name
+                CallAudioState.ROUTE_EARPIECE, // initialRoute
+                CallAudioState.ROUTE_EARPIECE, // availableRoutes
+                NONE, // speakerInteraction
+                NONE, // bluetoothInteraction
+                CallAudioRouteStateMachine.CONNECT_WIRED_HEADSET, // action
+                CallAudioState.ROUTE_WIRED_HEADSET, // expectedRoute
+                CallAudioState.ROUTE_WIRED_HEADSET // expectedAvailableRoutes
+        ));
+
+        params.add(new TestParameters(
+                "Connect headset during bluetooth", // name
+                CallAudioState.ROUTE_BLUETOOTH, // initialRoute
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
+                NONE, // speakerInteraction
+                OFF, // bluetoothInteraction
+                CallAudioRouteStateMachine.CONNECT_WIRED_HEADSET, // action
+                CallAudioState.ROUTE_WIRED_HEADSET, // expectedRoute
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH // expectedAvail
+        ));
+
+        params.add(new TestParameters(
+                "Connect headset during speakerphone", // name
+                CallAudioState.ROUTE_SPEAKER, // initialRoute
+                CallAudioState.ROUTE_EARPIECE, // availableRoutes
+                OFF, // speakerInteraction
+                NONE, // bluetoothInteraction
+                CallAudioRouteStateMachine.CONNECT_WIRED_HEADSET, // action
+                CallAudioState.ROUTE_WIRED_HEADSET, // expectedRoute
+                CallAudioState.ROUTE_WIRED_HEADSET // expectedAvailableRoutes
+        ));
+
+        params.add(new TestParameters(
+                "Disconnect headset during headset", // name
+                CallAudioState.ROUTE_WIRED_HEADSET, // initialRoute
+                CallAudioState.ROUTE_WIRED_HEADSET, // availableRoutes
+                NONE, // speakerInteraction
+                NONE, // bluetoothInteraction
+                CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET, // action
+                CallAudioState.ROUTE_EARPIECE, // expectedRoute
+                CallAudioState.ROUTE_EARPIECE // expectedAvailableRoutes
+        ));
+
+        params.add(new TestParameters(
+                "Disconnect headset during headset with bluetooth available", // name
+                CallAudioState.ROUTE_WIRED_HEADSET, // initialRoute
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
+                NONE, // speakerInteraction
+                NONE, // bluetoothInteraction
+                CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET, // action
+                CallAudioState.ROUTE_EARPIECE, // expectedRoute
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH // expectedAvailableR
+        ));
+
+        params.add(new TestParameters(
+                "Disconnect headset during bluetooth", // name
+                CallAudioState.ROUTE_BLUETOOTH, // initialRoute
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
+                NONE, // speakerInteraction
+                NONE, // bluetoothInteraction
+                CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET, // action
+                CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH // expectedAvailableR
+        ));
+
+        params.add(new TestParameters(
+                "Disconnect headset during speakerphone", // name
+                CallAudioState.ROUTE_SPEAKER, // initialRoute
+                CallAudioState.ROUTE_WIRED_HEADSET, // availableRoutes
+                NONE, // speakerInteraction
+                NONE, // bluetoothInteraction
+                CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET, // action
+                CallAudioState.ROUTE_SPEAKER, // expectedRoute
+                CallAudioState.ROUTE_EARPIECE // expectedAvailableRoutes
+        ));
+
+        params.add(new TestParameters(
+                "Disconnect headset during speakerphone with bluetooth available", // name
+                CallAudioState.ROUTE_SPEAKER, // initialRoute
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
+                NONE, // speakerInteraction
+                NONE, // bluetoothInteraction
+                CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET, // action
+                CallAudioState.ROUTE_SPEAKER, // expectedRoute
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH // expectedAvailableR
+        ));
+
+        params.add(new TestParameters(
+                "Connect bluetooth during earpiece", // name
+                CallAudioState.ROUTE_EARPIECE, // initialRoute
+                CallAudioState.ROUTE_EARPIECE, // availableRoutes
+                NONE, // speakerInteraction
+                ON, // bluetoothInteraction
+                CallAudioRouteStateMachine.CONNECT_BLUETOOTH, // action
+                CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
+                CallAudioState.ROUTE_BLUETOOTH | CallAudioState.ROUTE_EARPIECE // expectedAvailableR
+        ));
+
+        params.add(new TestParameters(
+                "Connect bluetooth during wired headset", // name
+                CallAudioState.ROUTE_WIRED_HEADSET, // initialRoute
+                CallAudioState.ROUTE_WIRED_HEADSET, // availableRoutes
+                NONE, // speakerInteraction
+                ON, // bluetoothInteraction
+                CallAudioRouteStateMachine.CONNECT_BLUETOOTH, // action
+                CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
+                CallAudioState.ROUTE_BLUETOOTH | CallAudioState.ROUTE_WIRED_HEADSET // expectedAvail
+        ));
+
+        params.add(new TestParameters(
+                "Connect bluetooth during speakerphone", // name
+                CallAudioState.ROUTE_SPEAKER, // initialRoute
+                CallAudioState.ROUTE_EARPIECE, // availableRoutes
+                OFF, // speakerInteraction
+                ON, // bluetoothInteraction
+                CallAudioRouteStateMachine.CONNECT_BLUETOOTH, // action
+                CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
+                CallAudioState.ROUTE_BLUETOOTH | CallAudioState.ROUTE_EARPIECE // expectedAvailableR
+        ));
+
+        params.add(new TestParameters(
+                "Disconnect bluetooth during bluetooth without headset in", // name
+                CallAudioState.ROUTE_BLUETOOTH, // initialRoute
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
+                NONE, // speakerInteraction
+                OFF, // bluetoothInteraction
+                CallAudioRouteStateMachine.DISCONNECT_BLUETOOTH, // action
+                CallAudioState.ROUTE_EARPIECE, // expectedRoute
+                CallAudioState.ROUTE_EARPIECE // expectedAvailableRoutes
+        ));
+
+        params.add(new TestParameters(
+                "Disconnect bluetooth during bluetooth with headset in", // name
+                CallAudioState.ROUTE_BLUETOOTH, // initialRoute
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
+                NONE, // speakerInteraction
+                OFF, // bluetoothInteraction
+                CallAudioRouteStateMachine.DISCONNECT_BLUETOOTH, // action
+                CallAudioState.ROUTE_WIRED_HEADSET, // expectedRoute
+                CallAudioState.ROUTE_WIRED_HEADSET // expectedAvailableRoutes
+        ));
+
+        params.add(new TestParameters(
+                "Disconnect bluetooth during speakerphone", // name
+                CallAudioState.ROUTE_SPEAKER, // initialRoute
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
+                NONE, // speakerInteraction
+                NONE, // bluetoothInteraction
+                CallAudioRouteStateMachine.DISCONNECT_BLUETOOTH, // action
+                CallAudioState.ROUTE_SPEAKER, // expectedRoute
+                CallAudioState.ROUTE_WIRED_HEADSET // expectedAvailableRoutes
+        ));
+
+        params.add(new TestParameters(
+                "Disconnect bluetooth during earpiece", // name
+                CallAudioState.ROUTE_EARPIECE, // initialRoute
+                CallAudioState.ROUTE_EARPIECE| CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
+                NONE, // speakerInteraction
+                NONE, // bluetoothInteraction
+                CallAudioRouteStateMachine.DISCONNECT_BLUETOOTH, // action
+                CallAudioState.ROUTE_EARPIECE, // expectedRoute
+                CallAudioState.ROUTE_EARPIECE // expectedAvailableRoutes
+        ));
+
+        params.add(new TestParameters(
+                "Switch to speakerphone from earpiece", // name
+                CallAudioState.ROUTE_EARPIECE, // initialRoute
+                CallAudioState.ROUTE_EARPIECE, // availableRoutes
+                ON, // speakerInteraction
+                NONE, // bluetoothInteraction
+                CallAudioRouteStateMachine.SWITCH_SPEAKER, // action
+                CallAudioState.ROUTE_SPEAKER, // expectedRoute
+                CallAudioState.ROUTE_EARPIECE // expectedAvailableRoutes
+        ));
+
+        params.add(new TestParameters(
+                "Switch to speakerphone from headset", // name
+                CallAudioState.ROUTE_WIRED_HEADSET, // initialRoute
+                CallAudioState.ROUTE_WIRED_HEADSET, // availableRoutes
+                ON, // speakerInteraction
+                NONE, // bluetoothInteraction
+                CallAudioRouteStateMachine.SWITCH_SPEAKER, // action
+                CallAudioState.ROUTE_SPEAKER, // expectedRoute
+                CallAudioState.ROUTE_WIRED_HEADSET // expectedAvailableRoutes
+        ));
+
+        params.add(new TestParameters(
+                "Switch to speakerphone from bluetooth", // name
+                CallAudioState.ROUTE_BLUETOOTH, // initialRoute
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
+                ON, // speakerInteraction
+                OFF, // bluetoothInteraction
+                CallAudioRouteStateMachine.SWITCH_SPEAKER, // action
+                CallAudioState.ROUTE_SPEAKER, // expectedRoute
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH // expectedAvail
+        ));
+
+        params.add(new TestParameters(
+                "Switch to earpiece from bluetooth", // name
+                CallAudioState.ROUTE_BLUETOOTH, // initialRoute
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
+                NONE, // speakerInteraction
+                OFF, // bluetoothInteraction
+                CallAudioRouteStateMachine.SWITCH_EARPIECE, // action
+                CallAudioState.ROUTE_EARPIECE, // expectedRoute
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH // expectedAvailableR
+        ));
+
+        params.add(new TestParameters(
+                "Switch to earpiece from speakerphone", // name
+                CallAudioState.ROUTE_SPEAKER, // initialRoute
+                CallAudioState.ROUTE_EARPIECE, // availableRoutes
+                OFF, // speakerInteraction
+                NONE, // bluetoothInteraction
+                CallAudioRouteStateMachine.SWITCH_EARPIECE, // action
+                CallAudioState.ROUTE_EARPIECE, // expectedRoute
+                CallAudioState.ROUTE_EARPIECE // expectedAvailableRoutes
+        ));
+
+        params.add(new TestParameters(
+                "Switch to bluetooth from speakerphone", // name
+                CallAudioState.ROUTE_SPEAKER, // initialRoute
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
+                OFF, // speakerInteraction
+                ON, // bluetoothInteraction
+                CallAudioRouteStateMachine.SWITCH_BLUETOOTH, // action
+                CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH // expectedAvailableR
+        ));
+
+        params.add(new TestParameters(
+                "Switch to bluetooth from earpiece", // name
+                CallAudioState.ROUTE_EARPIECE, // initialRoute
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
+                NONE, // speakerInteraction
+                ON, // bluetoothInteraction
+                CallAudioRouteStateMachine.SWITCH_BLUETOOTH, // action
+                CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
+                CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH // expectedAvailableR
+        ));
+
+        params.add(new TestParameters(
+                "Switch to bluetooth from wired headset", // name
+                CallAudioState.ROUTE_WIRED_HEADSET, // initialRoute
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
+                NONE, // speakerInteraction
+                ON, // bluetoothInteraction
+                CallAudioRouteStateMachine.SWITCH_BLUETOOTH, // action
+                CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
+                CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH // expectedAvail
+        ));
+
+        return params;
+    }
+
+    private void runParametrizedTestCaseWithFocus(TestParameters params) {
+        resetMocks();
+
+        // Construct a fresh state machine on every case
+        CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+                mContext,
+                mockCallsManager,
+                mockBluetoothManager,
+                mockWiredHeadsetManager,
+                mockStatusBarNotifier,
+                mAudioServiceFactory);
+
+        // Set up bluetooth and speakerphone state
+        when(mockBluetoothManager.isBluetoothAudioConnectedOrPending()).thenReturn(
+                params.initialRoute == CallAudioState.ROUTE_BLUETOOTH);
+        when(mockBluetoothManager.isBluetoothAvailable()).thenReturn(
+                (params.availableRoutes & CallAudioState.ROUTE_BLUETOOTH) != 0
+                        || (params.expectedAvailableRoutes & CallAudioState.ROUTE_BLUETOOTH) != 0);
+        when(mockAudioManager.isSpeakerphoneOn()).thenReturn(
+                params.initialRoute == CallAudioState.ROUTE_SPEAKER);
+
+        // Set the initial CallAudioState object
+        CallAudioState initState = new CallAudioState(false,
+                params.initialRoute, (params.availableRoutes | CallAudioState.ROUTE_SPEAKER));
+        stateMachine.initialize(initState);
+        // Make the state machine have focus so that we actually do something
+        stateMachine.sendMessage(CallAudioRouteStateMachine.SWITCH_FOCUS,
+                CallAudioRouteStateMachine.HAS_FOCUS);
+        stateMachine.sendMessage(params.action);
+
+        // Verify interactions with the speakerphone and bluetooth systems
+        switch(params.bluetoothInteraction) {
+            case NONE:
+                verify(mockBluetoothManager, never()).disconnectBluetoothAudio();
+                verify(mockBluetoothManager, never()).connectBluetoothAudio();
+                break;
+            case ON:
+                verify(mockBluetoothManager, timeout(TEST_TIMEOUT)).connectBluetoothAudio();
+                verify(mockBluetoothManager, never()).disconnectBluetoothAudio();
+                break;
+            case OFF:
+                verify(mockBluetoothManager, never()).connectBluetoothAudio();
+                verify(mockBluetoothManager, timeout(TEST_TIMEOUT)).disconnectBluetoothAudio();
+        }
+
+        switch (params.speakerInteraction) {
+            case NONE:
+                verify(mockAudioManager, never()).setSpeakerphoneOn(any(Boolean.class));
+                break;
+            case ON: // fall through
+            case OFF:
+                verify(mockAudioManager, timeout(TEST_TIMEOUT)).setSpeakerphoneOn(
+                        params.speakerInteraction == ON);
+        }
+
+        // Verify the end state
+        CallAudioState expectedState = new CallAudioState(false, params.expectedRoute,
+                params.expectedAvailableRoutes | CallAudioState.ROUTE_SPEAKER);
+        verifyNewSystemCallAudioState(initState, expectedState);
+    }
+
+    private void runParametrizedTestCaseWithoutFocus(TestParameters params) {
+        resetMocks();
+
+        // Construct a fresh state machine on every case
+        CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+                mContext,
+                mockCallsManager,
+                mockBluetoothManager,
+                mockWiredHeadsetManager,
+                mockStatusBarNotifier,
+                mAudioServiceFactory);
+
+        // Set up bluetooth and speakerphone state
+        when(mockBluetoothManager.isBluetoothAvailable()).thenReturn(
+                (params.availableRoutes & CallAudioState.ROUTE_BLUETOOTH) != 0
+                || (params.expectedAvailableRoutes & CallAudioState.ROUTE_BLUETOOTH) != 0);
+        when(mockAudioManager.isSpeakerphoneOn()).thenReturn(
+                params.initialRoute == CallAudioState.ROUTE_SPEAKER);
+
+        // Set the initial CallAudioState object
+        CallAudioState initState = new CallAudioState(false,
+                params.initialRoute, (params.availableRoutes | CallAudioState.ROUTE_SPEAKER));
+        stateMachine.initialize(initState);
+        // Omit the focus-getting statement
+        stateMachine.sendMessage(params.action);
+        try {
+            Thread.sleep(100L);
+        } catch (InterruptedException e) {
+            // Just a pause to make sure the state machine handler thread has a chance to update
+            // its state. Do nothing.
+        }
+
+        // Verify that no substantive interactions have taken place with the rest of the system
+        verify(mockBluetoothManager, never()).disconnectBluetoothAudio();
+        verify(mockBluetoothManager, never()).connectBluetoothAudio();
+        verify(mockAudioManager, never()).setSpeakerphoneOn(any(Boolean.class));
+        verify(mockCallsManager, never()).onCallAudioStateChanged(any(CallAudioState.class),
+                any(CallAudioState.class));
+        verify(mockConnectionServiceWrapper, never()).onCallAudioStateChanged(
+                any(Call.class), any(CallAudioState.class));
+
+        // Verify the end state
+        CallAudioState expectedState = new CallAudioState(false, params.expectedRoute,
+                params.expectedAvailableRoutes | CallAudioState.ROUTE_SPEAKER);
+        assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
+    }
+
+    private void verifyNewSystemCallAudioState(CallAudioState expectedOldState,
+            CallAudioState expectedNewState) {
+        ArgumentCaptor<CallAudioState> oldStateCaptor = ArgumentCaptor.forClass(
+                CallAudioState.class);
+        ArgumentCaptor<CallAudioState> newStateCaptor1 = ArgumentCaptor.forClass(
+                CallAudioState.class);
+        ArgumentCaptor<CallAudioState> newStateCaptor2 = ArgumentCaptor.forClass(
+                CallAudioState.class);
+        verify(mockCallsManager, timeout(TEST_TIMEOUT).atLeastOnce()).onCallAudioStateChanged(
+                oldStateCaptor.capture(), newStateCaptor1.capture());
+        verify(mockConnectionServiceWrapper, timeout(TEST_TIMEOUT).atLeastOnce())
+                .onCallAudioStateChanged(same(fakeCall), newStateCaptor2.capture());
+
+        assertTrue(oldStateCaptor.getValue().equals(expectedOldState));
+        assertTrue(newStateCaptor1.getValue().equals(expectedNewState));
+        assertTrue(newStateCaptor2.getValue().equals(expectedNewState));
+    }
+
+    private void resetMocks() {
+        reset(mockAudioManager, mockBluetoothManager, mockCallsManager,
+                mockConnectionServiceWrapper);
+        when(mockCallsManager.getForegroundCall()).thenReturn(fakeCall);
+        doNothing().when(mockConnectionServiceWrapper).onCallAudioStateChanged(any(Call.class),
+                any(CallAudioState.class));
+    }
+}
\ No newline at end of file