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