Commit c7c949d2 authored by Jie Jiang's avatar Jie Jiang Committed by Commit Bot

arc: bluetooth: Make RFCOMM available for arcvm

Currently RFCOMM is not available on arcvm because we need some
operations (listen(), connect(), etc.) on the bluetooth socket in
Android, which is not supported by VSockProxy. This change moves such
operations into Chrome.

See http://ag/9487891 for change in Android.

- For RFCOMM listen, we keep the listening socket in Chrome. When a new
connection is accepted, we give the new socket by accept() to Android.
- For RFCOMM connect, we keep the socket when connection is not
established. When connect is ready, we give this socket to Android.
- We use mojo interface to wrap the sockets live in Chrome. The
following events (e.g., connect() succeeds) are notified via these
interfaces.

BUG=b:142090057
TEST=cts-tradefed run commandAndExit cts -m CtsBluetoothTestCases;
manually wrote some apps, Android on DUT can communicate with my
workstation via RFCOMM, both as client and server

Change-Id: If366aaeca5297a011bbd9ff6584831afbf2b86ee
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1837329
Commit-Queue: Jie Jiang <jiejiang@chromium.org>
Reviewed-by: default avatarDaniel Cheng <dcheng@chromium.org>
Reviewed-by: default avatarHidehiko Abe <hidehiko@chromium.org>
Reviewed-by: default avatarMiao-chen Chou <mcchou@chromium.org>
Cr-Commit-Position: refs/heads/master@{#721893}
parent 8b426107
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
#include "chrome/browser/chromeos/arc/bluetooth/arc_bluetooth_bridge.h" #include "chrome/browser/chromeos/arc/bluetooth/arc_bluetooth_bridge.h"
#include <bluetooth/bluetooth.h> #include <bluetooth/bluetooth.h>
#include <bluetooth/rfcomm.h>
#include <fcntl.h> #include <fcntl.h>
#include <stddef.h> #include <stddef.h>
#include <sys/socket.h> #include <sys/socket.h>
...@@ -26,6 +27,7 @@ ...@@ -26,6 +27,7 @@
#include "base/strings/string_number_conversions.h" #include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h" #include "base/strings/utf_string_conversions.h"
#include "base/system/sys_info.h" #include "base/system/sys_info.h"
#include "base/task/post_task.h"
#include "base/threading/thread_task_runner_handle.h" #include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h" #include "base/time/time.h"
#include "chrome/browser/chromeos/profiles/profile_helper.h" #include "chrome/browser/chromeos/profiles/profile_helper.h"
...@@ -2725,4 +2727,258 @@ ArcBluetoothBridge::GattConnection::GattConnection( ...@@ -2725,4 +2727,258 @@ ArcBluetoothBridge::GattConnection::GattConnection(
ArcBluetoothBridge::GattConnection& ArcBluetoothBridge::GattConnection:: ArcBluetoothBridge::GattConnection& ArcBluetoothBridge::GattConnection::
operator=(ArcBluetoothBridge::GattConnection&&) = default; operator=(ArcBluetoothBridge::GattConnection&&) = default;
namespace {
constexpr int kMinRfcommChannelNum = 1;
constexpr int kMaxRfcommChannelNum = 30;
base::ScopedFD OpenRfcommSocket(int32_t optval) {
base::ScopedFD sock(socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM));
if (!sock.is_valid()) {
PLOG(ERROR) << "Failed to open bluetooth socket.";
return {};
}
if (setsockopt(sock.get(), SOL_RFCOMM, RFCOMM_LM, &optval, sizeof(optval)) ==
-1) {
PLOG(ERROR) << "Failed to setopt() on socket.";
return {};
}
if (fcntl(sock.get(), F_SETFL, O_NONBLOCK | fcntl(sock.get(), F_GETFL)) ==
-1) {
PLOG(ERROR) << "Failed to fcntl() on socket.";
return {};
}
return sock;
}
} // namespace
void ArcBluetoothBridge::RfcommListen(int32_t channel,
int32_t optval,
RfcommListenCallback callback) {
// |channel|=0 means selecting a available channel automatically.
if (channel != 0 &&
(channel < kMinRfcommChannelNum || channel > kMaxRfcommChannelNum)) {
LOG(ERROR) << "Invalid channel number";
std::move(callback).Run(
mojom::BluetoothStatus::FAIL, /*channel=*/0,
mojo::PendingReceiver<mojom::RfcommListeningSocketClient>());
return;
}
uint8_t listen_channel = static_cast<uint8_t>(channel);
auto sock_wrapper = RfcommCreateListenSocket(optval, &listen_channel);
if (!sock_wrapper) {
std::move(callback).Run(
mojom::BluetoothStatus::FAIL, /*channel=*/0,
mojo::PendingReceiver<mojom::RfcommListeningSocketClient>());
return;
}
std::move(callback).Run(mojom::BluetoothStatus::SUCCESS, listen_channel,
mojo::MakeRequest(&sock_wrapper->remote));
sock_wrapper->remote.set_connection_error_handler(
base::BindOnce(&ArcBluetoothBridge::RfcommCloseListeningSocket,
weak_factory_.GetWeakPtr(), sock_wrapper.get()));
listening_sockets_.insert(std::move(sock_wrapper));
}
void ArcBluetoothBridge::RfcommCloseListeningSocket(
RfcommListeningSocket* ptr) {
auto itr = listening_sockets_.find(ptr);
listening_sockets_.erase(itr);
}
void ArcBluetoothBridge::RfcommConnect(mojom::BluetoothAddressPtr remote_addr,
int32_t channel,
int32_t optval,
RfcommConnectCallback callback) {
if (channel < kMinRfcommChannelNum || channel > kMaxRfcommChannelNum) {
LOG(ERROR) << "Invalid channel number";
std::move(callback).Run(mojom::BluetoothStatus::FAIL,
mojom::RfcommConnectingSocketClientRequest());
return;
}
auto sock_wrapper = RfcommCreateConnectSocket(
std::move(remote_addr), static_cast<uint8_t>(channel), optval);
if (!sock_wrapper) {
std::move(callback).Run(mojom::BluetoothStatus::FAIL,
mojom::RfcommConnectingSocketClientRequest());
return;
}
std::move(callback).Run(mojom::BluetoothStatus::SUCCESS,
mojo::MakeRequest(&sock_wrapper->remote));
sock_wrapper->remote.set_connection_error_handler(
base::BindOnce(&ArcBluetoothBridge::RfcommCloseConnectingSocket,
weak_factory_.GetWeakPtr(), sock_wrapper.get()));
connecting_sockets_.insert(std::move(sock_wrapper));
}
void ArcBluetoothBridge::RfcommCloseConnectingSocket(
RfcommConnectingSocket* ptr) {
auto itr = connecting_sockets_.find(ptr);
connecting_sockets_.erase(itr);
}
std::unique_ptr<ArcBluetoothBridge::RfcommListeningSocket>
ArcBluetoothBridge::RfcommCreateListenSocket(int32_t optval, uint8_t* channel) {
base::ScopedFD sock = OpenRfcommSocket(optval);
if (!sock.is_valid()) {
LOG(ERROR) << "Failed to open listen socket.";
return nullptr;
}
DCHECK(channel);
struct sockaddr_rc my_addr = {};
my_addr.rc_family = AF_BLUETOOTH;
my_addr.rc_channel = *channel;
if (bind(sock.get(), reinterpret_cast<const struct sockaddr*>(&my_addr),
sizeof(my_addr)) == -1) {
PLOG(ERROR) << "Failed to bind()";
return nullptr;
}
if (listen(sock.get(), /*backlog=*/1) == -1) {
PLOG(ERROR) << "Failed to listen()";
return nullptr;
}
socklen_t addr_len = sizeof(my_addr);
if (getsockname(sock.get(), reinterpret_cast<struct sockaddr*>(&my_addr),
&addr_len) == -1) {
PLOG(ERROR) << "Failed to getsockname()";
return nullptr;
}
auto sock_wrapper = std::make_unique<RfcommListeningSocket>();
sock_wrapper->controller = base::FileDescriptorWatcher::WatchReadable(
sock.get(),
base::BindRepeating(&ArcBluetoothBridge::OnRfcommListeningSocketReady,
weak_factory_.GetWeakPtr(), sock_wrapper.get()));
sock_wrapper->file = std::move(sock);
*channel = my_addr.rc_channel;
return sock_wrapper;
}
void ArcBluetoothBridge::OnRfcommListeningSocketReady(
ArcBluetoothBridge::RfcommListeningSocket* sock_wrapper) {
struct sockaddr_rc sa;
socklen_t addr_len = sizeof(sa);
base::ScopedFD accept_fd(accept(sock_wrapper->file.get(),
reinterpret_cast<struct sockaddr*>(&sa),
&addr_len));
if (!accept_fd.is_valid()) {
PLOG(ERROR) << "Failed to accept()";
return;
}
if (fcntl(accept_fd.get(), F_SETFL,
O_NONBLOCK | fcntl(accept_fd.get(), F_GETFL)) == -1) {
PLOG(ERROR) << "Failed to fnctl()";
return;
}
mojo::ScopedHandle handle =
mojo::WrapPlatformHandle(mojo::PlatformHandle(std::move(accept_fd)));
// Tells Android we successfully accept() a new connection.
auto connection = mojom::BluetoothRfcommConnection::New();
connection->sock = std::move(handle);
connection->addr = mojom::BluetoothAddress::From<bdaddr_t>(sa.rc_bdaddr);
connection->channel = sa.rc_channel;
sock_wrapper->remote->OnAccepted(std::move(connection));
}
std::unique_ptr<ArcBluetoothBridge::RfcommConnectingSocket>
ArcBluetoothBridge::RfcommCreateConnectSocket(mojom::BluetoothAddressPtr addr,
uint8_t channel,
int32_t optval) {
base::ScopedFD sock = OpenRfcommSocket(optval);
if (!sock.is_valid()) {
LOG(ERROR) << "Failed to open connect socket.";
return nullptr;
}
struct sockaddr_rc sa = {};
sa.rc_family = AF_BLUETOOTH;
sa.rc_bdaddr = addr->To<bdaddr_t>();
sa.rc_channel = channel;
int ret = HANDLE_EINTR(connect(
sock.get(), reinterpret_cast<const struct sockaddr*>(&sa), sizeof(sa)));
auto sock_wrapper =
std::make_unique<ArcBluetoothBridge::RfcommConnectingSocket>();
if (ret == 0) {
// connect() returns success immediately.
sock_wrapper->file = std::move(sock);
PostTask(FROM_HERE,
base::BindOnce(&ArcBluetoothBridge::OnRfcommConnectingSocketReady,
weak_factory_.GetWeakPtr(), sock_wrapper.get()));
return sock_wrapper;
}
if (errno != EINPROGRESS) {
PLOG(ERROR) << "Failed to connect.";
return nullptr;
}
sock_wrapper->controller = base::FileDescriptorWatcher::WatchWritable(
sock.get(),
base::BindRepeating(&ArcBluetoothBridge::OnRfcommConnectingSocketReady,
weak_factory_.GetWeakPtr(), sock_wrapper.get()));
sock_wrapper->file = std::move(sock);
return sock_wrapper;
}
void ArcBluetoothBridge::OnRfcommConnectingSocketReady(
ArcBluetoothBridge::RfcommConnectingSocket* sock_wrapper) {
// When connect() is ready, we will transfer this fd to Android, and Android
// is responsible for closing it.
base::ScopedFD fd = std::move(sock_wrapper->file);
// Checks whether connect() succeeded.
int err = 0;
socklen_t len = sizeof(err);
int ret = getsockopt(fd.get(), SOL_SOCKET, SO_ERROR, &err, &len);
if (ret != 0 || err != 0) {
PLOG(ERROR) << "Failed to connect. err=" << err;
sock_wrapper->remote->OnConnectFailed();
return;
}
// Gets peer address.
struct sockaddr_rc sa;
socklen_t sa_len = sizeof(sa);
if (getpeername(fd.get(), reinterpret_cast<sockaddr*>(&sa), &sa_len) == -1) {
PLOG(ERROR) << "Failed to getpeername().";
sock_wrapper->remote->OnConnectFailed();
return;
}
// Gets our channel.
struct sockaddr_rc our_sa;
socklen_t our_sa_len = sizeof(sa);
if (getsockname(fd.get(), reinterpret_cast<sockaddr*>(&our_sa),
&our_sa_len) == -1) {
PLOG(ERROR) << "Failed to getsockname()";
sock_wrapper->remote->OnConnectFailed();
return;
}
mojo::ScopedHandle handle =
mojo::WrapPlatformHandle(mojo::PlatformHandle(std::move(fd)));
// Notifies Android.
auto connection = mojom::BluetoothRfcommConnection::New();
connection->sock = std::move(handle);
connection->addr = mojom::BluetoothAddress::From<bdaddr_t>(sa.rc_bdaddr);
connection->channel = our_sa.rc_channel;
sock_wrapper->remote->OnConnected(std::move(connection));
}
ArcBluetoothBridge::RfcommListeningSocket::RfcommListeningSocket() = default;
ArcBluetoothBridge::RfcommListeningSocket::~RfcommListeningSocket() = default;
ArcBluetoothBridge::RfcommConnectingSocket::RfcommConnectingSocket() = default;
ArcBluetoothBridge::RfcommConnectingSocket::~RfcommConnectingSocket() = default;
} // namespace arc } // namespace arc
...@@ -9,12 +9,16 @@ ...@@ -9,12 +9,16 @@
#include <map> #include <map>
#include <memory> #include <memory>
#include <set>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <unordered_set> #include <unordered_set>
#include <vector> #include <vector>
#include "base/callback_forward.h" #include "base/callback_forward.h"
#include "base/containers/unique_ptr_adapters.h"
#include "base/files/file.h"
#include "base/files/file_descriptor_watcher_posix.h"
#include "base/threading/thread_checker.h" #include "base/threading/thread_checker.h"
#include "base/timer/timer.h" #include "base/timer/timer.h"
#include "chrome/browser/chromeos/arc/bluetooth/arc_bluetooth_task_queue.h" #include "chrome/browser/chromeos/arc/bluetooth/arc_bluetooth_task_queue.h"
...@@ -306,6 +310,15 @@ class ArcBluetoothBridge ...@@ -306,6 +310,15 @@ class ArcBluetoothBridge
void RemoveSdpRecord(uint32_t service_handle, void RemoveSdpRecord(uint32_t service_handle,
RemoveSdpRecordCallback callback) override; RemoveSdpRecordCallback callback) override;
// Bluetooth Mojo host interface - Bluetooth RFCOMM functions
void RfcommListen(int32_t channel,
int32_t optval,
RfcommListenCallback callback) override;
void RfcommConnect(mojom::BluetoothAddressPtr remote_addr,
int32_t channel,
int32_t optval,
RfcommConnectCallback callback) override;
// Set up or disable multiple advertising. // Set up or disable multiple advertising.
void ReserveAdvertisementHandle( void ReserveAdvertisementHandle(
ReserveAdvertisementHandleCallback callback) override; ReserveAdvertisementHandleCallback callback) override;
...@@ -505,6 +518,49 @@ class ArcBluetoothBridge ...@@ -505,6 +518,49 @@ class ArcBluetoothBridge
void SendDevice(const device::BluetoothDevice* device) const; void SendDevice(const device::BluetoothDevice* device) const;
// Data structures for RFCOMM listening/connecting sockets that live in
// Chrome.
struct RfcommListeningSocket {
mojom::RfcommListeningSocketClientPtr remote;
base::ScopedFD file;
std::unique_ptr<base::FileDescriptorWatcher::Controller> controller;
RfcommListeningSocket();
~RfcommListeningSocket();
};
struct RfcommConnectingSocket {
mojom::RfcommConnectingSocketClientPtr remote;
base::ScopedFD file;
std::unique_ptr<base::FileDescriptorWatcher::Controller> controller;
RfcommConnectingSocket();
~RfcommConnectingSocket();
};
// Creates a bluetooth socket with socket option |optval|, and then bind()
// and listen() with requested RFCOMM |channel| number. The actual channel
// number will be filled in |channel| as the return value. Returns a
// RfcommListeningSocket that holds the socket.
std::unique_ptr<RfcommListeningSocket> RfcommCreateListenSocket(
int32_t optval,
uint8_t* channel);
// Creates a bluetooth socket with socket option |optval|, and then calls
// connect() to (|addr|, |channel|). This connect() call is non-blocking.
// Returns a RfcommConnectingSocket that holds the socket.
std::unique_ptr<RfcommConnectingSocket> RfcommCreateConnectSocket(
mojom::BluetoothAddressPtr addr,
uint8_t channel,
int32_t optval);
// Closes RFCOMM sockets. Releases the corresponding resources.
void RfcommCloseListeningSocket(RfcommListeningSocket* socket);
void RfcommCloseConnectingSocket(RfcommConnectingSocket* socket);
// Called when the listening socket is ready to accept().
void OnRfcommListeningSocketReady(
ArcBluetoothBridge::RfcommListeningSocket* socket);
// Called when the connecting socket is ready.
void OnRfcommConnectingSocketReady(
ArcBluetoothBridge::RfcommConnectingSocket* socket);
ArcBridgeService* const arc_bridge_service_; // Owned by ArcServiceManager. ArcBridgeService* const arc_bridge_service_; // Owned by ArcServiceManager.
scoped_refptr<bluez::BluetoothAdapterBlueZ> bluetooth_adapter_; scoped_refptr<bluez::BluetoothAdapterBlueZ> bluetooth_adapter_;
...@@ -573,6 +629,12 @@ class ArcBluetoothBridge ...@@ -573,6 +629,12 @@ class ArcBluetoothBridge
ArcBluetoothTaskQueue advertisement_queue_; ArcBluetoothTaskQueue advertisement_queue_;
ArcBluetoothTaskQueue discovery_queue_; ArcBluetoothTaskQueue discovery_queue_;
// Rfcomm sockets that live in Chrome.
std::set<std::unique_ptr<RfcommListeningSocket>, base::UniquePtrComparator>
listening_sockets_;
std::set<std::unique_ptr<RfcommConnectingSocket>, base::UniquePtrComparator>
connecting_sockets_;
THREAD_CHECKER(thread_checker_); THREAD_CHECKER(thread_checker_);
// WeakPtrFactory to use for callbacks. // WeakPtrFactory to use for callbacks.
......
...@@ -68,6 +68,29 @@ std::string TypeConverter<std::string, arc::mojom::BluetoothAddress>::Convert( ...@@ -68,6 +68,29 @@ std::string TypeConverter<std::string, arc::mojom::BluetoothAddress>::Convert(
return addr_stream.str(); return addr_stream.str();
} }
// static
arc::mojom::BluetoothAddressPtr
TypeConverter<arc::mojom::BluetoothAddressPtr, bdaddr_t>::Convert(
const bdaddr_t& address) {
arc::mojom::BluetoothAddressPtr mojo_addr =
arc::mojom::BluetoothAddress::New();
mojo_addr->address.resize(kAddressSize);
std::reverse_copy(std::begin(address.b), std::end(address.b),
std::begin(mojo_addr->address));
return mojo_addr;
}
// static
bdaddr_t TypeConverter<bdaddr_t, arc::mojom::BluetoothAddress>::Convert(
const arc::mojom::BluetoothAddress& address) {
bdaddr_t ret;
std::reverse_copy(std::begin(address.address), std::end(address.address),
std::begin(ret.b));
return ret;
}
// static // static
arc::mojom::BluetoothSdpAttributePtr arc::mojom::BluetoothSdpAttributePtr
TypeConverter<arc::mojom::BluetoothSdpAttributePtr, TypeConverter<arc::mojom::BluetoothSdpAttributePtr,
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
#ifndef COMPONENTS_ARC_BLUETOOTH_BLUETOOTH_TYPE_CONVERTERS_H_ #ifndef COMPONENTS_ARC_BLUETOOTH_BLUETOOTH_TYPE_CONVERTERS_H_
#define COMPONENTS_ARC_BLUETOOTH_BLUETOOTH_TYPE_CONVERTERS_H_ #define COMPONENTS_ARC_BLUETOOTH_BLUETOOTH_TYPE_CONVERTERS_H_
#include <bluetooth/bluetooth.h>
#include <stddef.h> #include <stddef.h>
#include <stdint.h> #include <stdint.h>
#include <string> #include <string>
...@@ -40,6 +41,16 @@ struct TypeConverter<std::string, arc::mojom::BluetoothAddress> { ...@@ -40,6 +41,16 @@ struct TypeConverter<std::string, arc::mojom::BluetoothAddress> {
static std::string Convert(const arc::mojom::BluetoothAddress& ptr); static std::string Convert(const arc::mojom::BluetoothAddress& ptr);
}; };
template <>
struct TypeConverter<arc::mojom::BluetoothAddressPtr, bdaddr_t> {
static arc::mojom::BluetoothAddressPtr Convert(const bdaddr_t& address);
};
template <>
struct TypeConverter<bdaddr_t, arc::mojom::BluetoothAddress> {
static bdaddr_t Convert(const arc::mojom::BluetoothAddress& address);
};
template <> template <>
struct TypeConverter<arc::mojom::BluetoothSdpAttributePtr, struct TypeConverter<arc::mojom::BluetoothSdpAttributePtr,
bluez::BluetoothServiceAttributeValueBlueZ> { bluez::BluetoothServiceAttributeValueBlueZ> {
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
// Next MinVersion: 15 // Next MinVersion: 16
module arc.mojom; module arc.mojom;
...@@ -286,7 +286,37 @@ struct BluetoothCreateSdpRecordResult { ...@@ -286,7 +286,37 @@ struct BluetoothCreateSdpRecordResult {
uint32 service_handle; uint32 service_handle;
}; };
// Next Method ID: 45 // BluetoothRfcommConnection contains the information for a new RFCOMM
// connection. Since we cannot get socket (or peer) name on the transferred
// socket on Android side, we also need to pass the peer address and channel
// number.
struct BluetoothRfcommConnection {
handle sock;
BluetoothAddress addr;
int32 channel;
};
// The mojo connection represents a listening socket.
// Next Method ID: 1
interface RfcommListeningSocketClient {
// Called when accept() succeeds. |channel| in |connection| is the peer
// channel number.
OnAccepted@0(BluetoothRfcommConnection connection);
};
// The mojo connection represents a connecting (not connected yet) socket.
// After connect() either succeeds or fails, Android is responsible for closing
// this mojo connection.
// Next Method ID: 2
interface RfcommConnectingSocketClient {
// Called when connect() succeeds. |channel| in |connection| is the channel
// number on our side.
OnConnected@0(BluetoothRfcommConnection connection);
// Called when connect() fails.
OnConnectFailed@1();
};
// Next Method ID: 47
// Deprecated Method ID: 4, 5, 6, 7, 20, 21 // Deprecated Method ID: 4, 5, 6, 7, 20, 21
interface BluetoothHost { interface BluetoothHost {
EnableAdapter@0() => (BluetoothAdapterState state); EnableAdapter@0() => (BluetoothAdapterState state);
...@@ -393,6 +423,24 @@ interface BluetoothHost { ...@@ -393,6 +423,24 @@ interface BluetoothHost {
=> (BluetoothGattStatus status); => (BluetoothGattStatus status);
[MinVersion=8] DisableAdvertisement@43(int32 adv_handle) [MinVersion=8] DisableAdvertisement@43(int32 adv_handle)
=> (BluetoothGattStatus status); => (BluetoothGattStatus status);
// RFCOMM functions
// Opens a bluetooth socket with socket option |optval|, and then bind() and
// listen() with the requested RFCOMM |channel| number. When |channel| = 0,
// we will select a channel number automatically. If this process succeeds,
// returns SUCCESS in |status|, the actual listening channel in |channel|,
// and a new mojo connection which represents the listening socket.
[MinVersion=15] RfcommListen@45(int32 channel, int32 optval)
=> (BluetoothStatus status, int32 channel,
RfcommListeningSocketClient&? client);
// Opens a bluetooth socket with socket option |optval|, and then connect()
// to (|remote_addr|, |channel|). If this process succeeds, returns SUCCESS
// in |status| and a new mojo connection which holds the connecting socket.
// Unlike in RfcommListen(), |channel| here could not be 0, since this is the
// peer channel number.
[MinVersion=15] RfcommConnect@46(BluetoothAddress remote_addr,
int32 channel, int32 optval)
=> (BluetoothStatus status, RfcommConnectingSocketClient&? client);
}; };
// Next Method ID: 22 // Next Method ID: 22
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment