summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--framework/tests/bumble/src/android/bluetooth/RfcommTest.kt85
-rw-r--r--pandora/server/bumble_experimental/rfcomm.py45
2 files changed, 122 insertions, 8 deletions
diff --git a/framework/tests/bumble/src/android/bluetooth/RfcommTest.kt b/framework/tests/bumble/src/android/bluetooth/RfcommTest.kt
index 404847df68..9a55a02242 100644
--- a/framework/tests/bumble/src/android/bluetooth/RfcommTest.kt
+++ b/framework/tests/bumble/src/android/bluetooth/RfcommTest.kt
@@ -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
@@ -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)
@@ -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(),
@@ -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
@@ -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,
@@ -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 {
@@ -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>()
diff --git a/pandora/server/bumble_experimental/rfcomm.py b/pandora/server/bumble_experimental/rfcomm.py
index aa59566cf9..2f7c05ed4c 100644
--- a/pandora/server/bumble_experimental/rfcomm.py
+++ b/pandora/server/bumble_experimental/rfcomm.py
@@ -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
@@ -30,6 +33,8 @@ from pandora_experimental.rfcomm_pb2 import (
AcceptConnectionResponse,
ConnectionRequest,
ConnectionResponse,
+ DisconnectionRequest,
+ DisconnectionResponse,
RfcommConnection,
RxRequest,
RxResponse,
@@ -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:
@@ -84,6 +92,27 @@ class RFCOMMService(RFCOMMServicer):
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)
logging.info(f"StartServer {uuid}")
@@ -119,6 +148,16 @@ class RFCOMMService(RFCOMMServicer):
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")
assert self.server_ports[request.server.id] is not None