Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f32474f5 authored by Charlie Boutier's avatar Charlie Boutier
Browse files

PandoraServer: Implement new L2CAP interface

Bug: 367808704
Test: atest pts-bot:L2CAP

Change-Id: I93cf1a764a8e2a30da692bf2a38e60688a6c03b2
parent bb29aaf4
Loading
Loading
Loading
Loading
+31 −36
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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)
@@ -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
@@ -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'
@@ -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",
@@ -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
@@ -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
@@ -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"

@@ -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"

@@ -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"

@@ -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
+1 −0
Original line number Diff line number Diff line
@@ -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",
+95 −116
Original line number Diff line number Diff line
@@ -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
@@ -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())!!
}