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

Commit 693cdf2c authored by Rahul Arya's avatar Rahul Arya
Browse files

[Pandora] Add control over BREDR connections

Determine whether we want to connect and pair automatically (which is
what most profiles want), connect and pair manually (if we are testing
pairing), or just connect (if we just want the ACL link).

Ideally we'd have Connect and Pair as entirely independent operations,
but this is impossible to implement on Android. So we are making a
slight compromise of interface purity for the sake of pragmatism. The
alternative, which we used to do, was to make Connect always do an
auto-bond on Android, but that makes the Pair() method basically
useless.

Bug: 248814583
Test: existing pts

Change-Id: I30e858973537e4e61f8c65e9fa771edc11820a9b
parent fe0c1d96
Loading
Loading
Loading
Loading
+20 −2
Original line number Diff line number Diff line
@@ -58,15 +58,33 @@ message ReadLocalAddressResponse {
// A Token representing an ACL connection.
// It's acquired via a Connect on the Host service.
message Connection {
  // Opaque value filled by the gRPC server, must not
  // be modified nor crafted.
  // Opaque value filled by the gRPC server, must not be modified nor crafted
  // Android specific: it's secretly an encoded InternelConnectionRef created using newConnection
  bytes cookie = 1;
}

// Internal representation of a Connection - not exposed to clients, included here
// just for code-generation convenience
message InternalConnectionRef {
  bytes address = 1;
  Transport transport = 2;
}

// WARNING: Leaving this enum empty will default to BREDR, so make sure that this is a
// valid default whenever used, and that we always populate this value.
enum Transport {
  TRANSPORT_BREDR = 0;
  TRANSPORT_LE = 1;
}

// Request of the `Connect` method.
message ConnectRequest {
  // Peer Bluetooth Device Address as array of 6 bytes.
  bytes address = 1;
  // Whether we want to initiate pairing as part of the connection
  bool skip_pairing = 2;
  // Whether confirmation prompts should be auto-accepted or handled manually
  bool manually_confirm = 3;
}

// Response of the `Connect` method.
+8 −8
Original line number Diff line number Diff line
@@ -74,7 +74,7 @@ class Gatt(private val context: Context) : GATTImplBase() {
    grpcUnary<ExchangeMTUResponse>(mScope, responseObserver) {
      val mtu = request.mtu
      Log.i(TAG, "exchangeMTU MTU=$mtu")
      if (!GattInstance.get(request.connection.cookie).mGatt.requestMtu(mtu)) {
      if (!GattInstance.get(request.connection.address).mGatt.requestMtu(mtu)) {
        Log.e(TAG, "Error on requesting MTU $mtu")
        throw Status.UNKNOWN.asException()
      }
@@ -88,7 +88,7 @@ class Gatt(private val context: Context) : GATTImplBase() {
  ) {
    grpcUnary<WriteResponse>(mScope, responseObserver) {
      Log.i(TAG, "writeAttFromHandle handle=${request.handle}")
      val gattInstance = GattInstance.get(request.connection.cookie)
      val gattInstance = GattInstance.get(request.connection.address)
      var characteristic: BluetoothGattCharacteristic? =
          getCharacteristicWithHandle(request.handle, gattInstance)
      if (characteristic == null) {
@@ -113,7 +113,7 @@ class Gatt(private val context: Context) : GATTImplBase() {
      responseObserver: StreamObserver<DiscoverServicesResponse>
  ) {
    grpcUnary<DiscoverServicesResponse>(mScope, responseObserver) {
      val gattInstance = GattInstance.get(request.connection.cookie)
      val gattInstance = GattInstance.get(request.connection.address)
      Log.i(TAG, "discoverServiceByUuid uuid=${request.uuid}")
      // In some cases, GATT starts a discovery immediately after being connected, so
      // we need to wait until the service discovery is finished to be able to discover again.
@@ -133,7 +133,7 @@ class Gatt(private val context: Context) : GATTImplBase() {
  ) {
    grpcUnary<DiscoverServicesResponse>(mScope, responseObserver) {
      Log.i(TAG, "discoverServices")
      val gattInstance = GattInstance.get(request.connection.cookie)
      val gattInstance = GattInstance.get(request.connection.address)
      check(gattInstance.mGatt.discoverServices())
      gattInstance.waitForDiscoveryEnd()
      DiscoverServicesResponse.newBuilder()
@@ -168,7 +168,7 @@ class Gatt(private val context: Context) : GATTImplBase() {
  ) {
    grpcUnary<ClearCacheResponse>(mScope, responseObserver) {
      Log.i(TAG, "clearCache")
      val gattInstance = GattInstance.get(request.connection.cookie)
      val gattInstance = GattInstance.get(request.connection.address)
      check(gattInstance.mGatt.refresh())
      ClearCacheResponse.newBuilder().build()
    }
@@ -180,7 +180,7 @@ class Gatt(private val context: Context) : GATTImplBase() {
  ) {
    grpcUnary<ReadCharacteristicResponse>(mScope, responseObserver) {
      Log.i(TAG, "readCharacteristicFromHandle handle=${request.handle}")
      val gattInstance = GattInstance.get(request.connection.cookie)
      val gattInstance = GattInstance.get(request.connection.address)
      val characteristic: BluetoothGattCharacteristic? =
          getCharacteristicWithHandle(request.handle, gattInstance)
      checkNotNull(characteristic) { "Characteristic handle ${request.handle} not found." }
@@ -198,7 +198,7 @@ class Gatt(private val context: Context) : GATTImplBase() {
  ) {
    grpcUnary<ReadCharacteristicsFromUuidResponse>(mScope, responseObserver) {
      Log.i(TAG, "readCharacteristicsFromUuid uuid=${request.uuid}")
      val gattInstance = GattInstance.get(request.connection.cookie)
      val gattInstance = GattInstance.get(request.connection.address)
      tryDiscoverServices(gattInstance)
      val readValues =
          gattInstance.readCharacteristicUuidBlocking(
@@ -215,7 +215,7 @@ class Gatt(private val context: Context) : GATTImplBase() {
  ) {
    grpcUnary<ReadCharacteristicDescriptorResponse>(mScope, responseObserver) {
      Log.i(TAG, "readCharacteristicDescriptorFromHandle handle=${request.handle}")
      val gattInstance = GattInstance.get(request.connection.cookie)
      val gattInstance = GattInstance.get(request.connection.address)
      val descriptor: BluetoothGattDescriptor? =
          getDescriptorWithHandle(request.handle, gattInstance)
      checkNotNull(descriptor) { "Descriptor handle ${request.handle} not found." }
+30 −27
Original line number Diff line number Diff line
@@ -37,6 +37,9 @@ import com.google.protobuf.ByteString
import com.google.protobuf.Empty
import io.grpc.Status
import io.grpc.stub.StreamObserver
import java.io.IOException
import java.time.Duration
import java.util.UUID
import kotlin.Result.Companion.failure
import kotlin.Result.Companion.success
import kotlin.coroutines.suspendCoroutine
@@ -57,6 +60,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import pandora.HostGrpc.HostImplBase
import pandora.HostProto.*

@@ -219,11 +223,7 @@ class Host(private val context: Context, private val server: Server) : HostImplB
      acceptPairingAndAwaitBonded(bluetoothDevice)

      WaitConnectionResponse.newBuilder()
        .setConnection(
          Connection.newBuilder()
            .setCookie(ByteString.copyFromUtf8(bluetoothDevice.address))
            .build()
        )
        .setConnection(newConnection(bluetoothDevice, Transport.TRANSPORT_BREDR))
        .build()
    }
  }
@@ -276,7 +276,17 @@ class Host(private val context: Context, private val server: Server) : HostImplB

      Log.i(TAG, "connect: address=$bluetoothDevice")

      bluetoothAdapter.cancelDiscovery()

      if (!bluetoothDevice.isConnected()) {
        if (request.skipPairing) {
          // do an SDP request to trigger a temporary BREDR connection
          try {
            withTimeout(1500) { bluetoothDevice.createRfcommSocket(3).connect() }
          } catch (e: IOException) {
            // ignore
          }
        } else {
          if (bluetoothDevice.bondState == BOND_BONDED) {
            // already bonded, just reconnect
            bluetoothDevice.connect()
@@ -284,16 +294,15 @@ class Host(private val context: Context, private val server: Server) : HostImplB
          } else {
            // need to bond
            bluetoothDevice.createBond()
            if (!request.manuallyConfirm) {
              acceptPairingAndAwaitBonded(bluetoothDevice)
            }
          }
        }
      }

      ConnectResponse.newBuilder()
        .setConnection(
          Connection.newBuilder()
            .setCookie(ByteString.copyFromUtf8(bluetoothDevice.address))
            .build()
        )
        .setConnection(newConnection(bluetoothDevice, Transport.TRANSPORT_BREDR))
        .build()
    }
  }
@@ -333,9 +342,7 @@ class Host(private val context: Context, private val server: Server) : HostImplB
      val device = scanLeDevice(address)
      GattInstance(device!!, TRANSPORT_LE, context).waitForState(BluetoothProfile.STATE_CONNECTED)
      ConnectLEResponse.newBuilder()
        .setConnection(
          Connection.newBuilder().setCookie(ByteString.copyFromUtf8(device.address)).build()
        )
        .setConnection(newConnection(device, Transport.TRANSPORT_LE))
        .build()
    }
  }
@@ -351,9 +358,7 @@ class Host(private val context: Context, private val server: Server) : HostImplB
        bluetoothAdapter.getRemoteLeDevice(address, BluetoothDevice.ADDRESS_TYPE_PUBLIC)
      if (device.isConnected) {
        GetLEConnectionResponse.newBuilder()
          .setConnection(
            Connection.newBuilder().setCookie(ByteString.copyFromUtf8(device.address)).build()
          )
          .setConnection(newConnection(device, Transport.TRANSPORT_LE))
          .build()
      } else {
        Log.e(TAG, "Device: $device is not connected")
@@ -364,7 +369,7 @@ class Host(private val context: Context, private val server: Server) : HostImplB

  override fun disconnectLE(request: DisconnectLERequest, responseObserver: StreamObserver<Empty>) {
    grpcUnary<Empty>(scope, responseObserver) {
      val address = request.connection.cookie.toByteArray().decodeToString()
      val address = request.connection.address
      Log.i(TAG, "disconnectLE: $address")
      val gattInstance = GattInstance.get(address)

@@ -432,9 +437,7 @@ class Host(private val context: Context, private val server: Server) : HostImplB
            .addDevice(
              Device.newBuilder()
                .setName(device.name)
                .setAddress(
                  ByteString.copyFrom(MacAddress.fromString(device.address).toByteArray())
                )
                .setAddress(device.toByteString())
            )
            .build()
        }
+12 −13
Original line number Diff line number Diff line
@@ -125,15 +125,14 @@ class Security(private val context: Context) : SecurityImplBase() {
        }
        .launchIn(this)

      flow.map { intent ->
      flow
        .filter { intent -> intent.action == ACTION_PAIRING_REQUEST }
        .map { intent ->
          val device = intent.getBluetoothDeviceExtra()
          val variant = intent.getIntExtra(EXTRA_PAIRING_VARIANT, BluetoothDevice.ERROR)
        Log.i(
          TAG,
          "OnPairing: Handling PairingEvent ${variant} for device ${device.address}"
        )
          Log.i(TAG, "OnPairing: Handling PairingEvent ${variant} for device ${device.address}")
          val eventBuilder =
          PairingEvent.newBuilder().setAddress(ByteString.copyFrom(device.toByteArray()))
            PairingEvent.newBuilder().setAddress(device.toByteString())
          when (variant) {
            // SSP / LE Just Works
            BluetoothDevice.PAIRING_VARIANT_CONSENT ->
+23 −3
Original line number Diff line number Diff line
@@ -53,6 +53,8 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.withTimeoutOrNull
import pandora.HostProto.Connection
import pandora.HostProto.InternalConnectionRef
import pandora.HostProto.Transport

fun shell(cmd: String): String {
  val fd = InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(cmd)
@@ -284,7 +286,7 @@ fun <T> getProfileProxy(context: Context, profile: Int): T {
}

fun Intent.getBluetoothDeviceExtra(): BluetoothDevice =
  this.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
  this.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)!!

fun ByteString.decodeAsMacAddressToString(): String =
  MacAddress.fromBytes(this.toByteArray()).toString().uppercase()
@@ -293,6 +295,24 @@ fun ByteString.toBluetoothDevice(adapter: BluetoothAdapter): BluetoothDevice =
  adapter.getRemoteDevice(this.decodeAsMacAddressToString())

fun Connection.toBluetoothDevice(adapter: BluetoothAdapter): BluetoothDevice =
  adapter.getRemoteDevice(this.cookie.toByteArray().decodeToString())
  adapter.getRemoteDevice(address)

fun BluetoothDevice.toByteArray(): ByteArray = MacAddress.fromString(this.address).toByteArray()
val Connection.address: String
  get() = InternalConnectionRef.parseFrom(this.cookie).address.decodeAsMacAddressToString()

val Connection.transport: Transport
  get() = InternalConnectionRef.parseFrom(this.cookie).transport

fun newConnection(device: BluetoothDevice, transport: Transport) =
  Connection.newBuilder()
    .setCookie(
      InternalConnectionRef.newBuilder()
        .setAddress(device.toByteString())
        .setTransport(transport)
        .build()
        .toByteString()
    )
    .build()!!

fun BluetoothDevice.toByteString() =
  ByteString.copyFrom(MacAddress.fromString(this.address).toByteArray())!!