• 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.app.PendingIntent
20 import android.bluetooth.BluetoothProfile.STATE_CONNECTED
21 import android.bluetooth.le.BluetoothLeScanner
22 import android.bluetooth.le.ScanCallback
23 import android.bluetooth.le.ScanFilter
24 import android.bluetooth.le.ScanResult
25 import android.bluetooth.le.ScanSettings
26 import android.content.BroadcastReceiver
27 import android.content.Context
28 import android.content.Intent
29 import android.content.IntentFilter
30 import com.google.protobuf.Empty
31 import io.grpc.Deadline
32 import java.util.UUID
33 import java.util.concurrent.TimeUnit
34 import kotlinx.coroutines.CoroutineScope
35 import kotlinx.coroutines.Dispatchers
36 import kotlinx.coroutines.cancel
37 import kotlinx.coroutines.channels.awaitClose
38 import kotlinx.coroutines.flow.SharingStarted
39 import kotlinx.coroutines.flow.callbackFlow
40 import kotlinx.coroutines.flow.conflate
41 import kotlinx.coroutines.flow.first
42 import kotlinx.coroutines.flow.shareIn
43 import kotlinx.coroutines.runBlocking
44 import kotlinx.coroutines.withTimeout
45 import org.junit.rules.TestRule
46 import org.junit.runner.Description
47 import org.junit.runners.model.Statement
48 import pandora.HostProto
49 import pandora.HostProto.AdvertiseRequest
50 import pandora.HostProto.OwnAddressType
51 
52 /** Test rule for DCK specific device and Bumble setup and teardown procedures */
53 class DckTestRule(
54     private val context: Context,
55     private val bumble: PandoraDevice,
56     private val isBluetoothToggled: Boolean = false,
57     private val isRemoteAdvertisingWithUuid: Boolean = false,
58     private val isGattConnected: Boolean = false,
59 ) : TestRule {
60     private val bluetoothManager = context.getSystemService(BluetoothManager::class.java)!!
61     private val bluetoothAdapter = bluetoothManager.adapter
62     private val leScanner = bluetoothAdapter.bluetoothLeScanner
63 
64     private val scope = CoroutineScope(Dispatchers.Default)
65     private val ioScope = CoroutineScope(Dispatchers.IO)
66 
67     // Internal Types
68 
69     /** Wrapper for [ScanResult] */
70     sealed class LeScanResult {
71 
72         /** Represents a [ScanResult] with the associated [callbackType] */
73         data class Success(val scanResult: ScanResult, val callbackType: Int) : LeScanResult()
74 
75         /** Represents a scan failure with an [errorCode] */
76         data class Failure(val errorCode: Int) : LeScanResult()
77     }
78 
79     /** Wrapper for [BluetoothGatt] along with its [state] and [status] */
80     data class GattState(val gatt: BluetoothGatt, val status: Int, val state: Int)
81 
82     // Public Methods
83 
84     /**
85      * Starts an LE scan with the given [scanFilter] and [scanSettings], using [ScanCallback] within
86      * the given [coroutine].
87      *
88      * The caller can stop the scan at any time by cancelling the coroutine they used to start the
89      * scan. If no coroutine was provided, a default coroutine is used and the scan will be stopped
90      * at the end of the test.
91      *
92      * @return SharedFlow of [LeScanResult] with a buffer of size 1
93      */
scanWithCallbacknull94     fun scanWithCallback(
95         scanFilter: ScanFilter,
96         scanSettings: ScanSettings,
97         coroutine: CoroutineScope = scope,
98     ) =
99         callbackFlow {
100                 val callback =
101                     object : ScanCallback() {
102                         override fun onScanResult(callbackType: Int, result: ScanResult) {
103                             trySend(LeScanResult.Success(result, callbackType))
104                         }
105 
106                         override fun onScanFailed(errorCode: Int) {
107                             trySend(LeScanResult.Failure(errorCode))
108                             channel.close()
109                         }
110                     }
111 
112                 leScanner.startScan(listOf(scanFilter), scanSettings, callback)
113 
114                 awaitClose { leScanner.stopScan(callback) }
115             }
116             .conflate()
117             .shareIn(coroutine, SharingStarted.Lazily)
118 
119     /**
120      * Starts an LE scan with the given [scanFilter] and [scanSettings], using [PendingIntent]
121      * within the given [coroutine].
122      *
123      * The caller can stop the scan at any time by cancelling the coroutine they used to start the
124      * scan. If no coroutine was provided, a default coroutine is used and the scan will be stopped
125      * at the end of the test.
126      *
127      * @return SharedFlow of [LeScanResult] with a buffer of size 1
128      */
scanWithPendingIntentnull129     fun scanWithPendingIntent(
130         scanFilter: ScanFilter,
131         scanSettings: ScanSettings,
132         coroutine: CoroutineScope = scope,
133     ) =
134         callbackFlow {
135                 val intentFilter = IntentFilter(ACTION_DYNAMIC_RECEIVER_SCAN_RESULT)
136                 val broadcastReceiver =
137                     object : BroadcastReceiver() {
138                         override fun onReceive(context: Context, intent: Intent) {
139                             if (ACTION_DYNAMIC_RECEIVER_SCAN_RESULT == intent.action) {
140                                 val results =
141                                     intent.getParcelableArrayListExtra<ScanResult>(
142                                         BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT
143                                     ) ?: return
144 
145                                 val callbackType =
146                                     intent.getIntExtra(BluetoothLeScanner.EXTRA_CALLBACK_TYPE, -1)
147 
148                                 for (result in results) {
149                                     trySend(LeScanResult.Success(result, callbackType))
150                                 }
151                             }
152                         }
153                     }
154 
155                 context.registerReceiver(broadcastReceiver, intentFilter, Context.RECEIVER_EXPORTED)
156 
157                 val scanIntent = Intent(ACTION_DYNAMIC_RECEIVER_SCAN_RESULT)
158                 val pendingIntent =
159                     PendingIntent.getBroadcast(
160                         context,
161                         0,
162                         scanIntent,
163                         PendingIntent.FLAG_MUTABLE or
164                             PendingIntent.FLAG_UPDATE_CURRENT or
165                             PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT,
166                     )
167 
168                 leScanner.startScan(listOf(scanFilter), scanSettings, pendingIntent)
169 
170                 awaitClose {
171                     context.unregisterReceiver(broadcastReceiver)
172                     leScanner.stopScan(pendingIntent)
173                 }
174             }
175             .conflate()
176             .shareIn(coroutine, SharingStarted.Lazily)
177 
178     /**
179      * Requests a GATT connection to the given [device] within the given [coroutine].
180      *
181      * Cancelling the coroutine will close the GATT client. If no coroutine was provided, a default
182      * coroutine is used and the GATT client will be closed at the end of the test.
183      *
184      * @return SharedFlow of [GattState] with a buffer of size 1
185      */
connectGattnull186     fun connectGatt(device: BluetoothDevice, coroutine: CoroutineScope = ioScope) =
187         callbackFlow {
188                 val callback =
189                     object : BluetoothGattCallback() {
190                         override fun onConnectionStateChange(
191                             gatt: BluetoothGatt,
192                             status: Int,
193                             newState: Int,
194                         ) {
195                             trySend(GattState(gatt, status, newState))
196                         }
197                     }
198 
199                 val gatt = device.connectGatt(context, false, callback)
200 
201                 awaitClose { gatt.close() }
202             }
203             .conflate()
204             .shareIn(coroutine, SharingStarted.Lazily)
205 
206     // TestRule Overrides
207 
applynull208     override fun apply(base: Statement, description: Description): Statement {
209         return object : Statement() {
210             override fun evaluate() {
211                 setup(base)
212             }
213         }
214     }
215 
216     // Private Methods
217 
setupnull218     private fun setup(base: Statement) {
219         // Register Bumble's DCK (Digital Car Key) service
220         registerDckService()
221         // Start LE advertisement on Bumble
222         advertiseWithBumble()
223 
224         try {
225             if (isBluetoothToggled) {
226                 toggleBluetooth()
227             }
228 
229             if (isGattConnected) {
230                 connectGatt()
231             }
232 
233             base.evaluate()
234         } finally {
235             reset()
236         }
237     }
238 
registerDckServicenull239     private fun registerDckService() {
240         bumble
241             .dckBlocking()
242             .withDeadline(Deadline.after(TIMEOUT_MS, TimeUnit.MILLISECONDS))
243             .register(Empty.getDefaultInstance())
244     }
245 
advertiseWithBumblenull246     private fun advertiseWithBumble() {
247         val requestBuilder =
248             AdvertiseRequest.newBuilder()
249                 .setLegacy(true) // Bumble currently only supports legacy advertising.
250                 .setOwnAddressType(OwnAddressType.RANDOM)
251                 .setConnectable(true)
252 
253         if (isRemoteAdvertisingWithUuid) {
254             val advertisementData =
255                 HostProto.DataTypes.newBuilder()
256                     .addCompleteServiceClassUuids128(CCC_DK_UUID.toString())
257                     .build()
258             requestBuilder.setData(advertisementData)
259         }
260 
261         bumble.hostBlocking().advertise(requestBuilder.build())
262     }
263 
<lambda>null264     private fun toggleBluetooth() = runBlocking {
265         val scope = CoroutineScope(Dispatchers.Default)
266         val bluetoothStateFlow = getBluetoothStateFlow(scope)
267 
268         try {
269             withTimeout(TIMEOUT_MS * 2) { // Combined timeout for enabling and disabling BT
270                 if (bluetoothAdapter.isEnabled()) {
271                     // Disable Bluetooth
272                     bluetoothAdapter.disable()
273                     // Wait for the BT state change to STATE_OFF
274                     bluetoothStateFlow.first { it == BluetoothAdapter.STATE_OFF }
275                 }
276 
277                 // Enable Bluetooth
278                 bluetoothAdapter.enable()
279                 // Wait for the BT state change to STATE_ON
280                 bluetoothStateFlow.first { it == BluetoothAdapter.STATE_ON }
281             }
282         } finally {
283             // Close the BT state change flow
284             scope.cancel("Done")
285         }
286     }
287 
getBluetoothStateFlownull288     private fun getBluetoothStateFlow(coroutine: CoroutineScope) =
289         callbackFlow {
290                 val bluetoothStateFilter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
291                 val bluetoothStateReceiver =
292                     object : BroadcastReceiver() {
293                         override fun onReceive(context: Context, intent: Intent) {
294                             if (BluetoothAdapter.ACTION_STATE_CHANGED == intent.action) {
295                                 trySend(
296                                     intent.getIntExtra(
297                                         BluetoothAdapter.EXTRA_STATE,
298                                         BluetoothAdapter.ERROR,
299                                     )
300                                 )
301                             }
302                         }
303                     }
304 
305                 context.registerReceiver(bluetoothStateReceiver, bluetoothStateFilter)
306 
307                 awaitClose { context.unregisterReceiver(bluetoothStateReceiver) }
308             }
309             .conflate()
310             .shareIn(coroutine, SharingStarted.Lazily)
311 
<lambda>null312     private fun connectGatt() = runBlocking {
313         // TODO(315852141): Use supported Bumble for the given type (LE Only vs. Dual Mode)
314         val bumbleDevice =
315             bluetoothAdapter.getRemoteLeDevice(
316                 Utils.BUMBLE_RANDOM_ADDRESS,
317                 BluetoothDevice.ADDRESS_TYPE_RANDOM,
318             )
319 
320         withTimeout(TIMEOUT_MS) { connectGatt(bumbleDevice).first { it.state == STATE_CONNECTED } }
321     }
322 
resetnull323     private fun reset() {
324         scope.cancel("Test Completed")
325         ioScope.cancel("Test Completed")
326     }
327 
328     companion object {
329         private const val TIMEOUT_MS = 3000L
330         private const val ACTION_DYNAMIC_RECEIVER_SCAN_RESULT =
331             "android.bluetooth.test.ACTION_DYNAMIC_RECEIVER_SCAN_RESULT"
332         // CCC DK Specification R3 1.2.0 r14 section 19.2.1.2 Bluetooth Le Pairing
333         private val CCC_DK_UUID = UUID.fromString("0000FFF5-0000-1000-8000-00805f9b34fb")
334     }
335 }
336