Loading android/app/src/com/android/bluetooth/hfpclient/HeadsetClientService.java +0 −1 Original line number Diff line number Diff line Loading @@ -217,7 +217,6 @@ public class HeadsetClientService extends ProfileService { return amVol; } @VisibleForTesting int amToHfVol(int amVol) { int amRange = (mMaxAmVcVol > mMinAmVcVol) ? (mMaxAmVcVol - mMinAmVcVol) : 1; int hfRange = MAX_HFP_SCO_VOICE_CALL_VOLUME - MIN_HFP_SCO_VOICE_CALL_VOLUME; Loading android/app/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachine.java +170 −22 Original line number Diff line number Diff line Loading @@ -14,11 +14,21 @@ * limitations under the License. */ /** * Bluetooth Headset Client StateMachine (Disconnected) | ^ ^ CONNECT | | | DISCONNECTED V | | * (Connecting) | | | CONNECTED | | DISCONNECT V | (Connected) | ^ CONNECT_AUDIO | | * DISCONNECT_AUDIO V | (AudioOn) */ // Bluetooth Headset Client State Machine // (Disconnected) // | ^ // CONNECT | | DISCONNECTED // V | // (Connecting) (Disconnecting) // | ^ // CONNECTED | | DISCONNECT // V | // (Connected) // | | // CONNECT_AUDIO | | DISCONNECT_AUDIO // | | // (Audio_On) package com.android.bluetooth.hfpclient; import static android.Manifest.permission.BLUETOOTH_CONNECT; Loading Loading @@ -56,6 +66,7 @@ import com.android.bluetooth.Utils; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.MetricsLogger; import com.android.bluetooth.btservice.ProfileService; import com.android.bluetooth.flags.Flags; import com.android.bluetooth.hfp.HeadsetService; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.IState; Loading Loading @@ -109,12 +120,14 @@ public class HeadsetClientStateMachine extends StateMachine { @VisibleForTesting static final int QUERY_OPERATOR_NAME = 51; @VisibleForTesting static final int SUBSCRIBER_INFO = 52; @VisibleForTesting static final int CONNECTING_TIMEOUT = 53; @VisibleForTesting static final int DISCONNECTING_TIMEOUT = 54; // special action to handle terminating specific call from multiparty call static final int TERMINATE_SPECIFIC_CALL = 53; // Timeouts. @VisibleForTesting static final int CONNECTING_TIMEOUT_MS = 10000; // 10s @VisibleForTesting static final int DISCONNECTING_TIMEOUT_MS = 10000; // 10s private static final int ROUTING_DELAY_MS = 250; static final int HF_ORIGINATED_CALL_ID = -1; Loading @@ -127,6 +140,7 @@ public class HeadsetClientStateMachine extends StateMachine { private final Disconnected mDisconnected; private final Connecting mConnecting; private final Connected mConnected; private final Disconnecting mDisconnecting; private final AudioOn mAudioOn; private State mPrevState; Loading Loading @@ -314,6 +328,8 @@ public class HeadsetClientStateMachine extends StateMachine { return "SUBSCRIBER_INFO"; case CONNECTING_TIMEOUT: return "CONNECTING_TIMEOUT"; case DISCONNECTING_TIMEOUT: return "DISCONNECTING_TIMEOUT"; default: return "UNKNOWN(" + what + ")"; } Loading Loading @@ -932,11 +948,15 @@ public class HeadsetClientStateMachine extends StateMachine { mConnecting = new Connecting(); mConnected = new Connected(); mAudioOn = new AudioOn(); mDisconnecting = new Disconnecting(); addState(mDisconnected); addState(mConnecting); addState(mConnected); addState(mAudioOn, mConnected); if (Flags.hfpClientDisconnectingState()) { addState(mDisconnecting); } setInitialState(mDisconnected); } Loading Loading @@ -1003,7 +1023,11 @@ public class HeadsetClientStateMachine extends StateMachine { class Disconnected extends State { @Override public void enter() { debug("Enter Disconnected: " + getCurrentMessage().what); debug( "Enter Disconnected: from state=" + mPrevState + ", message=" + getMessageName(getCurrentMessage().what)); // cleanup mIndicatorNetworkState = HeadsetClientHalConstants.NETWORK_STATE_NOT_AVAILABLE; Loading Loading @@ -1040,7 +1064,15 @@ public class HeadsetClientStateMachine extends StateMachine { mCurrentDevice, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTED); } else if (mPrevState != null) { // null is the default state before Disconnected } else if (Flags.hfpClientDisconnectingState()) { if (mPrevState == mDisconnecting) { broadcastConnectionState( mCurrentDevice, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_DISCONNECTING); } } else if (mPrevState != null) { // null is the default state before Disconnected error( "Disconnected: Illegal state transition from " + mPrevState.getName() Loading Loading @@ -1139,7 +1171,7 @@ public class HeadsetClientStateMachine extends StateMachine { @Override public void exit() { debug("Exit Disconnected: " + getCurrentMessage().what); debug("Exit Disconnected: " + getMessageName(getCurrentMessage().what)); mPrevState = this; } } Loading @@ -1147,7 +1179,7 @@ public class HeadsetClientStateMachine extends StateMachine { class Connecting extends State { @Override public void enter() { debug("Enter Connecting: " + getCurrentMessage().what); debug("Enter Connecting: " + getMessageName(getCurrentMessage().what)); // This message is either consumed in processMessage or // removed in exit. It is safe to send a CONNECTING_TIMEOUT here since // the only transition is when connection attempt is initiated. Loading Loading @@ -1351,7 +1383,7 @@ public class HeadsetClientStateMachine extends StateMachine { @Override public void exit() { debug("Exit Connecting: " + getCurrentMessage().what); debug("Exit Connecting: " + getMessageName(getCurrentMessage().what)); removeMessages(CONNECTING_TIMEOUT); mPrevState = this; } Loading @@ -1362,7 +1394,7 @@ public class HeadsetClientStateMachine extends StateMachine { @Override public void enter() { debug("Enter Connected: " + getCurrentMessage().what); debug("Enter Connected: " + getMessageName(getCurrentMessage().what)); mAudioWbs = false; mAudioSWB = false; mCommandedSpeakerVolume = -1; Loading Loading @@ -1409,7 +1441,12 @@ public class HeadsetClientStateMachine extends StateMachine { if (!mCurrentDevice.equals(dev)) { break; } if (!mNativeInterface.disconnect(dev)) { if (Flags.hfpClientDisconnectingState()) { if (!mNativeInterface.disconnect(mCurrentDevice)) { warn("disconnectNative failed for " + mCurrentDevice); } transitionTo(mDisconnecting); } else if (!mNativeInterface.disconnect(dev)) { error("disconnectNative failed for " + dev); } break; Loading Loading @@ -1569,7 +1606,7 @@ public class HeadsetClientStateMachine extends StateMachine { + event.device + ": " + event.valueInt); processConnectionEvent(event.valueInt, event.device); processConnectionEvent(message, event.valueInt, event.device); break; case StackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED: debug( Loading Loading @@ -1810,13 +1847,19 @@ public class HeadsetClientStateMachine extends StateMachine { } // in Connected state private void processConnectionEvent(int state, BluetoothDevice device) { private void processConnectionEvent(Message message, int state, BluetoothDevice device) { switch (state) { case HeadsetClientHalConstants.CONNECTION_STATE_DISCONNECTED: debug("Connected disconnects."); // AG disconnects if (mCurrentDevice.equals(device)) { if (Flags.hfpClientDisconnectingState()) { transitionTo(mDisconnecting); // message is deferred to be processed in the disconnecting state deferMessage(message); } else { transitionTo(mDisconnected); } } else { error("Disconnected from unknown device: " + device); } Loading Loading @@ -1915,7 +1958,101 @@ public class HeadsetClientStateMachine extends StateMachine { @Override public void exit() { debug("Exit Connected: " + getCurrentMessage().what); debug("Exit Connected: " + getMessageName(getCurrentMessage().what)); mPrevState = this; } } class Disconnecting extends State { @Override public void enter() { debug( "Disconnecting: enter disconnecting from state=" + mPrevState + ", message=" + getMessageName(getCurrentMessage().what)); if (mPrevState == mConnected || mPrevState == mAudioOn) { broadcastConnectionState( mCurrentDevice, BluetoothProfile.STATE_DISCONNECTING, BluetoothProfile.STATE_CONNECTED); } else { String prevStateName = mPrevState == null ? "null" : mPrevState.getName(); error( "Disconnecting: Illegal state transition from " + prevStateName + " to Disconnecting"); } sendMessageDelayed(DISCONNECTING_TIMEOUT, DISCONNECTING_TIMEOUT_MS); } @Override public synchronized boolean processMessage(Message message) { debug("Disconnecting: Process message: " + message.what); switch (message.what) { // Defering messages as state machine objects are meant to be reused and after // disconnect is complete we want honor other message requests case CONNECT: case CONNECT_AUDIO: case DISCONNECT: case DISCONNECT_AUDIO: deferMessage(message); break; case DISCONNECTING_TIMEOUT: // We timed out trying to disconnect, force transition to disconnected. warn("Disconnecting: Disconnection timeout for " + mCurrentDevice); transitionTo(mDisconnected); break; case StackEvent.STACK_EVENT: StackEvent event = (StackEvent) message.obj; switch (event.type) { case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED: debug( "Disconnecting: Connection state changed: " + event.device + ": " + event.valueInt); processConnectionEvent(event.valueInt, event.device); break; default: error("Disconnecting: Unknown stack event: " + event.type); break; } break; default: warn("Disconnecting: Message not handled " + message); return NOT_HANDLED; } return HANDLED; } private void processConnectionEvent(int state, BluetoothDevice device) { switch (state) { case HeadsetClientHalConstants.CONNECTION_STATE_DISCONNECTED: if (mCurrentDevice.equals(device)) { transitionTo(mDisconnected); } else { error("Disconnecting: Disconnected from unknown device: " + device); } break; default: error( "Disconnecting: Connection State Device: " + device + " bad state: " + state); break; } } @Override public void exit() { debug("Disconnecting: Exit Disconnecting: " + getMessageName(getCurrentMessage().what)); removeMessages(DISCONNECTING_TIMEOUT); mPrevState = this; } } Loading @@ -1923,7 +2060,7 @@ public class HeadsetClientStateMachine extends StateMachine { class AudioOn extends State { @Override public void enter() { debug("Enter AudioOn: " + getCurrentMessage().what); debug("Enter AudioOn: " + getMessageName(getCurrentMessage().what)); broadcastAudioState( mCurrentDevice, BluetoothHeadsetClient.STATE_AUDIO_CONNECTED, Loading Loading @@ -1975,7 +2112,7 @@ public class HeadsetClientStateMachine extends StateMachine { + event.device + ": " + event.valueInt); processConnectionEvent(event.valueInt, event.device); processConnectionEvent(message, event.valueInt, event.device); break; case StackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED: debug( Loading @@ -1996,13 +2133,20 @@ public class HeadsetClientStateMachine extends StateMachine { } // in AudioOn state. Can AG disconnect RFCOMM prior to SCO? Handle this private void processConnectionEvent(int state, BluetoothDevice device) { private void processConnectionEvent(Message message, int state, BluetoothDevice device) { switch (state) { case HeadsetClientHalConstants.CONNECTION_STATE_DISCONNECTED: if (mCurrentDevice.equals(device)) { processAudioEvent( HeadsetClientHalConstants.AUDIO_STATE_DISCONNECTED, device); if (Flags.hfpClientDisconnectingState()) { transitionTo(mDisconnecting); // message is deferred to be processed in the disconnecting state deferMessage(message); } else { transitionTo(mDisconnected); } } else { error("Disconnected from unknown device: " + device); } Loading Loading @@ -2041,7 +2185,7 @@ public class HeadsetClientStateMachine extends StateMachine { @Override public void exit() { debug("Exit AudioOn: " + getCurrentMessage().what); debug("Exit AudioOn: " + getMessageName(getCurrentMessage().what)); mPrevState = this; broadcastAudioState( mCurrentDevice, Loading @@ -2064,7 +2208,11 @@ public class HeadsetClientStateMachine extends StateMachine { return BluetoothProfile.STATE_CONNECTED; } error("Bad currentState: " + currentState); if (Flags.hfpClientDisconnectingState()) { if (currentState == mDisconnecting) { return BluetoothProfile.STATE_DISCONNECTING; } } return BluetoothProfile.STATE_DISCONNECTED; } Loading android/app/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineTest.java +163 −3 Original line number Diff line number Diff line Loading @@ -56,6 +56,7 @@ import android.os.Bundle; import android.os.Looper; import android.os.Message; import android.os.test.TestLooper; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.util.Pair; Loading @@ -67,6 +68,7 @@ import com.android.bluetooth.R; import com.android.bluetooth.TestUtils; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.RemoteDevices; import com.android.bluetooth.flags.Flags; import com.android.bluetooth.hfp.HeadsetService; import org.hamcrest.Matcher; Loading Loading @@ -133,6 +135,8 @@ public class HeadsetClientStateMachineTest { doReturn(mRemoteDevices).when(mAdapterService).getRemoteDevices(); doReturn(true).when(mNativeInterface).sendAndroidAt(anyObject(), anyString()); doReturn(true).when(mNativeInterface).disconnect(any(BluetoothDevice.class)); mTestLooper = new TestLooper(); mHeadsetClientStateMachine = new TestHeadsetClientStateMachine( Loading Loading @@ -980,7 +984,8 @@ public class HeadsetClientStateMachineTest { assertName(HeadsetClientStateMachine.QUERY_OPERATOR_NAME, "QUERY_OPERATOR_NAME"); assertName(HeadsetClientStateMachine.SUBSCRIBER_INFO, "SUBSCRIBER_INFO"); assertName(HeadsetClientStateMachine.CONNECTING_TIMEOUT, "CONNECTING_TIMEOUT"); int unknownMessageInt = 54; assertName(HeadsetClientStateMachine.DISCONNECTING_TIMEOUT, "DISCONNECTING_TIMEOUT"); int unknownMessageInt = 55; assertName(unknownMessageInt, "UNKNOWN(" + unknownMessageInt + ")"); } Loading Loading @@ -1231,6 +1236,27 @@ public class HeadsetClientStateMachineTest { verify(mNativeInterface).disconnect(any(BluetoothDevice.class)); } @Test @EnableFlags(Flags.FLAG_HFP_CLIENT_DISCONNECTING_STATE) public void testConnectedState_ProcessDisconnectMessage_TransitionToDisconnecting() { initToDisconnectingState(); assertThat(mHeadsetClientStateMachine.getCurrentState()) .isInstanceOf(HeadsetClientStateMachine.Disconnecting.class); } @Test @EnableFlags(Flags.FLAG_HFP_CLIENT_DISCONNECTING_STATE) public void testProcessStackEvent_ConnectionStateChanged_Disconnected_onConnectedState() { initToConnectedState(); StackEvent event = new StackEvent(StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED); event.valueInt = HeadsetClientHalConstants.CONNECTION_STATE_DISCONNECTED; event.device = mTestDevice; sendMessage(StackEvent.STACK_EVENT, event); assertThat(mHeadsetClientStateMachine.getCurrentState()) .isInstanceOf(HeadsetClientStateMachine.Disconnected.class); verify(mHeadsetService).updateInbandRinging(eq(mTestDevice), eq(false)); } @Test public void testProcessConnectAudioMessage_onConnectedState() { initToConnectedState(); Loading Loading @@ -1269,7 +1295,7 @@ public class HeadsetClientStateMachineTest { @Test public void testProcessDisconnectAudioMessage_onAudioOnState() { initToAudioOnState(); sendMessage(HeadsetClientStateMachine.DISCONNECT_AUDIO, mTestDevice); sendMessage(HeadsetClientStateMachine.DISCONNECT_AUDIO); verify(mNativeInterface).disconnectAudio(any(BluetoothDevice.class)); } Loading @@ -1282,7 +1308,7 @@ public class HeadsetClientStateMachineTest { mHeadsetClientStateMachine.mCalls.put(0, call); int[] states = new int[1]; states[0] = HfpClientCall.CALL_STATE_ACTIVE; sendMessage(HeadsetClientStateMachine.HOLD_CALL, mTestDevice); sendMessage(HeadsetClientStateMachine.HOLD_CALL); verify(mNativeInterface).handleCallAction(any(BluetoothDevice.class), anyInt(), eq(0)); } Loading Loading @@ -1340,6 +1366,132 @@ public class HeadsetClientStateMachineTest { assertThat(mHeadsetClientStateMachine.mAudioSWB).isTrue(); } @Test @EnableFlags(Flags.FLAG_HFP_CLIENT_DISCONNECTING_STATE) public void testDisconnectingState_TransitionToDisconnected() { initToDisconnectingState(); StackEvent event = new StackEvent(StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED); event.valueInt = HeadsetClientHalConstants.CONNECTION_STATE_DISCONNECTED; event.device = mTestDevice; sendMessage(StackEvent.STACK_EVENT, event); mTestLooper.dispatchAll(); verifySendBroadcastMultiplePermissions(hasExtra(EXTRA_STATE, STATE_DISCONNECTED)); assertThat(mHeadsetClientStateMachine.getCurrentState()) .isInstanceOf(HeadsetClientStateMachine.Disconnected.class); verify(mHeadsetService).updateInbandRinging(eq(mTestDevice), eq(false)); } @Test @EnableFlags(Flags.FLAG_HFP_CLIENT_DISCONNECTING_STATE) public void testDisconnectingState_ReceiveConnectMsg_DeferMessage() { // case CONNECT: initToDisconnectingState(); assertThat( mHeadsetClientStateMachine.doesSuperHaveDeferredMessages( HeadsetClientStateMachine.CONNECT)) .isFalse(); sendMessage(HeadsetClientStateMachine.CONNECT); assertThat( mHeadsetClientStateMachine.doesSuperHaveDeferredMessages( HeadsetClientStateMachine.CONNECT)) .isTrue(); assertThat(mHeadsetClientStateMachine.getCurrentState()) .isInstanceOf(HeadsetClientStateMachine.Disconnecting.class); } @Test @EnableFlags(Flags.FLAG_HFP_CLIENT_DISCONNECTING_STATE) public void testDisconnectingState_ReceiveConnectAudioMsg_DeferMessage() { // case CONNECT_AUDIO: initToDisconnectingState(); assertThat( mHeadsetClientStateMachine.doesSuperHaveDeferredMessages( HeadsetClientStateMachine.CONNECT_AUDIO)) .isFalse(); sendMessage(HeadsetClientStateMachine.CONNECT_AUDIO); assertThat( mHeadsetClientStateMachine.doesSuperHaveDeferredMessages( HeadsetClientStateMachine.CONNECT_AUDIO)) .isTrue(); assertThat(mHeadsetClientStateMachine.getCurrentState()) .isInstanceOf(HeadsetClientStateMachine.Disconnecting.class); } @Test @EnableFlags(Flags.FLAG_HFP_CLIENT_DISCONNECTING_STATE) public void testDisconnectingState_ReceiveDisconnectMsg_DeferMessage() { // case DISCONNECT: initToDisconnectingState(); sendMessage(HeadsetClientStateMachine.DISCONNECT); assertThat( mHeadsetClientStateMachine.doesSuperHaveDeferredMessages( HeadsetClientStateMachine.DISCONNECT)) .isTrue(); assertThat(mHeadsetClientStateMachine.getCurrentState()) .isInstanceOf(HeadsetClientStateMachine.Disconnecting.class); } @Test @EnableFlags(Flags.FLAG_HFP_CLIENT_DISCONNECTING_STATE) public void testDisconnectingState_ReceiveDisconnectAudioMsg_DeferMessage() { // case DISCONNECT_AUDIO: initToDisconnectingState(); assertThat( mHeadsetClientStateMachine.doesSuperHaveDeferredMessages( HeadsetClientStateMachine.DISCONNECT_AUDIO)) .isFalse(); sendMessage(HeadsetClientStateMachine.DISCONNECT_AUDIO); assertThat( mHeadsetClientStateMachine.doesSuperHaveDeferredMessages( HeadsetClientStateMachine.DISCONNECT_AUDIO)) .isTrue(); assertThat(mHeadsetClientStateMachine.getCurrentState()) .isInstanceOf(HeadsetClientStateMachine.Disconnecting.class); } @Test @EnableFlags(Flags.FLAG_HFP_CLIENT_DISCONNECTING_STATE) public void testDisconnectingState_ReceiveUnknownMsg_NotHandled() { initToDisconnectingState(); sendMessage(HeadsetClientStateMachine.NO_ACTION); assertThat(mHeadsetClientStateMachine.getCurrentState()) .isInstanceOf(HeadsetClientStateMachine.Disconnecting.class); } @Test @EnableFlags(Flags.FLAG_HFP_CLIENT_DISCONNECTING_STATE) public void testAudioOnState_ReceiveDisconnectMsg_DeferMessage() { initToAudioOnState(); assertThat( mHeadsetClientStateMachine.doesSuperHaveDeferredMessages( HeadsetClientStateMachine.DISCONNECT)) .isFalse(); sendMessage(HeadsetClientStateMachine.DISCONNECT, mTestDevice); assertThat( mHeadsetClientStateMachine.doesSuperHaveDeferredMessages( HeadsetClientStateMachine.DISCONNECT)) .isTrue(); assertThat(mHeadsetClientStateMachine.getCurrentState()) .isInstanceOf(HeadsetClientStateMachine.AudioOn.class); } @Test @EnableFlags(Flags.FLAG_HFP_CLIENT_DISCONNECTING_STATE) public void testDisconnectingState_DisconnectingTimeout_TransitionToDisconnected() { initToDisconnectingState(); // Trigger timeout mTestLooper.moveTimeForward(HeadsetClientStateMachine.DISCONNECTING_TIMEOUT_MS); mTestLooper.dispatchAll(); verifySendBroadcastMultiplePermissions(hasExtra(EXTRA_STATE, STATE_DISCONNECTED)); assertThat(mHeadsetClientStateMachine.getCurrentState()) .isInstanceOf(HeadsetClientStateMachine.Disconnected.class); verify(mHeadsetService).updateInbandRinging(eq(mTestDevice), eq(false)); } /** * Allow/disallow connection to any device * Loading Loading @@ -1380,6 +1532,14 @@ public class HeadsetClientStateMachineTest { .isInstanceOf(HeadsetClientStateMachine.AudioOn.class); } private void initToDisconnectingState() { initToConnectedState(); sendMessageAndVerifyTransition( mHeadsetClientStateMachine.obtainMessage( HeadsetClientStateMachine.DISCONNECT, mTestDevice), HeadsetClientStateMachine.Disconnecting.class); } private void verifySendBroadcastMultiplePermissions(Matcher<Intent>... matchers) { mInOrder.verify(mHeadsetClientService) .sendBroadcastMultiplePermissions( Loading Loading
android/app/src/com/android/bluetooth/hfpclient/HeadsetClientService.java +0 −1 Original line number Diff line number Diff line Loading @@ -217,7 +217,6 @@ public class HeadsetClientService extends ProfileService { return amVol; } @VisibleForTesting int amToHfVol(int amVol) { int amRange = (mMaxAmVcVol > mMinAmVcVol) ? (mMaxAmVcVol - mMinAmVcVol) : 1; int hfRange = MAX_HFP_SCO_VOICE_CALL_VOLUME - MIN_HFP_SCO_VOICE_CALL_VOLUME; Loading
android/app/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachine.java +170 −22 Original line number Diff line number Diff line Loading @@ -14,11 +14,21 @@ * limitations under the License. */ /** * Bluetooth Headset Client StateMachine (Disconnected) | ^ ^ CONNECT | | | DISCONNECTED V | | * (Connecting) | | | CONNECTED | | DISCONNECT V | (Connected) | ^ CONNECT_AUDIO | | * DISCONNECT_AUDIO V | (AudioOn) */ // Bluetooth Headset Client State Machine // (Disconnected) // | ^ // CONNECT | | DISCONNECTED // V | // (Connecting) (Disconnecting) // | ^ // CONNECTED | | DISCONNECT // V | // (Connected) // | | // CONNECT_AUDIO | | DISCONNECT_AUDIO // | | // (Audio_On) package com.android.bluetooth.hfpclient; import static android.Manifest.permission.BLUETOOTH_CONNECT; Loading Loading @@ -56,6 +66,7 @@ import com.android.bluetooth.Utils; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.MetricsLogger; import com.android.bluetooth.btservice.ProfileService; import com.android.bluetooth.flags.Flags; import com.android.bluetooth.hfp.HeadsetService; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.IState; Loading Loading @@ -109,12 +120,14 @@ public class HeadsetClientStateMachine extends StateMachine { @VisibleForTesting static final int QUERY_OPERATOR_NAME = 51; @VisibleForTesting static final int SUBSCRIBER_INFO = 52; @VisibleForTesting static final int CONNECTING_TIMEOUT = 53; @VisibleForTesting static final int DISCONNECTING_TIMEOUT = 54; // special action to handle terminating specific call from multiparty call static final int TERMINATE_SPECIFIC_CALL = 53; // Timeouts. @VisibleForTesting static final int CONNECTING_TIMEOUT_MS = 10000; // 10s @VisibleForTesting static final int DISCONNECTING_TIMEOUT_MS = 10000; // 10s private static final int ROUTING_DELAY_MS = 250; static final int HF_ORIGINATED_CALL_ID = -1; Loading @@ -127,6 +140,7 @@ public class HeadsetClientStateMachine extends StateMachine { private final Disconnected mDisconnected; private final Connecting mConnecting; private final Connected mConnected; private final Disconnecting mDisconnecting; private final AudioOn mAudioOn; private State mPrevState; Loading Loading @@ -314,6 +328,8 @@ public class HeadsetClientStateMachine extends StateMachine { return "SUBSCRIBER_INFO"; case CONNECTING_TIMEOUT: return "CONNECTING_TIMEOUT"; case DISCONNECTING_TIMEOUT: return "DISCONNECTING_TIMEOUT"; default: return "UNKNOWN(" + what + ")"; } Loading Loading @@ -932,11 +948,15 @@ public class HeadsetClientStateMachine extends StateMachine { mConnecting = new Connecting(); mConnected = new Connected(); mAudioOn = new AudioOn(); mDisconnecting = new Disconnecting(); addState(mDisconnected); addState(mConnecting); addState(mConnected); addState(mAudioOn, mConnected); if (Flags.hfpClientDisconnectingState()) { addState(mDisconnecting); } setInitialState(mDisconnected); } Loading Loading @@ -1003,7 +1023,11 @@ public class HeadsetClientStateMachine extends StateMachine { class Disconnected extends State { @Override public void enter() { debug("Enter Disconnected: " + getCurrentMessage().what); debug( "Enter Disconnected: from state=" + mPrevState + ", message=" + getMessageName(getCurrentMessage().what)); // cleanup mIndicatorNetworkState = HeadsetClientHalConstants.NETWORK_STATE_NOT_AVAILABLE; Loading Loading @@ -1040,7 +1064,15 @@ public class HeadsetClientStateMachine extends StateMachine { mCurrentDevice, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_CONNECTED); } else if (mPrevState != null) { // null is the default state before Disconnected } else if (Flags.hfpClientDisconnectingState()) { if (mPrevState == mDisconnecting) { broadcastConnectionState( mCurrentDevice, BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_DISCONNECTING); } } else if (mPrevState != null) { // null is the default state before Disconnected error( "Disconnected: Illegal state transition from " + mPrevState.getName() Loading Loading @@ -1139,7 +1171,7 @@ public class HeadsetClientStateMachine extends StateMachine { @Override public void exit() { debug("Exit Disconnected: " + getCurrentMessage().what); debug("Exit Disconnected: " + getMessageName(getCurrentMessage().what)); mPrevState = this; } } Loading @@ -1147,7 +1179,7 @@ public class HeadsetClientStateMachine extends StateMachine { class Connecting extends State { @Override public void enter() { debug("Enter Connecting: " + getCurrentMessage().what); debug("Enter Connecting: " + getMessageName(getCurrentMessage().what)); // This message is either consumed in processMessage or // removed in exit. It is safe to send a CONNECTING_TIMEOUT here since // the only transition is when connection attempt is initiated. Loading Loading @@ -1351,7 +1383,7 @@ public class HeadsetClientStateMachine extends StateMachine { @Override public void exit() { debug("Exit Connecting: " + getCurrentMessage().what); debug("Exit Connecting: " + getMessageName(getCurrentMessage().what)); removeMessages(CONNECTING_TIMEOUT); mPrevState = this; } Loading @@ -1362,7 +1394,7 @@ public class HeadsetClientStateMachine extends StateMachine { @Override public void enter() { debug("Enter Connected: " + getCurrentMessage().what); debug("Enter Connected: " + getMessageName(getCurrentMessage().what)); mAudioWbs = false; mAudioSWB = false; mCommandedSpeakerVolume = -1; Loading Loading @@ -1409,7 +1441,12 @@ public class HeadsetClientStateMachine extends StateMachine { if (!mCurrentDevice.equals(dev)) { break; } if (!mNativeInterface.disconnect(dev)) { if (Flags.hfpClientDisconnectingState()) { if (!mNativeInterface.disconnect(mCurrentDevice)) { warn("disconnectNative failed for " + mCurrentDevice); } transitionTo(mDisconnecting); } else if (!mNativeInterface.disconnect(dev)) { error("disconnectNative failed for " + dev); } break; Loading Loading @@ -1569,7 +1606,7 @@ public class HeadsetClientStateMachine extends StateMachine { + event.device + ": " + event.valueInt); processConnectionEvent(event.valueInt, event.device); processConnectionEvent(message, event.valueInt, event.device); break; case StackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED: debug( Loading Loading @@ -1810,13 +1847,19 @@ public class HeadsetClientStateMachine extends StateMachine { } // in Connected state private void processConnectionEvent(int state, BluetoothDevice device) { private void processConnectionEvent(Message message, int state, BluetoothDevice device) { switch (state) { case HeadsetClientHalConstants.CONNECTION_STATE_DISCONNECTED: debug("Connected disconnects."); // AG disconnects if (mCurrentDevice.equals(device)) { if (Flags.hfpClientDisconnectingState()) { transitionTo(mDisconnecting); // message is deferred to be processed in the disconnecting state deferMessage(message); } else { transitionTo(mDisconnected); } } else { error("Disconnected from unknown device: " + device); } Loading Loading @@ -1915,7 +1958,101 @@ public class HeadsetClientStateMachine extends StateMachine { @Override public void exit() { debug("Exit Connected: " + getCurrentMessage().what); debug("Exit Connected: " + getMessageName(getCurrentMessage().what)); mPrevState = this; } } class Disconnecting extends State { @Override public void enter() { debug( "Disconnecting: enter disconnecting from state=" + mPrevState + ", message=" + getMessageName(getCurrentMessage().what)); if (mPrevState == mConnected || mPrevState == mAudioOn) { broadcastConnectionState( mCurrentDevice, BluetoothProfile.STATE_DISCONNECTING, BluetoothProfile.STATE_CONNECTED); } else { String prevStateName = mPrevState == null ? "null" : mPrevState.getName(); error( "Disconnecting: Illegal state transition from " + prevStateName + " to Disconnecting"); } sendMessageDelayed(DISCONNECTING_TIMEOUT, DISCONNECTING_TIMEOUT_MS); } @Override public synchronized boolean processMessage(Message message) { debug("Disconnecting: Process message: " + message.what); switch (message.what) { // Defering messages as state machine objects are meant to be reused and after // disconnect is complete we want honor other message requests case CONNECT: case CONNECT_AUDIO: case DISCONNECT: case DISCONNECT_AUDIO: deferMessage(message); break; case DISCONNECTING_TIMEOUT: // We timed out trying to disconnect, force transition to disconnected. warn("Disconnecting: Disconnection timeout for " + mCurrentDevice); transitionTo(mDisconnected); break; case StackEvent.STACK_EVENT: StackEvent event = (StackEvent) message.obj; switch (event.type) { case StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED: debug( "Disconnecting: Connection state changed: " + event.device + ": " + event.valueInt); processConnectionEvent(event.valueInt, event.device); break; default: error("Disconnecting: Unknown stack event: " + event.type); break; } break; default: warn("Disconnecting: Message not handled " + message); return NOT_HANDLED; } return HANDLED; } private void processConnectionEvent(int state, BluetoothDevice device) { switch (state) { case HeadsetClientHalConstants.CONNECTION_STATE_DISCONNECTED: if (mCurrentDevice.equals(device)) { transitionTo(mDisconnected); } else { error("Disconnecting: Disconnected from unknown device: " + device); } break; default: error( "Disconnecting: Connection State Device: " + device + " bad state: " + state); break; } } @Override public void exit() { debug("Disconnecting: Exit Disconnecting: " + getMessageName(getCurrentMessage().what)); removeMessages(DISCONNECTING_TIMEOUT); mPrevState = this; } } Loading @@ -1923,7 +2060,7 @@ public class HeadsetClientStateMachine extends StateMachine { class AudioOn extends State { @Override public void enter() { debug("Enter AudioOn: " + getCurrentMessage().what); debug("Enter AudioOn: " + getMessageName(getCurrentMessage().what)); broadcastAudioState( mCurrentDevice, BluetoothHeadsetClient.STATE_AUDIO_CONNECTED, Loading Loading @@ -1975,7 +2112,7 @@ public class HeadsetClientStateMachine extends StateMachine { + event.device + ": " + event.valueInt); processConnectionEvent(event.valueInt, event.device); processConnectionEvent(message, event.valueInt, event.device); break; case StackEvent.EVENT_TYPE_AUDIO_STATE_CHANGED: debug( Loading @@ -1996,13 +2133,20 @@ public class HeadsetClientStateMachine extends StateMachine { } // in AudioOn state. Can AG disconnect RFCOMM prior to SCO? Handle this private void processConnectionEvent(int state, BluetoothDevice device) { private void processConnectionEvent(Message message, int state, BluetoothDevice device) { switch (state) { case HeadsetClientHalConstants.CONNECTION_STATE_DISCONNECTED: if (mCurrentDevice.equals(device)) { processAudioEvent( HeadsetClientHalConstants.AUDIO_STATE_DISCONNECTED, device); if (Flags.hfpClientDisconnectingState()) { transitionTo(mDisconnecting); // message is deferred to be processed in the disconnecting state deferMessage(message); } else { transitionTo(mDisconnected); } } else { error("Disconnected from unknown device: " + device); } Loading Loading @@ -2041,7 +2185,7 @@ public class HeadsetClientStateMachine extends StateMachine { @Override public void exit() { debug("Exit AudioOn: " + getCurrentMessage().what); debug("Exit AudioOn: " + getMessageName(getCurrentMessage().what)); mPrevState = this; broadcastAudioState( mCurrentDevice, Loading @@ -2064,7 +2208,11 @@ public class HeadsetClientStateMachine extends StateMachine { return BluetoothProfile.STATE_CONNECTED; } error("Bad currentState: " + currentState); if (Flags.hfpClientDisconnectingState()) { if (currentState == mDisconnecting) { return BluetoothProfile.STATE_DISCONNECTING; } } return BluetoothProfile.STATE_DISCONNECTED; } Loading
android/app/tests/unit/src/com/android/bluetooth/hfpclient/HeadsetClientStateMachineTest.java +163 −3 Original line number Diff line number Diff line Loading @@ -56,6 +56,7 @@ import android.os.Bundle; import android.os.Looper; import android.os.Message; import android.os.test.TestLooper; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.util.Pair; Loading @@ -67,6 +68,7 @@ import com.android.bluetooth.R; import com.android.bluetooth.TestUtils; import com.android.bluetooth.btservice.AdapterService; import com.android.bluetooth.btservice.RemoteDevices; import com.android.bluetooth.flags.Flags; import com.android.bluetooth.hfp.HeadsetService; import org.hamcrest.Matcher; Loading Loading @@ -133,6 +135,8 @@ public class HeadsetClientStateMachineTest { doReturn(mRemoteDevices).when(mAdapterService).getRemoteDevices(); doReturn(true).when(mNativeInterface).sendAndroidAt(anyObject(), anyString()); doReturn(true).when(mNativeInterface).disconnect(any(BluetoothDevice.class)); mTestLooper = new TestLooper(); mHeadsetClientStateMachine = new TestHeadsetClientStateMachine( Loading Loading @@ -980,7 +984,8 @@ public class HeadsetClientStateMachineTest { assertName(HeadsetClientStateMachine.QUERY_OPERATOR_NAME, "QUERY_OPERATOR_NAME"); assertName(HeadsetClientStateMachine.SUBSCRIBER_INFO, "SUBSCRIBER_INFO"); assertName(HeadsetClientStateMachine.CONNECTING_TIMEOUT, "CONNECTING_TIMEOUT"); int unknownMessageInt = 54; assertName(HeadsetClientStateMachine.DISCONNECTING_TIMEOUT, "DISCONNECTING_TIMEOUT"); int unknownMessageInt = 55; assertName(unknownMessageInt, "UNKNOWN(" + unknownMessageInt + ")"); } Loading Loading @@ -1231,6 +1236,27 @@ public class HeadsetClientStateMachineTest { verify(mNativeInterface).disconnect(any(BluetoothDevice.class)); } @Test @EnableFlags(Flags.FLAG_HFP_CLIENT_DISCONNECTING_STATE) public void testConnectedState_ProcessDisconnectMessage_TransitionToDisconnecting() { initToDisconnectingState(); assertThat(mHeadsetClientStateMachine.getCurrentState()) .isInstanceOf(HeadsetClientStateMachine.Disconnecting.class); } @Test @EnableFlags(Flags.FLAG_HFP_CLIENT_DISCONNECTING_STATE) public void testProcessStackEvent_ConnectionStateChanged_Disconnected_onConnectedState() { initToConnectedState(); StackEvent event = new StackEvent(StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED); event.valueInt = HeadsetClientHalConstants.CONNECTION_STATE_DISCONNECTED; event.device = mTestDevice; sendMessage(StackEvent.STACK_EVENT, event); assertThat(mHeadsetClientStateMachine.getCurrentState()) .isInstanceOf(HeadsetClientStateMachine.Disconnected.class); verify(mHeadsetService).updateInbandRinging(eq(mTestDevice), eq(false)); } @Test public void testProcessConnectAudioMessage_onConnectedState() { initToConnectedState(); Loading Loading @@ -1269,7 +1295,7 @@ public class HeadsetClientStateMachineTest { @Test public void testProcessDisconnectAudioMessage_onAudioOnState() { initToAudioOnState(); sendMessage(HeadsetClientStateMachine.DISCONNECT_AUDIO, mTestDevice); sendMessage(HeadsetClientStateMachine.DISCONNECT_AUDIO); verify(mNativeInterface).disconnectAudio(any(BluetoothDevice.class)); } Loading @@ -1282,7 +1308,7 @@ public class HeadsetClientStateMachineTest { mHeadsetClientStateMachine.mCalls.put(0, call); int[] states = new int[1]; states[0] = HfpClientCall.CALL_STATE_ACTIVE; sendMessage(HeadsetClientStateMachine.HOLD_CALL, mTestDevice); sendMessage(HeadsetClientStateMachine.HOLD_CALL); verify(mNativeInterface).handleCallAction(any(BluetoothDevice.class), anyInt(), eq(0)); } Loading Loading @@ -1340,6 +1366,132 @@ public class HeadsetClientStateMachineTest { assertThat(mHeadsetClientStateMachine.mAudioSWB).isTrue(); } @Test @EnableFlags(Flags.FLAG_HFP_CLIENT_DISCONNECTING_STATE) public void testDisconnectingState_TransitionToDisconnected() { initToDisconnectingState(); StackEvent event = new StackEvent(StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED); event.valueInt = HeadsetClientHalConstants.CONNECTION_STATE_DISCONNECTED; event.device = mTestDevice; sendMessage(StackEvent.STACK_EVENT, event); mTestLooper.dispatchAll(); verifySendBroadcastMultiplePermissions(hasExtra(EXTRA_STATE, STATE_DISCONNECTED)); assertThat(mHeadsetClientStateMachine.getCurrentState()) .isInstanceOf(HeadsetClientStateMachine.Disconnected.class); verify(mHeadsetService).updateInbandRinging(eq(mTestDevice), eq(false)); } @Test @EnableFlags(Flags.FLAG_HFP_CLIENT_DISCONNECTING_STATE) public void testDisconnectingState_ReceiveConnectMsg_DeferMessage() { // case CONNECT: initToDisconnectingState(); assertThat( mHeadsetClientStateMachine.doesSuperHaveDeferredMessages( HeadsetClientStateMachine.CONNECT)) .isFalse(); sendMessage(HeadsetClientStateMachine.CONNECT); assertThat( mHeadsetClientStateMachine.doesSuperHaveDeferredMessages( HeadsetClientStateMachine.CONNECT)) .isTrue(); assertThat(mHeadsetClientStateMachine.getCurrentState()) .isInstanceOf(HeadsetClientStateMachine.Disconnecting.class); } @Test @EnableFlags(Flags.FLAG_HFP_CLIENT_DISCONNECTING_STATE) public void testDisconnectingState_ReceiveConnectAudioMsg_DeferMessage() { // case CONNECT_AUDIO: initToDisconnectingState(); assertThat( mHeadsetClientStateMachine.doesSuperHaveDeferredMessages( HeadsetClientStateMachine.CONNECT_AUDIO)) .isFalse(); sendMessage(HeadsetClientStateMachine.CONNECT_AUDIO); assertThat( mHeadsetClientStateMachine.doesSuperHaveDeferredMessages( HeadsetClientStateMachine.CONNECT_AUDIO)) .isTrue(); assertThat(mHeadsetClientStateMachine.getCurrentState()) .isInstanceOf(HeadsetClientStateMachine.Disconnecting.class); } @Test @EnableFlags(Flags.FLAG_HFP_CLIENT_DISCONNECTING_STATE) public void testDisconnectingState_ReceiveDisconnectMsg_DeferMessage() { // case DISCONNECT: initToDisconnectingState(); sendMessage(HeadsetClientStateMachine.DISCONNECT); assertThat( mHeadsetClientStateMachine.doesSuperHaveDeferredMessages( HeadsetClientStateMachine.DISCONNECT)) .isTrue(); assertThat(mHeadsetClientStateMachine.getCurrentState()) .isInstanceOf(HeadsetClientStateMachine.Disconnecting.class); } @Test @EnableFlags(Flags.FLAG_HFP_CLIENT_DISCONNECTING_STATE) public void testDisconnectingState_ReceiveDisconnectAudioMsg_DeferMessage() { // case DISCONNECT_AUDIO: initToDisconnectingState(); assertThat( mHeadsetClientStateMachine.doesSuperHaveDeferredMessages( HeadsetClientStateMachine.DISCONNECT_AUDIO)) .isFalse(); sendMessage(HeadsetClientStateMachine.DISCONNECT_AUDIO); assertThat( mHeadsetClientStateMachine.doesSuperHaveDeferredMessages( HeadsetClientStateMachine.DISCONNECT_AUDIO)) .isTrue(); assertThat(mHeadsetClientStateMachine.getCurrentState()) .isInstanceOf(HeadsetClientStateMachine.Disconnecting.class); } @Test @EnableFlags(Flags.FLAG_HFP_CLIENT_DISCONNECTING_STATE) public void testDisconnectingState_ReceiveUnknownMsg_NotHandled() { initToDisconnectingState(); sendMessage(HeadsetClientStateMachine.NO_ACTION); assertThat(mHeadsetClientStateMachine.getCurrentState()) .isInstanceOf(HeadsetClientStateMachine.Disconnecting.class); } @Test @EnableFlags(Flags.FLAG_HFP_CLIENT_DISCONNECTING_STATE) public void testAudioOnState_ReceiveDisconnectMsg_DeferMessage() { initToAudioOnState(); assertThat( mHeadsetClientStateMachine.doesSuperHaveDeferredMessages( HeadsetClientStateMachine.DISCONNECT)) .isFalse(); sendMessage(HeadsetClientStateMachine.DISCONNECT, mTestDevice); assertThat( mHeadsetClientStateMachine.doesSuperHaveDeferredMessages( HeadsetClientStateMachine.DISCONNECT)) .isTrue(); assertThat(mHeadsetClientStateMachine.getCurrentState()) .isInstanceOf(HeadsetClientStateMachine.AudioOn.class); } @Test @EnableFlags(Flags.FLAG_HFP_CLIENT_DISCONNECTING_STATE) public void testDisconnectingState_DisconnectingTimeout_TransitionToDisconnected() { initToDisconnectingState(); // Trigger timeout mTestLooper.moveTimeForward(HeadsetClientStateMachine.DISCONNECTING_TIMEOUT_MS); mTestLooper.dispatchAll(); verifySendBroadcastMultiplePermissions(hasExtra(EXTRA_STATE, STATE_DISCONNECTED)); assertThat(mHeadsetClientStateMachine.getCurrentState()) .isInstanceOf(HeadsetClientStateMachine.Disconnected.class); verify(mHeadsetService).updateInbandRinging(eq(mTestDevice), eq(false)); } /** * Allow/disallow connection to any device * Loading Loading @@ -1380,6 +1532,14 @@ public class HeadsetClientStateMachineTest { .isInstanceOf(HeadsetClientStateMachine.AudioOn.class); } private void initToDisconnectingState() { initToConnectedState(); sendMessageAndVerifyTransition( mHeadsetClientStateMachine.obtainMessage( HeadsetClientStateMachine.DISCONNECT, mTestDevice), HeadsetClientStateMachine.Disconnecting.class); } private void verifySendBroadcastMultiplePermissions(Matcher<Intent>... matchers) { mInOrder.verify(mHeadsetClientService) .sendBroadcastMultiplePermissions( Loading