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.le.BluetoothLeScanner
21 import android.bluetooth.le.ScanCallback
22 import android.bluetooth.le.ScanFilter
23 import android.bluetooth.le.ScanResult
24 import android.bluetooth.le.ScanSettings
25 import android.content.BroadcastReceiver
26 import android.content.Context
27 import android.content.Intent
28 import android.content.IntentFilter
29 import com.google.protobuf.Empty
30 import io.grpc.Deadline
31 import java.util.UUID
32 import java.util.concurrent.TimeUnit
33 import kotlinx.coroutines.CoroutineScope
34 import kotlinx.coroutines.Dispatchers
35 import kotlinx.coroutines.cancel
36 import kotlinx.coroutines.channels.awaitClose
37 import kotlinx.coroutines.flow.SharingStarted
38 import kotlinx.coroutines.flow.callbackFlow
39 import kotlinx.coroutines.flow.conflate
40 import kotlinx.coroutines.flow.first
41 import kotlinx.coroutines.flow.shareIn
42 import kotlinx.coroutines.runBlocking
43 import kotlinx.coroutines.withTimeout
44 import org.junit.rules.TestRule
45 import org.junit.runner.Description
46 import org.junit.runners.model.Statement
47 import pandora.HostProto
48 import pandora.HostProto.AdvertiseRequest
49 import pandora.HostProto.OwnAddressType
50 
51 /** Test rule for DCK specific device and Bumble setup and teardown procedures */
52 class DckTestRule(
53     private val context: Context,
54     private val bumble: PandoraDevice,
55     private val isBluetoothToggled: Boolean = false,
56     private val isRemoteAdvertisingWithUuid: Boolean = false,
57     private val isGattConnected: Boolean = false,
58 ) : TestRule {
59     private val bluetoothManager = context.getSystemService(BluetoothManager::class.java)!!
60     private val bluetoothAdapter = bluetoothManager.adapter
61     private val leScanner = bluetoothAdapter.bluetoothLeScanner
62 
63     private val scope = CoroutineScope(Dispatchers.Default)
64     private val ioScope = CoroutineScope(Dispatchers.IO)
65 
66     // Internal Types
67 
68     /** Wrapper for [ScanResult] */
69     sealed class LeScanResult {
70 
71         /** Represents a [ScanResult] with the associated [callbackType] */
72         data class Success(val scanResult: ScanResult, val callbackType: Int) : LeScanResult()
73 
74         /** Represents a scan failure with an [errorCode] */
75         data class Failure(val errorCode: Int) : LeScanResult()
76     }
77 
78     /** Wrapper for [BluetoothGatt] along with its [state] and [status] */
79     data class GattState(val gatt: BluetoothGatt, val status: Int, val state: Int)
80 
81     // Public Methods
82 
83     /**
84      * Starts an LE scan with the given [scanFilter] and [scanSettings], using [ScanCallback] within
85      * the given [coroutine].
86      *
87      * The caller can stop the scan at any time by cancelling the coroutine they used to start the
88      * scan. If no coroutine was provided, a default coroutine is used and the scan will be stopped
89      * at the end of the test.
90      *
91      * @return SharedFlow of [LeScanResult] with a buffer of size 1
92      */
scanWithCallbacknull93     fun scanWithCallback(
94         scanFilter: ScanFilter,
95         scanSettings: ScanSettings,
96         coroutine: CoroutineScope = scope
97     ) =
98         callbackFlow {
99                 val callback =
100                     object : ScanCallback() {
101                         override fun onScanResult(callbackType: Int, result: ScanResult) {
102                             trySend(LeScanResult.Success(result, callbackType))
103                         }
104 
105                         override fun onScanFailed(errorCode: Int) {
106                             trySend(LeScanResult.Failure(errorCode))
107                             channel.close()
108                         }
109                     }
110 
111                 leScanner.startScan(listOf(scanFilter), scanSettings, callback)
112 
113                 awaitClose { leScanner.stopScan(callback) }
114             }
115             .conflate()
116             .shareIn(coroutine, SharingStarted.Lazily)
117 
118     /**
119      * Starts an LE scan with the given [scanFilter] and [scanSettings], using [PendingIntent]
120      * within the given [coroutine].
121      *
122      * The caller can stop the scan at any time by cancelling the coroutine they used to start the
123      * scan. If no coroutine was provided, a default coroutine is used and the scan will be stopped
124      * at the end of the test.
125      *
126      * @return SharedFlow of [LeScanResult] with a buffer of size 1
127      */
scanWithPendingIntentnull128     fun scanWithPendingIntent(
129         scanFilter: ScanFilter,
130         scanSettings: ScanSettings,
131         coroutine: CoroutineScope = scope
132     ) =
133         callbackFlow {
134                 val intentFilter = IntentFilter(ACTION_DYNAMIC_RECEIVER_SCAN_RESULT)
135                 val broadcastReceiver =
136                     object : BroadcastReceiver() {
137                         override fun onReceive(context: Context, intent: Intent) {
138                             if (ACTION_DYNAMIC_RECEIVER_SCAN_RESULT == intent.action) {
139                                 val results =
140                                     intent.getParcelableArrayListExtra<ScanResult>(
141                                         BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT
142                                     )
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) {
321             connectGatt(bumbleDevice).first { it.state == BluetoothProfile.STATE_CONNECTED }
322         }
323     }
324 
resetnull325     private fun reset() {
326         scope.cancel("Test Completed")
327         ioScope.cancel("Test Completed")
328     }
329 
330     companion object {
331         private const val TIMEOUT_MS = 3000L
332         private const val ACTION_DYNAMIC_RECEIVER_SCAN_RESULT =
333             "android.bluetooth.test.ACTION_DYNAMIC_RECEIVER_SCAN_RESULT"
334         // CCC DK Specification R3 1.2.0 r14 section 19.2.1.2 Bluetooth Le Pairing
335         private val CCC_DK_UUID = UUID.fromString("0000FFF5-0000-1000-8000-00805f9b34fb")
336     }
337 }
338