Loading framework/tests/bumble/src/android/bluetooth/PandoraDevice.java +11 −0 Original line number Diff line number Diff line Loading @@ -30,6 +30,7 @@ import io.grpc.okhttp.OkHttpChannelBuilder; import org.junit.rules.ExternalResource; import pandora.BumbleConfigGrpc; import pandora.DckGrpc; import pandora.GATTGrpc; import pandora.HIDGrpc; Loading Loading @@ -174,6 +175,16 @@ public final class PandoraDevice extends ExternalResource { return HostGrpc.newBlockingStub(mChannel); } /** Get Pandora BumbleConfig service */ public BumbleConfigGrpc.BumbleConfigStub bumbleConfig() { return BumbleConfigGrpc.newStub(mChannel); } /** Get Pandora BumbleConfig service */ public BumbleConfigGrpc.BumbleConfigBlockingStub bumbleConfigBlocking() { return BumbleConfigGrpc.newBlockingStub(mChannel); } /** Get Pandora HID service */ public HIDGrpc.HIDStub hid() { return HIDGrpc.newStub(mChannel); Loading framework/tests/bumble/src/android/bluetooth/RfcommTest.kt +88 −18 Original line number Diff line number Diff line Loading @@ -18,7 +18,10 @@ package android.bluetooth import android.Manifest import android.annotation.SuppressLint import android.bluetooth.test_utils.EnableBluetoothRule import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.platform.test.annotations.RequiresFlagsEnabled import android.platform.test.flag.junit.CheckFlagsRule import android.platform.test.flag.junit.DeviceFlagsValueProvider Loading @@ -37,8 +40,16 @@ import java.util.concurrent.TimeUnit import kotlin.concurrent.thread import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.withTimeout import org.junit.After import org.junit.Before import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith Loading @@ -47,6 +58,8 @@ import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.timeout import org.mockito.kotlin.verify import pandora.BumbleConfigProto import pandora.HostProto import pandora.RfcommProto import pandora.RfcommProto.ServerId Loading Loading @@ -79,27 +92,48 @@ class RfcommTest { @Rule(order = 3) @JvmField val enableBluetoothRule = EnableBluetoothRule(false, true) private lateinit var mRemoteDevice: BluetoothDevice private lateinit var host: Host private lateinit var mHost: Host private var mConnectionCounter = 1 private var mProfileServiceListener = mock<BluetoothProfile.ServiceListener>() private val mFlow: Flow<Intent> private val mScope: CoroutineScope = CoroutineScope(Dispatchers.Default.limitedParallelism(2)) @OptIn(ExperimentalStdlibApi::class) private val bdAddrFormat = HexFormat { bytes { byteSeparator = ":" } } @OptIn(ExperimentalStdlibApi::class) private val mLocalAddress: ByteString = ByteString.copyFrom("DA:4C:10:DE:17:00".hexToByteArray(bdAddrFormat)) init { val intentFilter = IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST) mFlow = intentFlow(mContext, intentFilter, mScope).shareIn(mScope, SharingStarted.Eagerly) } /* Setup: 1. Initialize host and mRemoteDevice 2. Disable A2DP, HFP, and HID profiles 3. Bond devices 4. Disconnect devices 2. Override pairing config to enable insecure tests 3. Disable A2DP, HFP, and HID profiles 4. Disconnect devices, if they are connected */ @Before fun setUp() { mRemoteDevice = mBumble.remoteDevice host = Host(mContext) mHost = Host(mContext) // Set Bonding val pairingConfig = BumbleConfigProto.PairingConfig.newBuilder() .setBonding(false) .setMitm(false) .setSc(false) .setIdentityAddressType(HostProto.OwnAddressType.PUBLIC) .build() val overrideRequest = BumbleConfigProto.OverrideRequest.newBuilder().setPairingConfig(pairingConfig).build() mBumble.bumbleConfigBlocking().override(overrideRequest) val bluetoothA2dp = getProfileProxy(mContext, BluetoothProfile.A2DP) as BluetoothA2dp bluetoothA2dp.setConnectionPolicy( mRemoteDevice, Loading @@ -116,23 +150,22 @@ class RfcommTest { mRemoteDevice, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN, ) host.createBondAndVerify(mRemoteDevice) if (mRemoteDevice.isConnected) { host.disconnectAndVerify(mRemoteDevice) mHost.disconnectAndVerify(mRemoteDevice) } } /* TearDown: 1. unbond 1. remove bond 2. shutdown host */ @After fun tearDown() { if (mAdapter.bondedDevices.contains(mRemoteDevice)) { host.removeBondAndVerify(mRemoteDevice) mHost.removeBondAndVerify(mRemoteDevice) } host.close() mHost.close() } /* Loading @@ -142,7 +175,7 @@ class RfcommTest { 3. Verify that devices are connected. */ @Test fun clientConnectToOpenServerSocketBondedInsecure() { fun clientConnectToOpenServerSocketInsecure() { startServer { serverId -> createConnectAcceptSocket(isSecure = false, serverId) } } Loading @@ -153,7 +186,7 @@ class RfcommTest { 3. Verify that devices are connected. */ @Test fun clientConnectToOpenServerSocketBondedSecure() { fun clientConnectToOpenServerSocketSecure() { startServer { serverId -> createConnectAcceptSocket(isSecure = true, serverId) } } Loading Loading @@ -361,13 +394,14 @@ class RfcommTest { 4. Repeat for secure socket 2 */ @Test @Ignore("b/380091558") fun connectTwoMixedClientsInsecureThenSecure() { startServer("ServerPort1", TEST_UUID) { serverId1 -> startServer("ServerPort2", SERIAL_PORT_UUID) { serverId2 -> val socket2 = createSocket(mRemoteDevice, isSecure = false, SERIAL_PORT_UUID) acceptSocket(serverId2) Truth.assertThat(socket2.isConnected).isTrue() Log.i(TAG, "Finished with socket number 2") val socket1 = createSocket(mRemoteDevice, isSecure = true, TEST_UUID) acceptSocket(serverId1) Truth.assertThat(socket1.isConnected).isTrue() Loading Loading @@ -405,11 +439,11 @@ class RfcommTest { @RequiresFlagsEnabled(Flags.FLAG_TRIGGER_SEC_PROC_ON_INC_ACCESS_REQ) @Test fun serverSecureConnectThenRemoteDisconnect() { // connect // step 1 val (serverSock, connection) = connectRemoteToListeningSocket() val disconnectRequest = RfcommProto.DisconnectionRequest.newBuilder().setConnection(connection).build() // disconnect from remote // step 2 mBumble.rfcommBlocking().disconnect(disconnectRequest) Truth.assertThat(serverSock.channel).isEqualTo(-1) // ensure disconnected at RFCOMM Layer } Loading @@ -422,8 +456,9 @@ class RfcommTest { @RequiresFlagsEnabled(Flags.FLAG_TRIGGER_SEC_PROC_ON_INC_ACCESS_REQ) @Test fun serverSecureConnectThenLocalDisconnect() { // connect // step 1 val (serverSock, _) = connectRemoteToListeningSocket() // step 2 serverSock.close() Truth.assertThat(serverSock.channel).isEqualTo(-1) // ensure disconnected at RFCOMM Layer } Loading @@ -434,7 +469,6 @@ class RfcommTest { uuid: String = TEST_UUID, ): Pair<BluetoothSocket, RfcommProto.RfcommConnection> { val socket = createSocket(mRemoteDevice, isSecure, uuid) val connection = acceptSocket(server) Truth.assertThat(socket.isConnected).isTrue() Loading @@ -452,7 +486,24 @@ class RfcommTest { } else { device.createInsecureRfcommSocketToServiceRecord(UUID.fromString(uuid)) } runBlocking(mScope.coroutineContext) { withTimeout(CONNECT_TIMEOUT.toMillis()) { // We need to reply to the pairing request in the case where the devices aren't // bonded yet if (isSecure && !mAdapter.bondedDevices.contains(device)) { launch { Log.i(TAG, "Waiting for ACTION_PAIRING_REQUEST") mFlow .filter { it.action == BluetoothDevice.ACTION_PAIRING_REQUEST } .filter { it.getBluetoothDeviceExtra() == device } .first() device.setPairingConfirmation(true) } } socket.connect() } } return socket } Loading Loading @@ -526,9 +577,28 @@ class RfcommTest { return proxyCaptor.lastValue } fun Intent.getBluetoothDeviceExtra(): BluetoothDevice = this.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)!! @kotlinx.coroutines.ExperimentalCoroutinesApi fun intentFlow(context: Context, intentFilter: IntentFilter, scope: CoroutineScope) = callbackFlow { val broadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { Log.d(TAG, "intentFlow: onReceive: ${intent.action}") scope.launch { trySendBlocking(intent) } } } context.registerReceiver(broadcastReceiver, intentFilter, Context.RECEIVER_EXPORTED) awaitClose { context.unregisterReceiver(broadcastReceiver) } } companion object { private val TAG = RfcommTest::class.java.getSimpleName() private val GRPC_TIMEOUT = Duration.ofSeconds(10) private val CONNECT_TIMEOUT = Duration.ofSeconds(7) private const val TEST_UUID = "2ac5d8f1-f58d-48ac-a16b-cdeba0892d65" private const val SERIAL_PORT_UUID = "00001101-0000-1000-8000-00805F9B34FB" private const val TEST_SERVER_NAME = "RFCOMM Server" Loading pandora/server/bumble_experimental/bumble_config.py +8 −5 Original line number Diff line number Diff line Loading @@ -20,19 +20,21 @@ from bumble.device import Device from bumble.pairing import PairingConfig from bumble.pairing import PairingDelegate as BasePairingDelegate from bumble.pandora import Config, utils from bumble.pandora.security import PairingDelegate from bumble.pandora.security import PairingDelegate, SecurityService from google.protobuf.empty_pb2 import Empty from pandora.host_pb2 import PUBLIC from pandora_experimental.bumble_config_grpc_aio import BumbleConfigServicer from pandora_experimental.bumble_config_pb2 import (KeyDistribution, OverrideRequest) from pandora_experimental.bumble_config_pb2 import (KeyDistribution, OverrideRequest) class BumbleConfigService(BumbleConfigServicer): device: Device def __init__(self, device: Device, server_config: Config) -> None: self.log = utils.BumbleServerLoggerAdapter(logging.getLogger(), {"service_name": "BumbleConfig", "device": device}) self.log = utils.BumbleServerLoggerAdapter(logging.getLogger(), { "service_name": "BumbleConfig", "device": device, }) self.device = device self.server_config = server_config Loading @@ -50,6 +52,7 @@ class BumbleConfigService(BumbleConfigServicer): def pairing_config_factory(connection: BumbleConnection) -> PairingConfig: pairing_delegate = PairingDelegate( connection=connection, service=SecurityService(self.device, self.server_config), io_capability=BasePairingDelegate.IoCapability(request.io_capability), local_initiator_key_distribution=parseProtoKeyDistribution(request.initiator_key_distribution), local_responder_key_distribution=parseProtoKeyDistribution(request.responder_key_distribution), Loading @@ -61,7 +64,7 @@ class BumbleConfigService(BumbleConfigServicer): mitm=pc_req.mitm, bonding=pc_req.bonding, identity_address_type=PairingConfig.AddressType.PUBLIC if request.identity_address_type == PUBLIC else PairingConfig.AddressType.RANDOM, if pc_req.identity_address_type == PUBLIC else PairingConfig.AddressType.RANDOM, delegate=pairing_delegate, ) self.log.debug(f"Override: {pairing_config}") Loading Loading
framework/tests/bumble/src/android/bluetooth/PandoraDevice.java +11 −0 Original line number Diff line number Diff line Loading @@ -30,6 +30,7 @@ import io.grpc.okhttp.OkHttpChannelBuilder; import org.junit.rules.ExternalResource; import pandora.BumbleConfigGrpc; import pandora.DckGrpc; import pandora.GATTGrpc; import pandora.HIDGrpc; Loading Loading @@ -174,6 +175,16 @@ public final class PandoraDevice extends ExternalResource { return HostGrpc.newBlockingStub(mChannel); } /** Get Pandora BumbleConfig service */ public BumbleConfigGrpc.BumbleConfigStub bumbleConfig() { return BumbleConfigGrpc.newStub(mChannel); } /** Get Pandora BumbleConfig service */ public BumbleConfigGrpc.BumbleConfigBlockingStub bumbleConfigBlocking() { return BumbleConfigGrpc.newBlockingStub(mChannel); } /** Get Pandora HID service */ public HIDGrpc.HIDStub hid() { return HIDGrpc.newStub(mChannel); Loading
framework/tests/bumble/src/android/bluetooth/RfcommTest.kt +88 −18 Original line number Diff line number Diff line Loading @@ -18,7 +18,10 @@ package android.bluetooth import android.Manifest import android.annotation.SuppressLint import android.bluetooth.test_utils.EnableBluetoothRule import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.platform.test.annotations.RequiresFlagsEnabled import android.platform.test.flag.junit.CheckFlagsRule import android.platform.test.flag.junit.DeviceFlagsValueProvider Loading @@ -37,8 +40,16 @@ import java.util.concurrent.TimeUnit import kotlin.concurrent.thread import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.withTimeout import org.junit.After import org.junit.Before import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith Loading @@ -47,6 +58,8 @@ import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.timeout import org.mockito.kotlin.verify import pandora.BumbleConfigProto import pandora.HostProto import pandora.RfcommProto import pandora.RfcommProto.ServerId Loading Loading @@ -79,27 +92,48 @@ class RfcommTest { @Rule(order = 3) @JvmField val enableBluetoothRule = EnableBluetoothRule(false, true) private lateinit var mRemoteDevice: BluetoothDevice private lateinit var host: Host private lateinit var mHost: Host private var mConnectionCounter = 1 private var mProfileServiceListener = mock<BluetoothProfile.ServiceListener>() private val mFlow: Flow<Intent> private val mScope: CoroutineScope = CoroutineScope(Dispatchers.Default.limitedParallelism(2)) @OptIn(ExperimentalStdlibApi::class) private val bdAddrFormat = HexFormat { bytes { byteSeparator = ":" } } @OptIn(ExperimentalStdlibApi::class) private val mLocalAddress: ByteString = ByteString.copyFrom("DA:4C:10:DE:17:00".hexToByteArray(bdAddrFormat)) init { val intentFilter = IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST) mFlow = intentFlow(mContext, intentFilter, mScope).shareIn(mScope, SharingStarted.Eagerly) } /* Setup: 1. Initialize host and mRemoteDevice 2. Disable A2DP, HFP, and HID profiles 3. Bond devices 4. Disconnect devices 2. Override pairing config to enable insecure tests 3. Disable A2DP, HFP, and HID profiles 4. Disconnect devices, if they are connected */ @Before fun setUp() { mRemoteDevice = mBumble.remoteDevice host = Host(mContext) mHost = Host(mContext) // Set Bonding val pairingConfig = BumbleConfigProto.PairingConfig.newBuilder() .setBonding(false) .setMitm(false) .setSc(false) .setIdentityAddressType(HostProto.OwnAddressType.PUBLIC) .build() val overrideRequest = BumbleConfigProto.OverrideRequest.newBuilder().setPairingConfig(pairingConfig).build() mBumble.bumbleConfigBlocking().override(overrideRequest) val bluetoothA2dp = getProfileProxy(mContext, BluetoothProfile.A2DP) as BluetoothA2dp bluetoothA2dp.setConnectionPolicy( mRemoteDevice, Loading @@ -116,23 +150,22 @@ class RfcommTest { mRemoteDevice, BluetoothProfile.CONNECTION_POLICY_FORBIDDEN, ) host.createBondAndVerify(mRemoteDevice) if (mRemoteDevice.isConnected) { host.disconnectAndVerify(mRemoteDevice) mHost.disconnectAndVerify(mRemoteDevice) } } /* TearDown: 1. unbond 1. remove bond 2. shutdown host */ @After fun tearDown() { if (mAdapter.bondedDevices.contains(mRemoteDevice)) { host.removeBondAndVerify(mRemoteDevice) mHost.removeBondAndVerify(mRemoteDevice) } host.close() mHost.close() } /* Loading @@ -142,7 +175,7 @@ class RfcommTest { 3. Verify that devices are connected. */ @Test fun clientConnectToOpenServerSocketBondedInsecure() { fun clientConnectToOpenServerSocketInsecure() { startServer { serverId -> createConnectAcceptSocket(isSecure = false, serverId) } } Loading @@ -153,7 +186,7 @@ class RfcommTest { 3. Verify that devices are connected. */ @Test fun clientConnectToOpenServerSocketBondedSecure() { fun clientConnectToOpenServerSocketSecure() { startServer { serverId -> createConnectAcceptSocket(isSecure = true, serverId) } } Loading Loading @@ -361,13 +394,14 @@ class RfcommTest { 4. Repeat for secure socket 2 */ @Test @Ignore("b/380091558") fun connectTwoMixedClientsInsecureThenSecure() { startServer("ServerPort1", TEST_UUID) { serverId1 -> startServer("ServerPort2", SERIAL_PORT_UUID) { serverId2 -> val socket2 = createSocket(mRemoteDevice, isSecure = false, SERIAL_PORT_UUID) acceptSocket(serverId2) Truth.assertThat(socket2.isConnected).isTrue() Log.i(TAG, "Finished with socket number 2") val socket1 = createSocket(mRemoteDevice, isSecure = true, TEST_UUID) acceptSocket(serverId1) Truth.assertThat(socket1.isConnected).isTrue() Loading Loading @@ -405,11 +439,11 @@ class RfcommTest { @RequiresFlagsEnabled(Flags.FLAG_TRIGGER_SEC_PROC_ON_INC_ACCESS_REQ) @Test fun serverSecureConnectThenRemoteDisconnect() { // connect // step 1 val (serverSock, connection) = connectRemoteToListeningSocket() val disconnectRequest = RfcommProto.DisconnectionRequest.newBuilder().setConnection(connection).build() // disconnect from remote // step 2 mBumble.rfcommBlocking().disconnect(disconnectRequest) Truth.assertThat(serverSock.channel).isEqualTo(-1) // ensure disconnected at RFCOMM Layer } Loading @@ -422,8 +456,9 @@ class RfcommTest { @RequiresFlagsEnabled(Flags.FLAG_TRIGGER_SEC_PROC_ON_INC_ACCESS_REQ) @Test fun serverSecureConnectThenLocalDisconnect() { // connect // step 1 val (serverSock, _) = connectRemoteToListeningSocket() // step 2 serverSock.close() Truth.assertThat(serverSock.channel).isEqualTo(-1) // ensure disconnected at RFCOMM Layer } Loading @@ -434,7 +469,6 @@ class RfcommTest { uuid: String = TEST_UUID, ): Pair<BluetoothSocket, RfcommProto.RfcommConnection> { val socket = createSocket(mRemoteDevice, isSecure, uuid) val connection = acceptSocket(server) Truth.assertThat(socket.isConnected).isTrue() Loading @@ -452,7 +486,24 @@ class RfcommTest { } else { device.createInsecureRfcommSocketToServiceRecord(UUID.fromString(uuid)) } runBlocking(mScope.coroutineContext) { withTimeout(CONNECT_TIMEOUT.toMillis()) { // We need to reply to the pairing request in the case where the devices aren't // bonded yet if (isSecure && !mAdapter.bondedDevices.contains(device)) { launch { Log.i(TAG, "Waiting for ACTION_PAIRING_REQUEST") mFlow .filter { it.action == BluetoothDevice.ACTION_PAIRING_REQUEST } .filter { it.getBluetoothDeviceExtra() == device } .first() device.setPairingConfirmation(true) } } socket.connect() } } return socket } Loading Loading @@ -526,9 +577,28 @@ class RfcommTest { return proxyCaptor.lastValue } fun Intent.getBluetoothDeviceExtra(): BluetoothDevice = this.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)!! @kotlinx.coroutines.ExperimentalCoroutinesApi fun intentFlow(context: Context, intentFilter: IntentFilter, scope: CoroutineScope) = callbackFlow { val broadcastReceiver: BroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { Log.d(TAG, "intentFlow: onReceive: ${intent.action}") scope.launch { trySendBlocking(intent) } } } context.registerReceiver(broadcastReceiver, intentFilter, Context.RECEIVER_EXPORTED) awaitClose { context.unregisterReceiver(broadcastReceiver) } } companion object { private val TAG = RfcommTest::class.java.getSimpleName() private val GRPC_TIMEOUT = Duration.ofSeconds(10) private val CONNECT_TIMEOUT = Duration.ofSeconds(7) private const val TEST_UUID = "2ac5d8f1-f58d-48ac-a16b-cdeba0892d65" private const val SERIAL_PORT_UUID = "00001101-0000-1000-8000-00805F9B34FB" private const val TEST_SERVER_NAME = "RFCOMM Server" Loading
pandora/server/bumble_experimental/bumble_config.py +8 −5 Original line number Diff line number Diff line Loading @@ -20,19 +20,21 @@ from bumble.device import Device from bumble.pairing import PairingConfig from bumble.pairing import PairingDelegate as BasePairingDelegate from bumble.pandora import Config, utils from bumble.pandora.security import PairingDelegate from bumble.pandora.security import PairingDelegate, SecurityService from google.protobuf.empty_pb2 import Empty from pandora.host_pb2 import PUBLIC from pandora_experimental.bumble_config_grpc_aio import BumbleConfigServicer from pandora_experimental.bumble_config_pb2 import (KeyDistribution, OverrideRequest) from pandora_experimental.bumble_config_pb2 import (KeyDistribution, OverrideRequest) class BumbleConfigService(BumbleConfigServicer): device: Device def __init__(self, device: Device, server_config: Config) -> None: self.log = utils.BumbleServerLoggerAdapter(logging.getLogger(), {"service_name": "BumbleConfig", "device": device}) self.log = utils.BumbleServerLoggerAdapter(logging.getLogger(), { "service_name": "BumbleConfig", "device": device, }) self.device = device self.server_config = server_config Loading @@ -50,6 +52,7 @@ class BumbleConfigService(BumbleConfigServicer): def pairing_config_factory(connection: BumbleConnection) -> PairingConfig: pairing_delegate = PairingDelegate( connection=connection, service=SecurityService(self.device, self.server_config), io_capability=BasePairingDelegate.IoCapability(request.io_capability), local_initiator_key_distribution=parseProtoKeyDistribution(request.initiator_key_distribution), local_responder_key_distribution=parseProtoKeyDistribution(request.responder_key_distribution), Loading @@ -61,7 +64,7 @@ class BumbleConfigService(BumbleConfigServicer): mitm=pc_req.mitm, bonding=pc_req.bonding, identity_address_type=PairingConfig.AddressType.PUBLIC if request.identity_address_type == PUBLIC else PairingConfig.AddressType.RANDOM, if pc_req.identity_address_type == PUBLIC else PairingConfig.AddressType.RANDOM, delegate=pairing_delegate, ) self.log.debug(f"Override: {pairing_config}") Loading