Loading framework/tests/bumble/src/android/bluetooth/RfcommTest.kt +80 −5 Original line number Diff line number Diff line Loading @@ -19,15 +19,22 @@ import android.Manifest import android.annotation.SuppressLint import android.bluetooth.test_utils.EnableBluetoothRule import android.content.Context import android.platform.test.annotations.RequiresFlagsEnabled import android.platform.test.flag.junit.CheckFlagsRule import android.platform.test.flag.junit.DeviceFlagsValueProvider import android.util.Log import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.android.bluetooth.flags.Flags import com.android.compatibility.common.util.AdoptShellPermissionsRule import com.google.common.truth.Truth import com.google.protobuf.ByteString import java.io.IOException import java.time.Duration import java.util.UUID import java.util.concurrent.TimeUnit import kotlin.concurrent.thread import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import org.junit.After Loading @@ -42,7 +49,6 @@ import org.mockito.kotlin.timeout import org.mockito.kotlin.verify import pandora.RfcommProto import pandora.RfcommProto.ServerId import pandora.RfcommProto.StartServerRequest @SuppressLint("MissingPermission") @RunWith(AndroidJUnit4::class) Loading @@ -52,9 +58,13 @@ class RfcommTest { private val mManager = mContext.getSystemService(BluetoothManager::class.java) private val mAdapter = mManager!!.adapter // Gives shell permissions during the test. @Rule(order = 0) @JvmField val mCheckFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() // Gives shell permissions during the test. @Rule(order = 1) @JvmField val mPermissionsRule = AdoptShellPermissionsRule( InstrumentationRegistry.getInstrumentation().getUiAutomation(), Loading @@ -64,15 +74,21 @@ class RfcommTest { ) // Set up a Bumble Pandora device for the duration of the test. @Rule(order = 1) @JvmField val mBumble = PandoraDevice() @Rule(order = 2) @JvmField val mBumble = PandoraDevice() @Rule(order = 2) @JvmField val enableBluetoothRule = EnableBluetoothRule(false, true) @Rule(order = 3) @JvmField val enableBluetoothRule = EnableBluetoothRule(false, true) private lateinit var mRemoteDevice: BluetoothDevice private lateinit var host: Host private var mConnectionCounter = 1 private var mProfileServiceListener = mock<BluetoothProfile.ServiceListener>() @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)) /* Setup: 1. Initialize host and mRemoteDevice Loading Loading @@ -381,6 +397,37 @@ class RfcommTest { } } /* Test Steps: 1. Create listening socket and connect 2. Disconnect RFCOMM from remote device */ @RequiresFlagsEnabled(Flags.FLAG_TRIGGER_SEC_PROC_ON_INC_ACCESS_REQ) @Test fun serverSecureConnectThenRemoteDisconnect() { // connect val (serverSock, connection) = connectRemoteToListeningSocket() val disconnectRequest = RfcommProto.DisconnectionRequest.newBuilder().setConnection(connection).build() // disconnect from remote mBumble.rfcommBlocking().disconnect(disconnectRequest) Truth.assertThat(serverSock.channel).isEqualTo(-1) // ensure disconnected at RFCOMM Layer } /* Test Steps: 1. Create listening socket and connect 2. Disconnect RFCOMM from local device */ @RequiresFlagsEnabled(Flags.FLAG_TRIGGER_SEC_PROC_ON_INC_ACCESS_REQ) @Test fun serverSecureConnectThenLocalDisconnect() { // connect val (serverSock, _) = connectRemoteToListeningSocket() serverSock.close() Truth.assertThat(serverSock.channel).isEqualTo(-1) // ensure disconnected at RFCOMM Layer } private fun createConnectAcceptSocket( isSecure: Boolean, server: ServerId, Loading Loading @@ -428,7 +475,8 @@ class RfcommTest { uuid: String = TEST_UUID, block: (ServerId) -> Unit, ) { val request = StartServerRequest.newBuilder().setName(name).setUuid(uuid).build() val request = RfcommProto.StartServerRequest.newBuilder().setName(name).setUuid(uuid).build() val response = mBumble.rfcommBlocking().startServer(request) try { Loading @@ -443,6 +491,33 @@ class RfcommTest { } } private fun connectRemoteToListeningSocket( name: String = TEST_SERVER_NAME, uuid: String = TEST_UUID, ): Pair<BluetoothServerSocket, RfcommProto.RfcommConnection> { var connection: RfcommProto.RfcommConnection? = null val connectRequest = RfcommProto.ConnectionRequest.newBuilder() .setAddress(mLocalAddress) .setUuid(uuid) .build() val t = thread { val connectResponse = mBumble.rfcommBlocking().connectToServer(connectRequest) connection = connectResponse.connection } val socket = mAdapter.listenUsingRfcommWithServiceRecord(name, UUID.fromString(uuid)) try { socket.accept(3000) // 3 second timeout } catch (e: IOException) { Log.e(TAG, "Unexpected IOException: $e") } t.join() Truth.assertThat(connection).isNotNull() return Pair(socket, connection!!) } private fun getProfileProxy(context: Context, profile: Int): BluetoothProfile { mAdapter.getProfileProxy(context, mProfileServiceListener, profile) val proxyCaptor = argumentCaptor<BluetoothProfile>() Loading pandora/server/bumble_experimental/rfcomm.py +42 −3 Original line number Diff line number Diff line Loading @@ -17,10 +17,13 @@ from typing import Dict, Optional from bumble import core from bumble.device import Device from bumble.hci import Address from bumble.rfcomm import ( Server, make_service_sdp_records, Client, DLC, make_service_sdp_records, find_rfcomm_channel_with_uuid, Server, ) from bumble.pandora import utils import grpc Loading @@ -30,6 +33,8 @@ from pandora_experimental.rfcomm_pb2 import ( AcceptConnectionResponse, ConnectionRequest, ConnectionResponse, DisconnectionRequest, DisconnectionResponse, RfcommConnection, RxRequest, RxResponse, Loading Loading @@ -59,9 +64,12 @@ class RFCOMMService(RFCOMMServicer): class Connection: def __init__(self, dlc): client: Optional[Client] def __init__(self, dlc, client=None): self.dlc = dlc self.data_queue = asyncio.Queue() self.client = client class ServerPort: Loading @@ -83,6 +91,27 @@ class RFCOMMService(RFCOMMServicer): else: self.saved_dlc = dlc @utils.rpc async def ConnectToServer(self, request: ConnectionRequest, context: grpc.ServicerContext) -> ConnectionResponse: logging.info(f"ConnectToServer") address = Address(address=bytes(reversed(request.address)), address_type=Address.PUBLIC_DEVICE_ADDRESS) acl_connection = self.device.find_connection_by_bd_addr(address, transport=0) # BR/EDR if acl_connection is None: acl_connection = await self.device.connect(address, transport=0) # BR/EDR transport channel = await find_rfcomm_channel_with_uuid(acl_connection, request.uuid) client = Client(acl_connection) mux = await client.start() assert mux is not None dlc = await mux.open_dlc(channel) id = self.next_conn_id self.next_conn_id += 1 self.connections[id] = self.Connection(dlc=dlc, client=client) self.connections[id].dlc.sink = self.connections[id].data_queue.put_nowait return ConnectionResponse(connection=RfcommConnection(id=id)) @utils.rpc async def StartServer(self, request: StartServerRequest, context: grpc.ServicerContext) -> StartServerResponse: uuid = core.UUID(request.uuid) Loading Loading @@ -118,6 +147,16 @@ class RFCOMMService(RFCOMMServicer): self.connections[id].dlc.sink = self.connections[id].data_queue.put_nowait return AcceptConnectionResponse(connection=RfcommConnection(id=id)) @utils.rpc async def Disconnect(self, request: DisconnectionRequest, context: grpc.ServicerContext) -> DisconnectionResponse: logging.info(f"Disconnect") rfcomm_connection = self.connections[request.connection.id] assert rfcomm_connection is not None if rfcomm_connection.client is not None: await rfcomm_connection.client.shutdown() del rfcomm_connection return DisconnectionResponse() @utils.rpc async def StopServer(self, request: StopServerRequest, context: grpc.ServicerContext) -> StopServerResponse: logging.info(f"StopServer") Loading Loading
framework/tests/bumble/src/android/bluetooth/RfcommTest.kt +80 −5 Original line number Diff line number Diff line Loading @@ -19,15 +19,22 @@ import android.Manifest import android.annotation.SuppressLint import android.bluetooth.test_utils.EnableBluetoothRule import android.content.Context import android.platform.test.annotations.RequiresFlagsEnabled import android.platform.test.flag.junit.CheckFlagsRule import android.platform.test.flag.junit.DeviceFlagsValueProvider import android.util.Log import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.android.bluetooth.flags.Flags import com.android.compatibility.common.util.AdoptShellPermissionsRule import com.google.common.truth.Truth import com.google.protobuf.ByteString import java.io.IOException import java.time.Duration import java.util.UUID import java.util.concurrent.TimeUnit import kotlin.concurrent.thread import kotlinx.coroutines.* import kotlinx.coroutines.channels.* import org.junit.After Loading @@ -42,7 +49,6 @@ import org.mockito.kotlin.timeout import org.mockito.kotlin.verify import pandora.RfcommProto import pandora.RfcommProto.ServerId import pandora.RfcommProto.StartServerRequest @SuppressLint("MissingPermission") @RunWith(AndroidJUnit4::class) Loading @@ -52,9 +58,13 @@ class RfcommTest { private val mManager = mContext.getSystemService(BluetoothManager::class.java) private val mAdapter = mManager!!.adapter // Gives shell permissions during the test. @Rule(order = 0) @JvmField val mCheckFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() // Gives shell permissions during the test. @Rule(order = 1) @JvmField val mPermissionsRule = AdoptShellPermissionsRule( InstrumentationRegistry.getInstrumentation().getUiAutomation(), Loading @@ -64,15 +74,21 @@ class RfcommTest { ) // Set up a Bumble Pandora device for the duration of the test. @Rule(order = 1) @JvmField val mBumble = PandoraDevice() @Rule(order = 2) @JvmField val mBumble = PandoraDevice() @Rule(order = 2) @JvmField val enableBluetoothRule = EnableBluetoothRule(false, true) @Rule(order = 3) @JvmField val enableBluetoothRule = EnableBluetoothRule(false, true) private lateinit var mRemoteDevice: BluetoothDevice private lateinit var host: Host private var mConnectionCounter = 1 private var mProfileServiceListener = mock<BluetoothProfile.ServiceListener>() @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)) /* Setup: 1. Initialize host and mRemoteDevice Loading Loading @@ -381,6 +397,37 @@ class RfcommTest { } } /* Test Steps: 1. Create listening socket and connect 2. Disconnect RFCOMM from remote device */ @RequiresFlagsEnabled(Flags.FLAG_TRIGGER_SEC_PROC_ON_INC_ACCESS_REQ) @Test fun serverSecureConnectThenRemoteDisconnect() { // connect val (serverSock, connection) = connectRemoteToListeningSocket() val disconnectRequest = RfcommProto.DisconnectionRequest.newBuilder().setConnection(connection).build() // disconnect from remote mBumble.rfcommBlocking().disconnect(disconnectRequest) Truth.assertThat(serverSock.channel).isEqualTo(-1) // ensure disconnected at RFCOMM Layer } /* Test Steps: 1. Create listening socket and connect 2. Disconnect RFCOMM from local device */ @RequiresFlagsEnabled(Flags.FLAG_TRIGGER_SEC_PROC_ON_INC_ACCESS_REQ) @Test fun serverSecureConnectThenLocalDisconnect() { // connect val (serverSock, _) = connectRemoteToListeningSocket() serverSock.close() Truth.assertThat(serverSock.channel).isEqualTo(-1) // ensure disconnected at RFCOMM Layer } private fun createConnectAcceptSocket( isSecure: Boolean, server: ServerId, Loading Loading @@ -428,7 +475,8 @@ class RfcommTest { uuid: String = TEST_UUID, block: (ServerId) -> Unit, ) { val request = StartServerRequest.newBuilder().setName(name).setUuid(uuid).build() val request = RfcommProto.StartServerRequest.newBuilder().setName(name).setUuid(uuid).build() val response = mBumble.rfcommBlocking().startServer(request) try { Loading @@ -443,6 +491,33 @@ class RfcommTest { } } private fun connectRemoteToListeningSocket( name: String = TEST_SERVER_NAME, uuid: String = TEST_UUID, ): Pair<BluetoothServerSocket, RfcommProto.RfcommConnection> { var connection: RfcommProto.RfcommConnection? = null val connectRequest = RfcommProto.ConnectionRequest.newBuilder() .setAddress(mLocalAddress) .setUuid(uuid) .build() val t = thread { val connectResponse = mBumble.rfcommBlocking().connectToServer(connectRequest) connection = connectResponse.connection } val socket = mAdapter.listenUsingRfcommWithServiceRecord(name, UUID.fromString(uuid)) try { socket.accept(3000) // 3 second timeout } catch (e: IOException) { Log.e(TAG, "Unexpected IOException: $e") } t.join() Truth.assertThat(connection).isNotNull() return Pair(socket, connection!!) } private fun getProfileProxy(context: Context, profile: Int): BluetoothProfile { mAdapter.getProfileProxy(context, mProfileServiceListener, profile) val proxyCaptor = argumentCaptor<BluetoothProfile>() Loading
pandora/server/bumble_experimental/rfcomm.py +42 −3 Original line number Diff line number Diff line Loading @@ -17,10 +17,13 @@ from typing import Dict, Optional from bumble import core from bumble.device import Device from bumble.hci import Address from bumble.rfcomm import ( Server, make_service_sdp_records, Client, DLC, make_service_sdp_records, find_rfcomm_channel_with_uuid, Server, ) from bumble.pandora import utils import grpc Loading @@ -30,6 +33,8 @@ from pandora_experimental.rfcomm_pb2 import ( AcceptConnectionResponse, ConnectionRequest, ConnectionResponse, DisconnectionRequest, DisconnectionResponse, RfcommConnection, RxRequest, RxResponse, Loading Loading @@ -59,9 +64,12 @@ class RFCOMMService(RFCOMMServicer): class Connection: def __init__(self, dlc): client: Optional[Client] def __init__(self, dlc, client=None): self.dlc = dlc self.data_queue = asyncio.Queue() self.client = client class ServerPort: Loading @@ -83,6 +91,27 @@ class RFCOMMService(RFCOMMServicer): else: self.saved_dlc = dlc @utils.rpc async def ConnectToServer(self, request: ConnectionRequest, context: grpc.ServicerContext) -> ConnectionResponse: logging.info(f"ConnectToServer") address = Address(address=bytes(reversed(request.address)), address_type=Address.PUBLIC_DEVICE_ADDRESS) acl_connection = self.device.find_connection_by_bd_addr(address, transport=0) # BR/EDR if acl_connection is None: acl_connection = await self.device.connect(address, transport=0) # BR/EDR transport channel = await find_rfcomm_channel_with_uuid(acl_connection, request.uuid) client = Client(acl_connection) mux = await client.start() assert mux is not None dlc = await mux.open_dlc(channel) id = self.next_conn_id self.next_conn_id += 1 self.connections[id] = self.Connection(dlc=dlc, client=client) self.connections[id].dlc.sink = self.connections[id].data_queue.put_nowait return ConnectionResponse(connection=RfcommConnection(id=id)) @utils.rpc async def StartServer(self, request: StartServerRequest, context: grpc.ServicerContext) -> StartServerResponse: uuid = core.UUID(request.uuid) Loading Loading @@ -118,6 +147,16 @@ class RFCOMMService(RFCOMMServicer): self.connections[id].dlc.sink = self.connections[id].data_queue.put_nowait return AcceptConnectionResponse(connection=RfcommConnection(id=id)) @utils.rpc async def Disconnect(self, request: DisconnectionRequest, context: grpc.ServicerContext) -> DisconnectionResponse: logging.info(f"Disconnect") rfcomm_connection = self.connections[request.connection.id] assert rfcomm_connection is not None if rfcomm_connection.client is not None: await rfcomm_connection.client.shutdown() del rfcomm_connection return DisconnectionResponse() @utils.rpc async def StopServer(self, request: StopServerRequest, context: grpc.ServicerContext) -> StopServerResponse: logging.info(f"StopServer") Loading