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

Commit 7b456c17 authored by Treehugger Robot's avatar Treehugger Robot Committed by Gerrit Code Review
Browse files

Merge changes I5a9793af,I93cf1a76 into main

* changes:
  Pandora: remove experiemental l2cap.proto
  PandoraServer: Implement new L2CAP interface
parents c9164e9e 2a8fc4d1
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())!!
}
+0 −62
Original line number Diff line number Diff line
syntax = "proto3";

package pandora;

option java_outer_classname = "L2capProto";

import "google/protobuf/empty.proto";
import "pandora/host.proto";

service L2CAP {
  // Create a L2CAP connection to a peer.
  rpc CreateLECreditBasedChannel(CreateLECreditBasedChannelRequest) returns (CreateLECreditBasedChannelResponse);
  // Send some data
  rpc SendData(SendDataRequest) returns (SendDataResponse);
  // Receive data
  rpc ReceiveData(ReceiveDataRequest) returns (ReceiveDataResponse);
  // Listen L2CAP channel for connection
  rpc ListenL2CAPChannel(ListenL2CAPChannelRequest) returns (ListenL2CAPChannelResponse);
  // Accept L2CAP connection
  rpc AcceptL2CAPChannel(AcceptL2CAPChannelRequest) returns (AcceptL2CAPChannelResponse);
}

// Request for the `OpenSource` method.
message CreateLECreditBasedChannelRequest {
  // The connection that will open the stream.
  Connection connection = 1;
  int32 psm = 2;
  bool secure = 3;
}

// Request for the `OpenSource` method.
message CreateLECreditBasedChannelResponse {}

message SendDataRequest {
  // The connection that will open the stream.
  Connection connection = 1;
  bytes data = 2;
}

message SendDataResponse {}

message ReceiveDataRequest {
  // The connection that will open the stream.
  Connection connection = 1;
}

message ReceiveDataResponse {
  bytes data = 1;
}

message ListenL2CAPChannelRequest{
  Connection connection = 1;
  bool secure = 2;
}

message ListenL2CAPChannelResponse {}

message AcceptL2CAPChannelRequest{
  Connection connection = 1;
}

message AcceptL2CAPChannelResponse {}
 No newline at end of file
+0 −5
Original line number Diff line number Diff line
@@ -54,10 +54,6 @@ genrule {
        "pandora_experimental/hid_grpc_aio.py",
        "pandora_experimental/hid_pb2.py",
        "pandora_experimental/hid_pb2.pyi",
        "pandora_experimental/l2cap_grpc.py",
        "pandora_experimental/l2cap_grpc_aio.py",
        "pandora_experimental/l2cap_pb2.py",
        "pandora_experimental/l2cap_pb2.pyi",
        "pandora_experimental/le_audio_grpc.py",
        "pandora_experimental/le_audio_grpc_aio.py",
        "pandora_experimental/le_audio_pb2.py",
@@ -118,7 +114,6 @@ filegroup {
        ":pandora_experimental-python-gen-src{pandora_experimental/hap_pb2.pyi}",
        ":pandora_experimental-python-gen-src{pandora_experimental/hfp_pb2.pyi}",
        ":pandora_experimental-python-gen-src{pandora_experimental/hid_pb2.pyi}",
        ":pandora_experimental-python-gen-src{pandora_experimental/l2cap_pb2.pyi}",
        ":pandora_experimental-python-gen-src{pandora_experimental/le_audio_pb2.pyi}",
        ":pandora_experimental-python-gen-src{pandora_experimental/map_pb2.pyi}",
        ":pandora_experimental-python-gen-src{pandora_experimental/mediaplayer_pb2.pyi}",