1 /* 2 * Copyright (C) 2023 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 android.bluetooth; 18 19 import static com.google.common.truth.Truth.assertThat; 20 import static com.google.common.truth.Truth8.assertThat; 21 22 import static org.mockito.Mockito.any; 23 import static org.mockito.Mockito.anyInt; 24 import static org.mockito.Mockito.eq; 25 import static org.mockito.Mockito.inOrder; 26 import static org.mockito.Mockito.mock; 27 import static org.mockito.Mockito.mockingDetails; 28 import static org.mockito.Mockito.never; 29 import static org.mockito.Mockito.timeout; 30 import static org.mockito.Mockito.verify; 31 32 import android.bluetooth.le.BluetoothLeScanner; 33 import android.content.Context; 34 import android.platform.test.annotations.RequiresFlagsEnabled; 35 import android.platform.test.flag.junit.CheckFlagsRule; 36 import android.platform.test.flag.junit.DeviceFlagsValueProvider; 37 import android.util.Log; 38 39 import androidx.test.core.app.ApplicationProvider; 40 41 import com.android.bluetooth.flags.Flags; 42 import com.android.compatibility.common.util.AdoptShellPermissionsRule; 43 44 import com.google.protobuf.ByteString; 45 import com.google.testing.junit.testparameterinjector.TestParameter; 46 import com.google.testing.junit.testparameterinjector.TestParameterInjector; 47 48 import org.junit.Assume; 49 import org.junit.ClassRule; 50 import org.junit.Ignore; 51 import org.junit.Rule; 52 import org.junit.Test; 53 import org.junit.runner.RunWith; 54 import org.mockito.InOrder; 55 import org.mockito.invocation.Invocation; 56 57 import pandora.GattProto.AttStatusCode; 58 import pandora.GattProto.GattCharacteristicParams; 59 import pandora.GattProto.GattServiceParams; 60 import pandora.GattProto.IndicateOnCharacteristicRequest; 61 import pandora.GattProto.IndicateOnCharacteristicResponse; 62 import pandora.GattProto.NotifyOnCharacteristicRequest; 63 import pandora.GattProto.NotifyOnCharacteristicResponse; 64 import pandora.GattProto.RegisterServiceRequest; 65 import pandora.HostProto.AdvertiseRequest; 66 import pandora.HostProto.AdvertiseResponse; 67 import pandora.HostProto.OwnAddressType; 68 69 import java.nio.charset.StandardCharsets; 70 import java.util.Collection; 71 import java.util.UUID; 72 73 @RunWith(TestParameterInjector.class) 74 public class GattClientTest { 75 private static final String TAG = "GattClientTest"; 76 private static final int ANDROID_MTU = 517; 77 private static final int MTU_REQUESTED = 23; 78 private static final int ANOTHER_MTU_REQUESTED = 42; 79 private static final String NOTIFICATION_VALUE = "hello world"; 80 81 private static final UUID GAP_UUID = UUID.fromString("00001800-0000-1000-8000-00805f9b34fb"); 82 private static final UUID CCCD_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); 83 84 private static final UUID TEST_SERVICE_UUID = 85 UUID.fromString("00000000-0000-0000-0000-00000000000"); 86 private static final UUID TEST_CHARACTERISTIC_UUID = 87 UUID.fromString("00010001-0000-0000-0000-000000000000"); 88 @ClassRule public static final AdoptShellPermissionsRule PERM = new AdoptShellPermissionsRule(); 89 90 @Rule public final PandoraDevice mBumble = new PandoraDevice(); 91 92 @Rule 93 public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); 94 95 private final Context mContext = ApplicationProvider.getApplicationContext(); 96 private final BluetoothManager mManager = mContext.getSystemService(BluetoothManager.class); 97 private final BluetoothAdapter mAdapter = mManager.getAdapter(); 98 private final BluetoothLeScanner mLeScanner = mAdapter.getBluetoothLeScanner(); 99 100 @Test directConnectGattAfterClose()101 public void directConnectGattAfterClose() throws Exception { 102 advertiseWithBumble(); 103 104 BluetoothDevice device = 105 mAdapter.getRemoteLeDevice( 106 Utils.BUMBLE_RANDOM_ADDRESS, BluetoothDevice.ADDRESS_TYPE_RANDOM); 107 108 BluetoothGattCallback gattCallback = mock(BluetoothGattCallback.class); 109 BluetoothGatt gatt = device.connectGatt(mContext, false, gattCallback); 110 gatt.close(); 111 112 // Save the number of call in the callback to be checked later 113 Collection<Invocation> invocations = mockingDetails(gattCallback).getInvocations(); 114 int numberOfCalls = invocations.size(); 115 116 BluetoothGattCallback gattCallback2 = mock(BluetoothGattCallback.class); 117 BluetoothGatt gatt2 = device.connectGatt(mContext, false, gattCallback2); 118 verify(gattCallback2, timeout(1000)) 119 .onConnectionStateChange(any(), anyInt(), eq(BluetoothProfile.STATE_CONNECTED)); 120 disconnectAndWaitDisconnection(gatt2, gattCallback2); 121 122 // After reconnecting, verify the first callback was not invoked. 123 Collection<Invocation> invocationsAfterSomeTimes = 124 mockingDetails(gattCallback).getInvocations(); 125 int numberOfCallsAfterSomeTimes = invocationsAfterSomeTimes.size(); 126 assertThat(numberOfCallsAfterSomeTimes).isEqualTo(numberOfCalls); 127 } 128 129 @Test fullGattClientLifecycle()130 public void fullGattClientLifecycle() throws Exception { 131 BluetoothGattCallback gattCallback = mock(BluetoothGattCallback.class); 132 BluetoothGatt gatt = connectGattAndWaitConnection(gattCallback); 133 disconnectAndWaitDisconnection(gatt, gattCallback); 134 } 135 136 @Test reconnectExistingClient()137 public void reconnectExistingClient() throws Exception { 138 advertiseWithBumble(); 139 140 BluetoothDevice device = 141 mAdapter.getRemoteLeDevice( 142 Utils.BUMBLE_RANDOM_ADDRESS, BluetoothDevice.ADDRESS_TYPE_RANDOM); 143 BluetoothGattCallback gattCallback = mock(BluetoothGattCallback.class); 144 InOrder inOrder = inOrder(gattCallback); 145 146 BluetoothGatt gatt = device.connectGatt(mContext, false, gattCallback); 147 inOrder.verify(gattCallback, timeout(1000)) 148 .onConnectionStateChange(any(), anyInt(), eq(BluetoothProfile.STATE_CONNECTED)); 149 150 gatt.disconnect(); 151 inOrder.verify(gattCallback, timeout(1000)) 152 .onConnectionStateChange(any(), anyInt(), eq(BluetoothProfile.STATE_DISCONNECTED)); 153 154 gatt.connect(); 155 inOrder.verify(gattCallback, timeout(1000)) 156 .onConnectionStateChange(any(), anyInt(), eq(BluetoothProfile.STATE_CONNECTED)); 157 158 // TODO(323889717): Fix callback being called after gatt.close(). This disconnect shouldn't 159 // be necessary. 160 gatt.disconnect(); 161 inOrder.verify(gattCallback, timeout(1000)) 162 .onConnectionStateChange(any(), anyInt(), eq(BluetoothProfile.STATE_DISCONNECTED)); 163 gatt.close(); 164 } 165 166 @Test clientGattDiscoverServices()167 public void clientGattDiscoverServices() throws Exception { 168 BluetoothGattCallback gattCallback = mock(BluetoothGattCallback.class); 169 BluetoothGatt gatt = connectGattAndWaitConnection(gattCallback); 170 171 try { 172 gatt.discoverServices(); 173 verify(gattCallback, timeout(10000)) 174 .onServicesDiscovered(any(), eq(BluetoothGatt.GATT_SUCCESS)); 175 176 assertThat(gatt.getServices().stream().map(BluetoothGattService::getUuid)) 177 .contains(GAP_UUID); 178 179 } finally { 180 disconnectAndWaitDisconnection(gatt, gattCallback); 181 } 182 } 183 184 @Test clientGattReadCharacteristics()185 public void clientGattReadCharacteristics() throws Exception { 186 BluetoothGattCallback gattCallback = mock(BluetoothGattCallback.class); 187 BluetoothGatt gatt = connectGattAndWaitConnection(gattCallback); 188 189 try { 190 gatt.discoverServices(); 191 verify(gattCallback, timeout(10000)) 192 .onServicesDiscovered(any(), eq(BluetoothGatt.GATT_SUCCESS)); 193 194 BluetoothGattService firstService = gatt.getServices().get(0); 195 196 BluetoothGattCharacteristic firstCharacteristic = 197 firstService.getCharacteristics().get(0); 198 199 gatt.readCharacteristic(firstCharacteristic); 200 201 verify(gattCallback, timeout(5000)).onCharacteristicRead(any(), any(), any(), anyInt()); 202 203 } finally { 204 disconnectAndWaitDisconnection(gatt, gattCallback); 205 } 206 } 207 208 @Test clientGattWriteCharacteristic()209 public void clientGattWriteCharacteristic() throws Exception { 210 registerWritableGattService(); 211 212 BluetoothGattCallback gattCallback = mock(BluetoothGattCallback.class); 213 BluetoothGatt gatt = connectGattAndWaitConnection(gattCallback); 214 215 try { 216 gatt.discoverServices(); 217 verify(gattCallback, timeout(10000)) 218 .onServicesDiscovered(any(), eq(BluetoothGatt.GATT_SUCCESS)); 219 220 BluetoothGattCharacteristic characteristic = 221 gatt.getService(TEST_SERVICE_UUID).getCharacteristic(TEST_CHARACTERISTIC_UUID); 222 223 byte[] newValue = new byte[] {13}; 224 225 gatt.writeCharacteristic( 226 characteristic, newValue, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT); 227 228 verify(gattCallback, timeout(5000)) 229 .onCharacteristicWrite( 230 any(), eq(characteristic), eq(BluetoothGatt.GATT_SUCCESS)); 231 232 } finally { 233 disconnectAndWaitDisconnection(gatt, gattCallback); 234 } 235 } 236 237 @Test clientGattNotifyOrIndicateCharacteristic(@estParameter boolean isIndicate)238 public void clientGattNotifyOrIndicateCharacteristic(@TestParameter boolean isIndicate) 239 throws Exception { 240 registerNotificationIndicationGattService(isIndicate); 241 242 BluetoothGattCallback gattCallback = mock(BluetoothGattCallback.class); 243 BluetoothGatt gatt = connectGattAndWaitConnection(gattCallback); 244 245 try { 246 gatt.discoverServices(); 247 verify(gattCallback, timeout(10000)) 248 .onServicesDiscovered(any(), eq(BluetoothGatt.GATT_SUCCESS)); 249 250 BluetoothGattCharacteristic characteristic = 251 gatt.getService(TEST_SERVICE_UUID).getCharacteristic(TEST_CHARACTERISTIC_UUID); 252 253 BluetoothGattDescriptor descriptor = characteristic.getDescriptor(CCCD_UUID); 254 descriptor.setValue( 255 isIndicate 256 ? BluetoothGattDescriptor.ENABLE_INDICATION_VALUE 257 : BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); 258 assertThat(gatt.writeDescriptor(descriptor)).isTrue(); 259 260 verify(gattCallback, timeout(5000)) 261 .onDescriptorWrite(any(), eq(descriptor), eq(BluetoothGatt.GATT_SUCCESS)); 262 263 gatt.setCharacteristicNotification(characteristic, true); 264 265 if (isIndicate) { 266 Log.i(TAG, "Triggering characteristic indication"); 267 triggerCharacteristicIndication(characteristic.getInstanceId()); 268 } else { 269 Log.i(TAG, "Triggering characteristic notification"); 270 triggerCharacteristicNotification(characteristic.getInstanceId()); 271 } 272 273 verify(gattCallback, timeout(5000)) 274 .onCharacteristicChanged( 275 any(), any(), eq(NOTIFICATION_VALUE.getBytes(StandardCharsets.UTF_8))); 276 277 } finally { 278 disconnectAndWaitDisconnection(gatt, gattCallback); 279 } 280 } 281 282 @Test 283 @RequiresFlagsEnabled(Flags.FLAG_ENUMERATE_GATT_ERRORS) connectTimeout()284 public void connectTimeout() { 285 BluetoothDevice device = 286 mAdapter.getRemoteLeDevice( 287 Utils.BUMBLE_RANDOM_ADDRESS, BluetoothDevice.ADDRESS_TYPE_RANDOM); 288 BluetoothGattCallback gattCallback = mock(BluetoothGattCallback.class); 289 290 // Connecting to a device not advertising results in connection timeout after 30 seconds 291 device.connectGatt(mContext, false, gattCallback); 292 293 verify(gattCallback, timeout(35000)) 294 .onConnectionStateChange( 295 any(), 296 eq(BluetoothGatt.GATT_CONNECTION_TIMEOUT), 297 eq(BluetoothProfile.STATE_DISCONNECTED)); 298 } 299 300 @RequiresFlagsEnabled(Flags.FLAG_GATT_FIX_DEVICE_BUSY) 301 @Test consecutiveWriteCharacteristicFails_thenSuccess()302 public void consecutiveWriteCharacteristicFails_thenSuccess() throws Exception { 303 Assume.assumeTrue(Flags.gattFixDeviceBusy()); 304 305 registerWritableGattService(); 306 307 BluetoothGattCallback gattCallback = mock(BluetoothGattCallback.class); 308 BluetoothGattCallback gattCallback2 = mock(BluetoothGattCallback.class); 309 310 BluetoothGatt gatt = connectGattAndWaitConnection(gattCallback); 311 BluetoothGatt gatt2 = connectGattAndWaitConnection(gattCallback2); 312 313 try { 314 gatt.discoverServices(); 315 gatt2.discoverServices(); 316 verify(gattCallback, timeout(10000)) 317 .onServicesDiscovered(any(), eq(BluetoothGatt.GATT_SUCCESS)); 318 verify(gattCallback2, timeout(10000)) 319 .onServicesDiscovered(any(), eq(BluetoothGatt.GATT_SUCCESS)); 320 321 BluetoothGattCharacteristic characteristic = 322 gatt.getService(TEST_SERVICE_UUID).getCharacteristic(TEST_CHARACTERISTIC_UUID); 323 324 BluetoothGattCharacteristic characteristic2 = 325 gatt2.getService(TEST_SERVICE_UUID).getCharacteristic(TEST_CHARACTERISTIC_UUID); 326 327 byte[] newValue = new byte[] {13}; 328 329 gatt.writeCharacteristic( 330 characteristic, newValue, BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT); 331 332 // TODO: b/324355496 - Make the test consistent when Bumble supports holding a response. 333 // Skip the test if the second write succeeded. 334 Assume.assumeFalse( 335 gatt2.writeCharacteristic( 336 characteristic2, 337 newValue, 338 BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT) 339 == BluetoothStatusCodes.SUCCESS); 340 341 verify(gattCallback, timeout(5000)) 342 .onCharacteristicWrite( 343 any(), eq(characteristic), eq(BluetoothGatt.GATT_SUCCESS)); 344 verify(gattCallback2, never()) 345 .onCharacteristicWrite( 346 any(), eq(characteristic), eq(BluetoothGatt.GATT_SUCCESS)); 347 348 assertThat( 349 gatt2.writeCharacteristic( 350 characteristic2, 351 newValue, 352 BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT)) 353 .isEqualTo(BluetoothStatusCodes.SUCCESS); 354 verify(gattCallback2, timeout(5000)) 355 .onCharacteristicWrite( 356 any(), eq(characteristic2), eq(BluetoothGatt.GATT_SUCCESS)); 357 } finally { 358 disconnectAndWaitDisconnection(gatt, gattCallback); 359 disconnectAndWaitDisconnection(gatt2, gattCallback2); 360 } 361 } 362 registerWritableGattService()363 private void registerWritableGattService() { 364 GattCharacteristicParams characteristicParams = 365 GattCharacteristicParams.newBuilder() 366 .setProperties(BluetoothGattCharacteristic.PROPERTY_WRITE) 367 .setUuid(TEST_CHARACTERISTIC_UUID.toString()) 368 .build(); 369 370 GattServiceParams serviceParams = 371 GattServiceParams.newBuilder() 372 .addCharacteristics(characteristicParams) 373 .setUuid(TEST_SERVICE_UUID.toString()) 374 .build(); 375 376 RegisterServiceRequest request = 377 RegisterServiceRequest.newBuilder().setService(serviceParams).build(); 378 379 mBumble.gattBlocking().registerService(request); 380 } 381 registerNotificationIndicationGattService(boolean isIndicate)382 private void registerNotificationIndicationGattService(boolean isIndicate) { 383 GattCharacteristicParams characteristicParams = 384 GattCharacteristicParams.newBuilder() 385 .setProperties( 386 isIndicate 387 ? BluetoothGattCharacteristic.PROPERTY_INDICATE 388 : BluetoothGattCharacteristic.PROPERTY_NOTIFY) 389 .setUuid(TEST_CHARACTERISTIC_UUID.toString()) 390 .build(); 391 392 GattServiceParams serviceParams = 393 GattServiceParams.newBuilder() 394 .addCharacteristics(characteristicParams) 395 .setUuid(TEST_SERVICE_UUID.toString()) 396 .build(); 397 398 RegisterServiceRequest request = 399 RegisterServiceRequest.newBuilder().setService(serviceParams).build(); 400 401 mBumble.gattBlocking().registerService(request); 402 } 403 triggerCharacteristicNotification(int instanceId)404 private void triggerCharacteristicNotification(int instanceId) { 405 NotifyOnCharacteristicRequest req = 406 NotifyOnCharacteristicRequest.newBuilder() 407 .setHandle(instanceId) 408 .setValue(ByteString.copyFromUtf8(NOTIFICATION_VALUE)) 409 .build(); 410 NotifyOnCharacteristicResponse resp = mBumble.gattBlocking().notifyOnCharacteristic(req); 411 assertThat(resp.getStatus()).isEqualTo(AttStatusCode.SUCCESS); 412 } 413 triggerCharacteristicIndication(int instanceId)414 private void triggerCharacteristicIndication(int instanceId) { 415 IndicateOnCharacteristicRequest req = 416 IndicateOnCharacteristicRequest.newBuilder() 417 .setHandle(instanceId) 418 .setValue(ByteString.copyFromUtf8(NOTIFICATION_VALUE)) 419 .build(); 420 IndicateOnCharacteristicResponse resp = 421 mBumble.gattBlocking().indicateOnCharacteristic(req); 422 assertThat(resp.getStatus()).isEqualTo(AttStatusCode.SUCCESS); 423 } 424 advertiseWithBumble()425 private void advertiseWithBumble() { 426 AdvertiseRequest request = 427 AdvertiseRequest.newBuilder() 428 .setLegacy(true) 429 .setConnectable(true) 430 .setOwnAddressType(OwnAddressType.RANDOM) 431 .build(); 432 433 StreamObserverSpliterator<AdvertiseResponse> responseObserver = 434 new StreamObserverSpliterator<>(); 435 436 mBumble.host().advertise(request, responseObserver); 437 } 438 connectGattAndWaitConnection(BluetoothGattCallback callback)439 private BluetoothGatt connectGattAndWaitConnection(BluetoothGattCallback callback) { 440 final int status = BluetoothGatt.GATT_SUCCESS; 441 final int state = BluetoothProfile.STATE_CONNECTED; 442 443 advertiseWithBumble(); 444 445 BluetoothDevice device = 446 mAdapter.getRemoteLeDevice( 447 Utils.BUMBLE_RANDOM_ADDRESS, BluetoothDevice.ADDRESS_TYPE_RANDOM); 448 449 BluetoothGatt gatt = device.connectGatt(mContext, false, callback); 450 verify(callback, timeout(1000)).onConnectionStateChange(eq(gatt), eq(status), eq(state)); 451 452 return gatt; 453 } 454 disconnectAndWaitDisconnection( BluetoothGatt gatt, BluetoothGattCallback callback)455 private void disconnectAndWaitDisconnection( 456 BluetoothGatt gatt, BluetoothGattCallback callback) { 457 final int state = BluetoothProfile.STATE_DISCONNECTED; 458 gatt.disconnect(); 459 verify(callback, timeout(1000)).onConnectionStateChange(eq(gatt), anyInt(), eq(state)); 460 461 gatt.close(); 462 gatt = null; 463 } 464 465 @Test 466 @Ignore("b/307981748: requestMTU should return a direct error") requestMtu_notConnected_isFalse()467 public void requestMtu_notConnected_isFalse() { 468 advertiseWithBumble(); 469 470 BluetoothDevice device = 471 mAdapter.getRemoteLeDevice( 472 Utils.BUMBLE_RANDOM_ADDRESS, BluetoothDevice.ADDRESS_TYPE_RANDOM); 473 BluetoothGattCallback gattCallback = mock(BluetoothGattCallback.class); 474 475 BluetoothGatt gatt = device.connectGatt(mContext, false, gattCallback); 476 // Do not wait for connection state change callback and ask MTU directly 477 assertThat(gatt.requestMtu(MTU_REQUESTED)).isFalse(); 478 } 479 480 @Test 481 @Ignore("b/307981748: requestMTU should return a direct error or a error on the callback") requestMtu_invalidParamer_isFalse()482 public void requestMtu_invalidParamer_isFalse() { 483 BluetoothGattCallback gattCallback = mock(BluetoothGattCallback.class); 484 BluetoothGatt gatt = connectGattAndWaitConnection(gattCallback); 485 486 try { 487 assertThat(gatt.requestMtu(1024)).isTrue(); 488 // verify(gattCallback, timeout(5000).atLeast(1)).onMtuChanged(eq(gatt), 489 // eq(ANDROID_MTU), eq(BluetoothGatt.GATT_FAILURE)); 490 } finally { 491 disconnectAndWaitDisconnection(gatt, gattCallback); 492 } 493 } 494 495 @Test requestMtu_once_isSuccess()496 public void requestMtu_once_isSuccess() { 497 BluetoothGattCallback gattCallback = mock(BluetoothGattCallback.class); 498 BluetoothGatt gatt = connectGattAndWaitConnection(gattCallback); 499 500 try { 501 assertThat(gatt.requestMtu(MTU_REQUESTED)).isTrue(); 502 // Check that only the ANDROID_MTU is returned, not the MTU_REQUESTED 503 verify(gattCallback, timeout(5000)) 504 .onMtuChanged(eq(gatt), eq(ANDROID_MTU), eq(BluetoothGatt.GATT_SUCCESS)); 505 } finally { 506 disconnectAndWaitDisconnection(gatt, gattCallback); 507 } 508 } 509 510 @Test requestMtu_multipleTimeFromSameClient_isRejected()511 public void requestMtu_multipleTimeFromSameClient_isRejected() { 512 BluetoothGattCallback gattCallback = mock(BluetoothGattCallback.class); 513 BluetoothGatt gatt = connectGattAndWaitConnection(gattCallback); 514 515 try { 516 assertThat(gatt.requestMtu(MTU_REQUESTED)).isTrue(); 517 // Check that only the ANDROID_MTU is returned, not the MTU_REQUESTED 518 verify(gattCallback, timeout(5000)) 519 .onMtuChanged(eq(gatt), eq(ANDROID_MTU), eq(BluetoothGatt.GATT_SUCCESS)); 520 521 assertThat(gatt.requestMtu(ANOTHER_MTU_REQUESTED)).isTrue(); 522 verify(gattCallback, timeout(5000).times(2)) 523 .onMtuChanged(eq(gatt), eq(ANDROID_MTU), eq(BluetoothGatt.GATT_SUCCESS)); 524 } finally { 525 disconnectAndWaitDisconnection(gatt, gattCallback); 526 } 527 } 528 529 @Test requestMtu_onceFromMultipleClient_secondIsSuccessWithoutUpdate()530 public void requestMtu_onceFromMultipleClient_secondIsSuccessWithoutUpdate() { 531 BluetoothGattCallback gattCallback = mock(BluetoothGattCallback.class); 532 BluetoothGatt gatt = connectGattAndWaitConnection(gattCallback); 533 534 try { 535 assertThat(gatt.requestMtu(MTU_REQUESTED)).isTrue(); 536 verify(gattCallback, timeout(5000)) 537 .onMtuChanged(eq(gatt), eq(ANDROID_MTU), eq(BluetoothGatt.GATT_SUCCESS)); 538 539 BluetoothGattCallback gattCallback2 = mock(BluetoothGattCallback.class); 540 BluetoothGatt gatt2 = connectGattAndWaitConnection(gattCallback2); 541 try { 542 // first callback because there is already a connected device 543 verify(gattCallback2, timeout(9000)) 544 .onMtuChanged(eq(gatt2), eq(ANDROID_MTU), eq(BluetoothGatt.GATT_SUCCESS)); 545 assertThat(gatt2.requestMtu(ANOTHER_MTU_REQUESTED)).isTrue(); 546 verify(gattCallback2, timeout(9000).times(2)) 547 .onMtuChanged(eq(gatt2), eq(ANDROID_MTU), eq(BluetoothGatt.GATT_SUCCESS)); 548 } finally { 549 disconnectAndWaitDisconnection(gatt2, gattCallback2); 550 } 551 } finally { 552 disconnectAndWaitDisconnection(gatt, gattCallback); 553 } 554 } 555 } 556