Reland "[Fast Pair] Add support for retroactive pairing for BLE HID"
This is a reland of commit 0b3859cd0d
and fixes test failures on MSAN builder.
Original change's description:
> [Fast Pair] Add support for retroactive pairing for BLE HID
>
> For BLE devices, since we cannot connect to a message stream to
> retrieve the model ID and the BLE address is already known, the
> only remaining parameter needed is the model ID, which we
> retrieve via GATT characteristic
>
> BUG=b:308092093
> TEST=unit tests
> TEST=manually test initial and retroactive pairing for HID
>
> Change-Id: Ic93cffff190b5927f49e00d23e0781dd1c04efe0
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4995811
> Reviewed-by: Reilly Grant <reillyg@chromium.org>
> Reviewed-by: Jack Shira <jackshira@google.com>
> Commit-Queue: Katherine Lai <laikatherine@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#1224991}
BUG=b:308092093
TEST=MSAN browser_tests
TEST=ash_unittests
Change-Id: I941e3ee4fc1c90858f01d71bb1b7da988b59f366
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5038291
Reviewed-by: Reilly Grant <reillyg@chromium.org>
Commit-Queue: Katherine Lai <laikatherine@chromium.org>
Reviewed-by: Jack Shira <jackshira@google.com>
Cr-Commit-Position: refs/heads/main@{#1225663}
This commit is contained in:

committed by
Chromium LUCI CQ

parent
3a214002ca
commit
619c703d95
ash/quick_pair/pairing
retroactive_pairing_detector_impl.ccretroactive_pairing_detector_impl.hretroactive_pairing_detector_unittest.cc
device/bluetooth/test
@ -9,6 +9,7 @@
|
||||
#include "ash/quick_pair/common/constants.h"
|
||||
#include "ash/quick_pair/common/device.h"
|
||||
#include "ash/quick_pair/common/protocol.h"
|
||||
#include "ash/quick_pair/fast_pair_handshake/fast_pair_gatt_service_client_lookup_impl.h"
|
||||
#include "ash/quick_pair/message_stream/message_stream.h"
|
||||
#include "ash/quick_pair/repository/fast_pair_repository.h"
|
||||
#include "ash/session/session_controller_impl.h"
|
||||
@ -219,8 +220,27 @@ void RetroactivePairingDetectorImpl::AttemptRetroactivePairing(
|
||||
return;
|
||||
}
|
||||
|
||||
device::BluetoothDevice* device = adapter_->GetDevice(classic_address);
|
||||
if (!device) {
|
||||
CD_LOG(WARNING, Feature::FP)
|
||||
<< __func__ << ": Lost device to potentially retroactively pair to.";
|
||||
RemoveDeviceInformation(classic_address);
|
||||
return;
|
||||
}
|
||||
|
||||
CD_LOG(VERBOSE, Feature::FP) << __func__ << ": device = " << classic_address;
|
||||
|
||||
// For BLE devices, since we cannot connect to a message stream to retrieve
|
||||
// the model ID and the BLE address is already known, the only remaining
|
||||
// parameter needed is the model ID, which we retrieve via GATT characteristic
|
||||
if (ash::features::IsFastPairHIDEnabled() &&
|
||||
device->GetType() == device::BLUETOOTH_TRANSPORT_LE) {
|
||||
CD_LOG(VERBOSE, Feature::FP)
|
||||
<< __func__ << ": BLE device detected, creating GATT connection";
|
||||
CreateGattConnection(device);
|
||||
return;
|
||||
}
|
||||
|
||||
// Attempt to retrieve a MessageStream instance immediately, if it was
|
||||
// already connected.
|
||||
MessageStream* message_stream =
|
||||
@ -232,6 +252,98 @@ void RetroactivePairingDetectorImpl::AttemptRetroactivePairing(
|
||||
GetModelIdAndAddressFromMessageStream(classic_address, message_stream);
|
||||
}
|
||||
|
||||
void RetroactivePairingDetectorImpl::CreateGattConnection(
|
||||
device::BluetoothDevice* device) {
|
||||
auto* fast_pair_gatt_service_client =
|
||||
FastPairGattServiceClientLookup::GetInstance()->Get(device);
|
||||
|
||||
if (fast_pair_gatt_service_client) {
|
||||
if (fast_pair_gatt_service_client->IsConnected()) {
|
||||
CD_LOG(VERBOSE, Feature::FP)
|
||||
<< __func__
|
||||
<< ": Reusing existing GATT service client to retrieve model ID";
|
||||
fast_pair_gatt_service_client->ReadModelIdAsync(
|
||||
base::BindOnce(&RetroactivePairingDetectorImpl::OnReadModelId,
|
||||
weak_ptr_factory_.GetWeakPtr(), device->GetAddress()));
|
||||
return;
|
||||
} else {
|
||||
// If the previous gatt service client did not connect successfully
|
||||
// or is no longer connected, erase it before attempting to create a new
|
||||
// gatt connection for the device.
|
||||
FastPairGattServiceClientLookup::GetInstance()->Erase(device);
|
||||
}
|
||||
}
|
||||
|
||||
CD_LOG(VERBOSE, Feature::FP)
|
||||
<< __func__ << ": Creating new GATT service client to retrieve model ID";
|
||||
|
||||
FastPairGattServiceClientLookup::GetInstance()->Create(
|
||||
adapter_, device,
|
||||
base::BindOnce(
|
||||
&RetroactivePairingDetectorImpl::OnGattClientInitializedCallback,
|
||||
weak_ptr_factory_.GetWeakPtr(), device));
|
||||
}
|
||||
|
||||
void RetroactivePairingDetectorImpl::OnGattClientInitializedCallback(
|
||||
device::BluetoothDevice* device,
|
||||
absl::optional<PairFailure> failure) {
|
||||
if (failure) {
|
||||
CD_LOG(WARNING, Feature::FP)
|
||||
<< __func__
|
||||
<< ": Failed to initialize GATT service client with failure = "
|
||||
<< failure.value();
|
||||
return;
|
||||
}
|
||||
|
||||
// If |OnGattClientInitializedCallback| is called without a failure,
|
||||
// |device*| is expected to exist and be valid.
|
||||
auto* fast_pair_gatt_service_client =
|
||||
FastPairGattServiceClientLookup::GetInstance()->Get(device);
|
||||
|
||||
if (!fast_pair_gatt_service_client ||
|
||||
!fast_pair_gatt_service_client->IsConnected()) {
|
||||
CD_LOG(WARNING, Feature::FP) << __func__
|
||||
<< ": Fast Pair Gatt Service Client failed to "
|
||||
"be created or is no longer connected.";
|
||||
FastPairGattServiceClientLookup::GetInstance()->Erase(device);
|
||||
return;
|
||||
}
|
||||
|
||||
CD_LOG(VERBOSE, Feature::FP) << __func__
|
||||
<< ": Fast Pair GATT service client initialized "
|
||||
"successfully. Reading Model ID.";
|
||||
|
||||
fast_pair_gatt_service_client->ReadModelIdAsync(
|
||||
base::BindOnce(&RetroactivePairingDetectorImpl::OnReadModelId,
|
||||
weak_ptr_factory_.GetWeakPtr(), device->GetAddress()));
|
||||
}
|
||||
|
||||
void RetroactivePairingDetectorImpl::OnReadModelId(
|
||||
const std::string& address,
|
||||
absl::optional<device::BluetoothGattService::GattErrorCode> error_code,
|
||||
const std::vector<uint8_t>& value) {
|
||||
if (error_code) {
|
||||
CD_LOG(WARNING, Feature::FP)
|
||||
<< __func__ << ": Failed to read model ID with failure = "
|
||||
<< static_cast<uint32_t>(error_code.value());
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.size() != 3) {
|
||||
CD_LOG(WARNING, Feature::FP) << __func__ << ": model ID malformed.";
|
||||
return;
|
||||
}
|
||||
|
||||
std::string model_id;
|
||||
for (auto byte : value) {
|
||||
model_id.append(base::StringPrintf("%02X", byte));
|
||||
}
|
||||
|
||||
CD_LOG(INFO, Feature::FP) << __func__ << ": Model ID " << model_id
|
||||
<< " found for device " << address;
|
||||
NotifyDeviceFound(model_id, address, address);
|
||||
}
|
||||
|
||||
void RetroactivePairingDetectorImpl::OnMessageStreamConnected(
|
||||
const std::string& device_address,
|
||||
MessageStream* message_stream) {
|
||||
|
@ -158,6 +158,19 @@ class RetroactivePairingDetectorImpl final
|
||||
// |message_streams_| if a MessageStream exists for the device.
|
||||
void RemoveExpiredDevicesFromStoredDeviceData();
|
||||
|
||||
// Gets or creates a Gatt connection to |device|.
|
||||
void CreateGattConnection(device::BluetoothDevice* device);
|
||||
|
||||
// Internal method called when creating a FastPairGattServiceClient.
|
||||
void OnGattClientInitializedCallback(device::BluetoothDevice* device,
|
||||
absl::optional<PairFailure> failure);
|
||||
|
||||
// Internal method called to retrieve the model ID of a device.
|
||||
void OnReadModelId(
|
||||
const std::string& address,
|
||||
absl::optional<device::BluetoothGattService::GattErrorCode> error_code,
|
||||
const std::vector<uint8_t>& value);
|
||||
|
||||
// The classic pairing addresses of potential Retroactive Pair supported
|
||||
// devices that are found in the adapter. We have to store them and wait for a
|
||||
// MessageStream instance to be created for the device in order to fully
|
||||
|
@ -13,6 +13,9 @@
|
||||
#include "ash/quick_pair/common/logging.h"
|
||||
#include "ash/quick_pair/common/pair_failure.h"
|
||||
#include "ash/quick_pair/common/protocol.h"
|
||||
#include "ash/quick_pair/fast_pair_handshake/fake_fast_pair_gatt_service_client.h"
|
||||
#include "ash/quick_pair/fast_pair_handshake/fast_pair_gatt_service_client_impl.h"
|
||||
#include "ash/quick_pair/fast_pair_handshake/fast_pair_gatt_service_client_lookup_impl.h"
|
||||
#include "ash/quick_pair/message_stream/fake_bluetooth_socket.h"
|
||||
#include "ash/quick_pair/message_stream/fake_message_stream_lookup.h"
|
||||
#include "ash/quick_pair/message_stream/message_stream.h"
|
||||
@ -56,6 +59,7 @@ const std::vector<uint8_t> kModelIdBytes = {
|
||||
/*message_code=*/0x01,
|
||||
/*additional_data_length=*/0x00, 0x03,
|
||||
/*additional_data=*/0xAA, 0xBB, 0xCC};
|
||||
const std::vector<uint8_t> kModelIdBytesNoMetadata = {0xAA, 0xBB, 0xCC};
|
||||
const std::string kModelId = "AABBCC";
|
||||
|
||||
const std::vector<uint8_t> kBleAddressBytes = {
|
||||
@ -97,6 +101,36 @@ CreateTestBluetoothDevice(std::string address) {
|
||||
/*paired=*/true, /*connected=*/false);
|
||||
}
|
||||
|
||||
class FakeFastPairGattServiceClientImplFactory
|
||||
: public ash::quick_pair::FastPairGattServiceClientImpl::Factory {
|
||||
public:
|
||||
~FakeFastPairGattServiceClientImplFactory() override = default;
|
||||
|
||||
ash::quick_pair::FakeFastPairGattServiceClient*
|
||||
fake_fast_pair_gatt_service_client() {
|
||||
return fake_fast_pair_gatt_service_client_;
|
||||
}
|
||||
|
||||
private:
|
||||
// FastPairGattServiceClientImpl::Factory:
|
||||
std::unique_ptr<ash::quick_pair::FastPairGattServiceClient> CreateInstance(
|
||||
device::BluetoothDevice* device,
|
||||
scoped_refptr<device::BluetoothAdapter> adapter,
|
||||
base::OnceCallback<void(absl::optional<ash::quick_pair::PairFailure>)>
|
||||
on_initialized_callback) override {
|
||||
auto fake_fast_pair_gatt_service_client =
|
||||
std::make_unique<ash::quick_pair::FakeFastPairGattServiceClient>(
|
||||
device, adapter, std::move(on_initialized_callback));
|
||||
fake_fast_pair_gatt_service_client_ =
|
||||
fake_fast_pair_gatt_service_client.get();
|
||||
return fake_fast_pair_gatt_service_client;
|
||||
}
|
||||
|
||||
raw_ptr<ash::quick_pair::FakeFastPairGattServiceClient,
|
||||
DanglingUntriaged | ExperimentalAsh>
|
||||
fake_fast_pair_gatt_service_client_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace ash {
|
||||
@ -111,6 +145,9 @@ class RetroactivePairingDetectorTest
|
||||
|
||||
void SetUp() override {
|
||||
AshTestBase::SetUp();
|
||||
FastPairGattServiceClientImpl::Factory::SetFactoryForTesting(
|
||||
&fast_pair_gatt_service_factory_);
|
||||
|
||||
adapter_ = base::MakeRefCounted<FakeBluetoothAdapter>();
|
||||
device::BluetoothAdapterFactory::SetAdapterForTesting(adapter_);
|
||||
|
||||
@ -168,11 +205,23 @@ class RetroactivePairingDetectorTest
|
||||
mock_pairer_broker_->NotifyDevicePaired(fp_device);
|
||||
}
|
||||
|
||||
void PairFastPairDeviceWithClassicBluetooth(bool new_paired_status,
|
||||
std::string classic_address) {
|
||||
void PairFastPairDeviceWithClassicBluetooth(
|
||||
bool new_paired_status,
|
||||
std::string classic_address,
|
||||
bool test_hid_already_connected = false) {
|
||||
bluetooth_device_ = CreateTestBluetoothDevice(classic_address);
|
||||
bluetooth_device_->AddUUID(ash::quick_pair::kFastPairBluetoothUuid);
|
||||
bluetooth_device_->SetType(
|
||||
device::BluetoothTransport::BLUETOOTH_TRANSPORT_LE);
|
||||
auto* bt_device_ptr = bluetooth_device_.get();
|
||||
if (test_hid_already_connected) {
|
||||
// Simulate a GATT service client connection already open and connected
|
||||
auto gatt_service_client = FastPairGattServiceClientImpl::Factory::Create(
|
||||
bt_device_ptr, adapter_.get(), base::DoNothing());
|
||||
FastPairGattServiceClientLookup::GetInstance()->InsertFakeForTesting(
|
||||
bt_device_ptr, std::move(gatt_service_client));
|
||||
SetGattServiceClientConnected(true);
|
||||
}
|
||||
adapter_->AddMockDevice(std::move(bluetooth_device_));
|
||||
adapter_->NotifyDevicePairedChanged(bt_device_ptr, new_paired_status);
|
||||
}
|
||||
@ -200,6 +249,24 @@ class RetroactivePairingDetectorTest
|
||||
SimulateUserLogin(kUserEmail, user_type);
|
||||
}
|
||||
|
||||
void SetGattServiceClientConnected(bool connected) {
|
||||
fast_pair_gatt_service_factory_.fake_fast_pair_gatt_service_client()
|
||||
->SetConnected(connected);
|
||||
}
|
||||
|
||||
void RunGattClientInitializedCallback(
|
||||
absl::optional<PairFailure> pair_failure) {
|
||||
fast_pair_gatt_service_factory_.fake_fast_pair_gatt_service_client()
|
||||
->RunOnGattClientInitializedCallback(pair_failure);
|
||||
}
|
||||
|
||||
void RunReadModelIdCallback(
|
||||
absl::optional<device::BluetoothGattService::GattErrorCode> error_code,
|
||||
const std::vector<uint8_t>& value) {
|
||||
fast_pair_gatt_service_factory_.fake_fast_pair_gatt_service_client()
|
||||
->RunReadModelIdCallback(error_code, value);
|
||||
}
|
||||
|
||||
protected:
|
||||
bool retroactive_pair_found_ = false;
|
||||
scoped_refptr<Device> retroactive_device_;
|
||||
@ -216,6 +283,8 @@ class RetroactivePairingDetectorTest
|
||||
fake_message_stream_lookup_ = nullptr;
|
||||
std::unique_ptr<FakeFastPairRepository> fast_pair_repository_;
|
||||
|
||||
FakeFastPairGattServiceClientImplFactory fast_pair_gatt_service_factory_;
|
||||
|
||||
mojo::SharedRemote<mojom::FastPairDataParser> data_parser_remote_;
|
||||
mojo::PendingRemote<mojom::FastPairDataParser> fast_pair_data_parser_;
|
||||
std::unique_ptr<FastPairDataParser> data_parser_;
|
||||
@ -2078,5 +2147,112 @@ TEST_F(RetroactivePairingDetectorTest, NoCrashWhenFootprintsResponseIsSlow) {
|
||||
fast_pair_repository_->TriggerIsDeviceSavedToAccountCallback();
|
||||
}
|
||||
|
||||
TEST_F(RetroactivePairingDetectorTest, FastPairHID_Success) {
|
||||
Login(user_manager::UserType::USER_TYPE_REGULAR);
|
||||
base::test::ScopedFeatureList feature_list;
|
||||
feature_list.InitWithFeatures(
|
||||
/*enabled_features=*/{features::kFastPairSavedDevices,
|
||||
features::kFastPairSavedDevicesStrictOptIn,
|
||||
features::kFastPairHID},
|
||||
/*disabled_features=*/{});
|
||||
fast_pair_repository_->SetOptInStatus(
|
||||
nearby::fastpair::OptInStatus::STATUS_OPTED_IN);
|
||||
base::RunLoop().RunUntilIdle();
|
||||
CreateRetroactivePairingDetector();
|
||||
|
||||
EXPECT_FALSE(retroactive_pair_found_);
|
||||
|
||||
// Test the normal retroactive pair flow of a BLE HID
|
||||
PairFastPairDeviceWithClassicBluetooth(
|
||||
/*new_paired_status=*/true, kBleAddress);
|
||||
SetGattServiceClientConnected(true);
|
||||
RunGattClientInitializedCallback(/*pair_failure=*/absl::nullopt);
|
||||
RunReadModelIdCallback(/*error_code=*/absl::nullopt, kModelIdBytesNoMetadata);
|
||||
|
||||
EXPECT_TRUE(retroactive_pair_found_);
|
||||
EXPECT_EQ(retroactive_device_->ble_address(), kBleAddress);
|
||||
EXPECT_EQ(retroactive_device_->metadata_id(), kModelId);
|
||||
}
|
||||
|
||||
TEST_F(RetroactivePairingDetectorTest, FastPairHID_GattConnectionOpen_Success) {
|
||||
Login(user_manager::UserType::USER_TYPE_REGULAR);
|
||||
base::test::ScopedFeatureList feature_list;
|
||||
feature_list.InitWithFeatures(
|
||||
/*enabled_features=*/{features::kFastPairSavedDevices,
|
||||
features::kFastPairSavedDevicesStrictOptIn,
|
||||
features::kFastPairHID},
|
||||
/*disabled_features=*/{});
|
||||
fast_pair_repository_->SetOptInStatus(
|
||||
nearby::fastpair::OptInStatus::STATUS_OPTED_IN);
|
||||
base::RunLoop().RunUntilIdle();
|
||||
CreateRetroactivePairingDetector();
|
||||
|
||||
EXPECT_FALSE(retroactive_pair_found_);
|
||||
|
||||
// If GATT connection already open, we expect a read to Model ID
|
||||
// immediately after.
|
||||
PairFastPairDeviceWithClassicBluetooth(
|
||||
/*new_paired_status=*/true, kBleAddress,
|
||||
/*test_hid_already_connected=*/true);
|
||||
RunReadModelIdCallback(/*error_code*/ absl::nullopt, kModelIdBytesNoMetadata);
|
||||
|
||||
EXPECT_TRUE(retroactive_pair_found_);
|
||||
EXPECT_EQ(retroactive_device_->ble_address(), kBleAddress);
|
||||
EXPECT_EQ(retroactive_device_->metadata_id(), kModelId);
|
||||
}
|
||||
|
||||
TEST_F(RetroactivePairingDetectorTest, FastPairHID_GattConnectionFailure) {
|
||||
Login(user_manager::UserType::USER_TYPE_REGULAR);
|
||||
base::test::ScopedFeatureList feature_list;
|
||||
feature_list.InitWithFeatures(
|
||||
/*enabled_features=*/{features::kFastPairSavedDevices,
|
||||
features::kFastPairSavedDevicesStrictOptIn,
|
||||
features::kFastPairHID},
|
||||
/*disabled_features=*/{});
|
||||
fast_pair_repository_->SetOptInStatus(
|
||||
nearby::fastpair::OptInStatus::STATUS_OPTED_IN);
|
||||
base::RunLoop().RunUntilIdle();
|
||||
CreateRetroactivePairingDetector();
|
||||
|
||||
EXPECT_FALSE(retroactive_pair_found_);
|
||||
|
||||
PairFastPairDeviceWithClassicBluetooth(
|
||||
/*new_paired_status=*/true, kBleAddress);
|
||||
SetGattServiceClientConnected(true);
|
||||
|
||||
// If we get an error while create the GATT connection, we shouldn't
|
||||
// expect a retroactive pairable device to be found.
|
||||
RunGattClientInitializedCallback(PairFailure::kCreateGattConnection);
|
||||
EXPECT_FALSE(retroactive_pair_found_);
|
||||
}
|
||||
|
||||
TEST_F(RetroactivePairingDetectorTest, FastPairHID_ReadModelIdFailure) {
|
||||
Login(user_manager::UserType::USER_TYPE_REGULAR);
|
||||
base::test::ScopedFeatureList feature_list;
|
||||
feature_list.InitWithFeatures(
|
||||
/*enabled_features=*/{features::kFastPairSavedDevices,
|
||||
features::kFastPairSavedDevicesStrictOptIn,
|
||||
features::kFastPairHID},
|
||||
/*disabled_features=*/{});
|
||||
fast_pair_repository_->SetOptInStatus(
|
||||
nearby::fastpair::OptInStatus::STATUS_OPTED_IN);
|
||||
base::RunLoop().RunUntilIdle();
|
||||
CreateRetroactivePairingDetector();
|
||||
|
||||
EXPECT_FALSE(retroactive_pair_found_);
|
||||
|
||||
PairFastPairDeviceWithClassicBluetooth(
|
||||
/*new_paired_status=*/true, kBleAddress);
|
||||
SetGattServiceClientConnected(true);
|
||||
RunGattClientInitializedCallback(/*pair_failure=*/absl::nullopt);
|
||||
|
||||
// If we get an error while reading model ID, we shouldn't expect a
|
||||
// retroactive pairable device to be found.
|
||||
RunReadModelIdCallback(
|
||||
/*error_code=*/device::BluetoothGattService::GattErrorCode::kNotSupported,
|
||||
kModelIdBytesNoMetadata);
|
||||
EXPECT_FALSE(retroactive_pair_found_);
|
||||
}
|
||||
|
||||
} // namespace quick_pair
|
||||
} // namespace ash
|
||||
|
@ -40,6 +40,7 @@ MockBluetoothDevice::MockBluetoothDevice(MockBluetoothAdapter* adapter,
|
||||
ON_CALL(*this, GetNameForDisplay())
|
||||
.WillByDefault(
|
||||
Return(base::UTF8ToUTF16(name_ ? name_.value() : "Unnamed Device")));
|
||||
ON_CALL(*this, GetType()).WillByDefault(ReturnPointee(&transport_));
|
||||
ON_CALL(*this, GetDeviceType())
|
||||
.WillByDefault(Return(BluetoothDeviceType::UNKNOWN));
|
||||
ON_CALL(*this, IsPaired()).WillByDefault(ReturnPointee(&paired_));
|
||||
|
@ -155,6 +155,8 @@ class MockBluetoothDevice : public BluetoothDevice {
|
||||
|
||||
void SetPaired(bool paired) { paired_ = paired; }
|
||||
|
||||
void SetType(device::BluetoothTransport transport) { transport_ = transport; }
|
||||
|
||||
private:
|
||||
uint32_t bluetooth_class_;
|
||||
absl::optional<std::string> name_;
|
||||
@ -162,6 +164,8 @@ class MockBluetoothDevice : public BluetoothDevice {
|
||||
BluetoothDevice::UUIDSet uuids_;
|
||||
bool connected_;
|
||||
bool paired_;
|
||||
device::BluetoothTransport transport_ =
|
||||
device::BluetoothTransport::BLUETOOTH_TRANSPORT_INVALID;
|
||||
|
||||
// Used by tests to save callbacks that will be run in the future.
|
||||
base::queue<base::OnceClosure> pending_callbacks_;
|
||||
|
Reference in New Issue
Block a user