Merge "Remove MD5 hash initialization for logging"
diff --git a/src/com/android/server/telecom/BluetoothPhoneServiceImpl.java b/src/com/android/server/telecom/BluetoothPhoneServiceImpl.java
index 93d4f71..d0fcfa1 100644
--- a/src/com/android/server/telecom/BluetoothPhoneServiceImpl.java
+++ b/src/com/android/server/telecom/BluetoothPhoneServiceImpl.java
@@ -66,6 +66,7 @@
     private static final int CALL_STATE_INCOMING = 4;
     private static final int CALL_STATE_WAITING = 5;
     private static final int CALL_STATE_IDLE = 6;
+    private static final int CALL_STATE_DISCONNECTED = 7;
 
     // match up with bthf_call_state_t of bt_hf.h
     // Terminate all held or set UDUB("busy") to a waiting call
@@ -84,6 +85,7 @@
     private String mRingingAddress = null;
     private int mRingingAddressType = 0;
     private Call mOldHeldCall = null;
+    private boolean mIsDisconnectedTonePlaying = false;
 
     /**
      * Binder implementation of IBluetoothHeadsetPhone. Implements the command interface that the
@@ -387,6 +389,12 @@
             }
             updateHeadsetWithCallState(false /* force */);
         }
+
+        @Override
+        public void onDisconnectedTonePlaying(boolean isTonePlaying) {
+            mIsDisconnectedTonePlaying = isTonePlaying;
+            updateHeadsetWithCallState(false /* force */);
+        }
     };
 
     /**
@@ -816,6 +824,7 @@
         CallsManager callsManager = mCallsManager;
         Call ringingCall = mCallsManager.getRingingCall();
         Call dialingCall = mCallsManager.getOutgoingCall();
+        boolean hasOnlyDisconnectedCalls = mCallsManager.hasOnlyDisconnectedCalls();
 
         //
         // !! WARNING !!
@@ -831,6 +840,9 @@
             bluetoothCallState = CALL_STATE_INCOMING;
         } else if (dialingCall != null) {
             bluetoothCallState = CALL_STATE_ALERTING;
+        } else if (hasOnlyDisconnectedCalls || mIsDisconnectedTonePlaying) {
+            // Keep the DISCONNECTED state until the disconnect tone's playback is done
+            bluetoothCallState = CALL_STATE_DISCONNECTED;
         }
         return bluetoothCallState;
     }
diff --git a/src/com/android/server/telecom/CallAudioManager.java b/src/com/android/server/telecom/CallAudioManager.java
index bf1ef8f..ab06209 100644
--- a/src/com/android/server/telecom/CallAudioManager.java
+++ b/src/com/android/server/telecom/CallAudioManager.java
@@ -56,6 +56,7 @@
 
     private Call mForegroundCall;
     private boolean mIsTonePlaying = false;
+    private boolean mIsDisconnectedTonePlaying = false;
     private InCallTonePlayer mHoldTonePlayer;
 
     public CallAudioManager(CallAudioRouteStateMachine callAudioRouteStateMachine,
@@ -519,6 +520,11 @@
                 isTonePlaying ? CallAudioModeStateMachine.TONE_STARTED_PLAYING
                         : CallAudioModeStateMachine.TONE_STOPPED_PLAYING,
                 makeArgsForModeStateMachine());
+
+        if (!isTonePlaying && mIsDisconnectedTonePlaying) {
+            mCallsManager.onDisconnectedTonePlaying(false);
+            mIsDisconnectedTonePlaying = false;
+        }
     }
 
     private void onCallLeavingState(Call call, int state) {
@@ -699,6 +705,8 @@
 
             if (toneToPlay != InCallTonePlayer.TONE_INVALID) {
                 mPlayerFactory.createPlayer(toneToPlay).startTone();
+                mCallsManager.onDisconnectedTonePlaying(true);
+                mIsDisconnectedTonePlaying = true;
             }
         }
     }
@@ -792,4 +800,4 @@
     public SparseArray<LinkedHashSet<Call>> getCallStateToCalls() {
         return mCallStateToCalls;
     }
-}
\ No newline at end of file
+}
diff --git a/src/com/android/server/telecom/CallAudioRouteStateMachine.java b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
index 66bcb84..23e5e69 100644
--- a/src/com/android/server/telecom/CallAudioRouteStateMachine.java
+++ b/src/com/android/server/telecom/CallAudioRouteStateMachine.java
@@ -22,6 +22,7 @@
 import android.bluetooth.BluetoothHeadset;
 import android.content.Context;
 import android.content.pm.UserInfo;
+import android.media.AudioDeviceInfo;
 import android.media.AudioManager;
 import android.media.IAudioService;
 import android.os.Binder;
@@ -68,6 +69,12 @@
  * mIsMuted: a boolean indicating whether the audio is muted
  */
 public class CallAudioRouteStateMachine extends StateMachine {
+
+    /** Values for CallAudioRouteStateMachine constructor's earPieceRouting arg. */
+    public static final int EARPIECE_FORCE_DISABLED = 0;
+    public static final int EARPIECE_FORCE_ENABLED  = 1;
+    public static final int EARPIECE_AUTO_DETECT    = 2;
+
     /** Direct the audio stream through the device's earpiece. */
     public static final int ROUTE_EARPIECE      = CallAudioState.ROUTE_EARPIECE;
 
@@ -1259,7 +1266,7 @@
             WiredHeadsetManager wiredHeadsetManager,
             StatusBarNotifier statusBarNotifier,
             CallAudioManager.AudioServiceFactory audioServiceFactory,
-            boolean doesDeviceSupportEarpieceRoute) {
+            int earpieceControl) {
         super(NAME);
         addState(mActiveEarpieceRoute);
         addState(mActiveHeadsetRoute);
@@ -1278,7 +1285,16 @@
         mWiredHeadsetManager = wiredHeadsetManager;
         mStatusBarNotifier = statusBarNotifier;
         mAudioServiceFactory = audioServiceFactory;
-        mDoesDeviceSupportEarpieceRoute = doesDeviceSupportEarpieceRoute;
+        switch (earpieceControl) {
+            case EARPIECE_FORCE_DISABLED:
+                mDoesDeviceSupportEarpieceRoute = false;
+                break;
+            case EARPIECE_FORCE_ENABLED:
+                mDoesDeviceSupportEarpieceRoute = true;
+                break;
+            default:
+                mDoesDeviceSupportEarpieceRoute = checkForEarpieceSupport();
+        }
         mIsInbandRingSupported = BluetoothHeadset.isInbandRingingSupported(mContext);
         mLock = callsManager.getLock();
 
@@ -1412,11 +1428,9 @@
     }
 
     private void setSpeakerphoneOn(boolean on) {
-        if (mAudioManager.isSpeakerphoneOn() != on) {
-            Log.i(this, "turning speaker phone %s", on);
-            mAudioManager.setSpeakerphoneOn(on);
-            mStatusBarNotifier.notifySpeakerphone(on);
-        }
+        Log.i(this, "turning speaker phone %s", on);
+        mAudioManager.setSpeakerphoneOn(on);
+        mStatusBarNotifier.notifySpeakerphone(on);
     }
 
     private void setBluetoothOn(String address) {
@@ -1602,14 +1616,15 @@
         return currentState.isActive();
     }
 
-    public static boolean doesDeviceSupportEarpieceRoute() {
-        String[] characteristics = SystemProperties.get("ro.build.characteristics").split(",");
-        for (String characteristic : characteristics) {
-            if ("watch".equals(characteristic)) {
-                return false;
+    private boolean checkForEarpieceSupport() {
+        AudioDeviceInfo[] deviceList = mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS);
+        for (AudioDeviceInfo device: deviceList) {
+            if (device.getType() == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE) {
+                return true;
             }
         }
-        return true;
+        // No earpiece found
+        return false;
     }
 
     private int calculateBaselineRouteMessage(boolean isExplicitUserRequest,
@@ -1640,6 +1655,7 @@
         mDeviceSupportedRoutes = initState.getSupportedRouteMask();
         mAvailableRoutes = mDeviceSupportedRoutes & getCurrentCallSupportedRoutes();
         mIsMuted = initState.isMuted();
+        setSpeakerphoneOn(initState.getRoute() == CallAudioState.ROUTE_SPEAKER);
         setMuteOn(mIsMuted);
         mWasOnSpeaker = false;
         mHasUserExplicitlyLeftBluetooth = false;
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index f9cad9f..114fec9 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -119,6 +119,7 @@
         void onSessionModifyRequestReceived(Call call, VideoProfile videoProfile);
         void onHoldToneRequested(Call call);
         void onExternalCallChanged(Call call, boolean isExternalCall);
+        void onDisconnectedTonePlaying(boolean isTonePlaying);
     }
 
     private static final String TAG = "CallsManager";
@@ -339,7 +340,7 @@
                 wiredHeadsetManager,
                 statusBarNotifier,
                 audioServiceFactory,
-                CallAudioRouteStateMachine.doesDeviceSupportEarpieceRoute()
+                CallAudioRouteStateMachine.EARPIECE_AUTO_DETECT
         );
         callAudioRouteStateMachine.initialize();
 
@@ -773,7 +774,11 @@
         return false;
     }
 
-    boolean hasOnlyDisconnectedCalls() {
+    @VisibleForTesting
+    public boolean hasOnlyDisconnectedCalls() {
+        if (mCalls.size() == 0) {
+            return false;
+        }
         for (Call call : mCalls) {
             if (!call.isDisconnected()) {
                 return false;
@@ -1788,6 +1793,20 @@
         }
     }
 
+    /**
+     * Called when disconnect tone is started or stopped, including any InCallTone
+     * after disconnected call.
+     *
+     * @param isTonePlaying true if the disconnected tone is started, otherwise the disconnected
+     * tone is stopped.
+     */
+    void onDisconnectedTonePlaying(boolean isTonePlaying) {
+        Log.v(this, "onDisconnectedTonePlaying, %s", isTonePlaying ? "started" : "stopped");
+        for (CallsManagerListener listener : mListeners) {
+            listener.onDisconnectedTonePlaying(isTonePlaying);
+        }
+    }
+
     void markCallAsRinging(Call call) {
         setCallState(call, CallState.RINGING, "ringing set explicitly");
     }
diff --git a/src/com/android/server/telecom/CallsManagerListenerBase.java b/src/com/android/server/telecom/CallsManagerListenerBase.java
index a4c76c1..c0a71eb 100644
--- a/src/com/android/server/telecom/CallsManagerListenerBase.java
+++ b/src/com/android/server/telecom/CallsManagerListenerBase.java
@@ -88,4 +88,8 @@
     @Override
     public void onExternalCallChanged(Call call, boolean isExternalCall) {
     }
+
+    @Override
+    public void onDisconnectedTonePlaying(boolean isTonePlaying) {
+    }
 }
diff --git a/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java b/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
index 9851674..e02fd56 100644
--- a/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
+++ b/src/com/android/server/telecom/ui/MissedCallNotifierImpl.java
@@ -517,7 +517,7 @@
             UserHandle userHandle) {
         Intent intent = new Intent(action, data, mContext, TelecomBroadcastReceiver.class);
         intent.putExtra(TelecomBroadcastIntentProcessor.EXTRA_USERHANDLE, userHandle);
-        return PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
+        return PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
     }
 
     /**
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothPhoneServiceTest.java b/tests/src/com/android/server/telecom/tests/BluetoothPhoneServiceTest.java
index 300415a..885fbc4 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothPhoneServiceTest.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothPhoneServiceTest.java
@@ -78,6 +78,7 @@
     private static final int CALL_STATE_INCOMING = 4;
     private static final int CALL_STATE_WAITING = 5;
     private static final int CALL_STATE_IDLE = 6;
+    private static final int CALL_STATE_DISCONNECTED = 7;
     // Terminate all held or set UDUB("busy") to a waiting call
     private static final int CHLD_TYPE_RELEASEHELD = 0;
     // Terminate all active calls and accepts a waiting/held call
@@ -110,6 +111,7 @@
         doReturn(null).when(mMockCallsManager).getHeldCall();
         doReturn(null).when(mMockCallsManager).getOutgoingCall();
         doReturn(0).when(mMockCallsManager).getNumHeldCalls();
+        doReturn(false).when(mMockCallsManager).hasOnlyDisconnectedCalls();
         mBluetoothPhoneService = new BluetoothPhoneServiceImpl(mContext, mLock, mMockCallsManager,
                 mock(BluetoothAdapterProxy.class), mMockPhoneAccountRegistrar);
 
@@ -817,6 +819,25 @@
     }
 
     @MediumTest
+    public void testOnCallStateChangedDisconnected() throws Exception {
+        Call disconnectedCall = createDisconnectedCall();
+        doReturn(true).when(mMockCallsManager).hasOnlyDisconnectedCalls();
+        mBluetoothPhoneService.mCallsManagerListener.onCallStateChanged(disconnectedCall,
+                CallState.DISCONNECTING, CallState.DISCONNECTED);
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(0), eq(0), eq(CALL_STATE_DISCONNECTED),
+                eq(""), eq(128));
+
+        doReturn(false).when(mMockCallsManager).hasOnlyDisconnectedCalls();
+        mBluetoothPhoneService.mCallsManagerListener.onDisconnectedTonePlaying(true);
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(0), eq(0), eq(CALL_STATE_DISCONNECTED),
+                eq(""), eq(128));
+
+        mBluetoothPhoneService.mCallsManagerListener.onDisconnectedTonePlaying(false);
+        verify(mMockBluetoothHeadset).phoneStateChanged(eq(0), eq(0), eq(CALL_STATE_IDLE),
+                eq(""), eq(128));
+    }
+
+    @MediumTest
     public void testOnCallStateChanged() throws Exception {
         Call ringingCall = createRingingCall();
         when(ringingCall.getHandle()).thenReturn(Uri.parse("tel:555-0000"));
@@ -929,6 +950,12 @@
         return call;
     }
 
+    private Call createDisconnectedCall() {
+        Call call = mock(Call.class);
+        when(mMockCallsManager.getFirstCallWithState(CallState.DISCONNECTED)).thenReturn(call);
+        return call;
+    }
+
     private Call createForegroundCall() {
         Call call = mock(Call.class);
         when(mMockCallsManager.getForegroundCall()).thenReturn(call);
diff --git a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
index 4aaa4e6..5bd921a 100644
--- a/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
+++ b/tests/src/com/android/server/telecom/tests/CallAudioRouteStateMachineTest.java
@@ -68,6 +68,7 @@
     private static final int NONE = 0;
     private static final int ON = 1;
     private static final int OFF = 2;
+    private static final int OPTIONAL = 3;
 
     private static final BluetoothDevice bluetoothDevice1 =
             BluetoothRouteManagerTest.makeBluetoothDevice("00:00:00:00:00:01");
@@ -88,7 +89,7 @@
         public int expectedRoute;
         public BluetoothDevice expectedBluetoothDevice = null;
         public int expectedAvailableRoutes; // also may exclude the speakerphone.
-        public boolean doesDeviceSupportEarpiece; // set to false in the case of Wear devices
+        public int earpieceControl; // Allows disabling the earpiece to simulate Wear or Car
         public boolean shouldRunWithFocus;
 
         public int callSupportedRoutes = CallAudioState.ROUTE_ALL;
@@ -96,7 +97,7 @@
         public RoutingTestParameters(String name, int initialRoute,
                 int availableRoutes, int speakerInteraction,
                 int bluetoothInteraction, int action, int expectedRoute,
-                int expectedAvailableRoutes, boolean doesDeviceSupportEarpiece,
+                int expectedAvailableRoutes, int earpieceControl,
                 boolean shouldRunWithFocus) {
             this.name = name;
             this.initialRoute = initialRoute;
@@ -106,7 +107,7 @@
             this.action = action;
             this.expectedRoute = expectedRoute;
             this.expectedAvailableRoutes = expectedAvailableRoutes;
-            this.doesDeviceSupportEarpiece = doesDeviceSupportEarpiece;
+            this.earpieceControl = earpieceControl;
             this.shouldRunWithFocus = shouldRunWithFocus;
         }
 
@@ -141,7 +142,7 @@
                     ", action=" + action +
                     ", expectedRoute=" + expectedRoute +
                     ", expectedAvailableRoutes=" + expectedAvailableRoutes +
-                    ", doesDeviceSupportEarpiece=" + doesDeviceSupportEarpiece +
+                    ", earpieceControl=" + earpieceControl +
                     ", shouldRunWithFocus=" + shouldRunWithFocus +
                     '}';
         }
@@ -185,6 +186,23 @@
                 any(CallAudioState.class));
     }
 
+    @SmallTest
+    public void testEarpieceAutodetect() {
+        CallAudioRouteStateMachine stateMachine = new CallAudioRouteStateMachine(
+                mContext,
+                mockCallsManager,
+                mockBluetoothRouteManager,
+                mockWiredHeadsetManager,
+                mockStatusBarNotifier,
+                mAudioServiceFactory,
+                CallAudioRouteStateMachine.EARPIECE_AUTO_DETECT);
+
+        // Since we don't know if we're on a platform with an earpiece or not, all we can do
+        // is ensure the stateMachine construction didn't fail.  But at least we exercised the
+        // autodetection code...
+        assertNotNull(stateMachine);
+    }
+
     @LargeTest
     public void testStateMachineTransitionsWithFocus() throws Throwable {
         List<RoutingTestParameters> paramList = generateTransitionTests(true);
@@ -206,7 +224,7 @@
                 mockWiredHeadsetManager,
                 mockStatusBarNotifier,
                 mAudioServiceFactory,
-                true);
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED);
 
         when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
         when(mockBluetoothRouteManager.isBluetoothAvailable()).thenReturn(true);
@@ -249,7 +267,7 @@
                 mockWiredHeadsetManager,
                 mockStatusBarNotifier,
                 mAudioServiceFactory,
-                true);
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED);
 
         when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
         when(mockBluetoothRouteManager.isBluetoothAvailable()).thenReturn(true);
@@ -290,7 +308,7 @@
                 mockWiredHeadsetManager,
                 mockStatusBarNotifier,
                 mAudioServiceFactory,
-                true);
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED);
         Collection<BluetoothDevice> availableDevices = Collections.singleton(bluetoothDevice1);
 
         when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
@@ -361,7 +379,7 @@
                 mockWiredHeadsetManager,
                 mockStatusBarNotifier,
                 mAudioServiceFactory,
-                true);
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED);
 
         when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
         when(mockBluetoothRouteManager.isBluetoothAvailable()).thenReturn(true);
@@ -394,7 +412,7 @@
                 mockWiredHeadsetManager,
                 mockStatusBarNotifier,
                 mAudioServiceFactory,
-                true);
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED);
         setInBandRing(false);
         when(mockBluetoothRouteManager.isBluetoothAudioConnectedOrPending()).thenReturn(false);
         when(mockBluetoothRouteManager.isBluetoothAvailable()).thenReturn(false);
@@ -439,7 +457,7 @@
                 mockWiredHeadsetManager,
                 mockStatusBarNotifier,
                 mAudioServiceFactory,
-                true);
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED);
         List<BluetoothDevice> availableDevices =
                 Arrays.asList(bluetoothDevice1, bluetoothDevice2, bluetoothDevice3);
 
@@ -481,14 +499,14 @@
     public void testInitializationWithEarpieceNoHeadsetNoBluetooth() {
         CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_EARPIECE,
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER);
-        initializationTestHelper(expectedState, true);
+        initializationTestHelper(expectedState, CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED);
     }
 
     @SmallTest
     public void testInitializationWithEarpieceAndHeadsetNoBluetooth() {
         CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
                 CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER);
-        initializationTestHelper(expectedState, true);
+        initializationTestHelper(expectedState, CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED);
     }
 
     @SmallTest
@@ -496,7 +514,7 @@
         CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
                 CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER
                 | CallAudioState.ROUTE_BLUETOOTH);
-        initializationTestHelper(expectedState, true);
+        initializationTestHelper(expectedState, CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED);
     }
 
     @SmallTest
@@ -504,21 +522,21 @@
         CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_SPEAKER
                         | CallAudioState.ROUTE_BLUETOOTH);
-        initializationTestHelper(expectedState, true);
+        initializationTestHelper(expectedState, CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED);
     }
 
     @SmallTest
     public void testInitializationWithNoEarpieceNoHeadsetNoBluetooth() {
         CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_SPEAKER,
                 CallAudioState.ROUTE_SPEAKER);
-        initializationTestHelper(expectedState, false);
+        initializationTestHelper(expectedState, CallAudioRouteStateMachine.EARPIECE_FORCE_DISABLED);
     }
 
     @SmallTest
     public void testInitializationWithHeadsetNoBluetoothNoEarpiece() {
         CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_WIRED_HEADSET,
                 CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER);
-        initializationTestHelper(expectedState, false);
+        initializationTestHelper(expectedState, CallAudioRouteStateMachine.EARPIECE_FORCE_DISABLED);
     }
 
     @SmallTest
@@ -526,18 +544,18 @@
         CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
                 CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_SPEAKER
                 | CallAudioState.ROUTE_BLUETOOTH);
-        initializationTestHelper(expectedState, false);
+        initializationTestHelper(expectedState, CallAudioRouteStateMachine.EARPIECE_FORCE_DISABLED);
     }
 
     @SmallTest
     public void testInitializationWithBluetoothNoHeadsetNoEarpiece() {
         CallAudioState expectedState = new CallAudioState(false, CallAudioState.ROUTE_BLUETOOTH,
                 CallAudioState.ROUTE_SPEAKER | CallAudioState.ROUTE_BLUETOOTH);
-        initializationTestHelper(expectedState, false);
+        initializationTestHelper(expectedState, CallAudioRouteStateMachine.EARPIECE_FORCE_DISABLED);
     }
 
     private void initializationTestHelper(CallAudioState expectedState,
-            boolean doesDeviceSupportEarpiece) {
+            int earpieceControl) {
         when(mockWiredHeadsetManager.isPluggedIn()).thenReturn(
                 (expectedState.getSupportedRouteMask() & CallAudioState.ROUTE_WIRED_HEADSET) != 0);
         when(mockBluetoothRouteManager.isBluetoothAvailable()).thenReturn(
@@ -550,7 +568,7 @@
                 mockWiredHeadsetManager,
                 mockStatusBarNotifier,
                 mAudioServiceFactory,
-                doesDeviceSupportEarpiece);
+                earpieceControl);
         stateMachine.initialize();
         assertEquals(expectedState, stateMachine.getCurrentCallAudioState());
     }
@@ -561,12 +579,12 @@
                 "Connect headset during earpiece", // name
                 CallAudioState.ROUTE_EARPIECE, // initialRoute
                 CallAudioState.ROUTE_EARPIECE, // availableRoutes
-                NONE, // speakerInteraction
+                OPTIONAL, // speakerInteraction
                 NONE, // bluetoothInteraction
                 CallAudioRouteStateMachine.CONNECT_WIRED_HEADSET, // action
                 CallAudioState.ROUTE_WIRED_HEADSET, // expectedRoute
                 CallAudioState.ROUTE_WIRED_HEADSET, // expectedAvailableRoutes
-                true, // doesDeviceSupportEarpiece
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, // earpieceControl
                 shouldRunWithFocus
         ));
 
@@ -574,12 +592,12 @@
                 "Connect headset during bluetooth", // name
                 CallAudioState.ROUTE_BLUETOOTH, // initialRoute
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
-                NONE, // speakerInteraction
+                OPTIONAL, // speakerInteraction
                 OFF, // bluetoothInteraction
                 CallAudioRouteStateMachine.CONNECT_WIRED_HEADSET, // action
                 CallAudioState.ROUTE_WIRED_HEADSET, // expectedRoute
                 CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // expectedAvai
-                true, // doesDeviceSupportEarpiece
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, // earpieceControl
                 shouldRunWithFocus
         ));
 
@@ -592,7 +610,7 @@
                 CallAudioRouteStateMachine.CONNECT_WIRED_HEADSET, // action
                 CallAudioState.ROUTE_WIRED_HEADSET, // expectedRoute
                 CallAudioState.ROUTE_WIRED_HEADSET, // expectedAvailableRoutes
-                true, // doesDeviceSupportEarpiece
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, // earpieceControl
                 shouldRunWithFocus
         ));
 
@@ -600,12 +618,12 @@
                 "Disconnect headset during headset", // name
                 CallAudioState.ROUTE_WIRED_HEADSET, // initialRoute
                 CallAudioState.ROUTE_WIRED_HEADSET, // availableRoutes
-                NONE, // speakerInteraction
+                OPTIONAL, // speakerInteraction
                 NONE, // bluetoothInteraction
                 CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET, // action
                 CallAudioState.ROUTE_EARPIECE, // expectedRoute
                 CallAudioState.ROUTE_EARPIECE, // expectedAvailableRoutes
-                true, // doesDeviceSupportEarpiece
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, // earpieceControl
                 shouldRunWithFocus
         ));
 
@@ -613,12 +631,12 @@
                 "Disconnect headset during headset with bluetooth available", // name
                 CallAudioState.ROUTE_WIRED_HEADSET, // initialRoute
                 CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
-                NONE, // speakerInteraction
+                OPTIONAL, // speakerInteraction
                 ON, // bluetoothInteraction
                 CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET, // action
                 CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // expectedAvailable
-                true, // doesDeviceSupportEarpiece
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, // earpieceControl
                 shouldRunWithFocus
         ));
 
@@ -626,12 +644,12 @@
                 "Disconnect headset during bluetooth", // name
                 CallAudioState.ROUTE_BLUETOOTH, // initialRoute
                 CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
-                NONE, // speakerInteraction
+                OPTIONAL, // speakerInteraction
                 NONE, // bluetoothInteraction
                 CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET, // action
                 CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // expectedAvailable
-                true, // doesDeviceSupportEarpiece
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, // earpieceControl
                 shouldRunWithFocus
         ));
 
@@ -639,12 +657,12 @@
                 "Disconnect headset during speakerphone", // name
                 CallAudioState.ROUTE_SPEAKER, // initialRoute
                 CallAudioState.ROUTE_WIRED_HEADSET, // availableRoutes
-                NONE, // speakerInteraction
+                OPTIONAL, // speakerInteraction
                 NONE, // bluetoothInteraction
                 CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET, // action
                 CallAudioState.ROUTE_SPEAKER, // expectedRoute
                 CallAudioState.ROUTE_EARPIECE, // expectedAvailableRoutes
-                true, // doesDeviceSupportEarpiece
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, // earpieceControl
                 shouldRunWithFocus
         ));
 
@@ -652,12 +670,12 @@
                 "Disconnect headset during speakerphone with bluetooth available", // name
                 CallAudioState.ROUTE_SPEAKER, // initialRoute
                 CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
-                NONE, // speakerInteraction
+                OPTIONAL, // speakerInteraction
                 NONE, // bluetoothInteraction
                 CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET, // action
                 CallAudioState.ROUTE_SPEAKER, // expectedRoute
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // expectedAvailable
-                true, // doesDeviceSupportEarpiece
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, // earpieceControl
                 shouldRunWithFocus
         ));
 
@@ -665,12 +683,12 @@
                 "Connect bluetooth during earpiece", // name
                 CallAudioState.ROUTE_EARPIECE, // initialRoute
                 CallAudioState.ROUTE_EARPIECE, // availableRoutes
-                NONE, // speakerInteraction
+                OPTIONAL, // speakerInteraction
                 ON, // bluetoothInteraction
                 CallAudioRouteStateMachine.CONNECT_BLUETOOTH, // action
                 CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
                 CallAudioState.ROUTE_BLUETOOTH | CallAudioState.ROUTE_EARPIECE, // expectedAvailable
-                true, // doesDeviceSupportEarpiece
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, // earpieceControl
                 shouldRunWithFocus
         ));
 
@@ -678,12 +696,12 @@
                 "Connect bluetooth during wired headset", // name
                 CallAudioState.ROUTE_WIRED_HEADSET, // initialRoute
                 CallAudioState.ROUTE_WIRED_HEADSET, // availableRoutes
-                NONE, // speakerInteraction
+                OPTIONAL, // speakerInteraction
                 ON, // bluetoothInteraction
                 CallAudioRouteStateMachine.CONNECT_BLUETOOTH, // action
                 CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
                 CallAudioState.ROUTE_BLUETOOTH | CallAudioState.ROUTE_WIRED_HEADSET, // expectedAvai
-                true, // doesDeviceSupportEarpiece
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, // earpieceControl
                 shouldRunWithFocus
         ));
 
@@ -696,7 +714,7 @@
                 CallAudioRouteStateMachine.CONNECT_BLUETOOTH, // action
                 CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
                 CallAudioState.ROUTE_BLUETOOTH | CallAudioState.ROUTE_EARPIECE, // expectedAvailable
-                true, // doesDeviceSupportEarpiece
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, // earpieceControl
                 shouldRunWithFocus
         ));
 
@@ -704,12 +722,12 @@
                 "Disconnect bluetooth during bluetooth without headset in", // name
                 CallAudioState.ROUTE_BLUETOOTH, // initialRoute
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
-                NONE, // speakerInteraction
+                OPTIONAL, // speakerInteraction
                 OFF, // bluetoothInteraction
                 CallAudioRouteStateMachine.DISCONNECT_BLUETOOTH, // action
                 CallAudioState.ROUTE_EARPIECE, // expectedRoute
                 CallAudioState.ROUTE_EARPIECE, // expectedAvailableRoutes
-                true, // doesDeviceSupportEarpiece
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, // earpieceControl
                 shouldRunWithFocus
         ));
 
@@ -717,12 +735,12 @@
                 "Disconnect bluetooth during bluetooth without headset in, priority mode ", // name
                 CallAudioState.ROUTE_BLUETOOTH, // initialRoute
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
-                NONE, // speakerInteraction
+                OPTIONAL, // speakerInteraction
                 OFF, // bluetoothInteraction
                 CallAudioRouteStateMachine.DISCONNECT_BLUETOOTH, // action
                 CallAudioState.ROUTE_EARPIECE, // expectedRoute
                 CallAudioState.ROUTE_EARPIECE, // expectedAvailableRoutes
-                true, // doesDeviceSupportEarpiece
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, // earpieceControl
                 shouldRunWithFocus
         ));
 
@@ -730,12 +748,12 @@
                 "Disconnect bluetooth during bluetooth with headset in", // name
                 CallAudioState.ROUTE_BLUETOOTH, // initialRoute
                 CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
-                NONE, // speakerInteraction
+                OPTIONAL, // speakerInteraction
                 OFF, // bluetoothInteraction
                 CallAudioRouteStateMachine.DISCONNECT_BLUETOOTH, // action
                 CallAudioState.ROUTE_WIRED_HEADSET, // expectedRoute
                 CallAudioState.ROUTE_WIRED_HEADSET, // expectedAvailableRoutes
-                true, // doesDeviceSupportEarpiece
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, // earpieceControl
                 shouldRunWithFocus
         ));
 
@@ -743,12 +761,12 @@
                 "Disconnect bluetooth during speakerphone", // name
                 CallAudioState.ROUTE_SPEAKER, // initialRoute
                 CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
-                NONE, // speakerInteraction
+                OPTIONAL, // speakerInteraction
                 NONE, // bluetoothInteraction
                 CallAudioRouteStateMachine.DISCONNECT_BLUETOOTH, // action
                 CallAudioState.ROUTE_SPEAKER, // expectedRoute
                 CallAudioState.ROUTE_WIRED_HEADSET, // expectedAvailableRoutes
-                true, // doesDeviceSupportEarpiece
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, // earpieceControl
                 shouldRunWithFocus
         ));
 
@@ -756,12 +774,12 @@
                 "Disconnect bluetooth during earpiece", // name
                 CallAudioState.ROUTE_EARPIECE, // initialRoute
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
-                NONE, // speakerInteraction
+                OPTIONAL, // speakerInteraction
                 NONE, // bluetoothInteraction
                 CallAudioRouteStateMachine.DISCONNECT_BLUETOOTH, // action
                 CallAudioState.ROUTE_EARPIECE, // expectedRoute
                 CallAudioState.ROUTE_EARPIECE, // expectedAvailableRoutes
-                true, // doesDeviceSupportEarpiece
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, // earpieceControl
                 shouldRunWithFocus
         ));
 
@@ -774,7 +792,7 @@
                 CallAudioRouteStateMachine.SWITCH_SPEAKER, // action
                 CallAudioState.ROUTE_SPEAKER, // expectedRoute
                 CallAudioState.ROUTE_EARPIECE, // expectedAvailableRoutes
-                true, // doesDeviceSupportEarpiece
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, // earpieceControl
                 shouldRunWithFocus
         ));
 
@@ -787,7 +805,7 @@
                 CallAudioRouteStateMachine.SWITCH_SPEAKER, // action
                 CallAudioState.ROUTE_SPEAKER, // expectedRoute
                 CallAudioState.ROUTE_WIRED_HEADSET, // expectedAvailableRoutes
-                true, // doesDeviceSupportEarpiece
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, // earpieceControl
                 shouldRunWithFocus
         ));
 
@@ -800,7 +818,7 @@
                 CallAudioRouteStateMachine.SWITCH_SPEAKER, // action
                 CallAudioState.ROUTE_SPEAKER, // expectedRoute
                 CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // expectedAvai
-                true, // doesDeviceSupportEarpiece
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, // earpieceControl
                 shouldRunWithFocus
         ));
 
@@ -808,12 +826,12 @@
                 "Switch to earpiece from bluetooth", // name
                 CallAudioState.ROUTE_BLUETOOTH, // initialRoute
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
-                NONE, // speakerInteraction
+                OPTIONAL, // speakerInteraction
                 OFF, // bluetoothInteraction
                 CallAudioRouteStateMachine.SWITCH_EARPIECE, // action
                 CallAudioState.ROUTE_EARPIECE, // expectedRoute
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // expectedAvailable
-                true, // doesDeviceSupportEarpiece
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, // earpieceControl
                 shouldRunWithFocus
         ));
 
@@ -826,7 +844,7 @@
                 CallAudioRouteStateMachine.SWITCH_EARPIECE, // action
                 CallAudioState.ROUTE_EARPIECE, // expectedRoute
                 CallAudioState.ROUTE_EARPIECE, // expectedAvailableRoutes
-                true, // doesDeviceSupportEarpiece
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, // earpieceControl
                 shouldRunWithFocus
         ));
 
@@ -839,7 +857,7 @@
                 CallAudioRouteStateMachine.SWITCH_EARPIECE, // action
                 CallAudioState.ROUTE_EARPIECE, // expectedRoute
                 CallAudioState.ROUTE_EARPIECE, // expectedAvailableRoutes
-                true, // doesDeviceSupportEarpiece
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, // earpieceControl
                 shouldRunWithFocus
         ));
 
@@ -852,7 +870,7 @@
                 CallAudioRouteStateMachine.SWITCH_EARPIECE, // action
                 CallAudioState.ROUTE_EARPIECE, // expectedRoute
                 CallAudioState.ROUTE_EARPIECE, // expectedAvailableRoutes
-                true, // doesDeviceSupportEarpiece
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, // earpieceControl
                 shouldRunWithFocus
         ));
 
@@ -865,7 +883,7 @@
                 CallAudioRouteStateMachine.SWITCH_BLUETOOTH, // action
                 CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // expectedAvailable
-                true, // doesDeviceSupportEarpiece
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, // earpieceControl
                 shouldRunWithFocus
         ));
 
@@ -873,12 +891,12 @@
                 "Switch to bluetooth from earpiece", // name
                 CallAudioState.ROUTE_EARPIECE, // initialRoute
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // availableRoutes
-                NONE, // speakerInteraction
+                OPTIONAL, // speakerInteraction
                 ON, // bluetoothInteraction
                 CallAudioRouteStateMachine.SWITCH_BLUETOOTH, // action
                 CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
                 CallAudioState.ROUTE_EARPIECE | CallAudioState.ROUTE_BLUETOOTH, // expectedAvailable
-                true, // doesDeviceSupportEarpiece
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, // earpieceControl
                 shouldRunWithFocus
         ));
 
@@ -886,12 +904,12 @@
                 "Switch to bluetooth from wired headset", // name
                 CallAudioState.ROUTE_WIRED_HEADSET, // initialRoute
                 CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // availableRou
-                NONE, // speakerInteraction
+                OPTIONAL, // speakerInteraction
                 ON, // bluetoothInteraction
                 CallAudioRouteStateMachine.SWITCH_BLUETOOTH, // action
                 CallAudioState.ROUTE_BLUETOOTH, // expectedRoute
                 CallAudioState.ROUTE_WIRED_HEADSET | CallAudioState.ROUTE_BLUETOOTH, // expectedAvai
-                true, // doesDeviceSupportEarpiece
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, // earpieceControl
                 shouldRunWithFocus
         ));
 
@@ -904,7 +922,7 @@
                 CallAudioRouteStateMachine.SWITCH_BASELINE_ROUTE, // action
                 CallAudioState.ROUTE_SPEAKER, // expectedRoute
                 CallAudioState.ROUTE_BLUETOOTH, // expectedAvailableRoutes
-                false, // doesDeviceSupportEarpiece
+                CallAudioRouteStateMachine.EARPIECE_FORCE_DISABLED, // earpieceControl
                 shouldRunWithFocus
         ));
 
@@ -917,7 +935,7 @@
                 CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET, // action
                 CallAudioState.ROUTE_SPEAKER, // expectedRoute
                 CallAudioState.ROUTE_SPEAKER, // expectedAvailableRoutes
-                false, // doesDeviceSupportEarpiece
+                CallAudioRouteStateMachine.EARPIECE_FORCE_DISABLED, // earpieceControl
                 shouldRunWithFocus
         ));
 
@@ -930,7 +948,7 @@
                 CallAudioRouteStateMachine.DISCONNECT_WIRED_HEADSET, // action
                 CallAudioState.ROUTE_SPEAKER, // expectedRoute
                 CallAudioState.ROUTE_SPEAKER, // expectedAvailableRoutes
-                true, // doesDeviceSupportEarpiece
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, // earpieceControl
                 shouldRunWithFocus
         ).setCallSupportedRoutes(CallAudioState.ROUTE_ALL & ~CallAudioState.ROUTE_EARPIECE));
 
@@ -943,7 +961,7 @@
                 CallAudioRouteStateMachine.DISCONNECT_BLUETOOTH, // action
                 CallAudioState.ROUTE_SPEAKER, // expectedRoute
                 CallAudioState.ROUTE_SPEAKER, // expectedAvailableRoutes
-                true, // doesDeviceSupportEarpiece
+                CallAudioRouteStateMachine.EARPIECE_FORCE_ENABLED, // earpieceControl
                 shouldRunWithFocus
         ).setCallSupportedRoutes(CallAudioState.ROUTE_ALL & ~CallAudioState.ROUTE_EARPIECE));
 
@@ -972,7 +990,7 @@
                 mockWiredHeadsetManager,
                 mockStatusBarNotifier,
                 mAudioServiceFactory,
-                params.doesDeviceSupportEarpiece);
+                params.earpieceControl);
 
         setupMocksForParams(stateMachine, params);
 
@@ -1022,6 +1040,10 @@
                 verify(mockBluetoothRouteManager, never())
                         .connectBluetoothAudio(nullable(String.class));
                 verify(mockBluetoothRouteManager).disconnectBluetoothAudio();
+                break;
+            case OPTIONAL:
+                // optional, don't test
+                break;
         }
 
         switch (params.speakerInteraction) {
@@ -1031,6 +1053,10 @@
             case ON: // fall through
             case OFF:
                 verify(mockAudioManager).setSpeakerphoneOn(params.speakerInteraction == ON);
+                break;
+            case OPTIONAL:
+                // optional, don't test
+                break;
         }
 
         // Verify the end state
@@ -1077,7 +1103,7 @@
                 mockWiredHeadsetManager,
                 mockStatusBarNotifier,
                 mAudioServiceFactory,
-                params.doesDeviceSupportEarpiece);
+                params.earpieceControl);
 
         // Set up bluetooth and speakerphone state
         when(mockBluetoothRouteManager.isBluetoothAvailable()).thenReturn(