1 /* 2 * Copyright 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.bluetooth.a2dp; 18 19 import static org.mockito.Mockito.*; 20 21 import android.bluetooth.BluetoothA2dp; 22 import android.bluetooth.BluetoothAdapter; 23 import android.bluetooth.BluetoothCodecConfig; 24 import android.bluetooth.BluetoothCodecStatus; 25 import android.bluetooth.BluetoothDevice; 26 import android.bluetooth.BluetoothProfile; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.os.Bundle; 30 import android.os.HandlerThread; 31 32 import androidx.test.InstrumentationRegistry; 33 import androidx.test.filters.MediumTest; 34 import androidx.test.runner.AndroidJUnit4; 35 36 import com.android.bluetooth.R; 37 import com.android.bluetooth.TestUtils; 38 import com.android.bluetooth.btservice.AdapterService; 39 40 import org.hamcrest.core.IsInstanceOf; 41 import org.junit.After; 42 import org.junit.Assert; 43 import org.junit.Assume; 44 import org.junit.Before; 45 import org.junit.Test; 46 import org.junit.runner.RunWith; 47 import org.mockito.ArgumentCaptor; 48 import org.mockito.Mock; 49 import org.mockito.MockitoAnnotations; 50 51 @MediumTest 52 @RunWith(AndroidJUnit4.class) 53 public class A2dpStateMachineTest { 54 private BluetoothAdapter mAdapter; 55 private Context mTargetContext; 56 private HandlerThread mHandlerThread; 57 private A2dpStateMachine mA2dpStateMachine; 58 private BluetoothDevice mTestDevice; 59 private static final int TIMEOUT_MS = 1000; // 1s 60 61 private BluetoothCodecConfig mCodecConfigSbc; 62 private BluetoothCodecConfig mCodecConfigAac; 63 64 @Mock private AdapterService mAdapterService; 65 @Mock private A2dpService mA2dpService; 66 @Mock private A2dpNativeInterface mA2dpNativeInterface; 67 68 @Before setUp()69 public void setUp() throws Exception { 70 mTargetContext = InstrumentationRegistry.getTargetContext(); 71 Assume.assumeTrue("Ignore test when A2dpService is not enabled", 72 mTargetContext.getResources().getBoolean(R.bool.profile_supported_a2dp)); 73 // Set up mocks and test assets 74 MockitoAnnotations.initMocks(this); 75 TestUtils.setAdapterService(mAdapterService); 76 77 mAdapter = BluetoothAdapter.getDefaultAdapter(); 78 79 // Get a device for testing 80 mTestDevice = mAdapter.getRemoteDevice("00:01:02:03:04:05"); 81 82 // Set up sample codec config 83 mCodecConfigSbc = new BluetoothCodecConfig( 84 BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC, 85 BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT, 86 BluetoothCodecConfig.SAMPLE_RATE_44100, 87 BluetoothCodecConfig.BITS_PER_SAMPLE_16, 88 BluetoothCodecConfig.CHANNEL_MODE_STEREO, 89 0, 0, 0, 0); // Codec-specific fields 90 mCodecConfigAac = new BluetoothCodecConfig( 91 BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC, 92 BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT, 93 BluetoothCodecConfig.SAMPLE_RATE_48000, 94 BluetoothCodecConfig.BITS_PER_SAMPLE_16, 95 BluetoothCodecConfig.CHANNEL_MODE_STEREO, 96 0, 0, 0, 0); // Codec-specific fields 97 98 // Set up thread and looper 99 mHandlerThread = new HandlerThread("A2dpStateMachineTestHandlerThread"); 100 mHandlerThread.start(); 101 mA2dpStateMachine = new A2dpStateMachine(mTestDevice, mA2dpService, 102 mA2dpNativeInterface, mHandlerThread.getLooper()); 103 // Override the timeout value to speed up the test 104 A2dpStateMachine.sConnectTimeoutMs = 1000; // 1s 105 mA2dpStateMachine.start(); 106 } 107 108 @After tearDown()109 public void tearDown() throws Exception { 110 if (!mTargetContext.getResources().getBoolean(R.bool.profile_supported_a2dp)) { 111 return; 112 } 113 mA2dpStateMachine.doQuit(); 114 mHandlerThread.quit(); 115 TestUtils.clearAdapterService(mAdapterService); 116 } 117 118 /** 119 * Test that default state is disconnected 120 */ 121 @Test testDefaultDisconnectedState()122 public void testDefaultDisconnectedState() { 123 Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED, 124 mA2dpStateMachine.getConnectionState()); 125 } 126 127 /** 128 * Allow/disallow connection to any device. 129 * 130 * @param allow if true, connection is allowed 131 */ allowConnection(boolean allow)132 private void allowConnection(boolean allow) { 133 doReturn(allow).when(mA2dpService).okToConnect(any(BluetoothDevice.class), 134 anyBoolean()); 135 } 136 137 /** 138 * Test that an incoming connection with low priority is rejected 139 */ 140 @Test testIncomingPriorityReject()141 public void testIncomingPriorityReject() { 142 allowConnection(false); 143 144 // Inject an event for when incoming connection is requested 145 A2dpStackEvent connStCh = 146 new A2dpStackEvent(A2dpStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED); 147 connStCh.device = mTestDevice; 148 connStCh.valueInt = A2dpStackEvent.CONNECTION_STATE_CONNECTED; 149 mA2dpStateMachine.sendMessage(A2dpStateMachine.STACK_EVENT, connStCh); 150 151 // Verify that no connection state broadcast is executed 152 verify(mA2dpService, after(TIMEOUT_MS).never()).sendBroadcast(any(Intent.class), 153 anyString(), any(Bundle.class)); 154 // Check that we are in Disconnected state 155 Assert.assertThat(mA2dpStateMachine.getCurrentState(), 156 IsInstanceOf.instanceOf(A2dpStateMachine.Disconnected.class)); 157 } 158 159 /** 160 * Test that an incoming connection with high priority is accepted 161 */ 162 @Test testIncomingPriorityAccept()163 public void testIncomingPriorityAccept() { 164 allowConnection(true); 165 166 // Inject an event for when incoming connection is requested 167 A2dpStackEvent connStCh = 168 new A2dpStackEvent(A2dpStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED); 169 connStCh.device = mTestDevice; 170 connStCh.valueInt = A2dpStackEvent.CONNECTION_STATE_CONNECTING; 171 mA2dpStateMachine.sendMessage(A2dpStateMachine.STACK_EVENT, connStCh); 172 173 // Verify that one connection state broadcast is executed 174 ArgumentCaptor<Intent> intentArgument1 = ArgumentCaptor.forClass(Intent.class); 175 verify(mA2dpService, timeout(TIMEOUT_MS).times(1)).sendBroadcast(intentArgument1.capture(), 176 anyString(), any(Bundle.class)); 177 Assert.assertEquals(BluetoothProfile.STATE_CONNECTING, 178 intentArgument1.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1)); 179 180 // Check that we are in Connecting state 181 Assert.assertThat(mA2dpStateMachine.getCurrentState(), 182 IsInstanceOf.instanceOf(A2dpStateMachine.Connecting.class)); 183 184 // Send a message to trigger connection completed 185 A2dpStackEvent connCompletedEvent = 186 new A2dpStackEvent(A2dpStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED); 187 connCompletedEvent.device = mTestDevice; 188 connCompletedEvent.valueInt = A2dpStackEvent.CONNECTION_STATE_CONNECTED; 189 mA2dpStateMachine.sendMessage(A2dpStateMachine.STACK_EVENT, connCompletedEvent); 190 191 // Verify that the expected number of broadcasts are executed: 192 // - two calls to broadcastConnectionState(): Disconnected -> Conecting -> Connected 193 // - one call to broadcastAudioState() when entering Connected state 194 ArgumentCaptor<Intent> intentArgument2 = ArgumentCaptor.forClass(Intent.class); 195 verify(mA2dpService, timeout(TIMEOUT_MS).times(3)).sendBroadcast(intentArgument2.capture(), 196 anyString(), any(Bundle.class)); 197 // Verify that the last broadcast was to change the A2DP playing state 198 // to STATE_NOT_PLAYING 199 Assert.assertEquals(BluetoothA2dp.ACTION_PLAYING_STATE_CHANGED, 200 intentArgument2.getValue().getAction()); 201 Assert.assertEquals(BluetoothA2dp.STATE_NOT_PLAYING, 202 intentArgument2.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1)); 203 // Check that we are in Connected state 204 Assert.assertThat(mA2dpStateMachine.getCurrentState(), 205 IsInstanceOf.instanceOf(A2dpStateMachine.Connected.class)); 206 } 207 208 /** 209 * Test that an outgoing connection times out 210 */ 211 @Test testOutgoingTimeout()212 public void testOutgoingTimeout() { 213 allowConnection(true); 214 doReturn(true).when(mA2dpNativeInterface).connectA2dp(any(BluetoothDevice.class)); 215 doReturn(true).when(mA2dpNativeInterface).disconnectA2dp(any(BluetoothDevice.class)); 216 217 // Send a connect request 218 mA2dpStateMachine.sendMessage(A2dpStateMachine.CONNECT, mTestDevice); 219 220 // Verify that one connection state broadcast is executed 221 ArgumentCaptor<Intent> intentArgument1 = ArgumentCaptor.forClass(Intent.class); 222 verify(mA2dpService, timeout(TIMEOUT_MS).times(1)).sendBroadcast(intentArgument1.capture(), 223 anyString(), any(Bundle.class)); 224 Assert.assertEquals(BluetoothProfile.STATE_CONNECTING, 225 intentArgument1.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1)); 226 227 // Check that we are in Connecting state 228 Assert.assertThat(mA2dpStateMachine.getCurrentState(), 229 IsInstanceOf.instanceOf(A2dpStateMachine.Connecting.class)); 230 231 // Verify that one connection state broadcast is executed 232 ArgumentCaptor<Intent> intentArgument2 = ArgumentCaptor.forClass(Intent.class); 233 verify(mA2dpService, timeout(A2dpStateMachine.sConnectTimeoutMs * 2).times( 234 2)).sendBroadcast(intentArgument2.capture(), anyString(), 235 any(Bundle.class)); 236 Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED, 237 intentArgument2.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1)); 238 239 // Check that we are in Disconnected state 240 Assert.assertThat(mA2dpStateMachine.getCurrentState(), 241 IsInstanceOf.instanceOf(A2dpStateMachine.Disconnected.class)); 242 } 243 244 /** 245 * Test that an incoming connection times out 246 */ 247 @Test testIncomingTimeout()248 public void testIncomingTimeout() { 249 allowConnection(true); 250 doReturn(true).when(mA2dpNativeInterface).connectA2dp(any(BluetoothDevice.class)); 251 doReturn(true).when(mA2dpNativeInterface).disconnectA2dp(any(BluetoothDevice.class)); 252 253 // Inject an event for when incoming connection is requested 254 A2dpStackEvent connStCh = 255 new A2dpStackEvent(A2dpStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED); 256 connStCh.device = mTestDevice; 257 connStCh.valueInt = A2dpStackEvent.CONNECTION_STATE_CONNECTING; 258 mA2dpStateMachine.sendMessage(A2dpStateMachine.STACK_EVENT, connStCh); 259 260 // Verify that one connection state broadcast is executed 261 ArgumentCaptor<Intent> intentArgument1 = ArgumentCaptor.forClass(Intent.class); 262 verify(mA2dpService, timeout(TIMEOUT_MS).times(1)).sendBroadcast(intentArgument1.capture(), 263 anyString(), any(Bundle.class)); 264 Assert.assertEquals(BluetoothProfile.STATE_CONNECTING, 265 intentArgument1.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1)); 266 267 // Check that we are in Connecting state 268 Assert.assertThat(mA2dpStateMachine.getCurrentState(), 269 IsInstanceOf.instanceOf(A2dpStateMachine.Connecting.class)); 270 271 // Verify that one connection state broadcast is executed 272 ArgumentCaptor<Intent> intentArgument2 = ArgumentCaptor.forClass(Intent.class); 273 verify(mA2dpService, timeout(A2dpStateMachine.sConnectTimeoutMs * 2).times( 274 2)).sendBroadcast(intentArgument2.capture(), anyString(), 275 any(Bundle.class)); 276 Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED, 277 intentArgument2.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1)); 278 279 // Check that we are in Disconnected state 280 Assert.assertThat(mA2dpStateMachine.getCurrentState(), 281 IsInstanceOf.instanceOf(A2dpStateMachine.Disconnected.class)); 282 } 283 284 /** 285 * Test that codec config change been reported to A2dpService properly. 286 */ 287 @Test testProcessCodecConfigEvent()288 public void testProcessCodecConfigEvent() { 289 testProcessCodecConfigEventCase(false); 290 } 291 292 /** 293 * Test that codec config change been reported to A2dpService properly when 294 * A2DP hardware offloading is enabled. 295 */ 296 @Test testProcessCodecConfigEvent_OffloadEnabled()297 public void testProcessCodecConfigEvent_OffloadEnabled() { 298 testProcessCodecConfigEventCase(true); 299 } 300 301 /** 302 * Helper methold to test processCodecConfigEvent() 303 */ testProcessCodecConfigEventCase(boolean offloadEnabled)304 public void testProcessCodecConfigEventCase(boolean offloadEnabled) { 305 if (offloadEnabled) { 306 mA2dpStateMachine.mA2dpOffloadEnabled = true; 307 } 308 309 doNothing().when(mA2dpService).codecConfigUpdated(any(BluetoothDevice.class), 310 any(BluetoothCodecStatus.class), anyBoolean()); 311 doNothing().when(mA2dpService).updateOptionalCodecsSupport(any(BluetoothDevice.class)); 312 allowConnection(true); 313 314 BluetoothCodecConfig[] codecsSelectableSbc; 315 codecsSelectableSbc = new BluetoothCodecConfig[1]; 316 codecsSelectableSbc[0] = mCodecConfigSbc; 317 318 BluetoothCodecConfig[] codecsSelectableSbcAac; 319 codecsSelectableSbcAac = new BluetoothCodecConfig[2]; 320 codecsSelectableSbcAac[0] = mCodecConfigSbc; 321 codecsSelectableSbcAac[1] = mCodecConfigAac; 322 323 BluetoothCodecStatus codecStatusSbcAndSbc = new BluetoothCodecStatus(mCodecConfigSbc, 324 codecsSelectableSbcAac, codecsSelectableSbc); 325 BluetoothCodecStatus codecStatusSbcAndSbcAac = new BluetoothCodecStatus(mCodecConfigSbc, 326 codecsSelectableSbcAac, codecsSelectableSbcAac); 327 BluetoothCodecStatus codecStatusAacAndSbcAac = new BluetoothCodecStatus(mCodecConfigAac, 328 codecsSelectableSbcAac, codecsSelectableSbcAac); 329 330 // Set default codec status when device disconnected 331 // Selected codec = SBC, selectable codec = SBC 332 mA2dpStateMachine.processCodecConfigEvent(codecStatusSbcAndSbc); 333 verify(mA2dpService).codecConfigUpdated(mTestDevice, codecStatusSbcAndSbc, false); 334 335 // Inject an event to change state machine to connected state 336 A2dpStackEvent connStCh = 337 new A2dpStackEvent(A2dpStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED); 338 connStCh.device = mTestDevice; 339 connStCh.valueInt = A2dpStackEvent.CONNECTION_STATE_CONNECTED; 340 mA2dpStateMachine.sendMessage(A2dpStateMachine.STACK_EVENT, connStCh); 341 342 // Verify that the expected number of broadcasts are executed: 343 // - two calls to broadcastConnectionState(): Disconnected -> Conecting -> Connected 344 // - one call to broadcastAudioState() when entering Connected state 345 ArgumentCaptor<Intent> intentArgument2 = ArgumentCaptor.forClass(Intent.class); 346 verify(mA2dpService, timeout(TIMEOUT_MS).times(2)).sendBroadcast(intentArgument2.capture(), 347 anyString(), any(Bundle.class)); 348 349 // Verify that state machine update optional codec when enter connected state 350 verify(mA2dpService, times(1)).updateOptionalCodecsSupport(mTestDevice); 351 352 // Change codec status when device connected. 353 // Selected codec = SBC, selectable codec = SBC+AAC 354 mA2dpStateMachine.processCodecConfigEvent(codecStatusSbcAndSbcAac); 355 if (!offloadEnabled) { 356 verify(mA2dpService).codecConfigUpdated(mTestDevice, codecStatusSbcAndSbcAac, true); 357 } 358 verify(mA2dpService, times(2)).updateOptionalCodecsSupport(mTestDevice); 359 360 // Update selected codec with selectable codec unchanged. 361 // Selected codec = AAC, selectable codec = SBC+AAC 362 mA2dpStateMachine.processCodecConfigEvent(codecStatusAacAndSbcAac); 363 verify(mA2dpService).codecConfigUpdated(mTestDevice, codecStatusAacAndSbcAac, false); 364 verify(mA2dpService, times(2)).updateOptionalCodecsSupport(mTestDevice); 365 } 366 } 367