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