Loading android/pandora/mmi2grpc/mmi2grpc/l2cap.py +31 −36 Original line number Diff line number Diff line Loading @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. import time import sys from mmi2grpc._helpers import assert_description Loading @@ -22,15 +21,16 @@ from mmi2grpc._rootcanal import Dongle from pandora.host_grpc import Host from pandora.host_pb2 import PUBLIC, RANDOM, Connection from pandora.l2cap_grpc import L2CAP from pandora.l2cap_pb2 import CreditBasedChannelRequest from pandora.security_pb2 import PairingEventAnswer from pandora.security_grpc import Security from pandora_experimental.l2cap_grpc import L2CAP from typing import Optional from typing import Optional, Dict class L2CAPProxy(ProfileProxy): test_status_map = {} # record test status and pass them between MMI test_status_map: Dict[str, str] = {} # record test status and pass them between MMI LE_DATA_PACKET_LARGE = "data: LE_DATA_PACKET_LARGE" LE_DATA_PACKET1 = "data: LE_PACKET1" connection: Optional[Connection] = None Loading @@ -44,6 +44,7 @@ class L2CAPProxy(ProfileProxy): self.connection = None self.pairing_events = None self.channel = None def test_started(self, test: str, **kwargs): self.rootcanal.select_pts_dongle(Dongle.CSR_RCK_PTS_DONGLE) Loading @@ -63,16 +64,12 @@ class L2CAPProxy(ProfileProxy): tests_target_to_fail = [ 'L2CAP/LE/CFC/BV-01-C', 'L2CAP/LE/CFC/BV-04-C', 'L2CAP/LE/CFC/BV-10-C', 'L2CAP/LE/CFC/BV-11-C', 'L2CAP/LE/CFC/BV-12-C', 'L2CAP/LE/CFC/BV-14-C', 'L2CAP/LE/CFC/BV-16-C', 'L2CAP/LE/CFC/BV-18-C', 'L2CAP/LE/CFC/BV-19-C', "L2CAP/LE/CFC/BV-21-C", ] tests_require_secure_connection = [] # This MMI is called twice in 'L2CAP/LE/CFC/BV-04-C' # We are not sure whether the lower tester’s BluetoothServerSocket Loading @@ -97,10 +94,13 @@ class L2CAPProxy(ProfileProxy): if test == 'L2CAP/LE/CFC/BV-12-C': psm = 0xF3 # default TSPX_psm_authorization_required value secure_connection = test in tests_require_secure_connection try: self.l2cap.CreateLECreditBasedChannel(connection=self.connection, psm=psm, secure=secure_connection) connect_response = self.l2cap.Connect(connection=self.connection, le_credit_based=CreditBasedChannelRequest(spsm=psm)) if connect_response.HasField('channel'): self.channel = connect_response.channel else: raise Exception(connect_response.error) except Exception as e: if test in tests_target_to_fail: self.test_status_map[test] = 'OK' Loading @@ -117,11 +117,13 @@ class L2CAPProxy(ProfileProxy): """ Place the IUT into LE connectable mode. """ self.advertise = self.host.Advertise( legacy=True, connectable=True, own_address_type=PUBLIC, ) # not strictly necessary, but can save time on waiting connection tests_to_open_bluetooth_server_socket = [ "L2CAP/COS/CFC/BV-01-C", Loading @@ -129,31 +131,19 @@ class L2CAPProxy(ProfileProxy): "L2CAP/COS/CFC/BV-03-C", "L2CAP/COS/CFC/BV-04-C", "L2CAP/LE/CFC/BV-03-C", "L2CAP/LE/CFC/BV-05-C", "L2CAP/LE/CFC/BV-06-C", "L2CAP/LE/CFC/BV-09-C", "L2CAP/LE/CFC/BV-13-C", "L2CAP/LE/CFC/BV-20-C", "L2CAP/LE/CFC/BI-01-C", ] tests_require_secure_connection = [ "L2CAP/LE/CFC/BV-13-C", ] tests_connection_target_to_failed = [ "L2CAP/LE/CFC/BV-05-C", ] if test in tests_to_open_bluetooth_server_socket: secure_connection = test in tests_require_secure_connection self.l2cap.ListenL2CAPChannel(connection=self.connection, secure=secure_connection) try: self.l2cap.AcceptL2CAPChannel(connection=self.connection) except Exception as e: if test in tests_connection_target_to_failed: self.test_status_map[test] = 'OK' print(test, 'connection targets to fail', file=sys.stderr) wait_connection_response = self.l2cap.WaitConnection(le_credit_based=CreditBasedChannelRequest(spsm=0)) if wait_connection_response.HasField('channel'): self.channel = wait_connection_response.channel else: raise e raise Exception(wait_connection_response.error) return "OK" @assert_description Loading @@ -171,7 +161,8 @@ class L2CAPProxy(ProfileProxy): # all data frames arrived # it seemed like when the time gap between the 1st frame and 2nd frame # larger than 100ms this problem will occur self.l2cap.SendData(connection=self.connection, data=bytes(self.LE_DATA_PACKET_LARGE, "utf-8")) assert self.channel self.l2cap.Send(channel=self.channel, data=bytes(self.LE_DATA_PACKET_LARGE, "utf-8")) return "OK" @match_description Loading @@ -198,8 +189,9 @@ class L2CAPProxy(ProfileProxy): Upper Tester command IUT to send at least 4 frames of LE data packets to the PTS. """ self.l2cap.SendData( connection=self.connection, assert self.channel self.l2cap.Send( channel=self.channel, data=b"this is a large data package with at least 4 frames: MMI_UPPER_TESTER_SEND_LE_DATA_PACKET_LARGE") return "OK" Loading @@ -208,8 +200,9 @@ class L2CAPProxy(ProfileProxy): """ IUT continue to send LE data packet(s) to the PTS. """ self.l2cap.SendData( connection=self.connection, assert self.channel self.l2cap.Send( channel=self.channel, data=b"this is a large data package with at least 4 frames: MMI_UPPER_TESTER_SEND_LE_DATA_PACKET_LARGE") return "OK" Loading @@ -232,7 +225,8 @@ class L2CAPProxy(ProfileProxy): """ Please confirm the Upper Tester receive data """ data = self.l2cap.ReceiveData(connection=self.connection) assert self.channel data = next(self.l2cap.Receive(channel=self.channel)).data assert data, "data received should not be empty" return "OK" Loading Loading @@ -504,7 +498,8 @@ class L2CAPProxy(ProfileProxy): Description : The Implementation Under Test(IUT) should send none segmantation LE frame of LE data to the PTS. """ self.l2cap.SendData(connection=self.connection, data=bytes(self.LE_DATA_PACKET1, "utf-8")) assert self.channel self.l2cap.Send(channel=self.channel, data=bytes(self.LE_DATA_PACKET1, "utf-8")) return "OK" @assert_description Loading android/pandora/server/configs/pts_bot_tests_config.json +1 −0 Original line number Diff line number Diff line Loading @@ -1048,6 +1048,7 @@ "L2CAP/CMC/BV-13-C", "L2CAP/CMC/BV-14-C", "L2CAP/CMC/BV-15-C", "L2CAP/COS/CED/BI-02-C", "L2CAP/COS/CED/BV-10-C", "L2CAP/COS/CED/BV-12-C", "L2CAP/COS/CED/BV-13-C", Loading android/pandora/server/src/L2cap.kt +95 −116 Original line number Diff line number Diff line Loading @@ -16,46 +16,35 @@ package com.android.pandora import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothServerSocket import android.bluetooth.BluetoothSocket import android.content.Context import android.util.Log import com.google.protobuf.Any import com.google.protobuf.ByteString import io.grpc.stub.StreamObserver import java.io.Closeable import java.io.IOException import java.io.InputStream import java.io.OutputStream import java.util.concurrent.atomic.AtomicLong import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.withContext import pandora.HostProto.Connection import pandora.L2CAPGrpc.L2CAPImplBase import pandora.L2capProto.AcceptL2CAPChannelRequest import pandora.L2capProto.AcceptL2CAPChannelResponse import pandora.L2capProto.CreateLECreditBasedChannelRequest import pandora.L2capProto.CreateLECreditBasedChannelResponse import pandora.L2capProto.ListenL2CAPChannelRequest import pandora.L2capProto.ListenL2CAPChannelResponse import pandora.L2capProto.ReceiveDataRequest import pandora.L2capProto.ReceiveDataResponse import pandora.L2capProto.SendDataRequest import pandora.L2capProto.SendDataResponse import kotlinx.coroutines.flow.flow import pandora.l2cap.L2CAPGrpc.L2CAPImplBase import pandora.l2cap.L2CAPProto.* @kotlinx.coroutines.ExperimentalCoroutinesApi class L2cap(val context: Context) : L2CAPImplBase(), Closeable { private val TAG = "PandoraL2cap" private val scope: CoroutineScope private val BLUETOOTH_SERVER_SOCKET_TIMEOUT: Int = 10000 private val channelIdCounter = AtomicLong(1) private val BUFFER_SIZE = 512 private val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager private val bluetoothAdapter = bluetoothManager.adapter private var connectionInStreamMap: HashMap<Connection, InputStream> = hashMapOf() private var connectionOutStreamMap: HashMap<Connection, OutputStream> = hashMapOf() private var connectionServerSocketMap: HashMap<Connection, BluetoothServerSocket> = hashMapOf() private val channels: HashMap<Long, BluetoothSocket> = hashMapOf() init { // Init the CoroutineScope Loading @@ -67,127 +56,117 @@ class L2cap(val context: Context) : L2CAPImplBase(), Closeable { scope.cancel() } suspend fun receive(inStream: InputStream): ByteArray { return withContext(Dispatchers.IO) { val buf = ByteArray(BUFFER_SIZE) inStream.read(buf, 0, BUFFER_SIZE) // blocking Log.i(TAG, "receive: $buf") buf } } /** Open a BluetoothServerSocket to accept connections */ override fun listenL2CAPChannel( request: ListenL2CAPChannelRequest, responseObserver: StreamObserver<ListenL2CAPChannelResponse>, override fun connect( request: ConnectRequest, responseObserver: StreamObserver<ConnectResponse>, ) { grpcUnary(scope, responseObserver) { Log.i(TAG, "listenL2CAPChannel: secure=${request.secure}") val connection = request.connection val bluetoothServerSocket = if (request.secure) { bluetoothAdapter.listenUsingL2capChannel() } else { bluetoothAdapter.listenUsingInsecureL2capChannel() val device = request.connection.toBluetoothDevice(bluetoothAdapter) val psm = when { request.hasBasic() -> request.basic.psm request.hasLeCreditBased() -> request.leCreditBased.spsm request.hasEnhancedCreditBased() -> request.enhancedCreditBased.spsm else -> throw RuntimeException("unreachable") } connectionServerSocketMap[connection] = bluetoothServerSocket ListenL2CAPChannelResponse.newBuilder().build() Log.i(TAG, "connect: $device psm: $psm") val bluetoothSocket = device.createInsecureL2capChannel(psm) bluetoothSocket.connect() val channelId = getNewChannelId() channels.put(channelId, bluetoothSocket) Log.d(TAG, "connect: channelId=$channelId") ConnectResponse.newBuilder().setChannel(craftChannel(channelId)).build() } } override fun acceptL2CAPChannel( request: AcceptL2CAPChannelRequest, responseObserver: StreamObserver<AcceptL2CAPChannelResponse>, override fun waitConnection( request: WaitConnectionRequest, responseObserver: StreamObserver<WaitConnectionResponse>, ) { grpcUnary(scope, responseObserver) { Log.i(TAG, "acceptL2CAPChannel") val connection = request.connection val bluetoothServerSocket = connectionServerSocketMap[connection] val device: BluetoothDevice? = try { val bluetoothSocket = bluetoothServerSocket!!.accept(BLUETOOTH_SERVER_SOCKET_TIMEOUT) connectionInStreamMap[connection] = bluetoothSocket.getInputStream()!! connectionOutStreamMap[connection] = bluetoothSocket.getOutputStream()!! } catch (e: IOException) { Log.e(TAG, "bluetoothServerSocket not accepted", e) throw e request.connection.toBluetoothDevice(bluetoothAdapter) } catch (e: Exception) { Log.w(TAG, e) null } AcceptL2CAPChannelResponse.newBuilder().build() } Log.i(TAG, "waitConnection: $device") val psm = when { request.hasBasic() -> request.basic.psm request.hasLeCreditBased() -> request.leCreditBased.spsm request.hasEnhancedCreditBased() -> request.enhancedCreditBased.spsm else -> throw RuntimeException("unreachable") } /** Set device to send LE based connection request */ override fun createLECreditBasedChannel( request: CreateLECreditBasedChannelRequest, responseObserver: StreamObserver<CreateLECreditBasedChannelResponse>, ) { // Creates a gRPC coroutine in a given coroutine scope which executes a given suspended // function // returning a gRPC response and sends it on a given gRPC stream observer. grpcUnary(scope, responseObserver) { Log.i(TAG, "createLECreditBasedChannel: secure=${request.secure}, psm=${request.psm}") val connection = request.connection val device = request.connection.toBluetoothDevice(bluetoothAdapter) val psm = request.psm var bluetoothSocket: BluetoothSocket? try { val bluetoothSocket = if (request.secure) { device.createL2capChannel(psm) while (true) { val bluetoothServerSocket = if (psm == 0) { bluetoothAdapter.listenUsingInsecureL2capChannel() } else { device.createInsecureL2capChannel(psm) bluetoothAdapter.listenUsingInsecureL2capOn(psm) } bluetoothSocket.connect() connectionInStreamMap[connection] = bluetoothSocket.getInputStream()!! connectionOutStreamMap[connection] = bluetoothSocket.getOutputStream()!! } catch (e: IOException) { Log.d(TAG, "bluetoothSocket not connected: $e") throw e bluetoothSocket = bluetoothServerSocket.accept() bluetoothServerSocket.close() if (device != null && !bluetoothSocket.getRemoteDevice().equals(device)) continue break } // Response sent to client CreateLECreditBasedChannelResponse.newBuilder().build() val channelId = getNewChannelId() channels.put(channelId, bluetoothSocket!!) Log.d(TAG, "waitConnection: channelId=$channelId") WaitConnectionResponse.newBuilder().setChannel(craftChannel(channelId)).build() } } /** send data packet */ override fun sendData( request: SendDataRequest, responseObserver: StreamObserver<SendDataResponse>, ) { override fun send(request: SendRequest, responseObserver: StreamObserver<SendResponse>) { grpcUnary(scope, responseObserver) { Log.i(TAG, "sendDataPacket: data=${request.data}") val buffer = request.data!!.toByteArray() val connection = request.connection val outputStream = connectionOutStreamMap[connection]!! Log.i(TAG, "send") val bluetoothSocket = request.channel.toBluetoothSocket(channels) val outputStream = bluetoothSocket.outputStream withContext(Dispatchers.IO) { try { outputStream.write(buffer) outputStream.write(request.data.toByteArray()) outputStream.flush() } catch (e: IOException) { Log.e(TAG, "Exception during writing to sendDataPacket output stream", e) SendResponse.newBuilder().build() } } // Response sent to client SendDataResponse.newBuilder().build() override fun receive( request: ReceiveRequest, responseObserver: StreamObserver<ReceiveResponse>, ) { Log.i(TAG, "receive") val bluetoothSocket = request.channel.toBluetoothSocket(channels) val inputStream = bluetoothSocket.inputStream grpcServerStream(scope, responseObserver) { flow { val buffer = ByteArray(BUFFER_SIZE) inputStream.read(buffer, 0, BUFFER_SIZE) val data = ByteString.copyFrom(buffer) val response = ReceiveResponse.newBuilder().setData(data).build() emit(response) } } } override fun receiveData( request: ReceiveDataRequest, responseObserver: StreamObserver<ReceiveDataResponse>, ) { grpcUnary(scope, responseObserver) { Log.i(TAG, "receiveData") val connection = request.connection val inputStream = connectionInStreamMap[connection]!! val buf = receive(inputStream) fun getNewChannelId(): Long = channelIdCounter.getAndIncrement() ReceiveDataResponse.newBuilder().setData(ByteString.copyFrom(buf)).build() } fun craftChannel(id: Long): Channel { val cookie = Any.newBuilder().setValue(ByteString.copyFromUtf8(id.toString())).build() val channel = Channel.newBuilder().setCookie(cookie).build() return channel } fun Channel.toBluetoothSocket(channels: HashMap<Long, BluetoothSocket>): BluetoothSocket = channels.get(this.cookie.value.toStringUtf8().toLong())!! } Loading
android/pandora/mmi2grpc/mmi2grpc/l2cap.py +31 −36 Original line number Diff line number Diff line Loading @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. import time import sys from mmi2grpc._helpers import assert_description Loading @@ -22,15 +21,16 @@ from mmi2grpc._rootcanal import Dongle from pandora.host_grpc import Host from pandora.host_pb2 import PUBLIC, RANDOM, Connection from pandora.l2cap_grpc import L2CAP from pandora.l2cap_pb2 import CreditBasedChannelRequest from pandora.security_pb2 import PairingEventAnswer from pandora.security_grpc import Security from pandora_experimental.l2cap_grpc import L2CAP from typing import Optional from typing import Optional, Dict class L2CAPProxy(ProfileProxy): test_status_map = {} # record test status and pass them between MMI test_status_map: Dict[str, str] = {} # record test status and pass them between MMI LE_DATA_PACKET_LARGE = "data: LE_DATA_PACKET_LARGE" LE_DATA_PACKET1 = "data: LE_PACKET1" connection: Optional[Connection] = None Loading @@ -44,6 +44,7 @@ class L2CAPProxy(ProfileProxy): self.connection = None self.pairing_events = None self.channel = None def test_started(self, test: str, **kwargs): self.rootcanal.select_pts_dongle(Dongle.CSR_RCK_PTS_DONGLE) Loading @@ -63,16 +64,12 @@ class L2CAPProxy(ProfileProxy): tests_target_to_fail = [ 'L2CAP/LE/CFC/BV-01-C', 'L2CAP/LE/CFC/BV-04-C', 'L2CAP/LE/CFC/BV-10-C', 'L2CAP/LE/CFC/BV-11-C', 'L2CAP/LE/CFC/BV-12-C', 'L2CAP/LE/CFC/BV-14-C', 'L2CAP/LE/CFC/BV-16-C', 'L2CAP/LE/CFC/BV-18-C', 'L2CAP/LE/CFC/BV-19-C', "L2CAP/LE/CFC/BV-21-C", ] tests_require_secure_connection = [] # This MMI is called twice in 'L2CAP/LE/CFC/BV-04-C' # We are not sure whether the lower tester’s BluetoothServerSocket Loading @@ -97,10 +94,13 @@ class L2CAPProxy(ProfileProxy): if test == 'L2CAP/LE/CFC/BV-12-C': psm = 0xF3 # default TSPX_psm_authorization_required value secure_connection = test in tests_require_secure_connection try: self.l2cap.CreateLECreditBasedChannel(connection=self.connection, psm=psm, secure=secure_connection) connect_response = self.l2cap.Connect(connection=self.connection, le_credit_based=CreditBasedChannelRequest(spsm=psm)) if connect_response.HasField('channel'): self.channel = connect_response.channel else: raise Exception(connect_response.error) except Exception as e: if test in tests_target_to_fail: self.test_status_map[test] = 'OK' Loading @@ -117,11 +117,13 @@ class L2CAPProxy(ProfileProxy): """ Place the IUT into LE connectable mode. """ self.advertise = self.host.Advertise( legacy=True, connectable=True, own_address_type=PUBLIC, ) # not strictly necessary, but can save time on waiting connection tests_to_open_bluetooth_server_socket = [ "L2CAP/COS/CFC/BV-01-C", Loading @@ -129,31 +131,19 @@ class L2CAPProxy(ProfileProxy): "L2CAP/COS/CFC/BV-03-C", "L2CAP/COS/CFC/BV-04-C", "L2CAP/LE/CFC/BV-03-C", "L2CAP/LE/CFC/BV-05-C", "L2CAP/LE/CFC/BV-06-C", "L2CAP/LE/CFC/BV-09-C", "L2CAP/LE/CFC/BV-13-C", "L2CAP/LE/CFC/BV-20-C", "L2CAP/LE/CFC/BI-01-C", ] tests_require_secure_connection = [ "L2CAP/LE/CFC/BV-13-C", ] tests_connection_target_to_failed = [ "L2CAP/LE/CFC/BV-05-C", ] if test in tests_to_open_bluetooth_server_socket: secure_connection = test in tests_require_secure_connection self.l2cap.ListenL2CAPChannel(connection=self.connection, secure=secure_connection) try: self.l2cap.AcceptL2CAPChannel(connection=self.connection) except Exception as e: if test in tests_connection_target_to_failed: self.test_status_map[test] = 'OK' print(test, 'connection targets to fail', file=sys.stderr) wait_connection_response = self.l2cap.WaitConnection(le_credit_based=CreditBasedChannelRequest(spsm=0)) if wait_connection_response.HasField('channel'): self.channel = wait_connection_response.channel else: raise e raise Exception(wait_connection_response.error) return "OK" @assert_description Loading @@ -171,7 +161,8 @@ class L2CAPProxy(ProfileProxy): # all data frames arrived # it seemed like when the time gap between the 1st frame and 2nd frame # larger than 100ms this problem will occur self.l2cap.SendData(connection=self.connection, data=bytes(self.LE_DATA_PACKET_LARGE, "utf-8")) assert self.channel self.l2cap.Send(channel=self.channel, data=bytes(self.LE_DATA_PACKET_LARGE, "utf-8")) return "OK" @match_description Loading @@ -198,8 +189,9 @@ class L2CAPProxy(ProfileProxy): Upper Tester command IUT to send at least 4 frames of LE data packets to the PTS. """ self.l2cap.SendData( connection=self.connection, assert self.channel self.l2cap.Send( channel=self.channel, data=b"this is a large data package with at least 4 frames: MMI_UPPER_TESTER_SEND_LE_DATA_PACKET_LARGE") return "OK" Loading @@ -208,8 +200,9 @@ class L2CAPProxy(ProfileProxy): """ IUT continue to send LE data packet(s) to the PTS. """ self.l2cap.SendData( connection=self.connection, assert self.channel self.l2cap.Send( channel=self.channel, data=b"this is a large data package with at least 4 frames: MMI_UPPER_TESTER_SEND_LE_DATA_PACKET_LARGE") return "OK" Loading @@ -232,7 +225,8 @@ class L2CAPProxy(ProfileProxy): """ Please confirm the Upper Tester receive data """ data = self.l2cap.ReceiveData(connection=self.connection) assert self.channel data = next(self.l2cap.Receive(channel=self.channel)).data assert data, "data received should not be empty" return "OK" Loading Loading @@ -504,7 +498,8 @@ class L2CAPProxy(ProfileProxy): Description : The Implementation Under Test(IUT) should send none segmantation LE frame of LE data to the PTS. """ self.l2cap.SendData(connection=self.connection, data=bytes(self.LE_DATA_PACKET1, "utf-8")) assert self.channel self.l2cap.Send(channel=self.channel, data=bytes(self.LE_DATA_PACKET1, "utf-8")) return "OK" @assert_description Loading
android/pandora/server/configs/pts_bot_tests_config.json +1 −0 Original line number Diff line number Diff line Loading @@ -1048,6 +1048,7 @@ "L2CAP/CMC/BV-13-C", "L2CAP/CMC/BV-14-C", "L2CAP/CMC/BV-15-C", "L2CAP/COS/CED/BI-02-C", "L2CAP/COS/CED/BV-10-C", "L2CAP/COS/CED/BV-12-C", "L2CAP/COS/CED/BV-13-C", Loading
android/pandora/server/src/L2cap.kt +95 −116 Original line number Diff line number Diff line Loading @@ -16,46 +16,35 @@ package com.android.pandora import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothServerSocket import android.bluetooth.BluetoothSocket import android.content.Context import android.util.Log import com.google.protobuf.Any import com.google.protobuf.ByteString import io.grpc.stub.StreamObserver import java.io.Closeable import java.io.IOException import java.io.InputStream import java.io.OutputStream import java.util.concurrent.atomic.AtomicLong import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.withContext import pandora.HostProto.Connection import pandora.L2CAPGrpc.L2CAPImplBase import pandora.L2capProto.AcceptL2CAPChannelRequest import pandora.L2capProto.AcceptL2CAPChannelResponse import pandora.L2capProto.CreateLECreditBasedChannelRequest import pandora.L2capProto.CreateLECreditBasedChannelResponse import pandora.L2capProto.ListenL2CAPChannelRequest import pandora.L2capProto.ListenL2CAPChannelResponse import pandora.L2capProto.ReceiveDataRequest import pandora.L2capProto.ReceiveDataResponse import pandora.L2capProto.SendDataRequest import pandora.L2capProto.SendDataResponse import kotlinx.coroutines.flow.flow import pandora.l2cap.L2CAPGrpc.L2CAPImplBase import pandora.l2cap.L2CAPProto.* @kotlinx.coroutines.ExperimentalCoroutinesApi class L2cap(val context: Context) : L2CAPImplBase(), Closeable { private val TAG = "PandoraL2cap" private val scope: CoroutineScope private val BLUETOOTH_SERVER_SOCKET_TIMEOUT: Int = 10000 private val channelIdCounter = AtomicLong(1) private val BUFFER_SIZE = 512 private val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager private val bluetoothAdapter = bluetoothManager.adapter private var connectionInStreamMap: HashMap<Connection, InputStream> = hashMapOf() private var connectionOutStreamMap: HashMap<Connection, OutputStream> = hashMapOf() private var connectionServerSocketMap: HashMap<Connection, BluetoothServerSocket> = hashMapOf() private val channels: HashMap<Long, BluetoothSocket> = hashMapOf() init { // Init the CoroutineScope Loading @@ -67,127 +56,117 @@ class L2cap(val context: Context) : L2CAPImplBase(), Closeable { scope.cancel() } suspend fun receive(inStream: InputStream): ByteArray { return withContext(Dispatchers.IO) { val buf = ByteArray(BUFFER_SIZE) inStream.read(buf, 0, BUFFER_SIZE) // blocking Log.i(TAG, "receive: $buf") buf } } /** Open a BluetoothServerSocket to accept connections */ override fun listenL2CAPChannel( request: ListenL2CAPChannelRequest, responseObserver: StreamObserver<ListenL2CAPChannelResponse>, override fun connect( request: ConnectRequest, responseObserver: StreamObserver<ConnectResponse>, ) { grpcUnary(scope, responseObserver) { Log.i(TAG, "listenL2CAPChannel: secure=${request.secure}") val connection = request.connection val bluetoothServerSocket = if (request.secure) { bluetoothAdapter.listenUsingL2capChannel() } else { bluetoothAdapter.listenUsingInsecureL2capChannel() val device = request.connection.toBluetoothDevice(bluetoothAdapter) val psm = when { request.hasBasic() -> request.basic.psm request.hasLeCreditBased() -> request.leCreditBased.spsm request.hasEnhancedCreditBased() -> request.enhancedCreditBased.spsm else -> throw RuntimeException("unreachable") } connectionServerSocketMap[connection] = bluetoothServerSocket ListenL2CAPChannelResponse.newBuilder().build() Log.i(TAG, "connect: $device psm: $psm") val bluetoothSocket = device.createInsecureL2capChannel(psm) bluetoothSocket.connect() val channelId = getNewChannelId() channels.put(channelId, bluetoothSocket) Log.d(TAG, "connect: channelId=$channelId") ConnectResponse.newBuilder().setChannel(craftChannel(channelId)).build() } } override fun acceptL2CAPChannel( request: AcceptL2CAPChannelRequest, responseObserver: StreamObserver<AcceptL2CAPChannelResponse>, override fun waitConnection( request: WaitConnectionRequest, responseObserver: StreamObserver<WaitConnectionResponse>, ) { grpcUnary(scope, responseObserver) { Log.i(TAG, "acceptL2CAPChannel") val connection = request.connection val bluetoothServerSocket = connectionServerSocketMap[connection] val device: BluetoothDevice? = try { val bluetoothSocket = bluetoothServerSocket!!.accept(BLUETOOTH_SERVER_SOCKET_TIMEOUT) connectionInStreamMap[connection] = bluetoothSocket.getInputStream()!! connectionOutStreamMap[connection] = bluetoothSocket.getOutputStream()!! } catch (e: IOException) { Log.e(TAG, "bluetoothServerSocket not accepted", e) throw e request.connection.toBluetoothDevice(bluetoothAdapter) } catch (e: Exception) { Log.w(TAG, e) null } AcceptL2CAPChannelResponse.newBuilder().build() } Log.i(TAG, "waitConnection: $device") val psm = when { request.hasBasic() -> request.basic.psm request.hasLeCreditBased() -> request.leCreditBased.spsm request.hasEnhancedCreditBased() -> request.enhancedCreditBased.spsm else -> throw RuntimeException("unreachable") } /** Set device to send LE based connection request */ override fun createLECreditBasedChannel( request: CreateLECreditBasedChannelRequest, responseObserver: StreamObserver<CreateLECreditBasedChannelResponse>, ) { // Creates a gRPC coroutine in a given coroutine scope which executes a given suspended // function // returning a gRPC response and sends it on a given gRPC stream observer. grpcUnary(scope, responseObserver) { Log.i(TAG, "createLECreditBasedChannel: secure=${request.secure}, psm=${request.psm}") val connection = request.connection val device = request.connection.toBluetoothDevice(bluetoothAdapter) val psm = request.psm var bluetoothSocket: BluetoothSocket? try { val bluetoothSocket = if (request.secure) { device.createL2capChannel(psm) while (true) { val bluetoothServerSocket = if (psm == 0) { bluetoothAdapter.listenUsingInsecureL2capChannel() } else { device.createInsecureL2capChannel(psm) bluetoothAdapter.listenUsingInsecureL2capOn(psm) } bluetoothSocket.connect() connectionInStreamMap[connection] = bluetoothSocket.getInputStream()!! connectionOutStreamMap[connection] = bluetoothSocket.getOutputStream()!! } catch (e: IOException) { Log.d(TAG, "bluetoothSocket not connected: $e") throw e bluetoothSocket = bluetoothServerSocket.accept() bluetoothServerSocket.close() if (device != null && !bluetoothSocket.getRemoteDevice().equals(device)) continue break } // Response sent to client CreateLECreditBasedChannelResponse.newBuilder().build() val channelId = getNewChannelId() channels.put(channelId, bluetoothSocket!!) Log.d(TAG, "waitConnection: channelId=$channelId") WaitConnectionResponse.newBuilder().setChannel(craftChannel(channelId)).build() } } /** send data packet */ override fun sendData( request: SendDataRequest, responseObserver: StreamObserver<SendDataResponse>, ) { override fun send(request: SendRequest, responseObserver: StreamObserver<SendResponse>) { grpcUnary(scope, responseObserver) { Log.i(TAG, "sendDataPacket: data=${request.data}") val buffer = request.data!!.toByteArray() val connection = request.connection val outputStream = connectionOutStreamMap[connection]!! Log.i(TAG, "send") val bluetoothSocket = request.channel.toBluetoothSocket(channels) val outputStream = bluetoothSocket.outputStream withContext(Dispatchers.IO) { try { outputStream.write(buffer) outputStream.write(request.data.toByteArray()) outputStream.flush() } catch (e: IOException) { Log.e(TAG, "Exception during writing to sendDataPacket output stream", e) SendResponse.newBuilder().build() } } // Response sent to client SendDataResponse.newBuilder().build() override fun receive( request: ReceiveRequest, responseObserver: StreamObserver<ReceiveResponse>, ) { Log.i(TAG, "receive") val bluetoothSocket = request.channel.toBluetoothSocket(channels) val inputStream = bluetoothSocket.inputStream grpcServerStream(scope, responseObserver) { flow { val buffer = ByteArray(BUFFER_SIZE) inputStream.read(buffer, 0, BUFFER_SIZE) val data = ByteString.copyFrom(buffer) val response = ReceiveResponse.newBuilder().setData(data).build() emit(response) } } } override fun receiveData( request: ReceiveDataRequest, responseObserver: StreamObserver<ReceiveDataResponse>, ) { grpcUnary(scope, responseObserver) { Log.i(TAG, "receiveData") val connection = request.connection val inputStream = connectionInStreamMap[connection]!! val buf = receive(inputStream) fun getNewChannelId(): Long = channelIdCounter.getAndIncrement() ReceiveDataResponse.newBuilder().setData(ByteString.copyFrom(buf)).build() } fun craftChannel(id: Long): Channel { val cookie = Any.newBuilder().setValue(ByteString.copyFromUtf8(id.toString())).build() val channel = Channel.newBuilder().setCookie(cookie).build() return channel } fun Channel.toBluetoothSocket(channels: HashMap<Long, BluetoothSocket>): BluetoothSocket = channels.get(this.cookie.value.toStringUtf8().toLong())!! }