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