1 package com.android.bluetooth.hfpclient; 2 3 import static com.android.bluetooth.hfpclient.HeadsetClientStateMachine.AT_OK; 4 5 import static org.mockito.Mockito.*; 6 7 import android.bluetooth.BluetoothAdapter; 8 import android.bluetooth.BluetoothDevice; 9 import android.bluetooth.BluetoothHeadsetClient; 10 import android.bluetooth.BluetoothHeadsetClientCall; 11 import android.bluetooth.BluetoothProfile; 12 import android.content.Context; 13 import android.content.Intent; 14 import android.content.res.Resources; 15 import android.media.AudioManager; 16 import android.os.HandlerThread; 17 18 import androidx.test.InstrumentationRegistry; 19 import androidx.test.espresso.intent.matcher.IntentMatchers; 20 import androidx.test.filters.LargeTest; 21 import androidx.test.filters.MediumTest; 22 import androidx.test.filters.SmallTest; 23 import androidx.test.runner.AndroidJUnit4; 24 25 import com.android.bluetooth.R; 26 27 import org.hamcrest.core.AllOf; 28 import org.hamcrest.core.IsInstanceOf; 29 import org.junit.After; 30 import org.junit.Assert; 31 import org.junit.Assume; 32 import org.junit.Before; 33 import org.junit.Test; 34 import org.junit.runner.RunWith; 35 import org.mockito.ArgumentCaptor; 36 import org.mockito.Mock; 37 import org.mockito.MockitoAnnotations; 38 import org.mockito.hamcrest.MockitoHamcrest; 39 40 @LargeTest 41 @RunWith(AndroidJUnit4.class) 42 public class HeadsetClientStateMachineTest { 43 private BluetoothAdapter mAdapter; 44 private HandlerThread mHandlerThread; 45 private HeadsetClientStateMachine mHeadsetClientStateMachine; 46 private BluetoothDevice mTestDevice; 47 private Context mTargetContext; 48 49 @Mock 50 private Resources mMockHfpResources; 51 @Mock 52 private HeadsetClientService mHeadsetClientService; 53 @Mock 54 private AudioManager mAudioManager; 55 56 private static final int STANDARD_WAIT_MILLIS = 1000; 57 private static final int QUERY_CURRENT_CALLS_WAIT_MILLIS = 2000; 58 private static final int QUERY_CURRENT_CALLS_TEST_WAIT_MILLIS = QUERY_CURRENT_CALLS_WAIT_MILLIS 59 * 3 / 2; 60 61 @Before setUp()62 public void setUp() { 63 mTargetContext = InstrumentationRegistry.getTargetContext(); 64 Assume.assumeTrue("Ignore test when HeadsetClientService is not enabled", 65 mTargetContext.getResources().getBoolean(R.bool.profile_supported_hfpclient)); 66 // Setup mocks and test assets 67 MockitoAnnotations.initMocks(this); 68 // Set a valid volume 69 when(mAudioManager.getStreamVolume(anyInt())).thenReturn(2); 70 when(mAudioManager.getStreamMaxVolume(anyInt())).thenReturn(10); 71 when(mAudioManager.getStreamMinVolume(anyInt())).thenReturn(1); 72 when(mHeadsetClientService.getAudioManager()).thenReturn( 73 mAudioManager); 74 when(mHeadsetClientService.getResources()).thenReturn(mMockHfpResources); 75 when(mMockHfpResources.getBoolean(R.bool.hfp_clcc_poll_during_call)).thenReturn(true); 76 77 // This line must be called to make sure relevant objects are initialized properly 78 mAdapter = BluetoothAdapter.getDefaultAdapter(); 79 // Get a device for testing 80 mTestDevice = mAdapter.getRemoteDevice("00:01:02:03:04:05"); 81 82 // Setup thread and looper 83 mHandlerThread = new HandlerThread("HeadsetClientStateMachineTestHandlerThread"); 84 mHandlerThread.start(); 85 // Manage looper execution in main test thread explicitly to guarantee timing consistency 86 mHeadsetClientStateMachine = 87 new HeadsetClientStateMachine(mHeadsetClientService, mHandlerThread.getLooper()); 88 mHeadsetClientStateMachine.start(); 89 } 90 91 @After tearDown()92 public void tearDown() { 93 if (!mTargetContext.getResources().getBoolean(R.bool.profile_supported_hfpclient)) { 94 return; 95 } 96 mHeadsetClientStateMachine.doQuit(); 97 mHandlerThread.quit(); 98 } 99 100 /** 101 * Test that default state is disconnected 102 */ 103 @SmallTest 104 @Test testDefaultDisconnectedState()105 public void testDefaultDisconnectedState() { 106 Assert.assertEquals(mHeadsetClientStateMachine.getConnectionState(null), 107 BluetoothProfile.STATE_DISCONNECTED); 108 } 109 110 /** 111 * Test that an incoming connection with low priority is rejected 112 */ 113 @MediumTest 114 @Test testIncomingPriorityReject()115 public void testIncomingPriorityReject() { 116 // Return false for priority. 117 when(mHeadsetClientService.getPriority(any(BluetoothDevice.class))).thenReturn( 118 BluetoothProfile.PRIORITY_OFF); 119 120 // Inject an event for when incoming connection is requested 121 StackEvent connStCh = new StackEvent(StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED); 122 connStCh.valueInt = HeadsetClientHalConstants.CONNECTION_STATE_CONNECTED; 123 connStCh.device = mTestDevice; 124 mHeadsetClientStateMachine.sendMessage(StackEvent.STACK_EVENT, connStCh); 125 126 // Verify that only DISCONNECTED -> DISCONNECTED broadcast is fired 127 verify(mHeadsetClientService, timeout(STANDARD_WAIT_MILLIS)).sendBroadcast(MockitoHamcrest 128 .argThat( 129 AllOf.allOf(IntentMatchers.hasAction( 130 BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED), 131 IntentMatchers.hasExtra(BluetoothProfile.EXTRA_STATE, 132 BluetoothProfile.STATE_DISCONNECTED), 133 IntentMatchers.hasExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, 134 BluetoothProfile.STATE_DISCONNECTED))), anyString()); 135 // Check we are in disconnected state still. 136 Assert.assertThat(mHeadsetClientStateMachine.getCurrentState(), 137 IsInstanceOf.instanceOf(HeadsetClientStateMachine.Disconnected.class)); 138 } 139 140 /** 141 * Test that an incoming connection with high priority is accepted 142 */ 143 @MediumTest 144 @Test testIncomingPriorityAccept()145 public void testIncomingPriorityAccept() { 146 // Return true for priority. 147 when(mHeadsetClientService.getPriority(any(BluetoothDevice.class))).thenReturn( 148 BluetoothProfile.PRIORITY_ON); 149 150 // Inject an event for when incoming connection is requested 151 StackEvent connStCh = new StackEvent(StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED); 152 connStCh.valueInt = HeadsetClientHalConstants.CONNECTION_STATE_CONNECTED; 153 connStCh.device = mTestDevice; 154 mHeadsetClientStateMachine.sendMessage(StackEvent.STACK_EVENT, connStCh); 155 156 // Verify that one connection state broadcast is executed 157 ArgumentCaptor<Intent> intentArgument1 = ArgumentCaptor.forClass(Intent.class); 158 verify(mHeadsetClientService, timeout(STANDARD_WAIT_MILLIS)).sendBroadcast(intentArgument1 159 .capture(), 160 anyString()); 161 Assert.assertEquals(BluetoothProfile.STATE_CONNECTING, 162 intentArgument1.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1)); 163 164 // Check we are in connecting state now. 165 Assert.assertThat(mHeadsetClientStateMachine.getCurrentState(), 166 IsInstanceOf.instanceOf(HeadsetClientStateMachine.Connecting.class)); 167 168 // Send a message to trigger SLC connection 169 StackEvent slcEvent = new StackEvent(StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED); 170 slcEvent.valueInt = HeadsetClientHalConstants.CONNECTION_STATE_SLC_CONNECTED; 171 slcEvent.valueInt2 = HeadsetClientHalConstants.PEER_FEAT_ECS; 172 slcEvent.device = mTestDevice; 173 mHeadsetClientStateMachine.sendMessage(StackEvent.STACK_EVENT, slcEvent); 174 175 // Verify that one connection state broadcast is executed 176 ArgumentCaptor<Intent> intentArgument2 = ArgumentCaptor.forClass(Intent.class); 177 verify(mHeadsetClientService, timeout(STANDARD_WAIT_MILLIS).times(2)).sendBroadcast( 178 intentArgument2.capture(), anyString()); 179 Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, 180 intentArgument2.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1)); 181 // Check we are in connecting state now. 182 Assert.assertThat(mHeadsetClientStateMachine.getCurrentState(), 183 IsInstanceOf.instanceOf(HeadsetClientStateMachine.Connected.class)); 184 } 185 186 /** 187 * Test that an incoming connection that times out 188 */ 189 @MediumTest 190 @Test testIncomingTimeout()191 public void testIncomingTimeout() { 192 // Return true for priority. 193 when(mHeadsetClientService.getPriority(any(BluetoothDevice.class))).thenReturn( 194 BluetoothProfile.PRIORITY_ON); 195 196 // Inject an event for when incoming connection is requested 197 StackEvent connStCh = new StackEvent(StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED); 198 connStCh.valueInt = HeadsetClientHalConstants.CONNECTION_STATE_CONNECTED; 199 connStCh.device = mTestDevice; 200 mHeadsetClientStateMachine.sendMessage(StackEvent.STACK_EVENT, connStCh); 201 202 // Verify that one connection state broadcast is executed 203 ArgumentCaptor<Intent> intentArgument1 = ArgumentCaptor.forClass(Intent.class); 204 verify(mHeadsetClientService, timeout(STANDARD_WAIT_MILLIS)).sendBroadcast(intentArgument1 205 .capture(), 206 anyString()); 207 Assert.assertEquals(BluetoothProfile.STATE_CONNECTING, 208 intentArgument1.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1)); 209 210 // Check we are in connecting state now. 211 Assert.assertThat(mHeadsetClientStateMachine.getCurrentState(), 212 IsInstanceOf.instanceOf(HeadsetClientStateMachine.Connecting.class)); 213 214 // Verify that one connection state broadcast is executed 215 ArgumentCaptor<Intent> intentArgument2 = ArgumentCaptor.forClass(Intent.class); 216 verify(mHeadsetClientService, 217 timeout(HeadsetClientStateMachine.CONNECTING_TIMEOUT_MS * 2).times( 218 2)).sendBroadcast(intentArgument2.capture(), anyString()); 219 Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED, 220 intentArgument2.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1)); 221 222 // Check we are in connecting state now. 223 Assert.assertThat(mHeadsetClientStateMachine.getCurrentState(), 224 IsInstanceOf.instanceOf(HeadsetClientStateMachine.Disconnected.class)); 225 } 226 227 /** 228 * Test that In Band Ringtone information is relayed from phone. 229 */ 230 @LargeTest 231 @Test testInBandRingtone()232 public void testInBandRingtone() { 233 // Return true for priority. 234 when(mHeadsetClientService.getPriority(any(BluetoothDevice.class))).thenReturn( 235 BluetoothProfile.PRIORITY_ON); 236 237 Assert.assertEquals(false, mHeadsetClientStateMachine.getInBandRing()); 238 239 // Inject an event for when incoming connection is requested 240 StackEvent connStCh = new StackEvent(StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED); 241 connStCh.valueInt = HeadsetClientHalConstants.CONNECTION_STATE_CONNECTED; 242 connStCh.device = mTestDevice; 243 mHeadsetClientStateMachine.sendMessage(StackEvent.STACK_EVENT, connStCh); 244 245 // Verify that one connection state broadcast is executed 246 ArgumentCaptor<Intent> intentArgument = ArgumentCaptor.forClass(Intent.class); 247 verify(mHeadsetClientService, timeout(STANDARD_WAIT_MILLIS)).sendBroadcast(intentArgument 248 .capture(), 249 anyString()); 250 Assert.assertEquals(BluetoothProfile.STATE_CONNECTING, 251 intentArgument.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1)); 252 253 // Send a message to trigger SLC connection 254 StackEvent slcEvent = new StackEvent(StackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED); 255 slcEvent.valueInt = HeadsetClientHalConstants.CONNECTION_STATE_SLC_CONNECTED; 256 slcEvent.valueInt2 = HeadsetClientHalConstants.PEER_FEAT_ECS; 257 slcEvent.device = mTestDevice; 258 mHeadsetClientStateMachine.sendMessage(StackEvent.STACK_EVENT, slcEvent); 259 260 verify(mHeadsetClientService, timeout(STANDARD_WAIT_MILLIS).times(2)).sendBroadcast( 261 intentArgument.capture(), 262 anyString()); 263 264 Assert.assertEquals(BluetoothProfile.STATE_CONNECTED, 265 intentArgument.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1)); 266 267 StackEvent event = new StackEvent(StackEvent.EVENT_TYPE_IN_BAND_RINGTONE); 268 event.valueInt = 0; 269 event.device = mTestDevice; 270 271 // Enable In Band Ring and verify state gets propagated. 272 StackEvent eventInBandRing = new StackEvent(StackEvent.EVENT_TYPE_IN_BAND_RINGTONE); 273 eventInBandRing.valueInt = 1; 274 eventInBandRing.device = mTestDevice; 275 mHeadsetClientStateMachine.sendMessage(StackEvent.STACK_EVENT, eventInBandRing); 276 verify(mHeadsetClientService, timeout(STANDARD_WAIT_MILLIS).times(3)).sendBroadcast( 277 intentArgument.capture(), 278 anyString()); 279 Assert.assertEquals(1, 280 intentArgument.getValue().getIntExtra(BluetoothHeadsetClient.EXTRA_IN_BAND_RING, 281 -1)); 282 Assert.assertEquals(true, mHeadsetClientStateMachine.getInBandRing()); 283 284 285 // Simulate a new incoming phone call 286 StackEvent eventCallStatusUpdated = new StackEvent(StackEvent.EVENT_TYPE_CLIP); 287 mHeadsetClientStateMachine.sendMessage(StackEvent.STACK_EVENT, eventCallStatusUpdated); 288 verify(mHeadsetClientService, timeout(STANDARD_WAIT_MILLIS).times(3)).sendBroadcast( 289 intentArgument.capture(), 290 anyString()); 291 292 // Provide information about the new call 293 StackEvent eventIncomingCall = new StackEvent(StackEvent.EVENT_TYPE_CURRENT_CALLS); 294 eventIncomingCall.valueInt = 1; //index 295 eventIncomingCall.valueInt2 = 1; //direction 296 eventIncomingCall.valueInt3 = 4; //state 297 eventIncomingCall.valueInt4 = 0; //multi party 298 eventIncomingCall.valueString = "5551212"; //phone number 299 eventIncomingCall.device = mTestDevice; 300 301 mHeadsetClientStateMachine.sendMessage(StackEvent.STACK_EVENT, eventIncomingCall); 302 verify(mHeadsetClientService, timeout(STANDARD_WAIT_MILLIS).times(3)).sendBroadcast( 303 intentArgument.capture(), 304 anyString()); 305 306 307 // Signal that the complete list of calls was received. 308 StackEvent eventCommandStatus = new StackEvent(StackEvent.EVENT_TYPE_CMD_RESULT); 309 eventCommandStatus.valueInt = AT_OK; 310 mHeadsetClientStateMachine.sendMessage(StackEvent.STACK_EVENT, eventCommandStatus); 311 verify(mHeadsetClientService, timeout(QUERY_CURRENT_CALLS_TEST_WAIT_MILLIS).times(4)) 312 .sendBroadcast( 313 intentArgument.capture(), 314 anyString()); 315 316 // Verify that the new call is being registered with the inBandRing flag set. 317 Assert.assertEquals(true, 318 ((BluetoothHeadsetClientCall) intentArgument.getValue().getParcelableExtra( 319 BluetoothHeadsetClient.EXTRA_CALL)).isInBandRing()); 320 321 // Disable In Band Ring and verify state gets propagated. 322 eventInBandRing.valueInt = 0; 323 mHeadsetClientStateMachine.sendMessage(StackEvent.STACK_EVENT, eventInBandRing); 324 verify(mHeadsetClientService, timeout(STANDARD_WAIT_MILLIS).times(5)).sendBroadcast( 325 intentArgument.capture(), 326 anyString()); 327 Assert.assertEquals(0, 328 intentArgument.getValue().getIntExtra(BluetoothHeadsetClient.EXTRA_IN_BAND_RING, 329 -1)); 330 Assert.assertEquals(false, mHeadsetClientStateMachine.getInBandRing()); 331 332 } 333 } 334