Merge "AbsoluteVolume change custom call to official API"
diff --git a/src/com/android/bluetooth/a2dpsink/A2dpSinkService.java b/src/com/android/bluetooth/a2dpsink/A2dpSinkService.java
index db7f67e..fc54444 100644
--- a/src/com/android/bluetooth/a2dpsink/A2dpSinkService.java
+++ b/src/com/android/bluetooth/a2dpsink/A2dpSinkService.java
@@ -353,7 +353,15 @@
         return true;
     }
 
-    void removeStateMachine(A2dpSinkStateMachine stateMachine) {
+    /**
+     * Remove a device's state machine.
+     *
+     * Called by the state machines when they disconnect.
+     *
+     * Visible for testing so it can be mocked and verified on.
+     */
+    @VisibleForTesting
+    public void removeStateMachine(A2dpSinkStateMachine stateMachine) {
         mDeviceStateMap.remove(stateMachine.getDevice());
     }
 
diff --git a/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachine.java b/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachine.java
index 85ebf2b..921753c 100644
--- a/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachine.java
+++ b/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachine.java
@@ -86,10 +86,6 @@
         setInitialState(mDisconnected);
     }
 
-    protected String getConnectionStateChangedIntent() {
-        return BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED;
-    }
-
     /**
      * Get the current connection state
      *
diff --git a/src/com/android/bluetooth/hfp/HeadsetStateMachine.java b/src/com/android/bluetooth/hfp/HeadsetStateMachine.java
index fe3f98a..cddb9cc 100644
--- a/src/com/android/bluetooth/hfp/HeadsetStateMachine.java
+++ b/src/com/android/bluetooth/hfp/HeadsetStateMachine.java
@@ -114,6 +114,10 @@
     // NOTE: the value is not "final" - it is modified in the unit tests
     @VisibleForTesting static int sConnectTimeoutMs = 30000;
 
+    // Number of times we should retry disconnecting audio before
+    // disconnecting the device.
+    private static final int MAX_RETRY_DISCONNECT_AUDIO = 3;
+
     private static final HeadsetAgIndicatorEnableState DEFAULT_AG_INDICATOR_ENABLE_STATE =
             new HeadsetAgIndicatorEnableState(true, true, true, true);
 
@@ -149,6 +153,8 @@
     private final AtPhonebook mPhonebook;
     // HSP specific
     private boolean mNeedDialingOutReply;
+    // Audio disconnect timeout retry count
+    private int mAudioDisconnectRetry = 0;
 
     // Keys are AT commands, and values are the company IDs.
     private static final Map<String, Integer> VENDOR_SPECIFIC_AT_COMMAND_COMPANY_ID;
@@ -1042,6 +1048,10 @@
                 // state. This is to prevent auto connect attempts from disconnecting
                 // devices that previously successfully connected.
                 removeDeferredMessages(CONNECT);
+            } else if (mPrevState == mAudioDisconnecting) {
+                // Reset audio disconnecting retry count. Either the disconnection was successful
+                // or the retry count reached MAX_RETRY_DISCONNECT_AUDIO.
+                mAudioDisconnectRetry = 0;
             }
             broadcastStateTransitions();
         }
@@ -1283,7 +1293,7 @@
                         stateLogW("CONNECT_AUDIO device is not connected " + device);
                         break;
                     }
-                    stateLogW("CONNECT_AUDIO device auido is already connected " + device);
+                    stateLogW("CONNECT_AUDIO device audio is already connected " + device);
                     break;
                 }
                 case DISCONNECT_AUDIO: {
@@ -1385,8 +1395,18 @@
                         stateLogW("CONNECT_TIMEOUT for unknown device " + device);
                         break;
                     }
-                    stateLogW("CONNECT_TIMEOUT");
-                    transitionTo(mConnected);
+                    if (mAudioDisconnectRetry == MAX_RETRY_DISCONNECT_AUDIO) {
+                        stateLogW("CONNECT_TIMEOUT: Disconnecting device");
+                        // Restoring state to Connected with message DISCONNECT
+                        deferMessage(obtainMessage(DISCONNECT, mDevice));
+                        transitionTo(mConnected);
+                    } else {
+                        mAudioDisconnectRetry += 1;
+                        stateLogW("CONNECT_TIMEOUT: retrying "
+                                + (MAX_RETRY_DISCONNECT_AUDIO - mAudioDisconnectRetry)
+                                + " more time(s)");
+                        transitionTo(mAudioOn);
+                    }
                     break;
                 }
                 default:
@@ -1407,6 +1427,8 @@
                     break;
                 case HeadsetHalConstants.AUDIO_STATE_CONNECTED:
                     stateLogW("processAudioEvent: audio disconnection failed");
+                    // Audio connected, resetting disconnect retry.
+                    mAudioDisconnectRetry = 0;
                     transitionTo(mAudioOn);
                     break;
                 case HeadsetHalConstants.AUDIO_STATE_CONNECTING:
diff --git a/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachineTest.java b/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachineTest.java
new file mode 100644
index 0000000..74d3e3c
--- /dev/null
+++ b/tests/unit/src/com/android/bluetooth/a2dpsink/A2dpSinkStateMachineTest.java
@@ -0,0 +1,370 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.bluetooth.a2dpsink;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothAudioConfig;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.media.AudioFormat;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.bluetooth.R;
+import com.android.bluetooth.TestUtils;
+
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+public class A2dpSinkStateMachineTest {
+    private Context mTargetContext;
+
+    private BluetoothAdapter mAdapter;
+    private BluetoothDevice mDevice;
+    private final String mDeviceAddress = "11:11:11:11:11:11";
+    @Mock private A2dpSinkService mService;
+    @Mock private A2dpSinkNativeInterface mNativeInterface;
+
+    A2dpSinkStateMachine mStateMachine;
+    private static final int TIMEOUT_MS = 1000;
+    private static final int CONNECT_TIMEOUT_MS = 6000;
+    private static final int UNHANDLED_MESSAGE = 9999;
+
+    @Before
+    public void setUp() throws Exception {
+        mTargetContext = InstrumentationRegistry.getTargetContext();
+        Assume.assumeTrue("Ignore test when A2dpSinkService is not enabled",
+                mTargetContext.getResources().getBoolean(R.bool.profile_supported_a2dp_sink));
+        MockitoAnnotations.initMocks(this);
+
+        mAdapter = BluetoothAdapter.getDefaultAdapter();
+        assertThat(mAdapter).isNotNull();
+        mDevice = mAdapter.getRemoteDevice(mDeviceAddress);
+
+        doNothing().when(mService).removeStateMachine(any(A2dpSinkStateMachine.class));
+
+        mStateMachine = new A2dpSinkStateMachine(mDevice, mService, mNativeInterface);
+        mStateMachine.start();
+        assertThat(mStateMachine.getDevice()).isEqualTo(mDevice);
+        assertThat(mStateMachine.getAudioConfig()).isNull();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (!mTargetContext.getResources().getBoolean(R.bool.profile_supported_a2dp_sink)) {
+            return;
+        }
+        mStateMachine = null;
+        mDevice = null;
+        mAdapter = null;
+    }
+
+    private void mockDeviceConnectionPolicy(BluetoothDevice device, int policy) {
+        doReturn(policy).when(mService).getConnectionPolicy(device);
+    }
+
+    private void sendConnectionEvent(int state) {
+        mStateMachine.sendMessage(A2dpSinkStateMachine.STACK_EVENT,
+                StackEvent.connectionStateChanged(mDevice, state));
+    }
+
+    private void sendAudioConfigChangedEvent(int sampleRate, int channelCount) {
+        mStateMachine.sendMessage(A2dpSinkStateMachine.STACK_EVENT,
+                StackEvent.audioConfigChanged(mDevice, sampleRate, channelCount));
+    }
+
+    /**********************************************************************************************
+     * DISCONNECTED STATE TESTS                                                                   *
+     *********************************************************************************************/
+
+    @Test
+    public void testConnectInDisconnected() {
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+        mStateMachine.connect();
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        verify(mNativeInterface, timeout(TIMEOUT_MS).times(1)).connectA2dpSink(mDevice);
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+    }
+
+    @Test
+    public void testDisconnectInDisconnected() {
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+        mStateMachine.disconnect();
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+    }
+
+    @Test
+    public void testAudioConfigChangedInDisconnected() {
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+        sendAudioConfigChangedEvent(44, 1);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+        assertThat(mStateMachine.getAudioConfig()).isNull();
+    }
+
+    @Test
+    public void testIncomingConnectedInDisconnected() {
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+        sendConnectionEvent(BluetoothProfile.STATE_CONNECTED);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+    }
+
+    @Test
+    public void testAllowedIncomingConnectionInDisconnected() {
+        mockDeviceConnectionPolicy(mDevice, BluetoothProfile.CONNECTION_POLICY_ALLOWED);
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+        sendConnectionEvent(BluetoothProfile.STATE_CONNECTING);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+        verify(mNativeInterface, times(0)).connectA2dpSink(mDevice);
+    }
+
+    @Test
+    public void testForbiddenIncomingConnectionInDisconnected() {
+        mockDeviceConnectionPolicy(mDevice, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN);
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+        sendConnectionEvent(BluetoothProfile.STATE_CONNECTING);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        verify(mNativeInterface, times(1)).disconnectA2dpSink(mDevice);
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+    }
+
+    @Test
+    public void testUnknownIncomingConnectionInDisconnected() {
+        mockDeviceConnectionPolicy(mDevice, BluetoothProfile.CONNECTION_POLICY_UNKNOWN);
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+        sendConnectionEvent(BluetoothProfile.STATE_CONNECTING);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+        verify(mNativeInterface, times(0)).connectA2dpSink(mDevice);
+    }
+
+    @Test
+    public void testIncomingDisconnectInDisconnected() {
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+        sendConnectionEvent(BluetoothProfile.STATE_DISCONNECTED);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+        verify(mService, timeout(TIMEOUT_MS).times(1)).removeStateMachine(mStateMachine);
+    }
+
+    @Test
+    public void testIncomingDisconnectingInDisconnected() {
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+        sendConnectionEvent(BluetoothProfile.STATE_DISCONNECTING);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+        verify(mService, times(0)).removeStateMachine(mStateMachine);
+    }
+
+    @Test
+    public void testIncomingConnectingInDisconnected() {
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+        sendConnectionEvent(BluetoothProfile.STATE_CONNECTING);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+    }
+
+    @Test
+    public void testUnhandledMessageInDisconnected() {
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+        mStateMachine.sendMessage(UNHANDLED_MESSAGE);
+        mStateMachine.sendMessage(UNHANDLED_MESSAGE, 0 /* arbitrary payload */);
+    }
+
+    /**********************************************************************************************
+     * CONNECTING STATE TESTS                                                                     *
+     *********************************************************************************************/
+
+    @Test
+    public void testConnectedInConnecting() {
+        testConnectInDisconnected();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+        sendConnectionEvent(BluetoothProfile.STATE_CONNECTED);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+    }
+
+    @Test
+    public void testConnectingInConnecting() {
+        testConnectInDisconnected();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+        sendConnectionEvent(BluetoothProfile.STATE_CONNECTING);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+    }
+
+    @Test
+    public void testDisconnectingInConnecting() {
+        testConnectInDisconnected();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+        sendConnectionEvent(BluetoothProfile.STATE_DISCONNECTING);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+    }
+
+    @Test
+    public void testDisconnectedInConnecting() {
+        testConnectInDisconnected();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+        sendConnectionEvent(BluetoothProfile.STATE_DISCONNECTED);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        verify(mService, timeout(TIMEOUT_MS).times(1)).removeStateMachine(mStateMachine);
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+    }
+
+    @Test
+    public void testConnectionTimeoutInConnecting() {
+        testConnectInDisconnected();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+        verify(mService, timeout(CONNECT_TIMEOUT_MS).times(1)).removeStateMachine(mStateMachine);
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+    }
+
+    @Test
+    public void testAudioStateChangeInConnecting() {
+        testConnectInDisconnected();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+        sendAudioConfigChangedEvent(44, 1);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+        assertThat(mStateMachine.getAudioConfig()).isNull();
+    }
+
+    @Test
+    public void testConnectInConnecting() {
+        testConnectInDisconnected();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+        mStateMachine.connect();
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+    }
+
+    @Test
+    public void testDisconnectInConnecting() {
+        testConnectInDisconnected();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+        mStateMachine.disconnect();
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTING);
+    }
+
+    /**********************************************************************************************
+     * CONNECTED STATE TESTS                                                                      *
+     *********************************************************************************************/
+
+    @Test
+    public void testConnectInConnected() {
+        testConnectedInConnecting();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+        mStateMachine.connect();
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+    }
+
+    @Test
+    public void testDisconnectInConnected() {
+        testConnectedInConnecting();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+        mStateMachine.disconnect();
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        verify(mNativeInterface, times(1)).disconnectA2dpSink(mDevice);
+        verify(mService, timeout(TIMEOUT_MS).times(1)).removeStateMachine(mStateMachine);
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+    }
+
+    @Test
+    public void testAudioStateChangeInConnected() {
+        testConnectedInConnecting();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+        sendAudioConfigChangedEvent(44, 1);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+        BluetoothAudioConfig expected =
+                new BluetoothAudioConfig(44, 1, AudioFormat.ENCODING_PCM_16BIT);
+        BluetoothAudioConfig config = mStateMachine.getAudioConfig();
+        assertThat(config).isEqualTo(expected);
+    }
+
+    @Test
+    public void testConnectedInConnected() {
+        testConnectedInConnecting();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+        sendConnectionEvent(BluetoothProfile.STATE_CONNECTED);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+    }
+
+    @Test
+    public void testConnectingInConnected() {
+        testConnectedInConnecting();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+        sendConnectionEvent(BluetoothProfile.STATE_CONNECTING);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+    }
+
+    @Test
+    public void testDisconnectingInConnected() {
+        testConnectedInConnecting();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+        sendConnectionEvent(BluetoothProfile.STATE_DISCONNECTING);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        verify(mService, timeout(TIMEOUT_MS).times(1)).removeStateMachine(mStateMachine);
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+    }
+
+    @Test
+    public void testDisconnectedInConnected() {
+        testConnectedInConnecting();
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_CONNECTED);
+        sendConnectionEvent(BluetoothProfile.STATE_DISCONNECTED);
+        TestUtils.waitForLooperToFinishScheduledTask(mStateMachine.getHandler().getLooper());
+        verify(mService, timeout(TIMEOUT_MS).times(1)).removeStateMachine(mStateMachine);
+        assertThat(mStateMachine.getState()).isEqualTo(BluetoothProfile.STATE_DISCONNECTED);
+    }
+
+    /**********************************************************************************************
+     * OTHER TESTS                                                                                *
+     *********************************************************************************************/
+
+    @Test
+    public void testDump() {
+        StringBuilder sb = new StringBuilder();
+        mStateMachine.dump(sb);
+        assertThat(sb.toString()).isNotNull();
+    }
+}
diff --git a/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java b/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java
index 34d228d..fde7dd6 100644
--- a/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java
+++ b/tests/unit/src/com/android/bluetooth/hfp/HeadsetStateMachineTest.java
@@ -17,6 +17,7 @@
 package com.android.bluetooth.hfp;
 
 import static android.Manifest.permission.BLUETOOTH_CONNECT;
+
 import static org.mockito.Mockito.*;
 
 import android.bluetooth.BluetoothAdapter;
@@ -69,6 +70,7 @@
     private static final int CONNECT_TIMEOUT_TEST_WAIT_MILLIS = CONNECT_TIMEOUT_TEST_MILLIS * 3 / 2;
     private static final int ASYNC_CALL_TIMEOUT_MILLIS = 250;
     private static final String TEST_PHONE_NUMBER = "1234567890";
+    private static final int MAX_RETRY_DISCONNECT_AUDIO = 3;
     private Context mTargetContext;
     private BluetoothAdapter mAdapter;
     private HandlerThread mHandlerThread;
@@ -742,23 +744,50 @@
     }
 
     /**
-     * Test state transition from AudioDisconnecting to Connected state via
-     * CONNECT_TIMEOUT message
+     * Test state transition from AudioDisconnecting to AudioOn state via CONNECT_TIMEOUT message
+     * until retry count is reached, then test transition to Disconnecting state.
      */
     @Test
-    public void testStateTransition_AudioDisconnectingToConnected_Timeout() {
+    public void testStateTransition_AudioDisconnectingToAudioOnAndDisconnecting_Timeout() {
         int numBroadcastsSent = setUpAudioDisconnectingState();
         // Wait for connection to timeout
         numBroadcastsSent++;
-        verify(mHeadsetService, timeout(CONNECT_TIMEOUT_TEST_WAIT_MILLIS).times(
-                numBroadcastsSent)).sendBroadcastAsUser(mIntentArgument.capture(),
-                eq(UserHandle.ALL), eq(BLUETOOTH_CONNECT),
-                any(Bundle.class));
-        HeadsetTestUtils.verifyAudioStateBroadcast(mTestDevice,
-                BluetoothHeadset.STATE_AUDIO_DISCONNECTED, BluetoothHeadset.STATE_AUDIO_CONNECTED,
-                mIntentArgument.getValue());
-        Assert.assertThat(mHeadsetStateMachine.getCurrentState(),
-                IsInstanceOf.instanceOf(HeadsetStateMachine.Connected.class));
+        for (int i = 0; i <= MAX_RETRY_DISCONNECT_AUDIO; i++) {
+            if (i > 0) { // Skip first AUDIO_DISCONNECTING init as it was setup before the loop
+                mHeadsetStateMachine.sendMessage(HeadsetStateMachine.DISCONNECT_AUDIO, mTestDevice);
+                // No new broadcast due to lack of AUDIO_DISCONNECTING intent variable
+                verify(mHeadsetService, after(ASYNC_CALL_TIMEOUT_MILLIS)
+                        .times(numBroadcastsSent)).sendBroadcastAsUser(
+                        any(Intent.class), eq(UserHandle.ALL), eq(BLUETOOTH_CONNECT),
+                        any(Bundle.class));
+                Assert.assertThat(mHeadsetStateMachine.getCurrentState(),
+                        IsInstanceOf.instanceOf(HeadsetStateMachine.AudioDisconnecting.class));
+                if (i == MAX_RETRY_DISCONNECT_AUDIO) {
+                    // Increment twice numBroadcastsSent as DISCONNECT message is added on max retry
+                    numBroadcastsSent += 2;
+                } else {
+                    numBroadcastsSent++;
+                }
+            }
+            verify(mHeadsetService, timeout(CONNECT_TIMEOUT_TEST_WAIT_MILLIS).times(
+                    numBroadcastsSent)).sendBroadcastAsUser(mIntentArgument.capture(),
+                    eq(UserHandle.ALL), eq(BLUETOOTH_CONNECT), any(Bundle.class));
+            if (i < MAX_RETRY_DISCONNECT_AUDIO) { // Test if state is AudioOn before max retry
+                HeadsetTestUtils.verifyAudioStateBroadcast(mTestDevice,
+                        BluetoothHeadset.STATE_AUDIO_CONNECTED,
+                        BluetoothHeadset.STATE_AUDIO_CONNECTED,
+                        mIntentArgument.getValue());
+                Assert.assertThat(mHeadsetStateMachine.getCurrentState(),
+                        IsInstanceOf.instanceOf(HeadsetStateMachine.AudioOn.class));
+            } else { // Max retry count reached, test Disconnecting state
+                HeadsetTestUtils.verifyConnectionStateBroadcast(mTestDevice,
+                        BluetoothHeadset.STATE_DISCONNECTING,
+                        BluetoothHeadset.STATE_CONNECTED,
+                        mIntentArgument.getValue());
+                Assert.assertThat(mHeadsetStateMachine.getCurrentState(),
+                        IsInstanceOf.instanceOf(HeadsetStateMachine.Disconnecting.class));
+            }
+        }
     }
 
     /**