Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c1ce667e authored by Aditi Katragadda's avatar Aditi Katragadda
Browse files

Add Disconnecting State to HeadsetClientState Machine

HeadsetClientStateMachine could potentially enter a state where we do
not receive a termination event from remote when attempting to
disconnect a call. This was fixed by adding a Disconnecting state to the state machine and
including a DISCONNECT_TIMEOUT case.

Tag: #stability
Bug: 235893458
Bug: 366041766
Test: atest HeadsetClienStateMachinTest
Flag: com.android.bluetooth.flags.hfp_client_disconnecting_state
Change-Id: Ic8c22f200c180a1e50fc9cfabd687021f5a8a8ed
parent e83fb87c
Loading
Loading
Loading
Loading
+0 −1
Original line number Diff line number Diff line
@@ -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;
+170 −22
Original line number Diff line number Diff line
@@ -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;
@@ -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;
@@ -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;
@@ -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;

@@ -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 + ")";
        }
@@ -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);
    }
@@ -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;
@@ -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()
@@ -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;
        }
    }
@@ -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.
@@ -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;
        }
@@ -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;
@@ -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;
@@ -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(
@@ -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);
                    }
@@ -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;
        }
    }
@@ -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,
@@ -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(
@@ -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);
                    }
@@ -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,
@@ -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;
    }

+163 −3
Original line number Diff line number Diff line
@@ -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;

@@ -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;
@@ -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(
@@ -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 + ")");
    }

@@ -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();
@@ -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));
    }

@@ -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));
    }

@@ -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
     *
@@ -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(