• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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