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 android.bluetooth.le.ScanCallback 20 import android.bluetooth.le.ScanFilter 21 import android.bluetooth.le.ScanResult 22 import android.bluetooth.le.ScanSettings 23 import android.content.Context 24 import android.os.ParcelUuid 25 import androidx.test.core.app.ApplicationProvider 26 import com.android.compatibility.common.util.AdoptShellPermissionsRule 27 import com.google.common.truth.Truth.assertThat 28 import com.google.protobuf.Empty 29 import com.google.testing.junit.testparameterinjector.TestParameter 30 import com.google.testing.junit.testparameterinjector.TestParameterInjector 31 import io.grpc.Context as GrpcContext 32 import io.grpc.Deadline 33 import java.util.UUID 34 import java.util.concurrent.TimeUnit 35 import org.junit.After 36 import org.junit.Assume.assumeFalse 37 import org.junit.Before 38 import org.junit.Rule 39 import org.junit.Test 40 import org.junit.runner.RunWith 41 import org.mockito.kotlin.any 42 import org.mockito.kotlin.argumentCaptor 43 import org.mockito.kotlin.clearInvocations 44 import org.mockito.kotlin.doAnswer 45 import org.mockito.kotlin.eq 46 import org.mockito.kotlin.mock 47 import org.mockito.kotlin.timeout 48 import org.mockito.kotlin.verify 49 import pandora.HostProto 50 import pandora.HostProto.AdvertiseRequest 51 import pandora.HostProto.OwnAddressType 52 53 /** DCK GATT Tests */ 54 @RunWith(TestParameterInjector::class) 55 public class DckGattTest() { 56 57 private val context: Context = ApplicationProvider.getApplicationContext() 58 private val bluetoothManager = context.getSystemService(BluetoothManager::class.java)!! 59 private val bluetoothAdapter = bluetoothManager.adapter 60 private val leScanner = bluetoothAdapter.bluetoothLeScanner 61 62 private val scanResultCaptor = argumentCaptor<ScanResult>() 63 private val scanCallbackMock = mock<ScanCallback>() 64 private val gattCaptor = argumentCaptor<BluetoothGatt>() 65 private val gattCallbackMock = <lambda>null66 mock<BluetoothGattCallback> { 67 on { onConnectionStateChange(gattCaptor.capture(), any(), any()) } doAnswer {} 68 } 69 70 // A Rule live from a test setup through it's teardown. 71 // Gives shell permissions during the test. 72 @Rule @JvmField val mPermissionRule = AdoptShellPermissionsRule() 73 74 // Setup a Bumble Pandora device for the duration of the test. 75 // Acting as a Pandora client, it can be interacted with through the Pandora APIs. 76 @Rule @JvmField val mBumble = PandoraDevice() 77 78 @Before setUpnull79 fun setUp() { 80 // 1. Register Bumble's DCK (Digital Car Key) service via a gRPC call: 81 // - `dckBlocking()` is likely a stub that accesses the DCK service over gRPC in a 82 // blocking/synchronous manner. 83 // - `withDeadline(Deadline.after(TIMEOUT, TimeUnit.MILLISECONDS))` sets a timeout for the 84 // gRPC call. 85 // - `register(Empty.getDefaultInstance())` sends a registration request to the server. 86 mBumble 87 .dckBlocking() 88 .withDeadline(Deadline.after(TIMEOUT, TimeUnit.MILLISECONDS)) 89 .register(Empty.getDefaultInstance()) 90 91 if (connected) { 92 val advertiseContext = advertiseWithBumble() 93 94 // Connect DUT to Ref as prerequisite 95 val device = 96 bluetoothAdapter.getRemoteLeDevice( 97 Utils.BUMBLE_RANDOM_ADDRESS, 98 BluetoothDevice.ADDRESS_TYPE_RANDOM 99 ) 100 val gatt = device.connectGatt(context, false, gattCallbackMock) 101 verify(gattCallbackMock, timeout(TIMEOUT)) 102 .onConnectionStateChange( 103 eq(gatt), 104 eq(BluetoothGatt.GATT_SUCCESS), 105 eq(BluetoothProfile.STATE_CONNECTED) 106 ) 107 advertiseContext.cancel(null) 108 109 // Wait a bit for the advertising to stop. 110 // b/332322761 111 Thread.sleep(1000) 112 } 113 114 clearInvocations(gattCallbackMock) 115 } 116 117 @After tearDownnull118 fun tearDown() { 119 for (gatt in gattCaptor.allValues.toSet()) { 120 gatt.close() 121 } 122 } 123 124 /** 125 * Tests the discovery of the Digital Car Key (DCK) GATT service via Bluetooth on an Android 126 * device. 127 * 128 * <p>This test method goes through the following steps: 129 * <ul> 130 * <li>1. Register the Dck Gatt service on Bumble over a gRPC call.</li> 131 * <li>2. Advertises the host's (potentially the car's) Bluetooth capabilities through a gRPC 132 * call.</li> 133 * <li>3. Fetches a remote LE (Low Energy) Bluetooth device instance.</li> 134 * <li>4. Sets up a mock GATT callback for Bluetooth related events.</li> 135 * <li>5. Connects to the Bumble device and verifies a successful connection.</li> 136 * <li>6. Discovers the GATT services offered by Bumble and checks for a successful service 137 * discovery.</li> 138 * <li>7. Validates the presence of the required GATT service (CCC_DK_UUID) on the Bumble 139 * device.</li> 140 * <li>8. Disconnects from the Bumble device and ensures a successful disconnection.</li> 141 * </ul> 142 * 143 * </p> 144 * 145 * @throws AssertionError if any of the assertions (like service discovery or connection checks) 146 * fail. 147 * @see BluetoothGatt 148 * @see BluetoothGattCallback 149 */ 150 @Test testDiscoverDkGattServicenull151 fun testDiscoverDkGattService() { 152 153 // 2. Advertise the host's (presumably the car's) Bluetooth capabilities using another 154 // gRPC call: 155 // - `hostBlocking()` accesses another gRPC service related to the host. 156 // The following `advertise(...)` sends an advertise request to the server, setting 157 // specific attributes. 158 mBumble 159 .hostBlocking() 160 .advertise( 161 AdvertiseRequest.newBuilder() 162 .setLegacy( 163 true 164 ) // As of now, Bumble only support legacy advertising (b/266124496). 165 .setConnectable(true) 166 .setOwnAddressType( 167 OwnAddressType.RANDOM 168 ) // Ask Bumble to advertise it's `RANDOM` address. 169 .build() 170 ) 171 172 // 3. Fetch a remote Bluetooth device instance (here, Bumble). 173 val bumbleDevice = 174 bluetoothAdapter.getRemoteLeDevice( 175 // To keep things straightforward, the Bumble RANDOM address is set to a predefined 176 // constant. 177 // Typically, an LE scan would be conducted to identify the Bumble device, matching 178 // it based on its 179 // Advertising data. 180 Utils.BUMBLE_RANDOM_ADDRESS, 181 BluetoothDevice 182 .ADDRESS_TYPE_RANDOM // Specify address type as RANDOM because the device 183 // advertises with this address type. 184 ) 185 186 // 4. Create a mock callback to handle Bluetooth GATT (Generic Attribute Profile) related 187 // events. 188 val gattCallback = mock<BluetoothGattCallback>() 189 190 // 5. Connect to the Bumble device and expect a successful connection callback. 191 var bumbleGatt = bumbleDevice.connectGatt(context, false, gattCallback) 192 verify(gattCallback, timeout(TIMEOUT)) 193 .onConnectionStateChange( 194 any(), 195 eq(BluetoothGatt.GATT_SUCCESS), 196 eq(BluetoothProfile.STATE_CONNECTED) 197 ) 198 199 // 6. Discover GATT services offered by Bumble and expect successful service discovery. 200 bumbleGatt.discoverServices() 201 verify(gattCallback, timeout(TIMEOUT)) 202 .onServicesDiscovered(any(), eq(BluetoothGatt.GATT_SUCCESS)) 203 204 // 7. Check if the required service (CCC_DK_UUID) is available on Bumble. 205 assertThat(bumbleGatt.getService(CCC_DK_UUID)).isNotNull() 206 207 // 8. Disconnect from the Bumble device and expect a successful disconnection callback. 208 bumbleGatt.disconnect() 209 verify(gattCallback, timeout(TIMEOUT)) 210 .onConnectionStateChange( 211 any(), 212 eq(BluetoothGatt.GATT_SUCCESS), 213 eq(BluetoothProfile.STATE_DISCONNECTED) 214 ) 215 } 216 217 /* 218 * 2.1 GATT Connect - discovered using scan with Identity Address and IRK 219 * 220 * http://docs/document/d/1oQOpgI83HSJBdr5mBU00za_6XrDGo2KDGnCcX-hXPHk#heading=h.9nvtna3zum23 221 */ 222 @Test testGattConnect_fromIrkScannull223 fun testGattConnect_fromIrkScan() { 224 // TODO(b/317091743): Enable test when bug is fixed. 225 assumeFalse(connected) 226 227 // Start advertisement on Ref 228 val advertiseStreamObserver = advertiseWithBumble() 229 230 // Start IRK scan for Ref on DUT 231 val scanSettings = 232 ScanSettings.Builder() 233 .setScanMode(ScanSettings.SCAN_MODE_AMBIENT_DISCOVERY) 234 .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) 235 .build() 236 val scanFilter = 237 ScanFilter.Builder() 238 .setDeviceAddress( 239 TEST_ADDRESS_RANDOM_STATIC, 240 BluetoothDevice.ADDRESS_TYPE_RANDOM, 241 Utils.BUMBLE_IRK 242 ) 243 .build() 244 leScanner.startScan(listOf(scanFilter), scanSettings, scanCallbackMock) 245 246 // Await scan results 247 verify(scanCallbackMock, timeout(TIMEOUT).atLeastOnce()) 248 .onScanResult(eq(ScanSettings.CALLBACK_TYPE_ALL_MATCHES), scanResultCaptor.capture()) 249 250 // Verify correct scan result as prerequisite 251 val scanResult = scanResultCaptor.firstValue 252 assertThat(scanResult).isNotNull() 253 assertThat(scanResult.device.identityAddress).isEqualTo(TEST_ADDRESS_RANDOM_STATIC) 254 255 // Verify successful GATT connection 256 val device = scanResult.device 257 val gatt = device.connectGatt(context, false, gattCallbackMock) 258 verify(gattCallbackMock, timeout(TIMEOUT)) 259 .onConnectionStateChange( 260 eq(gatt), 261 eq(BluetoothGatt.GATT_SUCCESS), 262 eq(BluetoothProfile.STATE_CONNECTED) 263 ) 264 265 // Stop scan on DUT after GATT connect 266 leScanner.stopScan(scanCallbackMock) 267 advertiseStreamObserver.cancel(null) 268 } 269 270 /* 271 * 2.3 GATT Connect - discovered using scan with UUID 272 * 273 * http://docs/document/d/1oQOpgI83HSJBdr5mBU00za_6XrDGo2KDGnCcX-hXPHk#heading=h.7ofaj7vwknsr 274 */ 275 @Test testGattConnect_fromUuidScannull276 fun testGattConnect_fromUuidScan() { 277 // Start UUID advertisement on Ref 278 advertiseWithBumble(withUuid = true) 279 280 // Start UUID scan for Ref on DUT 281 val scanSettings = 282 ScanSettings.Builder() 283 .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) 284 .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) 285 .build() 286 val scanFilter = ScanFilter.Builder().setServiceUuid(ParcelUuid(CCC_DK_UUID)).build() 287 leScanner.startScan(listOf(scanFilter), scanSettings, scanCallbackMock) 288 289 // Await scan results 290 verify(scanCallbackMock, timeout(TIMEOUT).atLeastOnce()) 291 .onScanResult(eq(ScanSettings.CALLBACK_TYPE_ALL_MATCHES), scanResultCaptor.capture()) 292 293 // Stop scan on DUT before GATT connect 294 leScanner.stopScan(scanCallbackMock) 295 296 // Verify correct scan result as prerequisite 297 val scanResult = scanResultCaptor.firstValue 298 assertThat(scanResult).isNotNull() 299 assertThat(scanResult.scanRecord?.serviceUuids).contains(ParcelUuid(CCC_DK_UUID)) 300 301 // Verify successful GATT connection 302 val device = scanResult.device 303 val gatt = device.connectGatt(context, false, gattCallbackMock) 304 verify(gattCallbackMock, timeout(TIMEOUT)) 305 .onConnectionStateChange( 306 eq(gatt), 307 eq(BluetoothGatt.GATT_SUCCESS), 308 eq(BluetoothProfile.STATE_CONNECTED) 309 ) 310 } 311 advertiseWithBumblenull312 private fun advertiseWithBumble(withUuid: Boolean = false): GrpcContext.CancellableContext { 313 val requestBuilder = 314 AdvertiseRequest.newBuilder() 315 .setLegacy(true) 316 .setConnectable(true) 317 .setOwnAddressType(OwnAddressType.RANDOM) 318 319 if (withUuid) { 320 requestBuilder.data = 321 HostProto.DataTypes.newBuilder() 322 .addCompleteServiceClassUuids128(CCC_DK_UUID.toString()) 323 .build() 324 } 325 326 val cancellableContext = GrpcContext.current().withCancellation() 327 with(cancellableContext) { 328 run { mBumble.hostBlocking().advertise(requestBuilder.build()) } 329 } 330 331 return cancellableContext 332 } 333 334 companion object { 335 private const val TAG = "DckTest" 336 private const val TIMEOUT: Long = 2000 337 private const val TEST_ADDRESS_RANDOM_STATIC = "F0:43:A8:23:10:11" 338 339 // CCC DK Specification R3 1.2.0 r14 section 19.2.1.2 Bluetooth Le Pairing 340 private val CCC_DK_UUID = UUID.fromString("0000FFF5-0000-1000-8000-00805f9b34fb") 341 342 @TestParameter private val connected: Boolean = false 343 } 344 } 345