mirror of
https://github.com/kidfromjupiter/nearby.git
synced 2026-09-16 15:36:12 -04:00
Merge branch 'google3'
Change-Id: Ibebf84b98939ee8e5006beedc34d225e8c2dd413
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
cc_library(
|
||||
name = "internal",
|
||||
srcs = [
|
||||
"base_endpoint_channel.cc",
|
||||
"base_pcp_handler.cc",
|
||||
"ble_advertisement.cc",
|
||||
"client_proxy.cc",
|
||||
"encryption_runner.cc",
|
||||
"endpoint_channel_manager.cc",
|
||||
"endpoint_manager.cc",
|
||||
"offline_frames.cc",
|
||||
"service_controller_router.cc",
|
||||
"wifi_lan_service_info.cc",
|
||||
],
|
||||
hdrs = [
|
||||
"base_endpoint_channel.h",
|
||||
"base_pcp_handler.h",
|
||||
"ble_advertisement.h",
|
||||
"client_proxy.h",
|
||||
"encryption_runner.h",
|
||||
"endpoint_channel.h",
|
||||
"endpoint_channel_manager.h",
|
||||
"endpoint_manager.h",
|
||||
"offline_frames.h",
|
||||
"pcp.h",
|
||||
"pcp_handler.h",
|
||||
"service_controller.h",
|
||||
"service_controller_router.h",
|
||||
"wifi_lan_service_info.h",
|
||||
],
|
||||
visibility = [
|
||||
"//core_v2:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//core/internal:message_lite",
|
||||
"//core_v2:core_types",
|
||||
"//proto/connections:offline_wire_formats_portable_proto",
|
||||
"//platform_v2/base",
|
||||
"//platform_v2/public",
|
||||
"//platform_v2/public:logging",
|
||||
"//proto:connections_enums_portable_proto",
|
||||
"//securegcm:ukey2",
|
||||
"//absl/base:core_headers",
|
||||
"//absl/container:flat_hash_map",
|
||||
"//absl/container:flat_hash_set",
|
||||
"//absl/strings",
|
||||
"//absl/time",
|
||||
"//absl/types:span",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "internal_test",
|
||||
testonly = True,
|
||||
hdrs = [
|
||||
"mock_service_controller.h",
|
||||
],
|
||||
visibility = [
|
||||
"//core_v2:__subpackages__",
|
||||
],
|
||||
deps = [
|
||||
":internal",
|
||||
"//testing/base/public:gunit",
|
||||
],
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "core_v2_internal_test",
|
||||
size = "small",
|
||||
srcs = [
|
||||
"base_endpoint_channel_test.cc",
|
||||
"base_pcp_handler_test.cc",
|
||||
"ble_advertisement_test.cc",
|
||||
"client_proxy_test.cc",
|
||||
"encryption_runner_test.cc",
|
||||
"endpoint_channel_manager_test.cc",
|
||||
"endpoint_manager_test.cc",
|
||||
"offline_frames_test.cc",
|
||||
"service_controller_router_test.cc",
|
||||
"wifi_lan_service_info_test.cc",
|
||||
],
|
||||
shard_count = 16,
|
||||
deps = [
|
||||
":internal",
|
||||
":internal_test",
|
||||
"//core_v2:core_types",
|
||||
"//proto/connections:offline_wire_formats_portable_proto",
|
||||
"//platform_v2/base",
|
||||
"//platform_v2/impl/g3", # build_cleaner: keep
|
||||
"//platform_v2/public",
|
||||
"//platform_v2/public:logging",
|
||||
"//proto:connections_enums_portable_proto",
|
||||
"//securegcm:ukey2",
|
||||
"//testing/base/public:gunit",
|
||||
"//testing/base/public:gunit_main",
|
||||
"//absl/container:flat_hash_set",
|
||||
"//absl/synchronization",
|
||||
"//absl/time",
|
||||
"//absl/types:span",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,270 @@
|
||||
#include "core_v2/internal/base_endpoint_channel.h"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
#include "platform_v2/base/byte_array.h"
|
||||
#include "platform_v2/base/exception.h"
|
||||
#include "platform_v2/public/mutex.h"
|
||||
#include "platform_v2/public/mutex_lock.h"
|
||||
#include "proto/connections_enums.pb.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
|
||||
namespace {
|
||||
|
||||
std::int32_t BytesToInt(const ByteArray& bytes) {
|
||||
const char* int_bytes = bytes.data();
|
||||
|
||||
std::int32_t result = 0;
|
||||
result |= (static_cast<std::int32_t>(int_bytes[0]) & 0x0FF) << 24;
|
||||
result |= (static_cast<std::int32_t>(int_bytes[1]) & 0x0FF) << 16;
|
||||
result |= (static_cast<std::int32_t>(int_bytes[2]) & 0x0FF) << 8;
|
||||
result |= (static_cast<std::int32_t>(int_bytes[3]) & 0x0FF);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
ByteArray IntToBytes(std::int32_t value) {
|
||||
char int_bytes[sizeof(std::int32_t)];
|
||||
int_bytes[0] = static_cast<char>((value >> 24) & 0x0FF);
|
||||
int_bytes[1] = static_cast<char>((value >> 16) & 0x0FF);
|
||||
int_bytes[2] = static_cast<char>((value >> 8) & 0x0FF);
|
||||
int_bytes[3] = static_cast<char>((value)&0x0FF);
|
||||
|
||||
return ByteArray(int_bytes, sizeof(int_bytes));
|
||||
}
|
||||
|
||||
ExceptionOr<ByteArray> ReadExactly(InputStream* reader, std::int64_t size) {
|
||||
ByteArray buffer(size);
|
||||
std::int64_t current_pos = 0;
|
||||
|
||||
while (current_pos < size) {
|
||||
ExceptionOr<ByteArray> read_bytes = reader->Read(size - current_pos);
|
||||
if (!read_bytes.ok()) {
|
||||
return read_bytes;
|
||||
}
|
||||
ByteArray result = read_bytes.result();
|
||||
|
||||
if (result.Empty()) {
|
||||
return ExceptionOr<ByteArray>(Exception::kIo);
|
||||
}
|
||||
|
||||
buffer.CopyAt(current_pos, result);
|
||||
current_pos += result.size();
|
||||
}
|
||||
|
||||
return ExceptionOr<ByteArray>(std::move(buffer));
|
||||
}
|
||||
|
||||
ExceptionOr<std::int32_t> ReadInt(InputStream* reader) {
|
||||
ExceptionOr<ByteArray> read_bytes = ReadExactly(reader, sizeof(std::int32_t));
|
||||
if (!read_bytes.ok()) {
|
||||
return ExceptionOr<std::int32_t>(read_bytes.exception());
|
||||
}
|
||||
return ExceptionOr<std::int32_t>(BytesToInt(std::move(read_bytes.result())));
|
||||
}
|
||||
|
||||
Exception WriteInt(OutputStream* writer, std::int32_t value) {
|
||||
return writer->Write(IntToBytes(value));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
BaseEndpointChannel::BaseEndpointChannel(const std::string& channel_name,
|
||||
InputStream* reader,
|
||||
OutputStream* writer)
|
||||
: channel_name_(channel_name), reader_(reader), writer_(writer) {}
|
||||
|
||||
ExceptionOr<ByteArray> BaseEndpointChannel::Read() {
|
||||
ByteArray result;
|
||||
{
|
||||
MutexLock lock(&reader_mutex_);
|
||||
|
||||
ExceptionOr<std::int32_t> read_int = ReadInt(reader_);
|
||||
if (!read_int.ok()) {
|
||||
return ExceptionOr<ByteArray>(read_int.exception());
|
||||
}
|
||||
|
||||
if (read_int.result() < 0 || read_int.result() > kMaxAllowedReadBytes) {
|
||||
return ExceptionOr<ByteArray>(Exception::kIo);
|
||||
}
|
||||
|
||||
ExceptionOr<ByteArray> read_bytes = ReadExactly(reader_, read_int.result());
|
||||
if (!read_bytes.ok()) {
|
||||
return read_bytes;
|
||||
}
|
||||
result = std::move(read_bytes.result());
|
||||
}
|
||||
|
||||
// If encryption is enabled, decode the message.
|
||||
if (IsEncryptionEnabled()) {
|
||||
MutexLock crypto_lock(&crypto_mutex_);
|
||||
result = ByteArray(std::move(
|
||||
*encryption_context_->DecodeMessageFromPeer(std::string(result))));
|
||||
if (result.Empty()) {
|
||||
return ExceptionOr<ByteArray>(Exception::kInvalidProtocolBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
MutexLock lock(&last_read_mutex_);
|
||||
last_read_timestamp_ = SystemClock::ElapsedRealtime();
|
||||
}
|
||||
return ExceptionOr<ByteArray>(result);
|
||||
}
|
||||
|
||||
Exception BaseEndpointChannel::Write(const ByteArray& data) {
|
||||
{
|
||||
MutexLock pause_lock(&is_paused_mutex_);
|
||||
if (is_paused_) {
|
||||
BlockUntilUnpaused();
|
||||
}
|
||||
}
|
||||
|
||||
ByteArray encrypted_data;
|
||||
const ByteArray* data_to_write = &data;
|
||||
{
|
||||
MutexLock crypto_lock(&crypto_mutex_);
|
||||
// If encryption is enabled, encode the message.
|
||||
if (IsEncryptionEnabled()) {
|
||||
encrypted_data = ByteArray(std::move(
|
||||
*encryption_context_->EncodeMessageToPeer(std::string(data))));
|
||||
data_to_write = &encrypted_data;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
MutexLock lock(&writer_mutex_);
|
||||
Exception write_exception =
|
||||
WriteInt(writer_, static_cast<std::int32_t>(data_to_write->size()));
|
||||
if (!write_exception.Ok()) {
|
||||
return write_exception;
|
||||
}
|
||||
|
||||
write_exception = writer_->Write(*data_to_write);
|
||||
if (write_exception.Ok()) {
|
||||
return write_exception;
|
||||
}
|
||||
|
||||
Exception flush_exception = writer_->Flush();
|
||||
if (!flush_exception.Ok()) {
|
||||
return flush_exception;
|
||||
}
|
||||
}
|
||||
|
||||
return {Exception::kSuccess};
|
||||
}
|
||||
|
||||
void BaseEndpointChannel::Close() {
|
||||
{
|
||||
// In case channel is paused, resume it first thing.
|
||||
MutexLock lock(&is_paused_mutex_);
|
||||
UnblockPausedWriter();
|
||||
}
|
||||
CloseIo();
|
||||
CloseImpl();
|
||||
}
|
||||
|
||||
void BaseEndpointChannel::CloseIo() {
|
||||
// Keep this method dedicated to reader and writer handling an nothing else.
|
||||
{
|
||||
// Do not take reader_mutex_ here: read may be in progress, and it will
|
||||
// deadlock. Calling Close() with Read() in progress will terminate the
|
||||
// IO and Read() will proceed normally (with Exception::kIo).
|
||||
Exception exception = reader_->Close();
|
||||
if (!exception.Ok()) {
|
||||
// Add logging.
|
||||
}
|
||||
}
|
||||
{
|
||||
// Do not take writer_mutex_ here: write may be in progress, and it will
|
||||
// deadlock. Calling Close() with Write() in progress will terminate the
|
||||
// IO and Write() will proceed normally (with Exception::kIo).
|
||||
Exception exception = writer_->Close();
|
||||
if (!exception.Ok()) {
|
||||
// Add logging.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BaseEndpointChannel::Close(
|
||||
proto::connections::DisconnectionReason reason) {
|
||||
Close();
|
||||
}
|
||||
|
||||
std::string BaseEndpointChannel::GetType() const {
|
||||
std::string subtype = IsEncryptionEnabled() ? "ENCRYPTED_" : "";
|
||||
|
||||
switch (GetMedium()) {
|
||||
case proto::connections::Medium::BLUETOOTH:
|
||||
return absl::StrCat(subtype, "BLUETOOTH");
|
||||
case proto::connections::Medium::BLE:
|
||||
return absl::StrCat(subtype, "BLE");
|
||||
case proto::connections::Medium::MDNS:
|
||||
return absl::StrCat(subtype, "MDNS");
|
||||
case proto::connections::Medium::WIFI_HOTSPOT:
|
||||
return absl::StrCat(subtype, "WIFI_HOTSPOT");
|
||||
case proto::connections::Medium::WIFI_LAN:
|
||||
return absl::StrCat(subtype, "WIFI_LAN");
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
std::string BaseEndpointChannel::GetName() const { return channel_name_; }
|
||||
|
||||
void BaseEndpointChannel::EnableEncryption(
|
||||
securegcm::D2DConnectionContextV1* encryption_context) {
|
||||
MutexLock lock(&crypto_mutex_);
|
||||
encryption_context_ = encryption_context;
|
||||
}
|
||||
|
||||
bool BaseEndpointChannel::IsPaused() const {
|
||||
MutexLock lock(&is_paused_mutex_);
|
||||
return is_paused_;
|
||||
}
|
||||
|
||||
void BaseEndpointChannel::Pause() {
|
||||
MutexLock lock(&is_paused_mutex_);
|
||||
is_paused_ = true;
|
||||
}
|
||||
|
||||
void BaseEndpointChannel::Resume() {
|
||||
MutexLock lock(&is_paused_mutex_);
|
||||
is_paused_ = false;
|
||||
is_paused_cond_.Notify();
|
||||
}
|
||||
|
||||
absl::Time BaseEndpointChannel::GetLastReadTimestamp() const {
|
||||
MutexLock lock(&last_read_mutex_);
|
||||
return last_read_timestamp_;
|
||||
}
|
||||
|
||||
bool BaseEndpointChannel::IsEncryptionEnabled() const {
|
||||
return encryption_context_ != nullptr;
|
||||
}
|
||||
|
||||
void BaseEndpointChannel::BlockUntilUnpaused() {
|
||||
// For more on how this works, see
|
||||
// https://docs.oracle.com/javase/tutorial/essential/concurrency/guardmeth.html
|
||||
while (is_paused_) {
|
||||
Exception wait_succeeded = is_paused_cond_.Wait();
|
||||
if (!wait_succeeded.Ok()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BaseEndpointChannel::UnblockPausedWriter() {
|
||||
// For more on how this works, see
|
||||
// https://docs.oracle.com/javase/tutorial/essential/concurrency/guardmeth.html
|
||||
is_paused_ = false;
|
||||
is_paused_cond_.Notify();
|
||||
}
|
||||
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,113 @@
|
||||
#ifndef CORE_V2_INTERNAL_BASE_ENDPOINT_CHANNEL_H_
|
||||
#define CORE_V2_INTERNAL_BASE_ENDPOINT_CHANNEL_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "core_v2/internal/endpoint_channel.h"
|
||||
#include "platform_v2/base/byte_array.h"
|
||||
#include "platform_v2/base/input_stream.h"
|
||||
#include "platform_v2/base/output_stream.h"
|
||||
#include "platform_v2/public/atomic_reference.h"
|
||||
#include "platform_v2/public/condition_variable.h"
|
||||
#include "platform_v2/public/mutex.h"
|
||||
#include "platform_v2/public/system_clock.h"
|
||||
#include "proto/connections_enums.pb.h"
|
||||
#include "securegcm/d2d_connection_context_v1.h"
|
||||
#include "absl/base/thread_annotations.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
|
||||
class BaseEndpointChannel : public EndpointChannel {
|
||||
public:
|
||||
BaseEndpointChannel(const std::string& channel_name, InputStream* reader,
|
||||
OutputStream* writer);
|
||||
~BaseEndpointChannel() override = default;
|
||||
|
||||
ExceptionOr<ByteArray> Read()
|
||||
ABSL_LOCKS_EXCLUDED(reader_mutex_, crypto_mutex_,
|
||||
last_read_mutex_) override;
|
||||
|
||||
Exception Write(const ByteArray& data)
|
||||
ABSL_LOCKS_EXCLUDED(writer_mutex_, crypto_mutex_) override;
|
||||
|
||||
// Closes this EndpointChannel, without tracking the closure in analytics.
|
||||
void Close() ABSL_LOCKS_EXCLUDED(is_paused_mutex_) override;
|
||||
|
||||
// Closes this EndpointChannel and records the closure with the given reason.
|
||||
void Close(proto::connections::DisconnectionReason reason) override;
|
||||
|
||||
// Returns a one-word type descriptor for the concrete EndpointChannel
|
||||
// implementation that can be used in log messages; eg: BLUETOOTH, BLE,
|
||||
// WIFI.
|
||||
std::string GetType() const override;
|
||||
|
||||
// Returns the name of the EndpointChannel.
|
||||
std::string GetName() const override;
|
||||
|
||||
// Enables encryption on the EndpointChannel.
|
||||
// Should be called after connection is accepted by both parties, and
|
||||
// before entering data phase, where Payloads may be exchanged.
|
||||
void EnableEncryption(securegcm::D2DConnectionContextV1* context) override;
|
||||
|
||||
// True if the EndpointChannel is currently pausing all writes.
|
||||
bool IsPaused() const ABSL_LOCKS_EXCLUDED(is_paused_mutex_) override;
|
||||
|
||||
// Pauses all writes on this EndpointChannel until resume() is called.
|
||||
void Pause() ABSL_LOCKS_EXCLUDED(is_paused_mutex_) override;
|
||||
|
||||
// Resumes any writes on this EndpointChannel that were suspended when pause()
|
||||
// was called.
|
||||
void Resume() ABSL_LOCKS_EXCLUDED(is_paused_mutex_) override;
|
||||
|
||||
// Returns the timestamp (returned by ElapsedRealtime) of the last read from
|
||||
// this endpoint, or -1 if no reads have occurred.
|
||||
absl::Time GetLastReadTimestamp() const
|
||||
ABSL_LOCKS_EXCLUDED(last_read_mutex_) override;
|
||||
|
||||
protected:
|
||||
virtual void CloseImpl() = 0;
|
||||
|
||||
private:
|
||||
// Used to sanity check that our frame sizes are reasonable.
|
||||
static constexpr std::int32_t kMaxAllowedReadBytes = 1048576; // 1MB
|
||||
|
||||
bool IsEncryptionEnabled() const;
|
||||
void UnblockPausedWriter() ABSL_EXCLUSIVE_LOCKS_REQUIRED(is_paused_mutex_);
|
||||
void BlockUntilUnpaused() ABSL_EXCLUSIVE_LOCKS_REQUIRED(is_paused_mutex_);
|
||||
void CloseIo() ABSL_NO_THREAD_SAFETY_ANALYSIS;
|
||||
|
||||
// We need a separate mutex to pritect read timestamp, because if a read
|
||||
// blocks on IO, we don't want timestamp read access to block too.
|
||||
mutable Mutex last_read_mutex_;
|
||||
absl::Time last_read_timestamp_ ABSL_GUARDED_BY(last_read_mutex_) =
|
||||
absl::InfinitePast();
|
||||
const std::string channel_name_;
|
||||
|
||||
// The reader and writer are synchronized independently since we can't have
|
||||
// writes waiting on reads that might potentially block forever.
|
||||
Mutex reader_mutex_;
|
||||
InputStream* reader_ ABSL_PT_GUARDED_BY(reader_mutex_);
|
||||
|
||||
Mutex writer_mutex_;
|
||||
OutputStream* writer_ ABSL_PT_GUARDED_BY(writer_mutex_);
|
||||
|
||||
// Used by both read and write to protect payload encryption/decryption.
|
||||
Mutex crypto_mutex_;
|
||||
// An encryptor/decryptor. May be null.
|
||||
securegcm::D2DConnectionContextV1* encryption_context_
|
||||
ABSL_PT_GUARDED_BY(crypto_mutex_) = nullptr;
|
||||
|
||||
mutable Mutex is_paused_mutex_;
|
||||
ConditionVariable is_paused_cond_{&is_paused_mutex_};
|
||||
// If true, writes should block until this has been set to false.
|
||||
bool is_paused_ ABSL_GUARDED_BY(is_paused_mutex_) = false;
|
||||
};
|
||||
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
|
||||
#endif // CORE_V2_INTERNAL_BASE_ENDPOINT_CHANNEL_H_
|
||||
@@ -0,0 +1,342 @@
|
||||
#include "core_v2/internal/base_endpoint_channel.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "core_v2/internal/encryption_runner.h"
|
||||
#include "platform_v2/base/byte_array.h"
|
||||
#include "platform_v2/base/input_stream.h"
|
||||
#include "platform_v2/base/output_stream.h"
|
||||
#include "platform_v2/public/count_down_latch.h"
|
||||
#include "platform_v2/public/logging.h"
|
||||
#include "platform_v2/public/multi_thread_executor.h"
|
||||
#include "platform_v2/public/pipe.h"
|
||||
#include "platform_v2/public/single_thread_executor.h"
|
||||
#include "proto/connections_enums.pb.h"
|
||||
#include "proto/connections_enums.pb.h"
|
||||
#include "securegcm/d2d_connection_context_v1.h"
|
||||
#include "securegcm/ukey2_handshake.h"
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "absl/synchronization/mutex.h"
|
||||
#include "absl/time/time.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace {
|
||||
|
||||
using ::location::nearby::proto::connections::DisconnectionReason;
|
||||
using ::location::nearby::proto::connections::Medium;
|
||||
|
||||
class TestEndpointChannel : public BaseEndpointChannel {
|
||||
public:
|
||||
explicit TestEndpointChannel(InputStream* input, OutputStream* output)
|
||||
: BaseEndpointChannel("channel", input, output) {}
|
||||
|
||||
MOCK_METHOD(Medium, GetMedium, (), (const override));
|
||||
MOCK_METHOD(void, CloseImpl, (), (override));
|
||||
};
|
||||
|
||||
std::function<void()> MakeDataPump(
|
||||
std::string label, InputStream* input, OutputStream* output,
|
||||
std::function<void(const ByteArray&)> monitor = nullptr) {
|
||||
return [label, input, output, monitor]() {
|
||||
NEARBY_LOG(INFO, "streaming data thorough '%s'", label.c_str());
|
||||
while (true) {
|
||||
auto read_response = input->Read(Pipe::kChunkSize);
|
||||
if (!read_response.ok()) {
|
||||
NEARBY_LOG(INFO, "Peer reader closed on '%s'", label.c_str());
|
||||
output->Close();
|
||||
break;
|
||||
}
|
||||
if (monitor) {
|
||||
monitor(read_response.result());
|
||||
}
|
||||
auto write_response = output->Write(read_response.result());
|
||||
if (write_response.Raised()) {
|
||||
NEARBY_LOG(INFO, "Peer writer closed on '%s'", label.c_str());
|
||||
input->Close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
NEARBY_LOG(INFO, "streaming terminated on '%s'", label.c_str());
|
||||
};
|
||||
}
|
||||
|
||||
std::function<void(const ByteArray&)> MakeDataMonitor(const std::string& label,
|
||||
std::string* capture,
|
||||
absl::Mutex* mutex) {
|
||||
return [label, capture, mutex](const ByteArray& input) mutable {
|
||||
std::string s = std::string(input);
|
||||
{
|
||||
absl::MutexLock lock(mutex);
|
||||
*capture += s;
|
||||
}
|
||||
NEARBY_LOG(INFO, "source='%s'; message='%s'", label.c_str(), s.c_str());
|
||||
};
|
||||
}
|
||||
|
||||
std::pair<std::unique_ptr<securegcm::D2DConnectionContextV1>,
|
||||
std::unique_ptr<securegcm::D2DConnectionContextV1>>
|
||||
DoDhKeyExchange(BaseEndpointChannel* channel_a,
|
||||
BaseEndpointChannel* channel_b) {
|
||||
std::unique_ptr<securegcm::D2DConnectionContextV1> context_a;
|
||||
std::unique_ptr<securegcm::D2DConnectionContextV1> context_b;
|
||||
EncryptionRunner crypto_a;
|
||||
EncryptionRunner crypto_b;
|
||||
ClientProxy proxy_a;
|
||||
ClientProxy proxy_b;
|
||||
CountDownLatch latch(2);
|
||||
crypto_a.StartClient(
|
||||
&proxy_a, "endpoint_id", channel_a,
|
||||
{
|
||||
.on_success_cb =
|
||||
[&latch, &context_a](
|
||||
const string& endpoint_id,
|
||||
std::unique_ptr<securegcm::UKey2Handshake> ukey2,
|
||||
const string& auth_token, const ByteArray& raw_auth_token) {
|
||||
NEARBY_LOG(INFO, "client-A side key negotiation done");
|
||||
EXPECT_TRUE(ukey2->VerifyHandshake());
|
||||
auto context = ukey2->ToConnectionContext();
|
||||
EXPECT_NE (context, nullptr);
|
||||
context_a = std::move(context);
|
||||
latch.CountDown();
|
||||
},
|
||||
.on_failure_cb =
|
||||
[&latch](const string& endpoint_id, EndpointChannel* channel) {
|
||||
NEARBY_LOG(INFO, "client-A side key negotiation failed");
|
||||
latch.CountDown();
|
||||
},
|
||||
});
|
||||
crypto_b.StartServer(
|
||||
&proxy_b, "endpoint_id", channel_b,
|
||||
{
|
||||
.on_success_cb =
|
||||
[&latch, &context_b](
|
||||
const string& endpoint_id,
|
||||
std::unique_ptr<securegcm::UKey2Handshake> ukey2,
|
||||
const string& auth_token, const ByteArray& raw_auth_token) {
|
||||
NEARBY_LOG(INFO, "client-B side key negotiation done");
|
||||
EXPECT_TRUE(ukey2->VerifyHandshake());
|
||||
auto context = ukey2->ToConnectionContext();
|
||||
EXPECT_NE (context, nullptr);
|
||||
context_b = std::move(context);
|
||||
latch.CountDown();
|
||||
},
|
||||
.on_failure_cb =
|
||||
[&latch](const string& endpoint_id, EndpointChannel* channel) {
|
||||
NEARBY_LOG(INFO, "client-B side key negotiation failed");
|
||||
latch.CountDown();
|
||||
},
|
||||
});
|
||||
EXPECT_TRUE(latch.Await(absl::Milliseconds(5000)).result());
|
||||
return std::make_pair(std::move(context_a), std::move(context_b));
|
||||
}
|
||||
|
||||
TEST(BaseEndpointChannelTest, ConstructorDestructorWorks) {
|
||||
Pipe pipe;
|
||||
InputStream& input_stream = pipe.GetInputStream();
|
||||
OutputStream& output_stream = pipe.GetOutputStream();
|
||||
|
||||
TestEndpointChannel test_channel(&input_stream, &output_stream);
|
||||
}
|
||||
|
||||
TEST(BaseEndpointChannelTest, ReadWrite) {
|
||||
// Direct not-encrypted IO.
|
||||
Pipe pipe_a; // channel_a writes to pipe_a, reads from pipe_b.
|
||||
Pipe pipe_b; // channel_b writes to pipe_b, reads from pipe_a.
|
||||
TestEndpointChannel channel_a(&pipe_b.GetInputStream(),
|
||||
&pipe_a.GetOutputStream());
|
||||
TestEndpointChannel channel_b(&pipe_a.GetInputStream(),
|
||||
&pipe_b.GetOutputStream());
|
||||
ByteArray tx_message{"data message"};
|
||||
channel_a.Write(tx_message);
|
||||
ByteArray rx_message = std::move(channel_b.Read().result());
|
||||
EXPECT_EQ(rx_message, tx_message);
|
||||
}
|
||||
|
||||
TEST(BaseEndpointChannelTest, NotEncryptedReadWriteCanBeIntercepted) {
|
||||
// Not encrypted IO; MITM scenario.
|
||||
|
||||
// Setup test communication environment.
|
||||
absl::Mutex mutex;
|
||||
std::string capture_a;
|
||||
std::string capture_b;
|
||||
Pipe client_a; // Channel "a" writes to client "a", reads from server "a".
|
||||
Pipe client_b; // Channel "b" writes to client "b", reads from server "b".
|
||||
Pipe server_a; // Data pump "a" reads from client "a", writes to server "b".
|
||||
Pipe server_b; // Data pump "b" reads from client "b", writes to server "a".
|
||||
TestEndpointChannel channel_a(&server_a.GetInputStream(),
|
||||
&client_a.GetOutputStream());
|
||||
TestEndpointChannel channel_b(&server_b.GetInputStream(),
|
||||
&client_b.GetOutputStream());
|
||||
|
||||
ON_CALL(channel_a, GetMedium).WillByDefault([]() { return Medium::BLE; });
|
||||
ON_CALL(channel_b, GetMedium).WillByDefault([]() { return Medium::BLE; });
|
||||
|
||||
MultiThreadExecutor executor(2);
|
||||
executor.Execute(MakeDataPump(
|
||||
"pump_a", &client_a.GetInputStream(), &server_b.GetOutputStream(),
|
||||
MakeDataMonitor("monitor_a", &capture_a, &mutex)));
|
||||
executor.Execute(MakeDataPump(
|
||||
"pump_b", &client_b.GetInputStream(), &server_a.GetOutputStream(),
|
||||
MakeDataMonitor("monitor_b", &capture_b, &mutex)));
|
||||
|
||||
EXPECT_EQ(channel_a.GetType(), "BLE");
|
||||
EXPECT_EQ(channel_b.GetType(), "BLE");
|
||||
|
||||
// Start data transfer
|
||||
ByteArray tx_message{"data message"};
|
||||
channel_a.Write(tx_message);
|
||||
ByteArray rx_message = std::move(channel_b.Read().result());
|
||||
|
||||
// Verify expectations.
|
||||
EXPECT_EQ(rx_message, tx_message);
|
||||
{
|
||||
absl::MutexLock lock(&mutex);
|
||||
std::string message{tx_message};
|
||||
EXPECT_TRUE(capture_a.find(message) != std::string::npos ||
|
||||
capture_b.find(message) != std::string::npos);
|
||||
}
|
||||
|
||||
// Shutdown test environment.
|
||||
channel_a.Close(DisconnectionReason::LOCAL_DISCONNECTION);
|
||||
channel_b.Close(DisconnectionReason::REMOTE_DISCONNECTION);
|
||||
}
|
||||
|
||||
TEST(BaseEndpointChannelTest, EncryptedReadWriteCanNotBeIntercepted) {
|
||||
// Encrypted IO; MITM scenario.
|
||||
|
||||
// Setup test communication environment.
|
||||
absl::Mutex mutex;
|
||||
std::string capture_a;
|
||||
std::string capture_b;
|
||||
Pipe client_a; // Channel "a" writes to client "a", reads from server "a".
|
||||
Pipe client_b; // Channel "b" writes to client "b", reads from server "b".
|
||||
Pipe server_a; // Data pump "a" reads from client "a", writes to server "b".
|
||||
Pipe server_b; // Data pump "b" reads from client "b", writes to server "a".
|
||||
TestEndpointChannel channel_a(&server_a.GetInputStream(),
|
||||
&client_a.GetOutputStream());
|
||||
TestEndpointChannel channel_b(&server_b.GetInputStream(),
|
||||
&client_b.GetOutputStream());
|
||||
|
||||
ON_CALL(channel_a, GetMedium).WillByDefault([]() {
|
||||
return Medium::BLUETOOTH;
|
||||
});
|
||||
ON_CALL(channel_b, GetMedium).WillByDefault([]() {
|
||||
return Medium::BLUETOOTH;
|
||||
});
|
||||
|
||||
MultiThreadExecutor executor(2);
|
||||
executor.Execute(MakeDataPump(
|
||||
"pump_a", &client_a.GetInputStream(), &server_b.GetOutputStream(),
|
||||
MakeDataMonitor("monitor_a", &capture_a, &mutex)));
|
||||
executor.Execute(MakeDataPump(
|
||||
"pump_b", &client_b.GetInputStream(), &server_a.GetOutputStream(),
|
||||
MakeDataMonitor("monitor_b", &capture_b, &mutex)));
|
||||
|
||||
// Run DH key exchange; setup encryption contexts for channels.
|
||||
auto [context_a, context_b] = DoDhKeyExchange(&channel_a, &channel_b);
|
||||
ASSERT_NE(context_a, nullptr);
|
||||
ASSERT_NE(context_b, nullptr);
|
||||
channel_a.EnableEncryption(context_a.get());
|
||||
channel_b.EnableEncryption(context_b.get());
|
||||
|
||||
EXPECT_EQ(channel_a.GetType(), "ENCRYPTED_BLUETOOTH");
|
||||
EXPECT_EQ(channel_b.GetType(), "ENCRYPTED_BLUETOOTH");
|
||||
|
||||
// Start data transfer
|
||||
ByteArray tx_message{"data message"};
|
||||
channel_a.Write(tx_message);
|
||||
ByteArray rx_message = std::move(channel_b.Read().result());
|
||||
|
||||
// Verify expectations.
|
||||
EXPECT_EQ(rx_message, tx_message);
|
||||
{
|
||||
absl::MutexLock lock(&mutex);
|
||||
std::string message{tx_message};
|
||||
EXPECT_TRUE(capture_a.find(message) == std::string::npos &&
|
||||
capture_b.find(message) == std::string::npos);
|
||||
}
|
||||
|
||||
// Shutdown test environment.
|
||||
channel_a.Close(DisconnectionReason::LOCAL_DISCONNECTION);
|
||||
channel_b.Close(DisconnectionReason::REMOTE_DISCONNECTION);
|
||||
}
|
||||
|
||||
TEST(BaseEndpointChannelTest, CanBesuspendedAndResumed) {
|
||||
// Setup test communication environment.
|
||||
Pipe pipe_a; // channel_a writes to pipe_a, reads from pipe_b.
|
||||
Pipe pipe_b; // channel_b writes to pipe_b, reads from pipe_a.
|
||||
TestEndpointChannel channel_a(&pipe_b.GetInputStream(),
|
||||
&pipe_a.GetOutputStream());
|
||||
TestEndpointChannel channel_b(&pipe_a.GetInputStream(),
|
||||
&pipe_b.GetOutputStream());
|
||||
|
||||
ON_CALL(channel_a, GetMedium).WillByDefault([]() {
|
||||
return Medium::WIFI_LAN;
|
||||
});
|
||||
ON_CALL(channel_b, GetMedium).WillByDefault([]() {
|
||||
return Medium::WIFI_LAN;
|
||||
});
|
||||
|
||||
EXPECT_EQ(channel_a.GetType(), "WIFI_LAN");
|
||||
EXPECT_EQ(channel_b.GetType(), "WIFI_LAN");
|
||||
|
||||
// Start data transfer
|
||||
ByteArray tx_message{"data message"};
|
||||
ByteArray more_message{"more data"};
|
||||
channel_a.Write(tx_message);
|
||||
ByteArray rx_message = std::move(channel_b.Read().result());
|
||||
|
||||
// Pause and make sure reader blocks.
|
||||
MultiThreadExecutor pause_resume_executor(2);
|
||||
channel_a.Pause();
|
||||
pause_resume_executor.Execute([&channel_a, &more_message](){
|
||||
// Write will block until channel is resumed, or closed.
|
||||
EXPECT_TRUE(channel_a.Write(more_message).Ok());
|
||||
});
|
||||
std::atomic_bool done = false;
|
||||
ByteArray read_more;
|
||||
pause_resume_executor.Execute([&channel_b, &read_more, &done](){
|
||||
// Read will block until channel is resumed, or closed.
|
||||
auto response = channel_b.Read();
|
||||
EXPECT_TRUE(response.ok());
|
||||
read_more = std::move(response.result());
|
||||
done = true;
|
||||
});
|
||||
absl::SleepFor(absl::Milliseconds(500));
|
||||
EXPECT_TRUE(read_more.Empty());
|
||||
|
||||
// Resume; verify that data transfer comepleted.
|
||||
channel_a.Resume();
|
||||
absl::SleepFor(absl::Milliseconds(500));
|
||||
EXPECT_TRUE(done);
|
||||
EXPECT_EQ(read_more, more_message);
|
||||
|
||||
// Shutdown test environment.
|
||||
channel_a.Close(DisconnectionReason::LOCAL_DISCONNECTION);
|
||||
channel_b.Close(DisconnectionReason::REMOTE_DISCONNECTION);
|
||||
}
|
||||
|
||||
TEST(BaseEndpointChannelTest, ReadAfterInputStreamClosed) {
|
||||
Pipe pipe;
|
||||
InputStream& input_stream = pipe.GetInputStream();
|
||||
OutputStream& output_stream = pipe.GetOutputStream();
|
||||
|
||||
TestEndpointChannel test_channel(&input_stream, &output_stream);
|
||||
|
||||
// Close the output stream before trying to read from the input.
|
||||
output_stream.Close();
|
||||
|
||||
// Trying to read should fail gracefully with an IO error.
|
||||
ExceptionOr<ByteArray> read_data = test_channel.Read();
|
||||
|
||||
ASSERT_FALSE(read_data.ok());
|
||||
ASSERT_TRUE(read_data.GetException().Raised(Exception::kIo));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,143 @@
|
||||
#include "core_v2/internal/base_pcp_handler.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <cinttypes>
|
||||
#include <cstdlib>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
|
||||
#include "core_v2/internal/offline_frames.h"
|
||||
#include "platform_v2/public/logging.h"
|
||||
#include "platform_v2/public/system_clock.h"
|
||||
#include "securegcm/d2d_connection_context_v1.h"
|
||||
#include "securegcm/ukey2_handshake.h"
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
#include "absl/types/span.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
|
||||
BasePcpHandler::BasePcpHandler(EndpointManager* endpoint_manager,
|
||||
EndpointChannelManager* channel_manager)
|
||||
: endpoint_manager_(endpoint_manager), channel_manager_(channel_manager) {}
|
||||
|
||||
BasePcpHandler::~BasePcpHandler() {
|
||||
// Unregister ourselves from the FrameProcessors.
|
||||
endpoint_manager_->UnregisterFrameProcessor(V1Frame::CONNECTION_RESPONSE,
|
||||
handle_);
|
||||
|
||||
// Stop all the ongoing Runnables (as gracefully as possible).
|
||||
serial_executor_.Shutdown();
|
||||
alarm_executor_.Shutdown();
|
||||
}
|
||||
|
||||
Status BasePcpHandler::StartAdvertising(ClientProxy* client,
|
||||
const string& service_id,
|
||||
const ConnectionOptions& options,
|
||||
const ConnectionRequestInfo& info) {
|
||||
Future<Status> response;
|
||||
RunOnPcpHandlerThread(
|
||||
[this, client, &service_id, &info, &options, &response]() {
|
||||
auto result = StartAdvertisingImpl(client, service_id,
|
||||
client->GenerateLocalEndpointId(),
|
||||
info.name, options);
|
||||
if (!result.status.Ok()) {
|
||||
response.Set(result.status);
|
||||
return;
|
||||
}
|
||||
|
||||
// Now that we've succeeded, mark the client as advertising.
|
||||
advertising_options_ = options;
|
||||
advertising_listener_ = info.listener;
|
||||
client->StartedAdvertising(service_id, GetStrategy(), info.listener,
|
||||
absl::MakeSpan(result.mediums));
|
||||
response.Set({Status::kSuccess});
|
||||
});
|
||||
return WaitForResult(absl::StrCat("StartAdvertising(", info.name, ")"),
|
||||
client->GetClientId(), &response);
|
||||
}
|
||||
|
||||
void BasePcpHandler::StopAdvertising(ClientProxy* client) {
|
||||
CountDownLatch latch(1);
|
||||
RunOnPcpHandlerThread([this, client, &latch]() {
|
||||
StopAdvertisingImpl(client);
|
||||
client->StoppedAdvertising();
|
||||
advertising_options_.Clear();
|
||||
latch.CountDown();
|
||||
});
|
||||
WaitForLatch("StopAdvertising", &latch);
|
||||
}
|
||||
|
||||
Status BasePcpHandler::StartDiscovery(ClientProxy* client,
|
||||
const string& service_id,
|
||||
const ConnectionOptions& options,
|
||||
const DiscoveryListener& listener) {
|
||||
Future<Status> response;
|
||||
RunOnPcpHandlerThread(
|
||||
[this, client, service_id, options, listener, &response]() {
|
||||
// Ask the implementation to attempt to start discovery.
|
||||
auto result = StartDiscoveryImpl(client, service_id, options);
|
||||
if (!result.status.Ok()) {
|
||||
response.Set(result.status);
|
||||
return;
|
||||
}
|
||||
|
||||
// Now that we've succeeded, mark the client as discovering and clear
|
||||
// out any old endpoints we had discovered.
|
||||
discovery_options_ = options;
|
||||
discovered_endpoints_.clear();
|
||||
client->StartedDiscovery(service_id, GetStrategy(), listener,
|
||||
absl::MakeSpan(result.mediums));
|
||||
response.Set({Status::kSuccess});
|
||||
});
|
||||
return WaitForResult(absl::StrCat("StartDiscovery(", service_id, ")"),
|
||||
client->GetClientId(), &response);
|
||||
}
|
||||
|
||||
void BasePcpHandler::StopDiscovery(ClientProxy* client) {
|
||||
CountDownLatch latch(1);
|
||||
RunOnPcpHandlerThread([this, client, &latch]() {
|
||||
StopDiscoveryImpl(client);
|
||||
client->StoppedDiscovery();
|
||||
discovery_options_.Clear();
|
||||
latch.CountDown();
|
||||
});
|
||||
|
||||
WaitForLatch("stopDiscovery", &latch);
|
||||
}
|
||||
|
||||
void BasePcpHandler::WaitForLatch(const string& method_name,
|
||||
CountDownLatch* latch) {
|
||||
Exception await_exception = latch->Await();
|
||||
if (!await_exception.Ok()) {
|
||||
if (await_exception.Raised(Exception::kTimeout)) {
|
||||
NEARBY_LOG(INFO, "Blocked in %s", method_name.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Status BasePcpHandler::WaitForResult(const string& method_name,
|
||||
std::int64_t client_id,
|
||||
Future<Status>* future) {
|
||||
if (!future) {
|
||||
NEARBY_LOG(INFO, "No future to wait for; return with error");
|
||||
return {Status::kError};
|
||||
}
|
||||
NEARBY_LOG(INFO, "waiting for future to complete");
|
||||
ExceptionOr<Status> result = future->Get();
|
||||
if (!result.ok()) {
|
||||
NEARBY_LOG(INFO, "Future completed with exception: %d", result.exception());
|
||||
return {Status::kError};
|
||||
}
|
||||
NEARBY_LOG(INFO, "Future completed with status: %d", result.result().value);
|
||||
return result.result();
|
||||
}
|
||||
|
||||
void BasePcpHandler::RunOnPcpHandlerThread(Runnable runnable) {
|
||||
serial_executor_.Execute(std::move(runnable));
|
||||
}
|
||||
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,323 @@
|
||||
#ifndef CORE_V2_INTERNAL_BASE_PCP_HANDLER_H_
|
||||
#define CORE_V2_INTERNAL_BASE_PCP_HANDLER_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core_v2/internal/client_proxy.h"
|
||||
#include "core_v2/internal/encryption_runner.h"
|
||||
#include "core_v2/internal/endpoint_channel_manager.h"
|
||||
#include "core_v2/internal/endpoint_manager.h"
|
||||
#include "core_v2/internal/pcp.h"
|
||||
#include "core_v2/internal/pcp_handler.h"
|
||||
#include "core_v2/listeners.h"
|
||||
#include "core_v2/options.h"
|
||||
#include "core_v2/status.h"
|
||||
#include "proto/connections/offline_wire_formats.pb.h"
|
||||
#include "platform_v2/base/prng.h"
|
||||
#include "platform_v2/public/atomic_reference.h"
|
||||
#include "platform_v2/public/cancelable_alarm.h"
|
||||
#include "platform_v2/public/count_down_latch.h"
|
||||
#include "platform_v2/public/future.h"
|
||||
#include "platform_v2/public/scheduled_executor.h"
|
||||
#include "platform_v2/public/single_thread_executor.h"
|
||||
#include "platform_v2/public/system_clock.h"
|
||||
#include "proto/connections_enums.pb.h"
|
||||
#include "securegcm/d2d_connection_context_v1.h"
|
||||
#include "securegcm/ukey2_handshake.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/time/time.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
|
||||
// Define a class that supports move operation for pointers using std::swap.
|
||||
// It replicates std::unique_ptr<> behavior, but it does not own the pointer,
|
||||
// so it does not attempt destroy it.
|
||||
// This approach was recommended during code review, as a better alternative to
|
||||
// reuse of std::unique_ptr<> with custom no-op deleter, for the sake of
|
||||
// readability.
|
||||
template <typename T>
|
||||
class Swapper {
|
||||
public:
|
||||
Swapper(T* pointer) : pointer_(pointer) {} // NOLINT.
|
||||
Swapper(Swapper&& other) { *this = std::move(other); }
|
||||
Swapper& operator=(Swapper&& other) {
|
||||
std::swap(pointer_, other.pointer_);
|
||||
return *this;
|
||||
}
|
||||
T* operator->() const { return pointer_; }
|
||||
T& operator*() { return *pointer_; }
|
||||
operator T*() { return pointer_; } // NOLINT.
|
||||
T* get() const { return pointer_; }
|
||||
void reset() { pointer_ = nullptr; }
|
||||
|
||||
private:
|
||||
T* pointer_ = nullptr;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
Swapper<T> MakeSwapper(T* value) {
|
||||
return Swapper<T>(value);
|
||||
}
|
||||
|
||||
// A base implementation of the PcpHandler interface that takes care of all
|
||||
// bookkeeping and handshake protocols that are common across all PcpHandler
|
||||
// implementations -- thus, every concrete PcpHandler implementation must extend
|
||||
// this class, so that they can focus exclusively on the medium-specific
|
||||
// operations.
|
||||
class BasePcpHandler : public PcpHandler,
|
||||
public EndpointManager::FrameProcessor {
|
||||
public:
|
||||
using FrameProcessor = EndpointManager::FrameProcessor;
|
||||
|
||||
// TODO(tracyzhou): Add SecureRandom.
|
||||
BasePcpHandler(EndpointManager* endpoint_manager,
|
||||
EndpointChannelManager* channel_manager);
|
||||
~BasePcpHandler() override;
|
||||
BasePcpHandler(BasePcpHandler&&) = delete;
|
||||
BasePcpHandler& operator=(BasePcpHandler&&) = delete;
|
||||
|
||||
// We have been asked by the client to start advertising. Once we successfully
|
||||
// start advertising, we'll change the ClientProxy's state.
|
||||
// ConnectionListener (info.listener) will be notified in case of any event.
|
||||
// See
|
||||
// https://source.corp.google.com/piper///depot/google3/core_v2/listeners.h;l=78
|
||||
Status StartAdvertising(ClientProxy* client_proxy,
|
||||
const std::string& service_id,
|
||||
const ConnectionOptions& options,
|
||||
const ConnectionRequestInfo& info) override;
|
||||
|
||||
// If Advertising is active, stop it, and change CLientProxy state,
|
||||
// otherwise do nothing.
|
||||
void StopAdvertising(ClientProxy* client_proxy) override;
|
||||
|
||||
// Start discovery of endpoints that may be advertising.
|
||||
// Update ClientProxy state once discovery started.
|
||||
// DiscoveryListener will get called in case of any event.
|
||||
Status StartDiscovery(ClientProxy* client_proxy,
|
||||
const std::string& service_id,
|
||||
const ConnectionOptions& options,
|
||||
const DiscoveryListener& listener) override;
|
||||
|
||||
// If Discovery is active, stop it, and change CLientProxy state,
|
||||
// otherwise do nothing.
|
||||
void StopDiscovery(ClientProxy* client_proxy) override;
|
||||
|
||||
// If remote endpoint has been successfully discovered, request it to form a
|
||||
// connection, update state on ClientProxy.
|
||||
Status RequestConnection(ClientProxy* client_proxy,
|
||||
const std::string& endpoint_id,
|
||||
const ConnectionRequestInfo& info) override {
|
||||
return Status{Status::kError};
|
||||
}
|
||||
|
||||
// Either party may call this to accept connection on their part.
|
||||
// Until both parties call it, connection will not reach a data phase.
|
||||
// Update state in ClientProxy.
|
||||
Status AcceptConnection(ClientProxy* client_proxy,
|
||||
const std::string& endpoint_id,
|
||||
const PayloadListener& payload_listener) override {
|
||||
return Status{Status::kError};
|
||||
}
|
||||
|
||||
// Either party may call this to accept connection on their part.
|
||||
// If either party does call it, connection will terminate.
|
||||
// Update state in ClientProxy.
|
||||
Status RejectConnection(ClientProxy* client_proxy,
|
||||
const std::string& endpoint_id) override {
|
||||
return Status{Status::kError};
|
||||
}
|
||||
|
||||
// @EndpointManagerReaderThread
|
||||
void OnIncomingFrame(const OfflineFrame& frame,
|
||||
const std::string& endpoint_id, ClientProxy* client,
|
||||
proto::connections::Medium medium) override {}
|
||||
|
||||
// Called when an endpoint disconnects while we're waiting for both sides to
|
||||
// approve/reject the connection.
|
||||
// @EndpointManagerThread
|
||||
void OnEndpointDisconnect(ClientProxy* client_proxy,
|
||||
const std::string& endpoint_id,
|
||||
CountDownLatch* barrier) override {}
|
||||
|
||||
protected:
|
||||
// The result of a call to startAdvertisingImpl() or startDiscoveryImpl().
|
||||
struct StartOperationResult {
|
||||
Status status;
|
||||
// If success, the mediums on which we are now advertising/discovering, for
|
||||
// analytics.
|
||||
std::vector<proto::connections::Medium> mediums;
|
||||
};
|
||||
|
||||
// Represents an endpoint that we've discovered. Typically, the implementation
|
||||
// will know how to connect to this endpoint if asked. (eg. It holds on to a
|
||||
// BluetoothDevice)
|
||||
class DiscoveredEndpoint {
|
||||
public:
|
||||
virtual ~DiscoveredEndpoint() = default;
|
||||
|
||||
virtual std::string GetEndpointId() const = 0;
|
||||
virtual std::string GetEndpointName() const = 0;
|
||||
virtual std::string GetServiceId() const = 0;
|
||||
virtual proto::connections::Medium GetMedium() const = 0;
|
||||
};
|
||||
|
||||
struct ConnectImplResult {
|
||||
proto::connections::Medium medium =
|
||||
proto::connections::Medium::UNKNOWN_MEDIUM;
|
||||
Status status = {Status::kError};
|
||||
std::unique_ptr<EndpointChannel> endpoint_channel;
|
||||
};
|
||||
|
||||
void RunOnPcpHandlerThread(Runnable runnable);
|
||||
|
||||
ConnectionOptions GetConnectionOptions() const;
|
||||
|
||||
// @PcpHandlerThread
|
||||
void OnEndpointFound(ClientProxy* client_proxy,
|
||||
std::unique_ptr<DiscoveredEndpoint> endpoint);
|
||||
|
||||
// @PcpHandlerThread
|
||||
void OnEndpointLost(ClientProxy* client_proxy,
|
||||
const DiscoveredEndpoint* endpoint);
|
||||
|
||||
Exception OnIncomingConnection(
|
||||
ClientProxy* client_proxy, const std::string& remote_device_name,
|
||||
std::unique_ptr<EndpointChannel> endpoint_channel,
|
||||
proto::connections::Medium medium); // throws Exception::IO
|
||||
|
||||
// @PcpHandlerThread
|
||||
virtual StartOperationResult StartAdvertisingImpl(
|
||||
ClientProxy* client_proxy, const std::string& service_id,
|
||||
const std::string& local_endpoint_id,
|
||||
const std::string& local_endpoint_name,
|
||||
const ConnectionOptions& options) = 0;
|
||||
// @PcpHandlerThread
|
||||
virtual Status StopAdvertisingImpl(ClientProxy* client_proxy) = 0;
|
||||
|
||||
// @PcpHandlerThread
|
||||
virtual StartOperationResult StartDiscoveryImpl(
|
||||
ClientProxy* client_proxy, const std::string& service_id,
|
||||
const ConnectionOptions& options) = 0;
|
||||
// @PcpHandlerThread
|
||||
virtual Status StopDiscoveryImpl(ClientProxy* client_proxy) = 0;
|
||||
|
||||
// @PcpHandlerThread
|
||||
virtual ConnectImplResult ConnectImpl(ClientProxy* client_proxy,
|
||||
DiscoveredEndpoint* endpoint) = 0;
|
||||
|
||||
virtual std::vector<proto::connections::Medium>
|
||||
GetConnectionMediumsByPriority() = 0;
|
||||
virtual proto::connections::Medium GetDefaultUpgradeMedium() = 0;
|
||||
|
||||
EndpointManager* endpoint_manager_;
|
||||
EndpointChannelManager* channel_manager_;
|
||||
|
||||
private:
|
||||
static Exception WriteConnectionRequestFrame(
|
||||
EndpointChannel* endpoint_channel, const std::string& local_endpoint_id,
|
||||
const std::string& local_endpoint_name, std::int32_t nonce,
|
||||
const std::vector<proto::connections::Medium>& supported_mediums);
|
||||
|
||||
static constexpr absl::Duration kConnectionRequestReadTimeout =
|
||||
absl::Seconds(2);
|
||||
static constexpr absl::Duration kRejectedConnectionCloseDelay =
|
||||
absl::Seconds(2);
|
||||
|
||||
void OnConnectionResponse(ClientProxy* client_proxy,
|
||||
const std::string& endpoint_id,
|
||||
const OfflineFrame& frame);
|
||||
|
||||
// Returns true if the new endpoint is preferred over the old endpoint.
|
||||
bool IsPreferred(const BasePcpHandler::DiscoveredEndpoint& new_endpoint,
|
||||
const BasePcpHandler::DiscoveredEndpoint& old_endpoint);
|
||||
|
||||
// Called when an incoming connection has been accepted by both sides.
|
||||
//
|
||||
// @param client_proxy The client
|
||||
// @param endpoint_id The id of the remote device
|
||||
// @param supported_mediums The mediums supported by the remote device.
|
||||
// Empty
|
||||
// for outgoing connections and older devices that don't report their
|
||||
// supported mediums.
|
||||
void InitiateBandwidthUpgrade(
|
||||
ClientProxy* client_proxy, const std::string& endpoint_id,
|
||||
const std::vector<proto::connections::Medium>& supported_mediums);
|
||||
|
||||
// Returns the optimal medium supported by both devices.
|
||||
proto::connections::Medium ChooseBestUpgradeMedium(
|
||||
const std::vector<proto::connections::Medium>& supported_mediums);
|
||||
|
||||
void ProcessPreConnectionInitiationFailure(const std::string& endpoint_id,
|
||||
EndpointChannel* channel,
|
||||
Status status,
|
||||
Future<Status>* result);
|
||||
void ProcessPreConnectionResultFailure(ClientProxy* client_proxy,
|
||||
const std::string& endpoint_id);
|
||||
DiscoveredEndpoint* GetDiscoveredEndpoint(const std::string& endpoint_id);
|
||||
|
||||
// Called when either side accepts/rejects the connection, but only takes
|
||||
// effect after both have accepted or one side has rejected.
|
||||
//
|
||||
// NOTE: We also take in a 'can_close_immediately' variable. This is because
|
||||
// any writes in transit are dropped when we close. To avoid having a reject
|
||||
// write being dropped (which causes the other side to report
|
||||
// onResult(DISCONNECTED) instead of onResult(REJECTED)), we delay our
|
||||
// close. If the other side behaves properly, we shouldn't even see the
|
||||
// delay (because they will also close the connection).
|
||||
void EvaluateConnectionResult(ClientProxy* client_proxy,
|
||||
const std::string& endpoint_id,
|
||||
bool can_close_immediately);
|
||||
|
||||
ExceptionOr<OfflineFrame> ReadConnectionRequestFrame(
|
||||
EndpointChannel* channel);
|
||||
|
||||
void WaitForLatch(const std::string& method_name, CountDownLatch* latch);
|
||||
Status WaitForResult(const std::string& method_name, std::int64_t client_id,
|
||||
Future<Status>* future);
|
||||
|
||||
AtomicReference<proto::connections::Medium> bandwidth_upgrade_medium_{
|
||||
proto::connections::Medium::UNKNOWN_MEDIUM};
|
||||
ScheduledExecutor alarm_executor_;
|
||||
SingleThreadExecutor serial_executor_;
|
||||
|
||||
// A map of endpoint id -> DiscoveredEndpoint.
|
||||
absl::flat_hash_map<std::string, std::unique_ptr<DiscoveredEndpoint>>
|
||||
discovered_endpoints_;
|
||||
// A map of endpoint id -> alarm. These alarms delay closing the
|
||||
// EndpointChannel to give the other side enough time to read the rejection
|
||||
// message. It's expected that the other side will close the connection
|
||||
// after reading the message (in which case, this alarm should be cancelled
|
||||
// as it's no longer needed), but this alarm is the fallback in case that
|
||||
// doesn't happen.
|
||||
absl::flat_hash_map<std::string, CancelableAlarm> pending_alarms_;
|
||||
|
||||
// The active ClientProxy's advertising constraints. Empty()
|
||||
// returns true if the client hasn't started advertising false otherwise.
|
||||
// Note: this is not cleared when the client stops advertising because it
|
||||
// might still be useful downstream of advertising (eg: establishing
|
||||
// connections, performing bandwidth upgrades, etc.)
|
||||
ConnectionOptions advertising_options_;
|
||||
// The active ClientProxy's connection lifecycle listener. Non-null while
|
||||
// advertising.
|
||||
ConnectionListener advertising_listener_;
|
||||
|
||||
// The active ClientProxy's discovery constraints. Null if the client
|
||||
// hasn't started discovering. Note: this is not cleared when the client
|
||||
// stops discovering because it might still be useful downstream of
|
||||
// discovery (eg: connection speed, etc.)
|
||||
ConnectionOptions discovery_options_;
|
||||
Prng prng_;
|
||||
EncryptionRunner encryption_runner_;
|
||||
EndpointManager::FrameProcessor::Handle handle_;
|
||||
};
|
||||
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
|
||||
#endif // CORE_V2_INTERNAL_BASE_PCP_HANDLER_H_
|
||||
@@ -0,0 +1,287 @@
|
||||
#include "core_v2/internal/base_pcp_handler.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "core_v2/internal/base_endpoint_channel.h"
|
||||
#include "core_v2/internal/client_proxy.h"
|
||||
#include "core_v2/internal/encryption_runner.h"
|
||||
#include "core_v2/internal/offline_frames.h"
|
||||
#include "core_v2/listeners.h"
|
||||
#include "core_v2/params.h"
|
||||
#include "proto/connections/offline_wire_formats.pb.h"
|
||||
#include "platform_v2/base/byte_array.h"
|
||||
#include "platform_v2/public/count_down_latch.h"
|
||||
#include "platform_v2/public/pipe.h"
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "absl/time/time.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace {
|
||||
|
||||
using ::location::nearby::proto::connections::Medium;
|
||||
using ::testing::_;
|
||||
using ::testing::Invoke;
|
||||
using ::testing::MockFunction;
|
||||
using ::testing::Return;
|
||||
using ::testing::StrictMock;
|
||||
|
||||
class MockEndpointChannel : public BaseEndpointChannel {
|
||||
public:
|
||||
explicit MockEndpointChannel(Pipe* reader, Pipe* writer)
|
||||
: BaseEndpointChannel("channel", &reader->GetInputStream(),
|
||||
&writer->GetOutputStream()) {}
|
||||
|
||||
ExceptionOr<ByteArray> DoRead() { return BaseEndpointChannel::Read(); }
|
||||
Exception DoWrite(const ByteArray& data) {
|
||||
return BaseEndpointChannel::Write(data);
|
||||
}
|
||||
absl::Time DoGetLastReadTimestamp() {
|
||||
return BaseEndpointChannel::GetLastReadTimestamp();
|
||||
}
|
||||
|
||||
MOCK_METHOD(ExceptionOr<ByteArray>, Read, (), (override));
|
||||
MOCK_METHOD(Exception, Write, (const ByteArray& data), (override));
|
||||
MOCK_METHOD(void, CloseImpl, (), (override));
|
||||
MOCK_METHOD(proto::connections::Medium, GetMedium, (), (const override));
|
||||
MOCK_METHOD(std::string, GetType, (), (const override));
|
||||
MOCK_METHOD(std::string, GetName, (), (const override));
|
||||
MOCK_METHOD(bool, IsPaused, (), (const override));
|
||||
MOCK_METHOD(void, Pause, (), (override));
|
||||
MOCK_METHOD(void, Resume, (), (override));
|
||||
MOCK_METHOD(absl::Time, GetLastReadTimestamp, (), (const override));
|
||||
};
|
||||
|
||||
class MockPcpHandler : public BasePcpHandler {
|
||||
public:
|
||||
MockPcpHandler(EndpointManager* em, EndpointChannelManager* ecm)
|
||||
: BasePcpHandler(em, ecm) {}
|
||||
|
||||
// Expose protected inner types of a base type for mocking.
|
||||
using BasePcpHandler::ConnectImplResult;
|
||||
using BasePcpHandler::DiscoveredEndpoint;
|
||||
using BasePcpHandler::StartOperationResult;
|
||||
|
||||
MOCK_METHOD(Strategy, GetStrategy, (), (override));
|
||||
MOCK_METHOD(Pcp, GetPcp, (), (override));
|
||||
|
||||
MOCK_METHOD(StartOperationResult, StartAdvertisingImpl,
|
||||
(ClientProxy * client, const string& service_id,
|
||||
const string& local_endpoint_id,
|
||||
const string& local_endpoint_name,
|
||||
const ConnectionOptions& options),
|
||||
(override));
|
||||
MOCK_METHOD(Status, StopAdvertisingImpl, (ClientProxy * client), (override));
|
||||
MOCK_METHOD(StartOperationResult, StartDiscoveryImpl,
|
||||
(ClientProxy * client, const string& service_id,
|
||||
const ConnectionOptions& options),
|
||||
(override));
|
||||
MOCK_METHOD(Status, StopDiscoveryImpl, (ClientProxy * client), (override));
|
||||
MOCK_METHOD(ConnectImplResult, ConnectImpl,
|
||||
(ClientProxy * client, DiscoveredEndpoint* endpoint), (override));
|
||||
MOCK_METHOD(std::vector<proto::connections::Medium>,
|
||||
GetConnectionMediumsByPriority, (), (override));
|
||||
MOCK_METHOD(proto::connections::Medium, GetDefaultUpgradeMedium, (),
|
||||
(override));
|
||||
|
||||
// Mock adapters for protected non-virtual methods of a base class.
|
||||
void OnEndpointFound(ClientProxy* client,
|
||||
std::unique_ptr<DiscoveredEndpoint> endpoint) {
|
||||
BasePcpHandler::OnEndpointFound(client, std::move(endpoint));
|
||||
}
|
||||
void OnEndpointLost(ClientProxy* client, DiscoveredEndpoint* endpoint) {
|
||||
BasePcpHandler::OnEndpointLost(client, endpoint);
|
||||
}
|
||||
};
|
||||
|
||||
class MockDiscoveredEndpoint final : public MockPcpHandler::DiscoveredEndpoint {
|
||||
public:
|
||||
MOCK_METHOD(std::string, GetEndpointId, (), (const override));
|
||||
MOCK_METHOD(std::string, GetEndpointName, (), (const override));
|
||||
MOCK_METHOD(std::string, GetServiceId, (), (const override));
|
||||
MOCK_METHOD(Medium, GetMedium, (), (const override));
|
||||
};
|
||||
|
||||
class BasePcpHandlerTest : public ::testing::Test {
|
||||
protected:
|
||||
struct MockConnectionListener {
|
||||
StrictMock<MockFunction<void(const std::string& endpoint_id,
|
||||
const ConnectionResponseInfo& info)>>
|
||||
initiated_cb;
|
||||
StrictMock<MockFunction<void(const std::string& endpoint_id)>> accepted_cb;
|
||||
StrictMock<MockFunction<void(const std::string& endpoint_id,
|
||||
const Status& status)>>
|
||||
rejected_cb;
|
||||
StrictMock<MockFunction<void(const std::string& endpoint_id)>>
|
||||
disconnected_cb;
|
||||
StrictMock<MockFunction<void(const std::string& endpoint_id,
|
||||
std::int32_t quality)>>
|
||||
bandwidth_changed_cb;
|
||||
};
|
||||
struct MockDiscoveryListener {
|
||||
StrictMock<MockFunction<void(const std::string& endpoint_id,
|
||||
const std::string& endpoint_name,
|
||||
const std::string& service_id)>>
|
||||
endpoint_found_cb;
|
||||
StrictMock<MockFunction<void(const std::string& endpoint_id)>>
|
||||
endpoint_lost_cb;
|
||||
StrictMock<
|
||||
MockFunction<void(const std::string& endpoint_id, DistanceInfo info)>>
|
||||
endpoint_distance_changed_cb;
|
||||
};
|
||||
|
||||
void StartAdvertising(ClientProxy* client, MockPcpHandler* pcp_handler) {
|
||||
std::string service_id{"service"};
|
||||
ConnectionOptions options{
|
||||
.strategy = Strategy::kP2pCluster,
|
||||
.auto_upgrade_bandwidth = true,
|
||||
.enforce_topology_constraints = true,
|
||||
};
|
||||
ConnectionRequestInfo info{
|
||||
.name = "remote_endpoint_name",
|
||||
.listener = connection_listener_,
|
||||
};
|
||||
EXPECT_CALL(*pcp_handler,
|
||||
StartAdvertisingImpl(client, service_id, _, info.name, _))
|
||||
.WillOnce(Return(MockPcpHandler::StartOperationResult{
|
||||
.status = {Status::kSuccess},
|
||||
.mediums = {Medium::BLE},
|
||||
}));
|
||||
EXPECT_EQ(pcp_handler->StartAdvertising(client, service_id, options, info),
|
||||
Status{Status::kSuccess});
|
||||
EXPECT_TRUE(client->IsAdvertising());
|
||||
}
|
||||
|
||||
void StartDiscovery(ClientProxy* client, MockPcpHandler* pcp_handler) {
|
||||
std::string service_id{"service"};
|
||||
ConnectionOptions options{
|
||||
.strategy = Strategy::kP2pCluster,
|
||||
.auto_upgrade_bandwidth = true,
|
||||
.enforce_topology_constraints = true,
|
||||
};
|
||||
EXPECT_CALL(*pcp_handler, StartDiscoveryImpl(client, service_id, _))
|
||||
.WillOnce(Return(MockPcpHandler::StartOperationResult{
|
||||
.status = {Status::kSuccess},
|
||||
.mediums = {Medium::BLE},
|
||||
}));
|
||||
EXPECT_EQ(pcp_handler->StartDiscovery(client, service_id, options,
|
||||
discovery_listener_),
|
||||
Status{Status::kSuccess});
|
||||
EXPECT_TRUE(client->IsDiscovering());
|
||||
}
|
||||
|
||||
std::pair<std::unique_ptr<MockEndpointChannel>,
|
||||
std::unique_ptr<MockEndpointChannel>>
|
||||
SetupConnection(Pipe& pipe_a, Pipe& pipe_b) { // NOLINT
|
||||
auto channel_a = std::make_unique<MockEndpointChannel>(&pipe_b, &pipe_a);
|
||||
auto channel_b = std::make_unique<MockEndpointChannel>(&pipe_a, &pipe_b);
|
||||
// On initiator (A) side, we drop the first write, since this is a
|
||||
// connection establishment packet, and we don't have the peer entity, just
|
||||
// the peer channel. The rest of the exchange must happen for the benefit of
|
||||
// DH key exchange.
|
||||
EXPECT_CALL(*channel_a, Read())
|
||||
.WillRepeatedly(Invoke(
|
||||
[channel = channel_a.get()]() { return channel->DoRead(); }));
|
||||
EXPECT_CALL(*channel_a, Write(_))
|
||||
.WillOnce(Return(Exception{Exception::kSuccess}))
|
||||
.WillRepeatedly(
|
||||
Invoke([channel = channel_a.get()](const ByteArray& data) {
|
||||
return channel->DoWrite(data);
|
||||
}));
|
||||
EXPECT_CALL(*channel_a, GetMedium).WillRepeatedly(Return(Medium::BLE));
|
||||
EXPECT_CALL(*channel_a, GetLastReadTimestamp)
|
||||
.WillRepeatedly(Return(absl::Now()));
|
||||
EXPECT_CALL(*channel_a, IsPaused)
|
||||
.WillRepeatedly(Return(false));
|
||||
EXPECT_CALL(*channel_b, Read())
|
||||
.WillRepeatedly(Invoke(
|
||||
[channel = channel_b.get()]() { return channel->DoRead(); }));
|
||||
EXPECT_CALL(*channel_b, Write(_))
|
||||
.WillRepeatedly(
|
||||
Invoke([channel = channel_b.get()](const ByteArray& data) {
|
||||
return channel->DoWrite(data);
|
||||
}));
|
||||
EXPECT_CALL(*channel_b, GetMedium).WillRepeatedly(Return(Medium::BLE));
|
||||
EXPECT_CALL(*channel_b, GetLastReadTimestamp)
|
||||
.WillRepeatedly(Return(absl::Now()));
|
||||
EXPECT_CALL(*channel_b, IsPaused)
|
||||
.WillRepeatedly(Return(false));
|
||||
return std::make_pair(std::move(channel_a), std::move(channel_b));
|
||||
}
|
||||
|
||||
Pipe pipe_a_;
|
||||
Pipe pipe_b_;
|
||||
MockConnectionListener mock_connection_listener_;
|
||||
MockDiscoveryListener mock_discovery_listener_;
|
||||
ConnectionListener connection_listener_{
|
||||
.initiated_cb = mock_connection_listener_.initiated_cb.AsStdFunction(),
|
||||
.accepted_cb = mock_connection_listener_.accepted_cb.AsStdFunction(),
|
||||
.rejected_cb = mock_connection_listener_.rejected_cb.AsStdFunction(),
|
||||
.disconnected_cb =
|
||||
mock_connection_listener_.disconnected_cb.AsStdFunction(),
|
||||
.bandwidth_changed_cb =
|
||||
mock_connection_listener_.bandwidth_changed_cb.AsStdFunction(),
|
||||
};
|
||||
DiscoveryListener discovery_listener_{
|
||||
.endpoint_found_cb =
|
||||
mock_discovery_listener_.endpoint_found_cb.AsStdFunction(),
|
||||
.endpoint_lost_cb =
|
||||
mock_discovery_listener_.endpoint_lost_cb.AsStdFunction(),
|
||||
.endpoint_distance_changed_cb =
|
||||
mock_discovery_listener_.endpoint_distance_changed_cb.AsStdFunction(),
|
||||
};
|
||||
};
|
||||
|
||||
TEST_F(BasePcpHandlerTest, ConstructorDestructorWorks) {
|
||||
auto ecm = std::make_unique<EndpointChannelManager>();
|
||||
auto em = std::make_unique<EndpointManager>(ecm.get());
|
||||
auto pcp_handler = std::make_unique<MockPcpHandler>(em.get(), ecm.get());
|
||||
SUCCEED();
|
||||
}
|
||||
|
||||
TEST_F(BasePcpHandlerTest, StartAdvertisingChangesState) {
|
||||
auto client = std::make_unique<ClientProxy>();
|
||||
auto ecm = std::make_unique<EndpointChannelManager>();
|
||||
auto em = std::make_unique<EndpointManager>(ecm.get());
|
||||
auto pcp_handler = std::make_unique<MockPcpHandler>(em.get(), ecm.get());
|
||||
StartAdvertising(client.get(), pcp_handler.get());
|
||||
}
|
||||
|
||||
TEST_F(BasePcpHandlerTest, StopAdvertisingChangesState) {
|
||||
auto client = std::make_unique<ClientProxy>();
|
||||
auto ecm = std::make_unique<EndpointChannelManager>();
|
||||
auto em = std::make_unique<EndpointManager>(ecm.get());
|
||||
auto pcp_handler = std::make_unique<MockPcpHandler>(em.get(), ecm.get());
|
||||
StartAdvertising(client.get(), pcp_handler.get());
|
||||
EXPECT_CALL(*pcp_handler, StopAdvertisingImpl(client.get())).Times(1);
|
||||
EXPECT_TRUE(client->IsAdvertising());
|
||||
pcp_handler->StopAdvertising(client.get());
|
||||
EXPECT_FALSE(client->IsAdvertising());
|
||||
}
|
||||
|
||||
TEST_F(BasePcpHandlerTest, StartDiscoveryChangesState) {
|
||||
auto client = std::make_unique<ClientProxy>();
|
||||
auto ecm = std::make_unique<EndpointChannelManager>();
|
||||
auto em = std::make_unique<EndpointManager>(ecm.get());
|
||||
auto pcp_handler = std::make_unique<MockPcpHandler>(em.get(), ecm.get());
|
||||
StartDiscovery(client.get(), pcp_handler.get());
|
||||
}
|
||||
|
||||
TEST_F(BasePcpHandlerTest, StopDiscoveryChangesState) {
|
||||
auto client = std::make_unique<ClientProxy>();
|
||||
auto ecm = std::make_unique<EndpointChannelManager>();
|
||||
auto em = std::make_unique<EndpointManager>(ecm.get());
|
||||
auto pcp_handler = std::make_unique<MockPcpHandler>(em.get(), ecm.get());
|
||||
StartDiscovery(client.get(), pcp_handler.get());
|
||||
EXPECT_CALL(*pcp_handler, StopDiscoveryImpl(client.get())).Times(1);
|
||||
EXPECT_TRUE(client->IsDiscovering());
|
||||
pcp_handler->StopDiscovery(client.get());
|
||||
EXPECT_FALSE(client->IsDiscovering());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,222 @@
|
||||
#include "core_v2/internal/ble_advertisement.h"
|
||||
|
||||
#include <inttypes.h>
|
||||
|
||||
#include "platform_v2/public/logging.h"
|
||||
#include "absl/strings/escaping.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
|
||||
BleAdvertisement::BleAdvertisement(Version version, Pcp pcp,
|
||||
const ByteArray& service_id_hash,
|
||||
const std::string& endpoint_id,
|
||||
const std::string& endpoint_name,
|
||||
const std::string& bluetooth_mac_address) {
|
||||
if (version != Version::kV1 ||
|
||||
service_id_hash.size() != kServiceIdHashLength || endpoint_id.empty() ||
|
||||
endpoint_id.length() != kEndpointIdLength ||
|
||||
endpoint_name.length() > kMaxEndpointNameLength) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (pcp) {
|
||||
case Pcp::kP2pCluster: // Fall through
|
||||
case Pcp::kP2pStar: // Fall through
|
||||
case Pcp::kP2pPointToPoint:
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
version_ = version;
|
||||
pcp_ = pcp;
|
||||
service_id_hash_ = service_id_hash;
|
||||
endpoint_id_ = endpoint_id;
|
||||
endpoint_name_ = endpoint_name;
|
||||
if (!BluetoothMacAddressHexStringToBytes(bluetooth_mac_address).Empty()) {
|
||||
bluetooth_mac_address_ = bluetooth_mac_address;
|
||||
}
|
||||
}
|
||||
|
||||
BleAdvertisement::BleAdvertisement(const ByteArray& ble_advertisement_bytes) {
|
||||
if (ble_advertisement_bytes.Empty()) {
|
||||
NEARBY_LOG(ERROR,
|
||||
"Cannot deserialize BleAdvertisement: null bytes passed in.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ble_advertisement_bytes.size() < kMinAdvertisementLength) {
|
||||
NEARBY_LOG(ERROR,
|
||||
"Cannot deserialize BleAdvertisement: expecting min %d raw "
|
||||
"bytes, got %" PRIu64,
|
||||
kMinAdvertisementLength, ble_advertisement_bytes.size());
|
||||
return;
|
||||
}
|
||||
|
||||
// Start reading the bytes.
|
||||
auto* ble_advertisement_bytes_read_ptr = ble_advertisement_bytes.data();
|
||||
|
||||
// The first 3 bits are supposed to be the version.
|
||||
version_ = static_cast<Version>(
|
||||
(*ble_advertisement_bytes_read_ptr & kVersionBitmask) >> 5);
|
||||
if (version_ != Version::kV1) {
|
||||
NEARBY_LOG(ERROR,
|
||||
"Cannot deserialize BleAdvertisement: unsupported Version %d",
|
||||
version_);
|
||||
return;
|
||||
}
|
||||
|
||||
pcp_ = static_cast<Pcp>(*ble_advertisement_bytes_read_ptr & kPcpBitmask);
|
||||
ble_advertisement_bytes_read_ptr++;
|
||||
switch (pcp_) {
|
||||
case Pcp::kP2pCluster: // Fall through
|
||||
case Pcp::kP2pStar: // Fall through
|
||||
case Pcp::kP2pPointToPoint: {
|
||||
// The next 24 bits are supposed to be the service_id_hash.
|
||||
service_id_hash_ =
|
||||
ByteArray(ble_advertisement_bytes_read_ptr, kServiceIdHashLength);
|
||||
ble_advertisement_bytes_read_ptr += kServiceIdHashLength;
|
||||
|
||||
// The next 32 bits are supposed to be the endpoint_id.
|
||||
endpoint_id_ =
|
||||
std::string(ble_advertisement_bytes_read_ptr, kEndpointIdLength);
|
||||
ble_advertisement_bytes_read_ptr += kEndpointIdLength;
|
||||
|
||||
// The next 8 bits are the length of the endpoint name.
|
||||
auto expected_endpoint_name_length = static_cast<std::uint32_t>(
|
||||
*ble_advertisement_bytes_read_ptr & kEndpointNameLengthBitmask);
|
||||
ble_advertisement_bytes_read_ptr++;
|
||||
|
||||
// The next x bits are the endpoint name. (Max length is 131 bytes).
|
||||
// Check that the stated endpoint_name_length is the same as what we
|
||||
// received (based off of the length of ble_advertisement_bytes).
|
||||
auto actual_endpoint_name_length =
|
||||
ComputeEndpointNameLength(ble_advertisement_bytes);
|
||||
if (actual_endpoint_name_length < expected_endpoint_name_length) {
|
||||
NEARBY_LOG(
|
||||
ERROR,
|
||||
"Cannot deserialize BleAdvertisement: expected endpointName to "
|
||||
"be %d bytes, got %d bytes",
|
||||
expected_endpoint_name_length, actual_endpoint_name_length);
|
||||
|
||||
// Clear enpoint_id for validadity.
|
||||
endpoint_id_.clear();
|
||||
return;
|
||||
}
|
||||
endpoint_name_ = std::string(ble_advertisement_bytes_read_ptr,
|
||||
expected_endpoint_name_length);
|
||||
ble_advertisement_bytes_read_ptr += expected_endpoint_name_length;
|
||||
|
||||
// The next 48 bits are the bluetooth mac address.
|
||||
auto bluetooth_mac_address_bytes = ByteArray(
|
||||
ble_advertisement_bytes_read_ptr, kBluetoothMacAddressLength);
|
||||
// If the Bluetooth MAC Address bytes are unset or invalid, leave the
|
||||
// string empty. Otherwise, convert it to the proper colon delimited
|
||||
// format.
|
||||
if (!IsBluetoothMacAddressUnset(bluetooth_mac_address_bytes)) {
|
||||
bluetooth_mac_address_ =
|
||||
HexBytesToColonDelimitedString(bluetooth_mac_address_bytes);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
// TODO(edwinwu): [ANALYTICIZE] This either represents corruption over
|
||||
// the air, or older versions of GmsCore intermingling with newer
|
||||
// ones.
|
||||
NEARBY_LOG(ERROR,
|
||||
"Cannot deserialize BleAdvertisement: uunsupported V1 PCP %d",
|
||||
pcp_);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
BleAdvertisement::operator ByteArray() const {
|
||||
if (!IsValid()) {
|
||||
return ByteArray();
|
||||
}
|
||||
|
||||
std::string out;
|
||||
|
||||
// The first 3 bits are the Version.
|
||||
char version_and_pcp_byte =
|
||||
(static_cast<char>(version_) << 5) & kVersionBitmask;
|
||||
// The next 5 bits are the Pcp.
|
||||
version_and_pcp_byte |= static_cast<char>(pcp_) & kPcpBitmask;
|
||||
out.reserve(1 + service_id_hash_.size() + kEndpointIdLength + 1 +
|
||||
endpoint_name_.size() + kBluetoothMacAddressLength);
|
||||
out.append(1, version_and_pcp_byte);
|
||||
out.append(std::string(service_id_hash_));
|
||||
out.append(endpoint_id_);
|
||||
out.append(1, endpoint_name_.size());
|
||||
out.append(endpoint_name_);
|
||||
// The next 48 bits are the bluetooth mac address. If bluetooth_mac_address is
|
||||
// invalid or empty, we get back a null byte array.
|
||||
auto bluetooth_mac_address_bytes(
|
||||
BluetoothMacAddressHexStringToBytes(bluetooth_mac_address_));
|
||||
if (!bluetooth_mac_address_bytes.Empty()) {
|
||||
out.append(bluetooth_mac_address_bytes.data(), kBluetoothMacAddressLength);
|
||||
}
|
||||
|
||||
return ByteArray(std::move(out));
|
||||
}
|
||||
|
||||
std::uint32_t BleAdvertisement::ComputeEndpointNameLength(
|
||||
const ByteArray& ble_advertisement_bytes) const {
|
||||
return ble_advertisement_bytes.size() - kMinAdvertisementLength;
|
||||
}
|
||||
|
||||
ByteArray BleAdvertisement::BluetoothMacAddressHexStringToBytes(
|
||||
const std::string& bluetooth_mac_address) const {
|
||||
std::string bt_mac_address(bluetooth_mac_address);
|
||||
|
||||
// Remove the colon delimiters.
|
||||
bt_mac_address.erase(
|
||||
std::remove(bt_mac_address.begin(), bt_mac_address.end(), ':'),
|
||||
bt_mac_address.end());
|
||||
|
||||
// If the bluetooth mac address is invalid (wrong size), return a null byte
|
||||
// array.
|
||||
if (bt_mac_address.length() != kBluetoothMacAddressLength * 2) {
|
||||
return ByteArray();
|
||||
}
|
||||
|
||||
// Convert to bytes. If MAC Address bytes are unset, return a null byte array.
|
||||
auto bt_mac_address_string(absl::HexStringToBytes(bt_mac_address));
|
||||
auto bt_mac_address_bytes =
|
||||
ByteArray(bt_mac_address_string.data(), bt_mac_address_string.size());
|
||||
if (IsBluetoothMacAddressUnset(bt_mac_address_bytes)) {
|
||||
return ByteArray();
|
||||
}
|
||||
return bt_mac_address_bytes;
|
||||
}
|
||||
|
||||
std::string BleAdvertisement::HexBytesToColonDelimitedString(
|
||||
const ByteArray& hex_bytes) const {
|
||||
// Convert the hex bytes to a string.
|
||||
std::string colon_delimited_string(
|
||||
absl::BytesToHexString(std::string(hex_bytes.data(), hex_bytes.size())));
|
||||
absl::AsciiStrToUpper(&colon_delimited_string);
|
||||
|
||||
// Insert the colons.
|
||||
for (int i = colon_delimited_string.length() - 2; i > 0; i -= 2) {
|
||||
colon_delimited_string.insert(i, ":");
|
||||
}
|
||||
return colon_delimited_string;
|
||||
}
|
||||
|
||||
bool BleAdvertisement::IsBluetoothMacAddressUnset(
|
||||
const ByteArray& bluetooth_mac_address_bytes) const {
|
||||
for (int i = 0; i < bluetooth_mac_address_bytes.size(); i++) {
|
||||
if (bluetooth_mac_address_bytes.data()[i] != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,90 @@
|
||||
#ifndef CORE_V2_INTERNAL_BLE_ADVERTISEMENT_H_
|
||||
#define CORE_V2_INTERNAL_BLE_ADVERTISEMENT_H_
|
||||
|
||||
#include "core_v2/internal/pcp.h"
|
||||
#include "platform_v2/base/byte_array.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
|
||||
// Represents the format of the Connections Ble Advertisement used in
|
||||
// Advertising + Discovery.
|
||||
//
|
||||
// <p>[VERSION][PCP][SERVICE_ID_HASH][ENDPOINT_ID][ENDPOINT_NAME_SIZE]
|
||||
// [ENDPOINT_NAME][BLUETOOTH_MAC]
|
||||
//
|
||||
// <p>See go/connections-ble-advertisement for more information.
|
||||
class BleAdvertisement {
|
||||
public:
|
||||
// Versions of the BleAdvertisement.
|
||||
enum class Version {
|
||||
kUndefined = 0,
|
||||
kV1 = 1,
|
||||
// Version is only allocated 3 bits in the BleAdvertisement, so this
|
||||
// can never go beyond V7.
|
||||
};
|
||||
|
||||
static constexpr int kServiceIdHashLength = 3;
|
||||
static constexpr int kVersionAndPcpLength = 1;
|
||||
// Should be defined as EndpointManager<Platform>::kEndpointIdLength, but that
|
||||
// involves making BleAdvertisement templatized on Platform just for
|
||||
// that one little thing, so forget it (at least for now).
|
||||
static constexpr int kEndpointIdLength = 4;
|
||||
static constexpr int kEndpointNameSizeLength = 1;
|
||||
static constexpr int kBluetoothMacAddressLength = 6;
|
||||
static constexpr int kMinAdvertisementLength =
|
||||
kVersionAndPcpLength + kServiceIdHashLength + kEndpointIdLength +
|
||||
kEndpointNameSizeLength + kBluetoothMacAddressLength;
|
||||
static constexpr int kMaxEndpointNameLength = 131;
|
||||
static constexpr int kVersionBitmask = 0x0E0;
|
||||
static constexpr int kPcpBitmask = 0x01F;
|
||||
static constexpr int kEndpointNameLengthBitmask = 0x0FF;
|
||||
|
||||
BleAdvertisement() = default;
|
||||
BleAdvertisement(Version version, Pcp pcp, const ByteArray& service_id_hash,
|
||||
const std::string& endpoint_id,
|
||||
const std::string& endpoint_name,
|
||||
const std::string& bluetooth_mac_address);
|
||||
explicit BleAdvertisement(const ByteArray& ble_advertisement_bytes);
|
||||
~BleAdvertisement() = default;
|
||||
|
||||
BleAdvertisement(const BleAdvertisement&) = default;
|
||||
BleAdvertisement& operator=(const BleAdvertisement&) = default;
|
||||
BleAdvertisement(BleAdvertisement&&) = default;
|
||||
BleAdvertisement& operator=(BleAdvertisement&&) = default;
|
||||
|
||||
explicit operator ByteArray() const;
|
||||
|
||||
inline bool IsValid() const { return !endpoint_id_.empty(); }
|
||||
inline Version GetVersion() const { return version_; }
|
||||
inline Pcp GetPcp() const { return pcp_; }
|
||||
inline ByteArray GetServiceIdHash() const{ return service_id_hash_; }
|
||||
inline std::string GetEndpointId() const { return endpoint_id_; }
|
||||
inline std::string GetEndpointName() const { return endpoint_name_; }
|
||||
inline std::string GetBluetoothMacAddress() const {
|
||||
return bluetooth_mac_address_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::uint32_t ComputeEndpointNameLength(
|
||||
const ByteArray& ble_advertisement_bytes) const;
|
||||
ByteArray BluetoothMacAddressHexStringToBytes(
|
||||
const std::string& bluetooth_mac_address) const;
|
||||
std::string HexBytesToColonDelimitedString(const ByteArray& hex_bytes) const;
|
||||
bool IsBluetoothMacAddressUnset(
|
||||
const ByteArray& bluetooth_mac_address_bytes) const;
|
||||
|
||||
Version version_ = Version::kUndefined;
|
||||
Pcp pcp_ = Pcp::kUnknown;
|
||||
ByteArray service_id_hash_;
|
||||
std::string endpoint_id_;
|
||||
std::string endpoint_name_;
|
||||
std::string bluetooth_mac_address_;
|
||||
};
|
||||
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
|
||||
#endif // CORE_V2_INTERNAL_BLE_ADVERTISEMENT_H_
|
||||
@@ -0,0 +1,258 @@
|
||||
#include "core_v2/internal/ble_advertisement.h"
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace {
|
||||
|
||||
const BleAdvertisement::Version kVersion = BleAdvertisement::Version::kV1;
|
||||
const Pcp kPcp = Pcp::kP2pCluster;
|
||||
const char kServiceIDHashBytes[] = {0x0A, 0x0B, 0x0C};
|
||||
const char kEndPointID[] = "AB12";
|
||||
const char kEndpointName[] =
|
||||
"How much wood can a woodchuck chuck if a wood chuck would chuck wood?";
|
||||
const char kBluetoothMacAddress[] = "00:00:E6:88:64:13";
|
||||
|
||||
TEST(BleAdvertisementTest, ConstructionWorks) {
|
||||
auto service_id_hash = ByteArray(kServiceIDHashBytes,
|
||||
sizeof(kServiceIDHashBytes) / sizeof(char));
|
||||
auto ble_advertisement =
|
||||
BleAdvertisement(kVersion, kPcp, service_id_hash, kEndPointID,
|
||||
kEndpointName, kBluetoothMacAddress);
|
||||
auto is_valid = ble_advertisement.IsValid();
|
||||
|
||||
EXPECT_TRUE(is_valid);
|
||||
EXPECT_EQ(kVersion, ble_advertisement.GetVersion());
|
||||
EXPECT_EQ(kPcp, ble_advertisement.GetPcp());
|
||||
EXPECT_EQ(service_id_hash, ble_advertisement.GetServiceIdHash());
|
||||
EXPECT_EQ(kEndPointID, ble_advertisement.GetEndpointId());
|
||||
EXPECT_EQ(kEndpointName, ble_advertisement.GetEndpointName());
|
||||
EXPECT_EQ(kBluetoothMacAddress, ble_advertisement.GetBluetoothMacAddress());
|
||||
}
|
||||
|
||||
TEST(BleAdvertisementTest, ConstructionWorksWithEmptyEndpointName) {
|
||||
std::string empty_endpoint_name;
|
||||
|
||||
auto service_id_hash = ByteArray(kServiceIDHashBytes,
|
||||
sizeof(kServiceIDHashBytes) / sizeof(char));
|
||||
auto ble_advertisement =
|
||||
BleAdvertisement(kVersion, kPcp, service_id_hash, kEndPointID,
|
||||
empty_endpoint_name, kBluetoothMacAddress);
|
||||
auto is_valid = ble_advertisement.IsValid();
|
||||
|
||||
EXPECT_TRUE(is_valid);
|
||||
EXPECT_EQ(kVersion, ble_advertisement.GetVersion());
|
||||
EXPECT_EQ(kPcp, ble_advertisement.GetPcp());
|
||||
EXPECT_EQ(service_id_hash, ble_advertisement.GetServiceIdHash());
|
||||
EXPECT_EQ(kEndPointID, ble_advertisement.GetEndpointId());
|
||||
EXPECT_EQ(empty_endpoint_name, ble_advertisement.GetEndpointName());
|
||||
EXPECT_EQ(kBluetoothMacAddress, ble_advertisement.GetBluetoothMacAddress());
|
||||
}
|
||||
|
||||
TEST(BleAdvertisementTest, ConstructionWorksWithEmojiEndpointName) {
|
||||
std::string emoji_endpoint_name("\u0001F450 \u0001F450");
|
||||
|
||||
auto service_id_hash = ByteArray(kServiceIDHashBytes,
|
||||
sizeof(kServiceIDHashBytes) / sizeof(char));
|
||||
auto ble_advertisement =
|
||||
BleAdvertisement(kVersion, kPcp, service_id_hash, kEndPointID,
|
||||
emoji_endpoint_name, kBluetoothMacAddress);
|
||||
auto is_valid = ble_advertisement.IsValid();
|
||||
|
||||
EXPECT_TRUE(is_valid);
|
||||
EXPECT_EQ(kVersion, ble_advertisement.GetVersion());
|
||||
EXPECT_EQ(kPcp, ble_advertisement.GetPcp());
|
||||
EXPECT_EQ(service_id_hash, ble_advertisement.GetServiceIdHash());
|
||||
EXPECT_EQ(kEndPointID, ble_advertisement.GetEndpointId());
|
||||
EXPECT_EQ(emoji_endpoint_name, ble_advertisement.GetEndpointName());
|
||||
EXPECT_EQ(kBluetoothMacAddress, ble_advertisement.GetBluetoothMacAddress());
|
||||
}
|
||||
|
||||
TEST(BleAdvertisementTest, ConstructionFailsWithLongEndpointName) {
|
||||
std::string long_endpoint_name(BleAdvertisement::kMaxEndpointNameLength + 1,
|
||||
'x');
|
||||
|
||||
auto service_id_hash = ByteArray(kServiceIDHashBytes,
|
||||
sizeof(kServiceIDHashBytes) / sizeof(char));
|
||||
auto ble_advertisement =
|
||||
BleAdvertisement(kVersion, kPcp, service_id_hash, kEndPointID,
|
||||
long_endpoint_name, kBluetoothMacAddress);
|
||||
|
||||
auto is_valid = ble_advertisement.IsValid();
|
||||
|
||||
EXPECT_FALSE(is_valid);
|
||||
}
|
||||
|
||||
TEST(BleAdvertisementTest, ConstructionFailsWithBadVersion) {
|
||||
auto bad_version = static_cast<BleAdvertisement::Version>(666);
|
||||
|
||||
auto service_id_hash = ByteArray(kServiceIDHashBytes,
|
||||
sizeof(kServiceIDHashBytes) / sizeof(char));
|
||||
auto ble_advertisement =
|
||||
BleAdvertisement(bad_version, kPcp, service_id_hash, kEndPointID,
|
||||
kEndpointName, kBluetoothMacAddress);
|
||||
|
||||
auto is_valid = ble_advertisement.IsValid();
|
||||
|
||||
EXPECT_FALSE(is_valid);
|
||||
}
|
||||
|
||||
TEST(BleAdvertisementTest, ConstructionFailsWithBadPCP) {
|
||||
auto bad_pcp = static_cast<Pcp>(666);
|
||||
|
||||
auto service_id_hash = ByteArray(kServiceIDHashBytes,
|
||||
sizeof(kServiceIDHashBytes) / sizeof(char));
|
||||
auto ble_advertisement =
|
||||
BleAdvertisement(kVersion, bad_pcp, service_id_hash, kEndPointID,
|
||||
kEndpointName, kBluetoothMacAddress);
|
||||
|
||||
auto is_valid = ble_advertisement.IsValid();
|
||||
|
||||
EXPECT_FALSE(is_valid);
|
||||
}
|
||||
|
||||
TEST(BleAdvertisementTest, ConstructionSucceedsWithEmptyBluetoothMacAddress) {
|
||||
std::string empty_bluetooth_mac_address = "";
|
||||
|
||||
auto service_id_hash = ByteArray(kServiceIDHashBytes,
|
||||
sizeof(kServiceIDHashBytes) / sizeof(char));
|
||||
auto ble_advertisement =
|
||||
BleAdvertisement(kVersion, kPcp, service_id_hash, kEndPointID,
|
||||
kEndpointName, empty_bluetooth_mac_address);
|
||||
|
||||
auto is_valid = ble_advertisement.IsValid();
|
||||
|
||||
EXPECT_TRUE(is_valid);
|
||||
}
|
||||
|
||||
TEST(BleAdvertisementTest, ConstructionSucceedsWithInvalidBluetoothMacAddress) {
|
||||
std::string bad_bluetooth_mac_address = "022:00";
|
||||
|
||||
auto service_id_hash = ByteArray(kServiceIDHashBytes,
|
||||
sizeof(kServiceIDHashBytes) / sizeof(char));
|
||||
auto ble_advertisement =
|
||||
BleAdvertisement(kVersion, kPcp, service_id_hash, kEndPointID,
|
||||
kEndpointName, bad_bluetooth_mac_address);
|
||||
auto is_valid = ble_advertisement.IsValid();
|
||||
|
||||
EXPECT_TRUE(is_valid);
|
||||
EXPECT_EQ(kVersion, ble_advertisement.GetVersion());
|
||||
EXPECT_EQ(kPcp, ble_advertisement.GetPcp());
|
||||
EXPECT_EQ(service_id_hash, ble_advertisement.GetServiceIdHash());
|
||||
EXPECT_EQ(kEndPointID, ble_advertisement.GetEndpointId());
|
||||
EXPECT_EQ(kEndpointName, ble_advertisement.GetEndpointName());
|
||||
EXPECT_TRUE(ble_advertisement.GetBluetoothMacAddress().empty());
|
||||
}
|
||||
|
||||
TEST(BleAdvertisementTest, ConstructionFromBytesWorks) {
|
||||
// Serialize good data into a good Ble Advertisement.
|
||||
auto service_id_hash = ByteArray(kServiceIDHashBytes,
|
||||
sizeof(kServiceIDHashBytes) / sizeof(char));
|
||||
auto org_ble_advertisement =
|
||||
BleAdvertisement(kVersion, kPcp, service_id_hash, kEndPointID,
|
||||
kEndpointName, kBluetoothMacAddress);
|
||||
auto ble_advertisement_bytes = ByteArray(org_ble_advertisement);
|
||||
|
||||
auto ble_advertisement = BleAdvertisement(ble_advertisement_bytes);
|
||||
auto is_valid = ble_advertisement.IsValid();
|
||||
|
||||
EXPECT_TRUE(is_valid);
|
||||
EXPECT_EQ(kVersion, ble_advertisement.GetVersion());
|
||||
EXPECT_EQ(kPcp, ble_advertisement.GetPcp());
|
||||
EXPECT_EQ(service_id_hash, ble_advertisement.GetServiceIdHash());
|
||||
EXPECT_EQ(kEndPointID, ble_advertisement.GetEndpointId());
|
||||
EXPECT_EQ(kEndpointName, ble_advertisement.GetEndpointName());
|
||||
EXPECT_EQ(kBluetoothMacAddress, ble_advertisement.GetBluetoothMacAddress());
|
||||
}
|
||||
|
||||
// Bytes at the end should be ignored so that they can be used as reserve bytes
|
||||
// in the future.
|
||||
TEST(BleAdvertisementTest, ConstructionFromLongLengthBytesWorks) {
|
||||
// Serialize good data into a good Ble Advertisement.
|
||||
auto service_id_hash = ByteArray(kServiceIDHashBytes,
|
||||
sizeof(kServiceIDHashBytes) / sizeof(char));
|
||||
auto ble_advertisement =
|
||||
BleAdvertisement(kVersion, kPcp, service_id_hash, kEndPointID,
|
||||
kEndpointName, kBluetoothMacAddress);
|
||||
auto ble_advertisement_bytes = ByteArray(ble_advertisement);
|
||||
|
||||
// Add bytes to the end of the valid Ble advertisement.
|
||||
auto long_ble_advertisement_bytes =
|
||||
ByteArray(BleAdvertisement::kMinAdvertisementLength + 1000);
|
||||
ASSERT_LE(ble_advertisement_bytes.size(),
|
||||
long_ble_advertisement_bytes.size());
|
||||
memcpy(long_ble_advertisement_bytes.data(),
|
||||
ble_advertisement_bytes.data(),
|
||||
ble_advertisement_bytes.size());
|
||||
|
||||
auto long_ble_advertisement = BleAdvertisement(long_ble_advertisement_bytes);
|
||||
auto is_valid = long_ble_advertisement.IsValid();
|
||||
|
||||
EXPECT_TRUE(is_valid);
|
||||
EXPECT_EQ(kVersion, long_ble_advertisement.GetVersion());
|
||||
EXPECT_EQ(kPcp, long_ble_advertisement.GetPcp());
|
||||
EXPECT_EQ(service_id_hash, long_ble_advertisement.GetServiceIdHash());
|
||||
EXPECT_EQ(kEndPointID, long_ble_advertisement.GetEndpointId());
|
||||
EXPECT_EQ(kEndpointName, long_ble_advertisement.GetEndpointName());
|
||||
EXPECT_EQ(kBluetoothMacAddress,
|
||||
long_ble_advertisement.GetBluetoothMacAddress());
|
||||
}
|
||||
|
||||
TEST(BleAdvertisementTest, ConstructionFromNullBytesFails) {
|
||||
auto ble_advertisement = BleAdvertisement(ByteArray());
|
||||
auto is_valid = ble_advertisement.IsValid();
|
||||
|
||||
EXPECT_FALSE(is_valid);
|
||||
}
|
||||
|
||||
TEST(BleAdvertisementTest, ConstructionFromShortLengthBytesFails) {
|
||||
// Serialize good data into a good Ble Advertisement.
|
||||
auto service_id_hash = ByteArray(kServiceIDHashBytes,
|
||||
sizeof(kServiceIDHashBytes) / sizeof(char));
|
||||
auto ble_advertisement =
|
||||
BleAdvertisement(kVersion, kPcp, service_id_hash, kEndPointID,
|
||||
kEndpointName, kBluetoothMacAddress);
|
||||
auto ble_advertisement_bytes = ByteArray(ble_advertisement);
|
||||
|
||||
// Shorten the valid Ble Advertisement.
|
||||
auto short_ble_advertisement_bytes(
|
||||
ByteArray(ble_advertisement_bytes.data(),
|
||||
BleAdvertisement::kMinAdvertisementLength - 1));
|
||||
|
||||
auto short_ble_advertisement =
|
||||
BleAdvertisement(short_ble_advertisement_bytes);
|
||||
auto is_valid = short_ble_advertisement.IsValid();
|
||||
|
||||
EXPECT_FALSE(is_valid);
|
||||
}
|
||||
|
||||
TEST(BleAdvertisementTest,
|
||||
ConstructionFromByesWithWrongEndpointNameLengthFails) {
|
||||
// Serialize good data into a good Ble Advertisement.
|
||||
auto service_id_hash = ByteArray(kServiceIDHashBytes,
|
||||
sizeof(kServiceIDHashBytes) / sizeof(char));
|
||||
auto ble_advertisement =
|
||||
BleAdvertisement(kVersion, kPcp, service_id_hash, kEndPointID,
|
||||
kEndpointName, kBluetoothMacAddress);
|
||||
auto ble_advertisement_bytes = ByteArray(ble_advertisement);
|
||||
|
||||
// Corrupt the EndpointNameLength bits.
|
||||
std::string corrupt_ble_advertisement_string(ble_advertisement_bytes.data(),
|
||||
ble_advertisement_bytes.size());
|
||||
corrupt_ble_advertisement_string[8] ^= 0x0FF;
|
||||
auto corrupt_ble_advertisement_bytes =
|
||||
ByteArray(corrupt_ble_advertisement_string);
|
||||
|
||||
auto corrupt_ble_advertisement =
|
||||
BleAdvertisement(corrupt_ble_advertisement_bytes);
|
||||
auto is_valid = corrupt_ble_advertisement.IsValid();
|
||||
|
||||
EXPECT_FALSE(is_valid);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,461 @@
|
||||
#include "core_v2/internal/client_proxy.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
|
||||
#include "platform_v2/base/base64_utils.h"
|
||||
#include "platform_v2/base/prng.h"
|
||||
#include "platform_v2/public/crypto.h"
|
||||
#include "platform_v2/public/logging.h"
|
||||
#include "platform_v2/public/mutex_lock.h"
|
||||
#include "proto/connections_enums.pb.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
#include "absl/strings/str_cat.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
|
||||
ClientProxy::ClientProxy() : client_id_(Prng().NextInt64()) {}
|
||||
|
||||
ClientProxy::~ClientProxy() { Reset(); }
|
||||
|
||||
std::int64_t ClientProxy::GetClientId() const { return client_id_; }
|
||||
|
||||
std::string ClientProxy::GenerateLocalEndpointId() {
|
||||
// 1) Concatenate the DeviceID with this ClientID.
|
||||
// 2) Compute a hash of that concatenation.
|
||||
// 3) Base64-encode that hash, to make it human-readable.
|
||||
// 4) Use only the first 4 bytes of that Base64 encoding.
|
||||
ByteArray id_hash(Crypto::Sha256(
|
||||
absl::StrCat(api::ImplementationPlatform::GetDeviceId(), GetClientId())));
|
||||
|
||||
return Base64Utils::Encode(id_hash).substr(0, kEndpointIdLength);
|
||||
}
|
||||
|
||||
void ClientProxy::Reset() {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
StoppedAdvertising();
|
||||
StoppedDiscovery();
|
||||
RemoveAllEndpoints();
|
||||
}
|
||||
|
||||
void ClientProxy::StartedAdvertising(
|
||||
const std::string& service_id, Strategy strategy,
|
||||
const ConnectionListener& listener,
|
||||
absl::Span<proto::connections::Medium> mediums) {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
advertising_info_ = {service_id, listener};
|
||||
}
|
||||
|
||||
void ClientProxy::StoppedAdvertising() {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
if (IsAdvertising()) {
|
||||
advertising_info_.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
bool ClientProxy::IsAdvertising() const {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
return !advertising_info_.IsEmpty();
|
||||
}
|
||||
|
||||
std::string ClientProxy::GetAdvertisingServiceId() const {
|
||||
MutexLock lock(&mutex_);
|
||||
return advertising_info_.service_id;
|
||||
}
|
||||
|
||||
void ClientProxy::StartedDiscovery(
|
||||
const std::string& service_id, Strategy strategy,
|
||||
const DiscoveryListener& listener,
|
||||
absl::Span<proto::connections::Medium> mediums) {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
discovery_info_ = DiscoveryInfo{service_id, listener};
|
||||
}
|
||||
|
||||
void ClientProxy::StoppedDiscovery() {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
if (IsDiscovering()) {
|
||||
discovered_endpoint_ids_.clear();
|
||||
discovery_info_.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
bool ClientProxy::IsDiscoveringServiceId(const std::string& service_id) const {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
return IsDiscovering() && service_id == discovery_info_.service_id;
|
||||
}
|
||||
|
||||
bool ClientProxy::IsDiscovering() const {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
return !discovery_info_.IsEmpty();
|
||||
}
|
||||
|
||||
std::string ClientProxy::GetDiscoveryServiceId() const {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
return discovery_info_.service_id;
|
||||
}
|
||||
|
||||
void ClientProxy::OnEndpointFound(const std::string& service_id,
|
||||
const std::string& endpoint_id,
|
||||
const std::string& endpoint_name,
|
||||
proto::connections::Medium medium) {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
if (!IsDiscoveringServiceId(service_id)) return;
|
||||
if (discovered_endpoint_ids_.count(endpoint_id)) {
|
||||
// TODO(tracyzhou): Add logging.
|
||||
return;
|
||||
}
|
||||
discovered_endpoint_ids_.insert(endpoint_id);
|
||||
discovery_info_.listener.endpoint_found_cb(endpoint_id, endpoint_name,
|
||||
service_id);
|
||||
}
|
||||
|
||||
void ClientProxy::OnEndpointLost(const std::string& service_id,
|
||||
const std::string& endpoint_id) {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
if (!IsDiscoveringServiceId(service_id)) return;
|
||||
const auto it = discovered_endpoint_ids_.find(endpoint_id);
|
||||
if (it == discovered_endpoint_ids_.end()) return;
|
||||
discovered_endpoint_ids_.erase(it);
|
||||
discovery_info_.listener.endpoint_lost_cb(endpoint_id);
|
||||
}
|
||||
|
||||
void ClientProxy::OnConnectionInitiated(const std::string& endpoint_id,
|
||||
const ConnectionResponseInfo& info,
|
||||
const ConnectionListener& listener) {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
// Whether this is incoming or outgoing, the local and remote endpoints both
|
||||
// still need to accept this connection, so set its establishment status to
|
||||
// PENDING.
|
||||
auto result = connections_.emplace(
|
||||
endpoint_id, Connection{
|
||||
.is_incoming = info.is_incoming_connection,
|
||||
.connection_listener = listener,
|
||||
});
|
||||
// Instead of using structured binding which is nice, but banned
|
||||
// (can not use c++17 features, until chromium does) we unpack manually.
|
||||
auto& pair_iter = result.first;
|
||||
bool& inserted = result.second;
|
||||
DCHECK(inserted);
|
||||
const Connection& item = pair_iter->second;
|
||||
// Notify the client.
|
||||
//
|
||||
// Note: we allow devices to connect to an advertiser even after it stops
|
||||
// advertising, so no need to check IsAdvertising() here.
|
||||
item.connection_listener.initiated_cb(endpoint_id, info);
|
||||
}
|
||||
|
||||
void ClientProxy::OnConnectionAccepted(const std::string& endpoint_id) {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
if (!HasPendingConnectionToEndpoint(endpoint_id)) {
|
||||
// TODO(tracyzhou): Add logging.
|
||||
return;
|
||||
}
|
||||
|
||||
// Notify the client.
|
||||
Connection* item = LookupConnection(endpoint_id);
|
||||
if (item != nullptr) {
|
||||
item->connection_listener.accepted_cb(endpoint_id);
|
||||
item->status = Connection::kConnected;
|
||||
}
|
||||
}
|
||||
|
||||
void ClientProxy::OnConnectionRejected(const std::string& endpoint_id,
|
||||
const Status& status) {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
if (!HasPendingConnectionToEndpoint(endpoint_id)) {
|
||||
NEARBY_LOG(INFO, "ClientProxy [Rejected]: no pending connection; id=%s",
|
||||
endpoint_id.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// Notify the client.
|
||||
const Connection* item = LookupConnection(endpoint_id);
|
||||
if (item != nullptr) {
|
||||
item->connection_listener.rejected_cb(endpoint_id, status);
|
||||
OnDisconnected(endpoint_id, false /* notify */);
|
||||
}
|
||||
}
|
||||
|
||||
void ClientProxy::OnBandwidthChanged(const std::string& endpoint_id,
|
||||
std::int32_t quality) {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
const Connection* item = LookupConnection(endpoint_id);
|
||||
if (item != nullptr) {
|
||||
item->connection_listener.bandwidth_changed_cb(endpoint_id, quality);
|
||||
}
|
||||
}
|
||||
|
||||
void ClientProxy::OnDisconnected(const std::string& endpoint_id, bool notify) {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
const Connection* item = LookupConnection(endpoint_id);
|
||||
if (item != nullptr) {
|
||||
if (notify) {
|
||||
item->connection_listener.disconnected_cb({endpoint_id});
|
||||
}
|
||||
connections_.erase(endpoint_id);
|
||||
}
|
||||
}
|
||||
|
||||
bool ClientProxy::ConnectionStatusMatches(const std::string& endpoint_id,
|
||||
Connection::Status status) const {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
const Connection* item = LookupConnection(endpoint_id);
|
||||
if (item != nullptr) {
|
||||
return item->status == status;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ClientProxy::IsConnectedToEndpoint(const std::string& endpoint_id) const {
|
||||
return ConnectionStatusMatches(endpoint_id, Connection::kConnected);
|
||||
}
|
||||
|
||||
std::vector<std::string> ClientProxy::GetMatchingEndpoints(
|
||||
std::function<bool(const Connection&)> pred) const {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
std::vector<std::string> connected_endpoints;
|
||||
|
||||
for (const auto& pair : connections_) {
|
||||
const auto& endpoint_id = pair.first;
|
||||
const auto& connection = pair.second;
|
||||
if (pred(connection)) {
|
||||
connected_endpoints.push_back(endpoint_id);
|
||||
}
|
||||
}
|
||||
return connected_endpoints;
|
||||
}
|
||||
|
||||
std::vector<std::string> ClientProxy::GetPendingConnectedEndpoints() const {
|
||||
return GetMatchingEndpoints([](const Connection& connection) {
|
||||
return connection.status != Connection::kConnected;
|
||||
});
|
||||
}
|
||||
|
||||
std::vector<std::string> ClientProxy::GetConnectedEndpoints() const {
|
||||
return GetMatchingEndpoints([](const Connection& connection) {
|
||||
return connection.status == Connection::kConnected;
|
||||
});
|
||||
}
|
||||
|
||||
std::int32_t ClientProxy::GetNumOutgoingConnections() const {
|
||||
return GetMatchingEndpoints([](const Connection& connection) {
|
||||
return connection.status == Connection::kConnected &&
|
||||
!connection.is_incoming;
|
||||
})
|
||||
.size();
|
||||
}
|
||||
|
||||
std::int32_t ClientProxy::GetNumIncomingConnections() const {
|
||||
return GetMatchingEndpoints([](const Connection& connection) {
|
||||
return connection.status == Connection::kConnected &&
|
||||
connection.is_incoming;
|
||||
})
|
||||
.size();
|
||||
}
|
||||
|
||||
bool ClientProxy::HasPendingConnectionToEndpoint(
|
||||
const std::string& endpoint_id) const {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
const Connection* item = LookupConnection(endpoint_id);
|
||||
if (item != nullptr) {
|
||||
return item->status != Connection::kConnected;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ClientProxy::HasLocalEndpointResponded(
|
||||
const std::string& endpoint_id) const {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
return ConnectionStatusesContains(
|
||||
endpoint_id,
|
||||
static_cast<Connection::Status>(Connection::kLocalEndpointAccepted |
|
||||
Connection::kLocalEndpointRejected));
|
||||
}
|
||||
|
||||
bool ClientProxy::HasRemoteEndpointResponded(
|
||||
const std::string& endpoint_id) const {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
return ConnectionStatusesContains(
|
||||
endpoint_id,
|
||||
static_cast<Connection::Status>(Connection::kRemoteEndpointAccepted |
|
||||
Connection::kRemoteEndpointRejected));
|
||||
}
|
||||
|
||||
void ClientProxy::LocalEndpointAcceptedConnection(
|
||||
const std::string& endpoint_id, const PayloadListener& listener) {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
if (HasLocalEndpointResponded(endpoint_id)) {
|
||||
// TODO(tracyzhou): Add logging.
|
||||
return;
|
||||
}
|
||||
|
||||
AppendConnectionStatus(endpoint_id, Connection::kLocalEndpointAccepted);
|
||||
Connection* item = LookupConnection(endpoint_id);
|
||||
if (item != nullptr) {
|
||||
item->payload_listener = listener;
|
||||
}
|
||||
}
|
||||
|
||||
void ClientProxy::LocalEndpointRejectedConnection(
|
||||
const std::string& endpoint_id) {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
if (HasLocalEndpointResponded(endpoint_id)) {
|
||||
// TODO(tracyzhou): Add logging.
|
||||
return;
|
||||
}
|
||||
|
||||
AppendConnectionStatus(endpoint_id, Connection::kLocalEndpointRejected);
|
||||
}
|
||||
|
||||
void ClientProxy::RemoteEndpointAcceptedConnection(
|
||||
const std::string& endpoint_id) {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
if (HasRemoteEndpointResponded(endpoint_id)) {
|
||||
// TODO(tracyzhou): Add logging.
|
||||
return;
|
||||
}
|
||||
|
||||
AppendConnectionStatus(endpoint_id, Connection::kRemoteEndpointAccepted);
|
||||
}
|
||||
|
||||
void ClientProxy::RemoteEndpointRejectedConnection(
|
||||
const std::string& endpoint_id) {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
if (HasRemoteEndpointResponded(endpoint_id)) {
|
||||
// TODO(tracyzhou): Add logging.
|
||||
return;
|
||||
}
|
||||
|
||||
AppendConnectionStatus(endpoint_id, Connection::kRemoteEndpointRejected);
|
||||
}
|
||||
|
||||
bool ClientProxy::IsConnectionAccepted(const std::string& endpoint_id) const {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
return ConnectionStatusesContains(endpoint_id,
|
||||
Connection::kLocalEndpointAccepted) &&
|
||||
ConnectionStatusesContains(endpoint_id,
|
||||
Connection::kRemoteEndpointAccepted);
|
||||
}
|
||||
|
||||
bool ClientProxy::IsConnectionRejected(const std::string& endpoint_id) const {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
return ConnectionStatusesContains(
|
||||
endpoint_id,
|
||||
static_cast<Connection::Status>(Connection::kLocalEndpointRejected |
|
||||
Connection::kRemoteEndpointRejected));
|
||||
}
|
||||
|
||||
bool ClientProxy::LocalConnectionIsAccepted(std::string endpoint_id) const {
|
||||
return ConnectionStatusesContains(
|
||||
endpoint_id, ClientProxy::Connection::kLocalEndpointAccepted);
|
||||
}
|
||||
|
||||
bool ClientProxy::RemoteConnectionIsAccepted(std::string endpoint_id) const {
|
||||
return ConnectionStatusesContains(
|
||||
endpoint_id, ClientProxy::Connection::kRemoteEndpointAccepted);
|
||||
}
|
||||
|
||||
void ClientProxy::OnPayload(const std::string& endpoint_id, Payload payload) {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
if (IsConnectedToEndpoint(endpoint_id)) {
|
||||
const Connection* item = LookupConnection(endpoint_id);
|
||||
if (item != nullptr) {
|
||||
item->payload_listener.payload_cb(endpoint_id, std::move(payload));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ClientProxy::Connection* ClientProxy::LookupConnection(
|
||||
const std::string& endpoint_id) const {
|
||||
auto item = connections_.find(endpoint_id);
|
||||
return item != connections_.end() ? &item->second : nullptr;
|
||||
}
|
||||
|
||||
ClientProxy::Connection* ClientProxy::LookupConnection(
|
||||
const std::string& endpoint_id) {
|
||||
auto item = connections_.find(endpoint_id);
|
||||
return item != connections_.end() ? &item->second : nullptr;
|
||||
}
|
||||
|
||||
void ClientProxy::OnPayloadProgress(const std::string& endpoint_id,
|
||||
const PayloadProgressInfo& info) {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
if (IsConnectedToEndpoint(endpoint_id)) {
|
||||
Connection* item = LookupConnection(endpoint_id);
|
||||
if (item != nullptr) {
|
||||
item->payload_listener.payload_progress_cb(endpoint_id, info);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool operator==(const ClientProxy& lhs, const ClientProxy& rhs) {
|
||||
return lhs.GetClientId() == rhs.GetClientId();
|
||||
}
|
||||
|
||||
bool operator<(const ClientProxy& lhs, const ClientProxy& rhs) {
|
||||
return lhs.GetClientId() < rhs.GetClientId();
|
||||
}
|
||||
|
||||
void ClientProxy::RemoveAllEndpoints() {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
// Note: we may want to notify the client of onDisconnected() for each
|
||||
// endpoint, in the case when this is called from stopAllEndpoints(). For now,
|
||||
// just remove without notifying.
|
||||
connections_.clear();
|
||||
}
|
||||
|
||||
bool ClientProxy::ConnectionStatusesContains(
|
||||
const std::string& endpoint_id, Connection::Status status_to_match) const {
|
||||
const Connection* item = LookupConnection(endpoint_id);
|
||||
if (item != nullptr) {
|
||||
return (item->status & status_to_match) != 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void ClientProxy::AppendConnectionStatus(const std::string& endpoint_id,
|
||||
Connection::Status status_to_append) {
|
||||
Connection* item = LookupConnection(endpoint_id);
|
||||
if (item != nullptr) {
|
||||
item->status =
|
||||
static_cast<Connection::Status>(item->status | status_to_append);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,217 @@
|
||||
#ifndef CORE_V2_INTERNAL_CLIENT_PROXY_H_
|
||||
#define CORE_V2_INTERNAL_CLIENT_PROXY_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core_v2/listeners.h"
|
||||
#include "core_v2/status.h"
|
||||
#include "core_v2/strategy.h"
|
||||
#include "platform_v2/base/byte_array.h"
|
||||
#include "platform_v2/public/mutex.h"
|
||||
#include "proto/connections_enums.pb.h"
|
||||
// Prefer using absl:: versions of a set and a map; they tend to be more
|
||||
// efficient: implementation is using open-addressing hash tables.
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
#include "absl/types/span.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
|
||||
// CLientProxy is tracking state of client's connection, and serves as
|
||||
// a proxy for notifications sent to this client.
|
||||
class ClientProxy final {
|
||||
public:
|
||||
static constexpr int kEndpointIdLength = 4;
|
||||
|
||||
ClientProxy();
|
||||
~ClientProxy();
|
||||
ClientProxy(ClientProxy&&) = default;
|
||||
ClientProxy& operator=(ClientProxy&&) = default;
|
||||
|
||||
std::int64_t GetClientId() const;
|
||||
|
||||
std::string GenerateLocalEndpointId();
|
||||
|
||||
// Clears all the runtime state of this client.
|
||||
void Reset();
|
||||
|
||||
// Marks this client as advertising with the given callbacks.
|
||||
void StartedAdvertising(
|
||||
const std::string& service_id, Strategy strategy,
|
||||
const ConnectionListener& connection_lifecycle_listener,
|
||||
absl::Span<proto::connections::Medium> mediums);
|
||||
// Marks this client as not advertising.
|
||||
void StoppedAdvertising();
|
||||
bool IsAdvertising() const;
|
||||
std::string GetAdvertisingServiceId() const;
|
||||
|
||||
// Marks this client as discovering with the given callback.
|
||||
void StartedDiscovery(
|
||||
const std::string& service_id, Strategy strategy,
|
||||
const DiscoveryListener& discovery_listener,
|
||||
absl::Span<proto::connections::Medium> mediums);
|
||||
// Marks this client as not discovering at all.
|
||||
void StoppedDiscovery();
|
||||
bool IsDiscoveringServiceId(const std::string& service_id) const;
|
||||
bool IsDiscovering() const;
|
||||
std::string GetDiscoveryServiceId() const;
|
||||
|
||||
// Proxies to the client's DiscoveryListener::OnEndpointFound() callback.
|
||||
void OnEndpointFound(const std::string& service_id,
|
||||
const std::string& endpoint_id,
|
||||
const std::string& endpoint_name,
|
||||
proto::connections::Medium medium);
|
||||
// Proxies to the client's DiscoveryListener::OnEndpointLost() callback.
|
||||
void OnEndpointLost(const std::string& service_id,
|
||||
const std::string& endpoint_id);
|
||||
|
||||
// Proxies to the client's ConnectionListener::OnInitiated() callback.
|
||||
void OnConnectionInitiated(const std::string& endpoint_id,
|
||||
const ConnectionResponseInfo& info,
|
||||
const ConnectionListener& listener);
|
||||
|
||||
// Proxies to the client's ConnectionListener::OnAccepted() callback.
|
||||
void OnConnectionAccepted(const std::string& endpoint_id);
|
||||
// Proxies to the client's ConnectionListener::OnRejected() callback.
|
||||
void OnConnectionRejected(const std::string& endpoint_id,
|
||||
const Status& status);
|
||||
|
||||
void OnBandwidthChanged(const std::string& endpoint_id, std::int32_t quality);
|
||||
|
||||
// Removes the endpoint from this client's list of connected endpoints. If
|
||||
// notify is true, also calls the client's
|
||||
// ConnectionListener.disconnected_cb() callback.
|
||||
void OnDisconnected(const std::string& endpoint_id, bool notify);
|
||||
|
||||
// Returns true if it's safe to send payloads to this endpoint.
|
||||
bool IsConnectedToEndpoint(const std::string& endpoint_id) const;
|
||||
// Returns all endpoints that can safely be sent payloads.
|
||||
std::vector<std::string> GetConnectedEndpoints() const;
|
||||
// Returns all endpoints that are still awaiting acceptance.
|
||||
std::vector<std::string> GetPendingConnectedEndpoints() const;
|
||||
// Returns the number of endpoints that are connected and outgoing.
|
||||
std::int32_t GetNumOutgoingConnections() const;
|
||||
// Returns the number of endpoints that are connected and incoming.
|
||||
std::int32_t GetNumIncomingConnections() const;
|
||||
// If true, then we're in the process of approving (or rejecting) a
|
||||
// connection. No payloads should be sent until isConnectedToEndpoint()
|
||||
// returns true.
|
||||
bool HasPendingConnectionToEndpoint(const std::string& endpoint_id) const;
|
||||
// Returns true if the local endpoint has already marked itself as
|
||||
// accepted/rejected.
|
||||
bool HasLocalEndpointResponded(const std::string& endpoint_id) const;
|
||||
// Returns true if the remote endpoint has already marked themselves as
|
||||
// accepted/rejected.
|
||||
bool HasRemoteEndpointResponded(const std::string& endpoint_id) const;
|
||||
// Marks the local endpoint as having accepted the connection.
|
||||
void LocalEndpointAcceptedConnection(const std::string& endpoint_id,
|
||||
const PayloadListener& listener);
|
||||
// Marks the local endpoint as having rejected the connection.
|
||||
void LocalEndpointRejectedConnection(const std::string& endpoint_id);
|
||||
// Marks the remote endpoint as having accepted the connection.
|
||||
void RemoteEndpointAcceptedConnection(const std::string& endpoint_id);
|
||||
// Marks the remote endpoint as having rejected the connection.
|
||||
void RemoteEndpointRejectedConnection(const std::string& endpoint_id);
|
||||
// Returns true if both the local endpoint and the remote endpoint have
|
||||
// accepted the connection.
|
||||
bool IsConnectionAccepted(const std::string& endpoint_id) const;
|
||||
// Returns true if either the local endpoint or the remote endpoint has
|
||||
// rejected the connection.
|
||||
bool IsConnectionRejected(const std::string& endpoint_id) const;
|
||||
|
||||
// Proxies to the client's PayloadListener::OnPayload() callback.
|
||||
void OnPayload(const std::string& endpoint_id, Payload payload);
|
||||
// Proxies to the client's PayloadListener::OnPayloadProgress() callback.
|
||||
void OnPayloadProgress(const std::string& endpoint_id,
|
||||
const PayloadProgressInfo& info);
|
||||
bool LocalConnectionIsAccepted(std::string endpoint_id) const;
|
||||
bool RemoteConnectionIsAccepted(std::string endpoint_id) const;
|
||||
|
||||
private:
|
||||
struct Connection {
|
||||
// Status: may be either:
|
||||
// Connection::PENDING, or combination of
|
||||
// Connection::LOCAL_ENDPOINT_ACCEPTED:
|
||||
// Connection::LOCAL_ENDPOINT_REJECTED and
|
||||
// Connection::REMOTE_ENDPOINT_ACCEPTED:
|
||||
// Connection::REMOTE_ENDPOINT_REJECTED, or
|
||||
// Connection::CONNECTED.
|
||||
// Only when this is set to CONNECTED should you allow payload transfers.
|
||||
//
|
||||
// We want this enum to be implicitly convertible to int, because
|
||||
// we perform bit operations on it.
|
||||
enum Status : uint8_t {
|
||||
kPending = 0,
|
||||
kLocalEndpointAccepted = 1 << 0,
|
||||
kLocalEndpointRejected = 1 << 1,
|
||||
kRemoteEndpointAccepted = 1 << 2,
|
||||
kRemoteEndpointRejected = 1 << 3,
|
||||
kConnected = 1 << 4,
|
||||
};
|
||||
bool is_incoming{false};
|
||||
Status status{kPending};
|
||||
ConnectionListener connection_listener;
|
||||
PayloadListener payload_listener;
|
||||
};
|
||||
|
||||
struct AdvertisingInfo {
|
||||
std::string service_id;
|
||||
ConnectionListener listener;
|
||||
void Clear() { service_id.clear(); }
|
||||
bool IsEmpty() const { return service_id.empty(); }
|
||||
};
|
||||
|
||||
struct DiscoveryInfo {
|
||||
std::string service_id;
|
||||
DiscoveryListener listener;
|
||||
void Clear() { service_id.clear(); }
|
||||
bool IsEmpty() const { return service_id.empty(); }
|
||||
};
|
||||
|
||||
void RemoveAllEndpoints();
|
||||
bool ConnectionStatusesContains(const std::string& endpoint_id,
|
||||
Connection::Status status_to_match) const;
|
||||
void AppendConnectionStatus(const std::string& endpoint_id,
|
||||
Connection::Status status_to_append);
|
||||
|
||||
const Connection* LookupConnection(const std::string& endpoint_id) const;
|
||||
Connection* LookupConnection(const std::string& endpoint_id);
|
||||
bool ConnectionStatusMatches(const std::string& endpoint_id,
|
||||
Connection::Status status) const;
|
||||
std::vector<std::string> GetMatchingEndpoints(
|
||||
std::function<bool(const Connection&)> pred) const;
|
||||
|
||||
mutable RecursiveMutex mutex_;
|
||||
std::int64_t client_id_;
|
||||
|
||||
// If not empty, we are currently advertising and accepting connection
|
||||
// requests for the given service_id.
|
||||
AdvertisingInfo advertising_info_;
|
||||
|
||||
// If not empty, we are currently discovering for the given service_id.
|
||||
DiscoveryInfo discovery_info_;
|
||||
|
||||
// Maps endpoint_id to endpoint connection state.
|
||||
absl::flat_hash_map<std::string, Connection> connections_;
|
||||
|
||||
// A cache of endpoint ids that we've already notified the discoverer of. We
|
||||
// check this cache before calling onEndpointFound() so that we don't notify
|
||||
// the client multiple times for the same endpoint. This would otherwise
|
||||
// happen because some mediums (like Bluetooth) repeatedly give us the same
|
||||
// endpoints after each scan.
|
||||
absl::flat_hash_set<std::string> discovered_endpoint_ids_;
|
||||
};
|
||||
|
||||
// Operator overloads when comparing Ptr<ClientProxy>.
|
||||
bool operator==(const ClientProxy& lhs, const ClientProxy& rhs);
|
||||
bool operator<(const ClientProxy& lhs, const ClientProxy& rhs);
|
||||
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
|
||||
#endif // CORE_V2_INTERNAL_CLIENT_PROXY_H_
|
||||
@@ -0,0 +1,357 @@
|
||||
#include "core_v2/internal/client_proxy.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "core_v2/listeners.h"
|
||||
#include "core_v2/strategy.h"
|
||||
#include "platform_v2/base/byte_array.h"
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
#include "absl/types/span.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace {
|
||||
|
||||
using ::testing::MockFunction;
|
||||
using ::testing::StrictMock;
|
||||
|
||||
class ClientProxyTest : public testing::Test {
|
||||
protected:
|
||||
struct MockDiscoveryListener {
|
||||
StrictMock<MockFunction<void(const std::string& endpoint_id,
|
||||
const std::string& endpoint_name,
|
||||
const std::string& service_id)>>
|
||||
endpoint_found_cb;
|
||||
StrictMock<MockFunction<void(const std::string& endpoint_id)>>
|
||||
endpoint_lost_cb;
|
||||
};
|
||||
struct MockConnectionListener {
|
||||
StrictMock<MockFunction<void(const std::string& endpoint_id,
|
||||
const ConnectionResponseInfo& info)>>
|
||||
initiated_cb;
|
||||
StrictMock<MockFunction<void(const std::string& endpoint_id)>> accepted_cb;
|
||||
StrictMock<MockFunction<void(const std::string& endpoint_id,
|
||||
const Status& status)>>
|
||||
rejected_cb;
|
||||
StrictMock<MockFunction<void(const std::string& endpoint_id)>>
|
||||
disconnected_cb;
|
||||
StrictMock<MockFunction<void(const std::string& endpoint_id,
|
||||
std::int32_t quality)>>
|
||||
bandwidth_changed_cb;
|
||||
};
|
||||
struct MockPayloadListener {
|
||||
StrictMock<
|
||||
MockFunction<void(const std::string& endpoint_id, Payload payload)>>
|
||||
payload_cb;
|
||||
StrictMock<MockFunction<void(const std::string& endpoint_id,
|
||||
const PayloadProgressInfo& info)>>
|
||||
payload_progress_cb;
|
||||
};
|
||||
|
||||
struct Endpoint {
|
||||
std::string name;
|
||||
std::string id;
|
||||
};
|
||||
|
||||
Endpoint StartAdvertising(ClientProxy* client, ConnectionListener listener) {
|
||||
Endpoint endpoint{
|
||||
.name = "advertising endpoint name",
|
||||
.id = client->GenerateLocalEndpointId(),
|
||||
};
|
||||
client->StartedAdvertising(service_id_, strategy_, listener,
|
||||
absl::MakeSpan(mediums_));
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
Endpoint StartDiscovery(ClientProxy* client, DiscoveryListener listener) {
|
||||
Endpoint endpoint{
|
||||
.name = "discovery endpoint name",
|
||||
.id = client->GenerateLocalEndpointId(),
|
||||
};
|
||||
client->StartedDiscovery(service_id_, strategy_, listener,
|
||||
absl::MakeSpan(mediums_));
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
void OnDiscoveryEndpointFound(ClientProxy* client, const Endpoint& endpoint) {
|
||||
EXPECT_CALL(mock_discovery_.endpoint_found_cb, Call).Times(1);
|
||||
client->OnEndpointFound(service_id_, endpoint.id, endpoint.name, medium_);
|
||||
}
|
||||
|
||||
void OnDiscoveryEndpointLost(ClientProxy* client, const Endpoint& endpoint) {
|
||||
EXPECT_CALL(mock_discovery_.endpoint_lost_cb, Call).Times(1);
|
||||
client->OnEndpointLost(service_id_, endpoint.id);
|
||||
}
|
||||
|
||||
void OnDiscoveryConnectionInitiated(ClientProxy* client,
|
||||
const Endpoint& endpoint) {
|
||||
EXPECT_CALL(mock_discovery_connection_.initiated_cb, Call).Times(1);
|
||||
const std::string auth_token{"auth_token"};
|
||||
const ByteArray raw_auth_token{auth_token};
|
||||
advertising_connection_info_.remote_endpoint_name = endpoint.name;
|
||||
client->OnConnectionInitiated(endpoint.id, advertising_connection_info_,
|
||||
discovery_connection_listener_);
|
||||
EXPECT_TRUE(client->HasPendingConnectionToEndpoint(endpoint.id));
|
||||
}
|
||||
|
||||
void OnDiscoveryConnectionLocalAccepted(ClientProxy* client,
|
||||
const Endpoint& endpoint) {
|
||||
EXPECT_TRUE(client->HasPendingConnectionToEndpoint(endpoint.id));
|
||||
EXPECT_FALSE(client->HasLocalEndpointResponded(endpoint.id));
|
||||
client->LocalEndpointAcceptedConnection(endpoint.id, payload_listener_);
|
||||
EXPECT_TRUE(client->HasLocalEndpointResponded(endpoint.id));
|
||||
EXPECT_TRUE(client->LocalConnectionIsAccepted(endpoint.id));
|
||||
}
|
||||
|
||||
void OnDiscoveryConnectionRemoteAccepted(ClientProxy* client,
|
||||
const Endpoint& endpoint) {
|
||||
EXPECT_TRUE(client->HasPendingConnectionToEndpoint(endpoint.id));
|
||||
EXPECT_FALSE(client->HasRemoteEndpointResponded(endpoint.id));
|
||||
client->RemoteEndpointAcceptedConnection(endpoint.id);
|
||||
EXPECT_TRUE(client->HasRemoteEndpointResponded(endpoint.id));
|
||||
EXPECT_TRUE(client->RemoteConnectionIsAccepted(endpoint.id));
|
||||
}
|
||||
|
||||
void OnDiscoveryConnectionLocalRejected(ClientProxy* client,
|
||||
const Endpoint& endpoint) {
|
||||
EXPECT_TRUE(client->HasPendingConnectionToEndpoint(endpoint.id));
|
||||
EXPECT_FALSE(client->HasLocalEndpointResponded(endpoint.id));
|
||||
client->LocalEndpointRejectedConnection(endpoint.id);
|
||||
EXPECT_TRUE(client->HasLocalEndpointResponded(endpoint.id));
|
||||
EXPECT_FALSE(client->LocalConnectionIsAccepted(endpoint.id));
|
||||
}
|
||||
|
||||
void OnDiscoveryConnectionRemoteRejected(ClientProxy* client,
|
||||
const Endpoint& endpoint) {
|
||||
EXPECT_TRUE(client->HasPendingConnectionToEndpoint(endpoint.id));
|
||||
EXPECT_FALSE(client->HasRemoteEndpointResponded(endpoint.id));
|
||||
client->RemoteEndpointRejectedConnection(endpoint.id);
|
||||
EXPECT_TRUE(client->HasRemoteEndpointResponded(endpoint.id));
|
||||
EXPECT_FALSE(client->RemoteConnectionIsAccepted(endpoint.id));
|
||||
}
|
||||
|
||||
void OnDiscoveryConnectionAccepted(ClientProxy* client,
|
||||
const Endpoint& endpoint) {
|
||||
EXPECT_CALL(mock_discovery_connection_.accepted_cb, Call).Times(1);
|
||||
EXPECT_TRUE(client->IsConnectionAccepted(endpoint.id));
|
||||
client->OnConnectionAccepted(endpoint.id);
|
||||
}
|
||||
|
||||
void OnDiscoveryConnectionRejected(ClientProxy* client,
|
||||
const Endpoint& endpoint) {
|
||||
EXPECT_CALL(mock_discovery_connection_.rejected_cb, Call).Times(1);
|
||||
EXPECT_TRUE(client->IsConnectionRejected(endpoint.id));
|
||||
client->OnConnectionRejected(endpoint.id, {Status::kConnectionRejected});
|
||||
}
|
||||
|
||||
void OnDiscoveryBandwidthChanged(ClientProxy* client,
|
||||
const Endpoint& endpoint) {
|
||||
EXPECT_CALL(mock_discovery_connection_.bandwidth_changed_cb, Call).Times(1);
|
||||
client->OnBandwidthChanged(endpoint.id, 1);
|
||||
}
|
||||
|
||||
void OnDiscoveryConnectionDisconnected(ClientProxy* client,
|
||||
const Endpoint& endpoint) {
|
||||
EXPECT_CALL(mock_discovery_connection_.disconnected_cb, Call).Times(1);
|
||||
client->OnDisconnected(endpoint.id, true);
|
||||
}
|
||||
|
||||
void OnPayload(ClientProxy* client, const Endpoint& endpoint) {
|
||||
EXPECT_CALL(mock_discovery_payload_.payload_cb, Call).Times(1);
|
||||
client->OnPayload(endpoint.id, Payload(payload_bytes_));
|
||||
}
|
||||
|
||||
void OnPayloadProgress(ClientProxy* client, const Endpoint& endpoint) {
|
||||
EXPECT_CALL(mock_discovery_payload_.payload_progress_cb, Call).Times(1);
|
||||
client->OnPayloadProgress(endpoint.id, {});
|
||||
}
|
||||
|
||||
MockDiscoveryListener mock_discovery_;
|
||||
MockConnectionListener mock_discovery_connection_;
|
||||
MockPayloadListener mock_discovery_payload_;
|
||||
|
||||
proto::connections::Medium medium_{proto::connections::Medium::BLUETOOTH};
|
||||
std::vector<proto::connections::Medium> mediums_{
|
||||
proto::connections::Medium::BLUETOOTH,
|
||||
};
|
||||
Strategy strategy_{Strategy::kP2pPointToPoint};
|
||||
const std::string service_id_{"service"};
|
||||
ClientProxy client1_;
|
||||
ClientProxy client2_;
|
||||
std::string auth_token_ = "auth_token";
|
||||
ByteArray raw_auth_token_ = ByteArray(auth_token_);
|
||||
ByteArray payload_bytes_{"bytes"};
|
||||
ConnectionResponseInfo advertising_connection_info_{
|
||||
.authentication_token = auth_token_,
|
||||
.raw_authentication_token = raw_auth_token_,
|
||||
.is_incoming_connection = true,
|
||||
};
|
||||
ConnectionListener advertising_connection_listener_;
|
||||
ConnectionListener discovery_connection_listener_{
|
||||
.initiated_cb = mock_discovery_connection_.initiated_cb.AsStdFunction(),
|
||||
.accepted_cb = mock_discovery_connection_.accepted_cb.AsStdFunction(),
|
||||
.rejected_cb = mock_discovery_connection_.rejected_cb.AsStdFunction(),
|
||||
.disconnected_cb =
|
||||
mock_discovery_connection_.disconnected_cb.AsStdFunction(),
|
||||
.bandwidth_changed_cb =
|
||||
mock_discovery_connection_.bandwidth_changed_cb.AsStdFunction(),
|
||||
};
|
||||
DiscoveryListener discovery_listener_{
|
||||
.endpoint_found_cb = mock_discovery_.endpoint_found_cb.AsStdFunction(),
|
||||
.endpoint_lost_cb = mock_discovery_.endpoint_lost_cb.AsStdFunction(),
|
||||
};
|
||||
PayloadListener payload_listener_{
|
||||
.payload_cb = mock_discovery_payload_.payload_cb.AsStdFunction(),
|
||||
.payload_progress_cb =
|
||||
mock_discovery_payload_.payload_progress_cb.AsStdFunction(),
|
||||
};
|
||||
};
|
||||
|
||||
TEST_F(ClientProxyTest, ConstructorDestructorWorks) { SUCCEED(); }
|
||||
|
||||
TEST_F(ClientProxyTest, ClientIdIsUnique) {
|
||||
EXPECT_NE(client1_.GetClientId(), client2_.GetClientId());
|
||||
}
|
||||
|
||||
TEST_F(ClientProxyTest, GeneratedEndpointIdIsUnique) {
|
||||
EXPECT_NE(client1_.GenerateLocalEndpointId(),
|
||||
client2_.GenerateLocalEndpointId());
|
||||
}
|
||||
|
||||
TEST_F(ClientProxyTest, ResetClearsState) {
|
||||
client1_.Reset();
|
||||
EXPECT_FALSE(client1_.IsAdvertising());
|
||||
EXPECT_FALSE(client1_.IsDiscovering());
|
||||
EXPECT_TRUE(client1_.GetAdvertisingServiceId().empty());
|
||||
EXPECT_TRUE(client1_.GetDiscoveryServiceId().empty());
|
||||
}
|
||||
|
||||
TEST_F(ClientProxyTest, StartedAdvertisingChangesStateFromIdle) {
|
||||
client1_.StartedAdvertising(service_id_, strategy_, {}, {});
|
||||
|
||||
EXPECT_TRUE(client1_.IsAdvertising());
|
||||
EXPECT_FALSE(client1_.IsDiscovering());
|
||||
EXPECT_EQ(client1_.GetAdvertisingServiceId(), service_id_);
|
||||
EXPECT_TRUE(client1_.GetDiscoveryServiceId().empty());
|
||||
}
|
||||
|
||||
TEST_F(ClientProxyTest, StartedDiscoveryChangesStateFromIdle) {
|
||||
client1_.StartedDiscovery(service_id_, strategy_, {}, {});
|
||||
|
||||
EXPECT_FALSE(client1_.IsAdvertising());
|
||||
EXPECT_TRUE(client1_.IsDiscovering());
|
||||
EXPECT_TRUE(client1_.GetAdvertisingServiceId().empty());
|
||||
EXPECT_EQ(client1_.GetDiscoveryServiceId(), service_id_);
|
||||
}
|
||||
|
||||
TEST_F(ClientProxyTest, OnEndpointFoundFiresNotificationInDiscovery) {
|
||||
Endpoint advertising_endpoint =
|
||||
StartAdvertising(&client1_, advertising_connection_listener_);
|
||||
StartDiscovery(&client2_, discovery_listener_);
|
||||
OnDiscoveryEndpointFound(&client2_, advertising_endpoint);
|
||||
}
|
||||
|
||||
TEST_F(ClientProxyTest, OnEndpointLostFiresNotificationInDiscovery) {
|
||||
Endpoint advertising_endpoint =
|
||||
StartAdvertising(&client1_, advertising_connection_listener_);
|
||||
StartDiscovery(&client2_, discovery_listener_);
|
||||
OnDiscoveryEndpointFound(&client2_, advertising_endpoint);
|
||||
OnDiscoveryEndpointLost(&client2_, advertising_endpoint);
|
||||
}
|
||||
|
||||
TEST_F(ClientProxyTest, OnConnectionInitiatedFiresNotificationInDiscovery) {
|
||||
Endpoint advertising_endpoint =
|
||||
StartAdvertising(&client1_, advertising_connection_listener_);
|
||||
StartDiscovery(&client2_, discovery_listener_);
|
||||
OnDiscoveryEndpointFound(&client2_, advertising_endpoint);
|
||||
OnDiscoveryConnectionInitiated(&client2_, advertising_endpoint);
|
||||
}
|
||||
|
||||
TEST_F(ClientProxyTest, OnBandwidthChangedFiresNotificationInDiscovery) {
|
||||
Endpoint advertising_endpoint =
|
||||
StartAdvertising(&client1_, advertising_connection_listener_);
|
||||
StartDiscovery(&client2_, discovery_listener_);
|
||||
OnDiscoveryEndpointFound(&client2_, advertising_endpoint);
|
||||
OnDiscoveryConnectionInitiated(&client2_, advertising_endpoint);
|
||||
OnDiscoveryConnectionLocalAccepted(&client2_, advertising_endpoint);
|
||||
OnDiscoveryConnectionRemoteAccepted(&client2_, advertising_endpoint);
|
||||
OnDiscoveryConnectionAccepted(&client2_, advertising_endpoint);
|
||||
OnDiscoveryBandwidthChanged(&client2_, advertising_endpoint);
|
||||
}
|
||||
|
||||
TEST_F(ClientProxyTest, OnDisconnectedFiresNotificationInDiscovery) {
|
||||
Endpoint advertising_endpoint =
|
||||
StartAdvertising(&client1_, advertising_connection_listener_);
|
||||
StartDiscovery(&client2_, discovery_listener_);
|
||||
OnDiscoveryEndpointFound(&client2_, advertising_endpoint);
|
||||
OnDiscoveryConnectionInitiated(&client2_, advertising_endpoint);
|
||||
OnDiscoveryConnectionDisconnected(&client2_, advertising_endpoint);
|
||||
}
|
||||
|
||||
TEST_F(ClientProxyTest, LocalEndpointAcceptedConnectionChangesState) {
|
||||
Endpoint advertising_endpoint =
|
||||
StartAdvertising(&client1_, advertising_connection_listener_);
|
||||
StartDiscovery(&client2_, discovery_listener_);
|
||||
OnDiscoveryEndpointFound(&client2_, advertising_endpoint);
|
||||
OnDiscoveryConnectionInitiated(&client2_, advertising_endpoint);
|
||||
OnDiscoveryConnectionLocalAccepted(&client2_, advertising_endpoint);
|
||||
}
|
||||
|
||||
TEST_F(ClientProxyTest, LocalEndpointRejectedConnectionChangesState) {
|
||||
Endpoint advertising_endpoint =
|
||||
StartAdvertising(&client1_, advertising_connection_listener_);
|
||||
StartDiscovery(&client2_, discovery_listener_);
|
||||
OnDiscoveryEndpointFound(&client2_, advertising_endpoint);
|
||||
OnDiscoveryConnectionInitiated(&client2_, advertising_endpoint);
|
||||
OnDiscoveryConnectionLocalRejected(&client2_, advertising_endpoint);
|
||||
}
|
||||
|
||||
TEST_F(ClientProxyTest, RemoteEndpointAcceptedConnectionChangesState) {
|
||||
Endpoint advertising_endpoint =
|
||||
StartAdvertising(&client1_, advertising_connection_listener_);
|
||||
StartDiscovery(&client2_, discovery_listener_);
|
||||
OnDiscoveryEndpointFound(&client2_, advertising_endpoint);
|
||||
OnDiscoveryConnectionInitiated(&client2_, advertising_endpoint);
|
||||
OnDiscoveryConnectionRemoteAccepted(&client2_, advertising_endpoint);
|
||||
}
|
||||
|
||||
TEST_F(ClientProxyTest, RemoteEndpointRejectedConnectionChangesState) {
|
||||
Endpoint advertising_endpoint =
|
||||
StartAdvertising(&client1_, advertising_connection_listener_);
|
||||
StartDiscovery(&client2_, discovery_listener_);
|
||||
OnDiscoveryEndpointFound(&client2_, advertising_endpoint);
|
||||
OnDiscoveryConnectionInitiated(&client2_, advertising_endpoint);
|
||||
OnDiscoveryConnectionRemoteRejected(&client2_, advertising_endpoint);
|
||||
}
|
||||
|
||||
TEST_F(ClientProxyTest, OnPayloadChangesState) {
|
||||
Endpoint advertising_endpoint =
|
||||
StartAdvertising(&client1_, advertising_connection_listener_);
|
||||
StartDiscovery(&client2_, discovery_listener_);
|
||||
OnDiscoveryEndpointFound(&client2_, advertising_endpoint);
|
||||
OnDiscoveryConnectionInitiated(&client2_, advertising_endpoint);
|
||||
OnDiscoveryConnectionLocalAccepted(&client2_, advertising_endpoint);
|
||||
OnDiscoveryConnectionRemoteAccepted(&client2_, advertising_endpoint);
|
||||
OnDiscoveryConnectionAccepted(&client2_, advertising_endpoint);
|
||||
OnPayload(&client2_, advertising_endpoint);
|
||||
}
|
||||
|
||||
TEST_F(ClientProxyTest, OnPayloadProgressChangesState) {
|
||||
Endpoint advertising_endpoint =
|
||||
StartAdvertising(&client1_, advertising_connection_listener_);
|
||||
StartDiscovery(&client2_, discovery_listener_);
|
||||
OnDiscoveryEndpointFound(&client2_, advertising_endpoint);
|
||||
OnDiscoveryConnectionInitiated(&client2_, advertising_endpoint);
|
||||
OnDiscoveryConnectionLocalAccepted(&client2_, advertising_endpoint);
|
||||
OnDiscoveryConnectionRemoteAccepted(&client2_, advertising_endpoint);
|
||||
OnDiscoveryConnectionAccepted(&client2_, advertising_endpoint);
|
||||
OnPayloadProgress(&client2_, advertising_endpoint);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,368 @@
|
||||
#include "core_v2/internal/encryption_runner.h"
|
||||
|
||||
#include <cinttypes>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include "platform_v2/base/base64_utils.h"
|
||||
#include "platform_v2/base/byte_array.h"
|
||||
#include "platform_v2/base/exception.h"
|
||||
#include "platform_v2/public/cancelable_alarm.h"
|
||||
#include "platform_v2/public/logging.h"
|
||||
#include "securegcm/ukey2_handshake.h"
|
||||
#include "absl/strings/ascii.h"
|
||||
#include "absl/time/clock.h"
|
||||
#include "absl/time/time.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace {
|
||||
|
||||
constexpr absl::Duration kTimeout = absl::Seconds(15);
|
||||
constexpr std::int32_t kMaxUkey2VerificationStringLength = 32;
|
||||
constexpr std::int32_t kTokenLength = 5;
|
||||
constexpr securegcm::UKey2Handshake::HandshakeCipher kCipher =
|
||||
securegcm::UKey2Handshake::HandshakeCipher::P256_SHA512;
|
||||
|
||||
// Transforms a raw UKEY2 token (which is a random ByteArray that's
|
||||
// kMaxUkey2VerificationStringLength long) into a kTokenLength string that only
|
||||
// uses [A-Z], [0-9], '_', '-' for each character.
|
||||
std::string ToHumanReadableString(const ByteArray& token) {
|
||||
std::string result = Base64Utils::Encode(token).substr(0, kTokenLength);
|
||||
absl::AsciiStrToUpper(&result);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool HandleEncryptionSuccess(const std::string& endpoint_id,
|
||||
std::unique_ptr<securegcm::UKey2Handshake> ukey2,
|
||||
const EncryptionRunner::ResultListener& listener) {
|
||||
std::unique_ptr<std::string> verification_string =
|
||||
ukey2->GetVerificationString(kMaxUkey2VerificationStringLength);
|
||||
if (verification_string == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ByteArray raw_authentication_token(*verification_string);
|
||||
|
||||
listener.on_success_cb(endpoint_id, std::move(ukey2),
|
||||
ToHumanReadableString(raw_authentication_token),
|
||||
raw_authentication_token);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void CancelableAlarmRunnable(ClientProxy* client_proxy,
|
||||
const std::string& endpoint_id,
|
||||
EndpointChannel* endpoint_channel) {
|
||||
NEARBY_LOG(INFO,
|
||||
"Timing out encryption for client %" PRId64
|
||||
" to endpoint %s after %" PRId64 " ms",
|
||||
client_proxy->GetClientId(), endpoint_id.c_str(),
|
||||
static_cast<std::int64_t>(absl::ToInt64Milliseconds(kTimeout)));
|
||||
endpoint_channel->Close();
|
||||
}
|
||||
|
||||
class ServerRunnable final {
|
||||
public:
|
||||
ServerRunnable(ClientProxy* client, ScheduledExecutor* alarm_executor,
|
||||
const std::string& endpoint_id, EndpointChannel* channel,
|
||||
EncryptionRunner::ResultListener&& listener)
|
||||
: client_(client),
|
||||
alarm_executor_(alarm_executor),
|
||||
endpoint_id_(endpoint_id),
|
||||
channel_(channel),
|
||||
listener_(std::move(listener)) {}
|
||||
|
||||
void operator()() const {
|
||||
CancelableAlarm timeout_alarm(
|
||||
"EncryptionRunner.startServer() timeout",
|
||||
[this]() { CancelableAlarmRunnable(client_, endpoint_id_, channel_); },
|
||||
kTimeout, alarm_executor_);
|
||||
|
||||
std::unique_ptr<securegcm::UKey2Handshake> server =
|
||||
securegcm::UKey2Handshake::ForResponder(kCipher);
|
||||
if (server == nullptr) {
|
||||
LogException();
|
||||
HandleHandshakeOrIoException(&timeout_alarm);
|
||||
return;
|
||||
}
|
||||
|
||||
// Message 1 (Client Init)
|
||||
ExceptionOr<ByteArray> client_init = channel_->Read();
|
||||
if (!client_init.ok()) {
|
||||
LogException();
|
||||
HandleHandshakeOrIoException(&timeout_alarm);
|
||||
return;
|
||||
}
|
||||
|
||||
securegcm::UKey2Handshake::ParseResult parse_result =
|
||||
server->ParseHandshakeMessage(std::string(client_init.result()));
|
||||
|
||||
// Java code throws a HandshakeException / AlertException.
|
||||
if (!parse_result.success) {
|
||||
LogException();
|
||||
if (parse_result.alert_to_send != nullptr) {
|
||||
HandleAlertException(parse_result);
|
||||
}
|
||||
HandleHandshakeOrIoException(&timeout_alarm);
|
||||
return;
|
||||
}
|
||||
|
||||
NEARBY_LOG(INFO, "In startServer(), read UKEY2 Message 1 from endpoint %s",
|
||||
endpoint_id_.c_str());
|
||||
|
||||
// Message 2 (Server Init)
|
||||
std::unique_ptr<std::string> server_init =
|
||||
server->GetNextHandshakeMessage();
|
||||
|
||||
// Java code throws a HandshakeException.
|
||||
if (server_init == nullptr) {
|
||||
LogException();
|
||||
HandleHandshakeOrIoException(&timeout_alarm);
|
||||
return;
|
||||
}
|
||||
|
||||
Exception write_exception =
|
||||
channel_->Write(ByteArray(std::move(*server_init)));
|
||||
if (!write_exception.Ok()) {
|
||||
LogException();
|
||||
HandleHandshakeOrIoException(&timeout_alarm);
|
||||
return;
|
||||
}
|
||||
|
||||
NEARBY_LOG(INFO, "In startServer(), wrote UKEY2 Message 2 to endpoint %s",
|
||||
endpoint_id_.c_str());
|
||||
|
||||
// Message 3 (Client Finish)
|
||||
ExceptionOr<ByteArray> client_finish = channel_->Read();
|
||||
|
||||
if (!client_finish.ok()) {
|
||||
LogException();
|
||||
HandleHandshakeOrIoException(&timeout_alarm);
|
||||
return;
|
||||
}
|
||||
|
||||
parse_result =
|
||||
server->ParseHandshakeMessage(std::string(client_finish.result()));
|
||||
|
||||
// Java code throws an AlertException or a HandshakeException.
|
||||
if (!parse_result.success) {
|
||||
LogException();
|
||||
if (parse_result.alert_to_send != nullptr) {
|
||||
HandleAlertException(parse_result);
|
||||
}
|
||||
HandleHandshakeOrIoException(&timeout_alarm);
|
||||
return;
|
||||
}
|
||||
|
||||
NEARBY_LOG(INFO, "In startServer(), read UKEY2 Message 3 from endpoint %s",
|
||||
endpoint_id_.c_str());
|
||||
|
||||
timeout_alarm.Cancel();
|
||||
|
||||
if (!HandleEncryptionSuccess(endpoint_id_, std::move(server), listener_)) {
|
||||
LogException();
|
||||
HandleHandshakeOrIoException(&timeout_alarm);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void LogException() const {
|
||||
NEARBY_LOG(ERROR, "In startServer(), UKEY2 failed with endpoint %s",
|
||||
endpoint_id_.c_str());
|
||||
}
|
||||
|
||||
void HandleHandshakeOrIoException(CancelableAlarm* timeout_alarm) const {
|
||||
timeout_alarm->Cancel();
|
||||
listener_.on_failure_cb(endpoint_id_, channel_);
|
||||
}
|
||||
|
||||
void HandleAlertException(
|
||||
const securegcm::UKey2Handshake::ParseResult& parse_result) const {
|
||||
Exception write_exception =
|
||||
channel_->Write(ByteArray(*parse_result.alert_to_send));
|
||||
if (!write_exception.Ok()) {
|
||||
NEARBY_LOG(WARNING,
|
||||
"In startServer(), client %" PRId64
|
||||
" failed to pass the alert error message to endpoint %s",
|
||||
client_->GetClientId(), endpoint_id_.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
ClientProxy* client_;
|
||||
ScheduledExecutor* alarm_executor_;
|
||||
const std::string endpoint_id_;
|
||||
EndpointChannel* channel_;
|
||||
EncryptionRunner::ResultListener listener_;
|
||||
};
|
||||
|
||||
class ClientRunnable final {
|
||||
public:
|
||||
ClientRunnable(ClientProxy* client, ScheduledExecutor* alarm_executor,
|
||||
const std::string& endpoint_id, EndpointChannel* channel,
|
||||
EncryptionRunner::ResultListener&& listener)
|
||||
: client_(client),
|
||||
alarm_executor_(alarm_executor),
|
||||
endpoint_id_(endpoint_id),
|
||||
channel_(channel),
|
||||
listener_(std::move(listener)) {}
|
||||
|
||||
void operator()() const {
|
||||
CancelableAlarm timeout_alarm(
|
||||
"EncryptionRunner.startClient() timeout",
|
||||
[this]() { CancelableAlarmRunnable(client_, endpoint_id_, channel_); },
|
||||
kTimeout, alarm_executor_);
|
||||
|
||||
std::unique_ptr<securegcm::UKey2Handshake> crypto =
|
||||
securegcm::UKey2Handshake::ForInitiator(kCipher);
|
||||
|
||||
// Java code throws a HandshakeException.
|
||||
if (crypto == nullptr) {
|
||||
LogException();
|
||||
HandleHandshakeOrIoException(&timeout_alarm);
|
||||
return;
|
||||
}
|
||||
|
||||
// Message 1 (Client Init)
|
||||
std::unique_ptr<std::string> client_init =
|
||||
crypto->GetNextHandshakeMessage();
|
||||
|
||||
// Java code throws a HandshakeException.
|
||||
if (client_init == nullptr) {
|
||||
LogException();
|
||||
HandleHandshakeOrIoException(&timeout_alarm);
|
||||
return;
|
||||
}
|
||||
|
||||
Exception write_init_exception = channel_->Write(ByteArray(*client_init));
|
||||
if (!write_init_exception.Ok()) {
|
||||
LogException();
|
||||
HandleHandshakeOrIoException(&timeout_alarm);
|
||||
return;
|
||||
}
|
||||
|
||||
NEARBY_LOG(INFO, "In startClient(), wrote UKEY2 Message 1 to endpoint %s",
|
||||
endpoint_id_.c_str());
|
||||
|
||||
// Message 2 (Server Init)
|
||||
ExceptionOr<ByteArray> server_init = channel_->Read();
|
||||
|
||||
if (!server_init.ok()) {
|
||||
LogException();
|
||||
HandleHandshakeOrIoException(&timeout_alarm);
|
||||
return;
|
||||
}
|
||||
|
||||
securegcm::UKey2Handshake::ParseResult parse_result =
|
||||
crypto->ParseHandshakeMessage(std::string(server_init.result()));
|
||||
|
||||
// Java code throws an AlertException or a HandshakeException.
|
||||
if (!parse_result.success) {
|
||||
LogException();
|
||||
if (parse_result.alert_to_send != nullptr) {
|
||||
HandleAlertException(parse_result);
|
||||
}
|
||||
HandleHandshakeOrIoException(&timeout_alarm);
|
||||
return;
|
||||
}
|
||||
|
||||
NEARBY_LOG(INFO, "In startClient(), read UKEY2 Message 2 from endpoint %s",
|
||||
endpoint_id_.c_str());
|
||||
|
||||
// Message 3 (Client Finish)
|
||||
std::unique_ptr<std::string> client_finish =
|
||||
crypto->GetNextHandshakeMessage();
|
||||
|
||||
// Java code throws a HandshakeException.
|
||||
if (client_finish == nullptr) {
|
||||
LogException();
|
||||
HandleHandshakeOrIoException(&timeout_alarm);
|
||||
return;
|
||||
}
|
||||
|
||||
Exception write_finish_exception =
|
||||
channel_->Write(ByteArray(*client_finish));
|
||||
if (!write_finish_exception.Ok()) {
|
||||
LogException();
|
||||
HandleHandshakeOrIoException(&timeout_alarm);
|
||||
return;
|
||||
}
|
||||
|
||||
NEARBY_LOG(INFO, "In startClient(), wrote UKEY2 Message 3 to endpoint %s",
|
||||
endpoint_id_.c_str());
|
||||
|
||||
timeout_alarm.Cancel();
|
||||
|
||||
if (!HandleEncryptionSuccess(endpoint_id_, std::move(crypto), listener_)) {
|
||||
LogException();
|
||||
HandleHandshakeOrIoException(&timeout_alarm);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void LogException() const {
|
||||
NEARBY_LOG(ERROR, "In startClient(), UKEY2 failed with endpoint %s",
|
||||
endpoint_id_.c_str());
|
||||
}
|
||||
|
||||
void HandleHandshakeOrIoException(CancelableAlarm* timeout_alarm) const {
|
||||
timeout_alarm->Cancel();
|
||||
listener_.on_failure_cb(endpoint_id_, channel_);
|
||||
}
|
||||
|
||||
void HandleAlertException(
|
||||
const securegcm::UKey2Handshake::ParseResult& parse_result) const {
|
||||
Exception write_exception =
|
||||
channel_->Write(ByteArray(*parse_result.alert_to_send));
|
||||
if (!write_exception.Ok()) {
|
||||
NEARBY_LOG(WARNING,
|
||||
"In startClient(), client %" PRId64
|
||||
" failed to pass the alert error message to endpoint %s",
|
||||
client_->GetClientId(), endpoint_id_.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
ClientProxy* client_;
|
||||
ScheduledExecutor* alarm_executor_;
|
||||
const std::string endpoint_id_;
|
||||
EndpointChannel* channel_;
|
||||
EncryptionRunner::ResultListener listener_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
EncryptionRunner::~EncryptionRunner() {
|
||||
// Stop all the ongoing Runnables (as gracefully as possible).
|
||||
client_executor_.Shutdown();
|
||||
server_executor_.Shutdown();
|
||||
alarm_executor_.Shutdown();
|
||||
}
|
||||
|
||||
void EncryptionRunner::StartServer(
|
||||
ClientProxy* client_proxy, const std::string& endpoint_id,
|
||||
EndpointChannel* endpoint_channel,
|
||||
EncryptionRunner::ResultListener&& listener) {
|
||||
server_executor_.Execute(
|
||||
[runnable{ServerRunnable(client_proxy, &alarm_executor_, endpoint_id,
|
||||
endpoint_channel, std::move(listener))}]() {
|
||||
runnable();
|
||||
});
|
||||
}
|
||||
|
||||
void EncryptionRunner::StartClient(
|
||||
ClientProxy* client_proxy, const std::string& endpoint_id,
|
||||
EndpointChannel* endpoint_channel,
|
||||
EncryptionRunner::ResultListener&& listener) {
|
||||
client_executor_.Execute(
|
||||
[runnable{ClientRunnable(client_proxy, &alarm_executor_, endpoint_id,
|
||||
endpoint_channel, std::move(listener))}]() {
|
||||
runnable();
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,72 @@
|
||||
#ifndef CORE_V2_INTERNAL_ENCRYPTION_RUNNER_H_
|
||||
#define CORE_V2_INTERNAL_ENCRYPTION_RUNNER_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "core_v2/internal/client_proxy.h"
|
||||
#include "core_v2/internal/endpoint_channel.h"
|
||||
#include "core_v2/listeners.h"
|
||||
#include "platform_v2/base/byte_array.h"
|
||||
#include "platform_v2/public/scheduled_executor.h"
|
||||
#include "platform_v2/public/single_thread_executor.h"
|
||||
#include "securegcm/ukey2_handshake.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
|
||||
// Encrypts a connection over UKEY2.
|
||||
//
|
||||
// NOTE: Stalled EndpointChannels will be disconnected after kTimeout.
|
||||
// This is to prevent unverified endpoints from maintaining an
|
||||
// indefinite connection to us.
|
||||
class EncryptionRunner {
|
||||
public:
|
||||
EncryptionRunner() = default;
|
||||
~EncryptionRunner();
|
||||
|
||||
struct ResultListener {
|
||||
// @EncryptionRunnerThread
|
||||
std::function<void(const std::string& endpoint_id,
|
||||
std::unique_ptr<securegcm::UKey2Handshake> ukey2,
|
||||
const std::string& auth_token,
|
||||
const ByteArray& raw_auth_token)>
|
||||
on_success_cb =
|
||||
DefaultCallback<const std::string&,
|
||||
std::unique_ptr<securegcm::UKey2Handshake>,
|
||||
const std::string&, const ByteArray&>();
|
||||
|
||||
// Encryption has failed. The remote_endpoint_id and channel are given so
|
||||
// that any pending state can be cleaned up.
|
||||
//
|
||||
// We return the EndpointChannel because, at this stage, simultaneous
|
||||
// connections are a possibility. Use this channel to verify that the state
|
||||
// you're cleaning up is for this EndpointChannel, and not state for another
|
||||
// channel to the same endpoint.
|
||||
//
|
||||
// @EncryptionRunnerThread
|
||||
std::function<void(const std::string& endpoint_id,
|
||||
EndpointChannel* channel)>
|
||||
on_failure_cb = DefaultCallback<const std::string&, EndpointChannel*>();
|
||||
};
|
||||
|
||||
// @AnyThread
|
||||
void StartServer(ClientProxy* client_proxy, const std::string& endpoint_id,
|
||||
EndpointChannel* endpoint_channel,
|
||||
ResultListener&& result_listener);
|
||||
// @AnyThread
|
||||
void StartClient(ClientProxy* client_proxy, const std::string& endpoint_id,
|
||||
EndpointChannel* endpoint_channel,
|
||||
ResultListener&& result_listener);
|
||||
|
||||
private:
|
||||
ScheduledExecutor alarm_executor_;
|
||||
SingleThreadExecutor server_executor_;
|
||||
SingleThreadExecutor client_executor_;
|
||||
};
|
||||
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
|
||||
#endif // CORE_V2_INTERNAL_ENCRYPTION_RUNNER_H_
|
||||
@@ -0,0 +1,128 @@
|
||||
#include "core_v2/internal/encryption_runner.h"
|
||||
|
||||
#include "core_v2/internal/client_proxy.h"
|
||||
#include "core_v2/internal/endpoint_channel.h"
|
||||
#include "platform_v2/base/byte_array.h"
|
||||
#include "platform_v2/public/count_down_latch.h"
|
||||
#include "platform_v2/public/pipe.h"
|
||||
#include "platform_v2/public/system_clock.h"
|
||||
#include "proto/connections_enums.pb.h"
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "absl/time/clock.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace {
|
||||
|
||||
using ::location::nearby::proto::connections::Medium;
|
||||
|
||||
class FakeEndpointChannel : public EndpointChannel {
|
||||
public:
|
||||
FakeEndpointChannel(InputStream* in, OutputStream* out)
|
||||
: in_(in), out_(out) {}
|
||||
ExceptionOr<ByteArray> Read() override {
|
||||
read_timestamp_ = SystemClock::ElapsedRealtime();
|
||||
return in_ ? in_->Read(Pipe::kChunkSize)
|
||||
: ExceptionOr<ByteArray>{Exception::kIo};
|
||||
}
|
||||
Exception Write(const ByteArray& data) override {
|
||||
return out_ ? out_->Write(data) : Exception{Exception::kIo};
|
||||
}
|
||||
void Close() override {
|
||||
if (in_) in_->Close();
|
||||
if (out_) out_->Close();
|
||||
}
|
||||
void Close(proto::connections::DisconnectionReason reason) override {
|
||||
Close();
|
||||
}
|
||||
std::string GetType() const override { return "fake-channel-type"; }
|
||||
std::string GetName() const override { return "fake-channel"; }
|
||||
Medium GetMedium() const override { return Medium::BLE; }
|
||||
void EnableEncryption(
|
||||
securegcm::D2DConnectionContextV1* connection_context) override {}
|
||||
bool IsPaused() const override { return false; }
|
||||
void Pause() override {}
|
||||
void Resume() override {}
|
||||
absl::Time GetLastReadTimestamp() const override { return read_timestamp_; }
|
||||
|
||||
private:
|
||||
InputStream* in_ = nullptr;
|
||||
OutputStream* out_ = nullptr;
|
||||
absl::Time read_timestamp_ = absl::InfinitePast();
|
||||
};
|
||||
|
||||
struct User {
|
||||
User(Pipe* reader, Pipe* writer)
|
||||
: channel(&reader->GetInputStream(), &writer->GetOutputStream()) {}
|
||||
|
||||
FakeEndpointChannel channel;
|
||||
EncryptionRunner crypto;
|
||||
ClientProxy client;
|
||||
};
|
||||
|
||||
struct Response {
|
||||
enum class Status {
|
||||
kUnknown = 0,
|
||||
kDone = 1,
|
||||
kFailed = 2,
|
||||
};
|
||||
|
||||
CountDownLatch latch{2};
|
||||
Status server_status = Status::kUnknown;
|
||||
Status client_status = Status::kUnknown;
|
||||
};
|
||||
|
||||
TEST(EncryptionRunnerTest, ConstructorDestructorWorks) { EncryptionRunner enc; }
|
||||
|
||||
TEST(EncryptionRunnerTest, ReadWrite) {
|
||||
Pipe from_a_to_b;
|
||||
Pipe from_b_to_a;
|
||||
User user_a(/*reader=*/&from_b_to_a, /*writer=*/&from_a_to_b);
|
||||
User user_b(/*reader=*/&from_a_to_b, /*writer=*/&from_b_to_a);
|
||||
Response response;
|
||||
|
||||
user_a.crypto.StartServer(
|
||||
&user_a.client, "endpoint_id", &user_a.channel,
|
||||
{
|
||||
.on_success_cb =
|
||||
[&response](const string& endpoint_id,
|
||||
std::unique_ptr<securegcm::UKey2Handshake> ukey2,
|
||||
const string& auth_token,
|
||||
const ByteArray& raw_auth_token) {
|
||||
response.server_status = Response::Status::kDone;
|
||||
response.latch.CountDown();
|
||||
},
|
||||
.on_failure_cb =
|
||||
[&response](const string& endpoint_id, EndpointChannel* channel) {
|
||||
response.server_status = Response::Status::kFailed;
|
||||
response.latch.CountDown();
|
||||
},
|
||||
});
|
||||
user_b.crypto.StartClient(
|
||||
&user_b.client, "endpoint_id", &user_b.channel,
|
||||
{
|
||||
.on_success_cb =
|
||||
[&response](const string& endpoint_id,
|
||||
std::unique_ptr<securegcm::UKey2Handshake> ukey2,
|
||||
const string& auth_token,
|
||||
const ByteArray& raw_auth_token) {
|
||||
response.client_status = Response::Status::kDone;
|
||||
response.latch.CountDown();
|
||||
},
|
||||
.on_failure_cb =
|
||||
[&response](const string& endpoint_id, EndpointChannel* channel) {
|
||||
response.client_status = Response::Status::kFailed;
|
||||
response.latch.CountDown();
|
||||
},
|
||||
});
|
||||
EXPECT_TRUE(response.latch.Await(absl::Milliseconds(5000)).result());
|
||||
EXPECT_EQ(response.server_status, Response::Status::kDone);
|
||||
EXPECT_EQ(response.client_status, Response::Status::kDone);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,74 @@
|
||||
#ifndef CORE_V2_INTERNAL_ENDPOINT_CHANNEL_H_
|
||||
#define CORE_V2_INTERNAL_ENDPOINT_CHANNEL_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "platform_v2/base/byte_array.h"
|
||||
#include "platform_v2/base/exception.h"
|
||||
#include "proto/connections_enums.pb.h"
|
||||
#include "securegcm/d2d_connection_context_v1.h"
|
||||
#include "absl/time/clock.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
|
||||
class EndpointChannel {
|
||||
public:
|
||||
virtual ~EndpointChannel() = default;
|
||||
|
||||
virtual ExceptionOr<ByteArray>
|
||||
Read() = 0; // throws Exception::IO, Exception::INTERRUPTED
|
||||
|
||||
virtual Exception Write(const ByteArray& data) = 0; // throws Exception::IO
|
||||
|
||||
// Closes this EndpointChannel, without tracking the closure in analytics.
|
||||
virtual void Close() = 0;
|
||||
|
||||
// Closes this EndpointChannel and records the closure with the given reason.
|
||||
virtual void Close(proto::connections::DisconnectionReason reason) = 0;
|
||||
|
||||
// Returns a one-word type descriptor for the concrete EndpointChannel
|
||||
// implementation that can be used in log messages; eg: BLUETOOTH, BLE, WIFI.
|
||||
virtual std::string GetType() const = 0;
|
||||
|
||||
// Returns the name of the EndpointChannel.
|
||||
virtual std::string GetName() const = 0;
|
||||
|
||||
// Returns the analytics enum representing the medium of this EndpointChannel.
|
||||
virtual proto::connections::Medium GetMedium() const = 0;
|
||||
|
||||
// Enables encryption on the EndpointChannel.
|
||||
virtual void EnableEncryption(
|
||||
securegcm::D2DConnectionContextV1* context) = 0;
|
||||
|
||||
// True if the EndpointChannel is currently pausing all writes.
|
||||
virtual bool IsPaused() const = 0;
|
||||
|
||||
// Pauses all writes on this EndpointChannel until resume() is called.
|
||||
virtual void Pause() = 0;
|
||||
|
||||
// Resumes any writes on this EndpointChannel that were suspended when pause()
|
||||
// was called.
|
||||
virtual void Resume() = 0;
|
||||
|
||||
// Returns the timestamp of the last read from this endpoint, or -1 if no
|
||||
// reads have occurred.
|
||||
virtual absl::Time GetLastReadTimestamp() const = 0;
|
||||
};
|
||||
|
||||
inline bool operator==(const EndpointChannel& lhs, const EndpointChannel& rhs) {
|
||||
return (lhs.GetType() == rhs.GetType()) && (lhs.GetName() == rhs.GetName()) &&
|
||||
(lhs.GetMedium() == rhs.GetMedium());
|
||||
}
|
||||
|
||||
inline bool operator!=(const EndpointChannel& lhs, const EndpointChannel& rhs) {
|
||||
return !(lhs == rhs);
|
||||
}
|
||||
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
|
||||
#endif // CORE_V2_INTERNAL_ENDPOINT_CHANNEL_H_
|
||||
@@ -0,0 +1,137 @@
|
||||
#include "core_v2/internal/endpoint_channel_manager.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "platform_v2/public/logging.h"
|
||||
#include "platform_v2/public/mutex.h"
|
||||
#include "platform_v2/public/mutex_lock.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
|
||||
EndpointChannelManager::~EndpointChannelManager() {
|
||||
MutexLock lock(&mutex_);
|
||||
channel_state_.DestroyAll();
|
||||
}
|
||||
|
||||
void EndpointChannelManager::RegisterChannelForEndpoint(
|
||||
ClientProxy* client, const std::string& endpoint_id,
|
||||
std::unique_ptr<EndpointChannel> channel) {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
SetActiveEndpointChannel(client, endpoint_id, std::move(channel));
|
||||
|
||||
NEARBY_LOG(INFO, "Registered channel: id=%s", endpoint_id.c_str());
|
||||
}
|
||||
|
||||
void EndpointChannelManager::ReplaceChannelForEndpoint(
|
||||
ClientProxy* client, const std::string& endpoint_id,
|
||||
std::unique_ptr<EndpointChannel> channel) {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
auto* endpoint = channel_state_.LookupEndpointData(endpoint_id);
|
||||
if (endpoint != nullptr && endpoint->channel == nullptr) {
|
||||
NEARBY_LOG(INFO, "Channel is missing while trying to update: id=%s",
|
||||
endpoint_id.c_str());
|
||||
}
|
||||
|
||||
SetActiveEndpointChannel(client, endpoint_id, std::move(channel));
|
||||
}
|
||||
|
||||
bool EndpointChannelManager::EncryptChannelForEndpoint(
|
||||
const std::string& endpoint_id,
|
||||
std::unique_ptr<EncryptionContext> context) {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
channel_state_.UpdateEncryptionContextForEndpoint(endpoint_id,
|
||||
std::move(context));
|
||||
auto* endpoint = channel_state_.LookupEndpointData(endpoint_id);
|
||||
return channel_state_.EncryptChannel(endpoint);
|
||||
}
|
||||
|
||||
std::shared_ptr<EndpointChannel> EndpointChannelManager::GetChannelForEndpoint(
|
||||
const std::string& endpoint_id) {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
auto* endpoint = channel_state_.LookupEndpointData(endpoint_id);
|
||||
if (endpoint == nullptr) {
|
||||
NEARBY_LOG(INFO, "No channel info: id=%s", endpoint_id.c_str());
|
||||
return {};
|
||||
}
|
||||
|
||||
return endpoint->channel;
|
||||
}
|
||||
|
||||
void EndpointChannelManager::SetActiveEndpointChannel(
|
||||
ClientProxy* client, const std::string& endpoint_id,
|
||||
std::unique_ptr<EndpointChannel> channel) {
|
||||
|
||||
// Update the channel first, then encrypt this new channel, if
|
||||
// crypto context is present.
|
||||
channel_state_.UpdateChannelForEndpoint(endpoint_id, std::move(channel));
|
||||
|
||||
auto* endpoint = channel_state_.LookupEndpointData(endpoint_id);
|
||||
if (endpoint->IsEncrypted()) channel_state_.EncryptChannel(endpoint);
|
||||
}
|
||||
|
||||
// endpoint - channel endpoint to encrypt
|
||||
bool EndpointChannelManager::ChannelState::EncryptChannel(
|
||||
EndpointChannelManager::ChannelState::EndpointData* endpoint) {
|
||||
if (endpoint != nullptr && endpoint->channel != nullptr &&
|
||||
endpoint->context != nullptr) {
|
||||
endpoint->channel->EnableEncryption(endpoint->context.get());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
///////////////////////////////// ChannelState /////////////////////////////////
|
||||
EndpointChannelManager::ChannelState::EndpointData*
|
||||
EndpointChannelManager::ChannelState::LookupEndpointData(
|
||||
const std::string& endpoint_id) {
|
||||
auto item = endpoints_.find(endpoint_id);
|
||||
return item != endpoints_.end() ? &item->second : nullptr;
|
||||
}
|
||||
|
||||
void EndpointChannelManager::ChannelState::UpdateChannelForEndpoint(
|
||||
const std::string& endpoint_id, std::unique_ptr<EndpointChannel> channel) {
|
||||
// Create EndpointData instance, if necessary, and populate channel.
|
||||
endpoints_[endpoint_id].channel = std::move(channel);
|
||||
}
|
||||
|
||||
void EndpointChannelManager::ChannelState::UpdateEncryptionContextForEndpoint(
|
||||
const std::string& endpoint_id,
|
||||
std::unique_ptr<EncryptionContext> context) {
|
||||
// Create EndpointData instance, if necessary, and populate crypto context.
|
||||
endpoints_[endpoint_id].context = std::move(context);
|
||||
}
|
||||
|
||||
bool EndpointChannelManager::ChannelState::RemoveEndpoint(
|
||||
const std::string& endpoint_id,
|
||||
proto::connections::DisconnectionReason reason) {
|
||||
auto item = endpoints_.find(endpoint_id);
|
||||
if (item == endpoints_.end()) return false;
|
||||
item->second.disconnect_reason = reason;
|
||||
endpoints_.erase(item);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EndpointChannelManager::UnregisterChannelForEndpoint(
|
||||
const std::string& endpoint_id) {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
if (!channel_state_.RemoveEndpoint(
|
||||
endpoint_id,
|
||||
proto::connections::DisconnectionReason::LOCAL_DISCONNECTION)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
NEARBY_LOG(INFO, "Unregistered channel: id=%s", endpoint_id.c_str());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,155 @@
|
||||
#ifndef CORE_V2_INTERNAL_ENDPOINT_CHANNEL_MANAGER_H_
|
||||
#define CORE_V2_INTERNAL_ENDPOINT_CHANNEL_MANAGER_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "core_v2/internal/client_proxy.h"
|
||||
#include "core_v2/internal/endpoint_channel.h"
|
||||
#include "platform_v2/public/logging.h"
|
||||
#include "platform_v2/public/mutex.h"
|
||||
#include "securegcm/d2d_connection_context_v1.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
|
||||
using EncryptionContext = ::securegcm::D2DConnectionContextV1;
|
||||
|
||||
// NOTE(std::string):
|
||||
// All the strings in internal class public interfaces should be exchanged as
|
||||
// const std::string& if they are immutable, and as std::string
|
||||
// it they are mutable.
|
||||
// This is to keep all the internal classes compatible with each other,
|
||||
// and minimize resources spent on the type conversion.
|
||||
// Project-wide, strings are either passed around as reference (which has
|
||||
// zero maintenance costs, and sizeof(void*) memory usage => passed around in a
|
||||
// CPU register), and whenever lifetime etension is required, it must be copied
|
||||
// to std::string instance (which will again propagate as a const reference
|
||||
// within it's lifetime domain).
|
||||
|
||||
// Manages the communication channels to all the remote endpoints with which we
|
||||
// are interacting.
|
||||
class EndpointChannelManager final {
|
||||
public:
|
||||
~EndpointChannelManager();
|
||||
|
||||
// Registers the initial EndpointChannel to be associated with an endpoint;
|
||||
// if there already exists a previously-associated EndpointChannel, that will
|
||||
// be closed before continuing the registration.
|
||||
void RegisterChannelForEndpoint(ClientProxy* client,
|
||||
const std::string& endpoint_id,
|
||||
std::unique_ptr<EndpointChannel> channel)
|
||||
ABSL_LOCKS_EXCLUDED(mutex_);
|
||||
|
||||
// Replaces the EndpointChannel to be associated with an endpoint from here on
|
||||
// in, transferring the encryption context from the previous EndpointChannel
|
||||
// to the newly-provided EndpointChannel.
|
||||
void ReplaceChannelForEndpoint(ClientProxy* client,
|
||||
const std::string& endpoint_id,
|
||||
std::unique_ptr<EndpointChannel> channel)
|
||||
ABSL_LOCKS_EXCLUDED(mutex_);
|
||||
|
||||
bool EncryptChannelForEndpoint(const std::string& endpoint_id,
|
||||
std::unique_ptr<EncryptionContext> context)
|
||||
ABSL_LOCKS_EXCLUDED(mutex_);
|
||||
|
||||
// NOTE(shared_ptr<> usage):
|
||||
//
|
||||
// EndpointChannelManager is holding an EndpointChannel instance;
|
||||
// GetChannelForEndpoint() is passing ownership over to a worker thread.
|
||||
// It is not a pointer passing but an ownership passing, to guarantee that
|
||||
// channel instance will not disappear underneath the feet of a worker thread
|
||||
// inside EndpointManager [ EndpointManager::EndpointChannelLoopRunnable() ].
|
||||
// If it is just a pointer, Channel will get destroyed while in use by a
|
||||
// worker thread. shared_ptr is a simple and reliable tool to avoid that.
|
||||
//
|
||||
// The reason why it can not be std::unique_ptr<> is: there are other code
|
||||
// paths that expect to be able to read the pointer value multiple times, from
|
||||
// multiple places (each of them needs "ownership" for the duration of their
|
||||
// use). EndpointManager::SendTransferFrameBytes() is another such place.
|
||||
// If EndpointChannelManager replaces the current channel, and any (or both)
|
||||
// EndpointManager methods that use a channel are running, it is better to
|
||||
// have a shared ownership.
|
||||
std::shared_ptr<EndpointChannel> GetChannelForEndpoint(
|
||||
const std::string& endpoint_id) ABSL_LOCKS_EXCLUDED(mutex_);
|
||||
|
||||
// Returns true if 'endpoint_id' actually had a registered EndpointChannel.
|
||||
// IOW, a return of false signifies a no-op.
|
||||
bool UnregisterChannelForEndpoint(const std::string& endpoint_id)
|
||||
ABSL_LOCKS_EXCLUDED(mutex_);
|
||||
|
||||
private:
|
||||
// Tracks channel state for all endpoints. This includes what EndpointChannel
|
||||
// the endpoint is currently using and whether or not the EndpointChannel has
|
||||
// been encrypted yet.
|
||||
class ChannelState {
|
||||
public:
|
||||
struct EndpointData {
|
||||
EndpointData() = default;
|
||||
EndpointData(EndpointData&&) = default;
|
||||
EndpointData& operator=(EndpointData&&) = default;
|
||||
~EndpointData() {
|
||||
if (channel != nullptr) {
|
||||
channel->Close(disconnect_reason);
|
||||
}
|
||||
}
|
||||
|
||||
// True if we have a 'context' for the endpoint.
|
||||
bool IsEncrypted() const { return context != nullptr; }
|
||||
|
||||
std::shared_ptr<EndpointChannel> channel;
|
||||
std::unique_ptr<EncryptionContext> context;
|
||||
proto::connections::DisconnectionReason disconnect_reason =
|
||||
proto::connections::DisconnectionReason::UNKNOWN_DISCONNECTION_REASON;
|
||||
};
|
||||
|
||||
ChannelState() = default;
|
||||
~ChannelState() { DestroyAll(); }
|
||||
ChannelState(ChannelState&&) = default;
|
||||
ChannelState& operator=(ChannelState&&) = default;
|
||||
|
||||
// Provides a way to destroy contents of a container, while holding a lock.
|
||||
void DestroyAll() { endpoints_.clear(); }
|
||||
// Return pointer to endpoint data, or nullptr, it not found.
|
||||
EndpointData* LookupEndpointData(const std::string& endpoint_id);
|
||||
|
||||
// Stores a new EndpointChannel for the endpoint.
|
||||
// Prevoius one is destroyed, if it existed.
|
||||
void UpdateChannelForEndpoint(const std::string& endpoint_id,
|
||||
std::unique_ptr<EndpointChannel> channel);
|
||||
|
||||
// Stores a new EncryptionContext for the endpoint.
|
||||
// Prevoius one is destroyed, if it existed.
|
||||
void UpdateEncryptionContextForEndpoint(
|
||||
const std::string& endpoint_id,
|
||||
std::unique_ptr<EncryptionContext> context);
|
||||
|
||||
// Removes all knowledge of this endpoint, cleaning up as necessary.
|
||||
// Returns false if the endpoint was not found.
|
||||
bool RemoveEndpoint(const std::string& endpoint_id,
|
||||
proto::connections::DisconnectionReason reason);
|
||||
|
||||
bool EncryptChannel(EndpointData* endpoint);
|
||||
|
||||
private:
|
||||
// Endpoint ID -> EndpointData. Contains everything we know about the
|
||||
// endpoint.
|
||||
absl::flat_hash_map<std::string, EndpointData> endpoints_;
|
||||
};
|
||||
|
||||
void SetActiveEndpointChannel(ClientProxy* client,
|
||||
const std::string& endpoint_id,
|
||||
std::unique_ptr<EndpointChannel> channel)
|
||||
ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
|
||||
|
||||
Mutex mutex_;
|
||||
ChannelState channel_state_ ABSL_GUARDED_BY(mutex_);
|
||||
};
|
||||
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
|
||||
#endif // CORE_V2_INTERNAL_ENDPOINT_CHANNEL_MANAGER_H_
|
||||
@@ -0,0 +1,17 @@
|
||||
#include "core_v2/internal/endpoint_channel_manager.h"
|
||||
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
|
||||
TEST(EndpointChannelManagerTest, ConstructorDestructorWorks) {
|
||||
EndpointChannelManager mgr;
|
||||
SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,477 @@
|
||||
#include "core_v2/internal/endpoint_manager.h"
|
||||
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#include "core_v2/internal/endpoint_channel.h"
|
||||
#include "core_v2/internal/offline_frames.h"
|
||||
#include "platform_v2/base/exception.h"
|
||||
#include "platform_v2/public/count_down_latch.h"
|
||||
#include "platform_v2/public/logging.h"
|
||||
#include "proto/connections_enums.pb.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
|
||||
using ::location::nearby::proto::connections::Medium;
|
||||
|
||||
// A Runnable that continuously grabs the most recent EndpointChannel available
|
||||
// for an endpoint.
|
||||
//
|
||||
// handler - Called whenever an EndpointChannel is available for endpointId.
|
||||
// Implementations are expected to read/write freely to the
|
||||
// EndpointChannel until an Exception::IO is thrown. Once an
|
||||
// Exception::IO occurs, a check will be performed to see if another
|
||||
// EndpointChannel is available for the given endpoint and, if so,
|
||||
// handler(EndpointChannel) will be called again. Return false to exit
|
||||
// the loop.
|
||||
void EndpointManager::EndpointChannelLoopRunnable(
|
||||
const std::string& runnable_name, ClientProxy* client,
|
||||
const std::string& endpoint_id, CountDownLatch* barrier,
|
||||
std::function<ExceptionOr<bool>(EndpointChannel*)> handler) {
|
||||
// EndpointChannelManager will not let multiple channels exist simultaneously
|
||||
// for the same endpoint_id; it will be closing "old" channels as new ones
|
||||
// come. (There will be a short overlap).
|
||||
// Closed channel will return Exception::kIo for any Read, and loop (below)
|
||||
// will retry and attempt to pick another channel.
|
||||
// If channel is deleted (no mapping), or it is still the same channel
|
||||
// (same Medium) on which we got the Exception::kIo, we terminate the loop.
|
||||
Medium last_failed_medium = Medium::UNKNOWN_MEDIUM;
|
||||
while (true) {
|
||||
// It's important to keep re-fetching the EndpointChannel for an endpoint
|
||||
// because it can be changed out from under us (for example, when we
|
||||
// upgrade from Bluetooth to Wifi).
|
||||
std::shared_ptr<EndpointChannel> channel =
|
||||
channel_manager_->GetChannelForEndpoint(endpoint_id);
|
||||
if (channel == nullptr) {
|
||||
// TODO(tracyzhou): Add logging.
|
||||
break;
|
||||
}
|
||||
|
||||
// If we're looping back around after a failure, and there's not a new
|
||||
// EndpointChannel for this endpoint, there's nothing more to do here.
|
||||
if ((last_failed_medium != Medium::UNKNOWN_MEDIUM) &&
|
||||
(channel->GetMedium() == last_failed_medium)) {
|
||||
// TODO(tracyzhou): Add logging.
|
||||
break;
|
||||
}
|
||||
|
||||
ExceptionOr<bool> keep_using_channel = handler(channel.get());
|
||||
|
||||
if (!keep_using_channel.ok()) {
|
||||
Exception exception = keep_using_channel.GetException();
|
||||
if (exception.Raised(Exception::kIo)) {
|
||||
last_failed_medium = channel->GetMedium();
|
||||
// TODO(tracyzhou): Add logging.
|
||||
continue;
|
||||
}
|
||||
if (exception.Raised(Exception::kInterrupted)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!keep_using_channel.result()) {
|
||||
// TODO(tracyzhou): Add logging.
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Indicate we're out of the loop and it is ok to schedule another instance
|
||||
// if needed.
|
||||
NEARBY_LOG(INFO, "Worker going down; name=%s; id=%s", runnable_name.c_str(),
|
||||
endpoint_id.c_str());
|
||||
barrier->CountDown();
|
||||
|
||||
// Always clear out all state related to this endpoint before terminating
|
||||
// this thread.
|
||||
DiscardEndpoint(client, endpoint_id);
|
||||
NEARBY_LOG(INFO, "Worker done; name=%s; id=%s", runnable_name.c_str(),
|
||||
endpoint_id.c_str());
|
||||
}
|
||||
|
||||
ExceptionOr<bool> EndpointManager::HandleData(
|
||||
const std::string& endpoint_id, ClientProxy* client,
|
||||
EndpointChannel* endpoint_channel) {
|
||||
// Read as much as we can from the healthy EndpointChannel - when it is no
|
||||
// longer in good shape (i.e. our read from it throws an Exception), our
|
||||
// super class will loop back around and try our luck in case there's been
|
||||
// a replacement for this endpoint since we last checked with the
|
||||
// EndpointChannelManager.
|
||||
while (true) {
|
||||
ExceptionOr<ByteArray> bytes = endpoint_channel->Read();
|
||||
if (!bytes.ok()) {
|
||||
NEARBY_LOG(INFO, "Stop reading on read-time exception: %d",
|
||||
bytes.exception());
|
||||
return ExceptionOr<bool>(bytes.exception());
|
||||
}
|
||||
ExceptionOr<OfflineFrame> wrapped_frame = parser::FromBytes(bytes.result());
|
||||
if (!wrapped_frame.ok()) {
|
||||
if (wrapped_frame.GetException().Raised(
|
||||
Exception::kInvalidProtocolBuffer)) {
|
||||
NEARBY_LOG(INFO, "failed to decode; endpoint=%s; channel=%s; skip",
|
||||
endpoint_id.c_str(), endpoint_channel->GetType().c_str());
|
||||
continue;
|
||||
} else {
|
||||
NEARBY_LOG(INFO, "Stop reading on parse-time exception: %d",
|
||||
wrapped_frame.exception());
|
||||
return ExceptionOr<bool>(wrapped_frame.exception());
|
||||
}
|
||||
}
|
||||
OfflineFrame& frame = wrapped_frame.result();
|
||||
|
||||
// Route the incoming offlineFrame to its registered processor.
|
||||
V1Frame::FrameType frame_type = parser::GetFrameType(frame);
|
||||
EndpointManager::FrameProcessor* frame_processor =
|
||||
GetFrameProcessor(frame_type);
|
||||
if (frame_processor == nullptr) {
|
||||
NEARBY_LOG(ERROR, "Unhandled message: type=%d", frame_type);
|
||||
continue;
|
||||
}
|
||||
|
||||
frame_processor->OnIncomingFrame(frame, endpoint_id, client,
|
||||
endpoint_channel->GetMedium());
|
||||
}
|
||||
}
|
||||
|
||||
ExceptionOr<bool> EndpointManager::HandleKeepAlive(
|
||||
EndpointChannel* endpoint_channel) {
|
||||
// Check if it has been too long since we received a frame from our
|
||||
// endpoint.
|
||||
if ((endpoint_channel->GetLastReadTimestamp() != kInvalidTimestamp) &&
|
||||
((endpoint_channel->GetLastReadTimestamp() +
|
||||
EndpointManager::kKeepAliveReadTimeout) <
|
||||
SystemClock::ElapsedRealtime())) {
|
||||
// TODO(tracyzhou): Add logging.
|
||||
return ExceptionOr<bool>(false);
|
||||
}
|
||||
|
||||
// Attempt to send the KeepAlive frame over the endpoint channel - if the
|
||||
// write fails, our super class will loop back around and try our luck again
|
||||
// in case there's been a replacement for this endpoint.
|
||||
Exception write_exception = endpoint_channel->Write(parser::ForKeepAlive());
|
||||
if (!write_exception.Ok()) {
|
||||
return ExceptionOr<bool>(write_exception);
|
||||
}
|
||||
|
||||
// We sleep as the very last step because we want to minimize the caching of
|
||||
// the EndpointChannel. If we do hold on to the EndpointChannel, and it's
|
||||
// switched out from under us in BandwidthUpgradeManager, our write will
|
||||
// trigger an erroneous write to the encryption context that will cascade
|
||||
// into all our remote endpoint's future reads failing.
|
||||
Exception sleep_exception =
|
||||
SystemClock::Sleep(EndpointManager::kKeepAliveWriteInterval);
|
||||
if (!sleep_exception.Ok()) {
|
||||
return ExceptionOr<bool>(sleep_exception);
|
||||
}
|
||||
|
||||
return ExceptionOr<bool>(true);
|
||||
}
|
||||
|
||||
bool operator==(const EndpointManager::FrameProcessor& lhs,
|
||||
const EndpointManager::FrameProcessor& rhs) {
|
||||
// We're comparing addresses because these objects are callbacks which need to
|
||||
// be matched by exact instances.
|
||||
return &lhs == &rhs;
|
||||
}
|
||||
|
||||
bool operator<(const EndpointManager::FrameProcessor& lhs,
|
||||
const EndpointManager::FrameProcessor& rhs) {
|
||||
// We're comparing addresses because these objects are callbacks which need to
|
||||
// be matched by exact instances.
|
||||
return &lhs < &rhs;
|
||||
}
|
||||
|
||||
EndpointManager::EndpointManager(EndpointChannelManager* manager)
|
||||
: channel_manager_(manager) {}
|
||||
|
||||
EndpointManager::~EndpointManager() {
|
||||
CountDownLatch latch(1);
|
||||
RunOnEndpointManagerThread([this, &latch]() {
|
||||
NEARBY_LOG(INFO, "Bringing down endpoints");
|
||||
for (auto& item : endpoints_) {
|
||||
const std::string& endpoint_id = item.first;
|
||||
EndpointState& state = item.second;
|
||||
// This will close the channel; all workers will sense that and
|
||||
// terminate.
|
||||
NEARBY_LOG(INFO, "Bringing down endpoint channels: id=%s",
|
||||
endpoint_id.c_str());
|
||||
WaitForEndpointDisconnectionProcessing(state.client, endpoint_id);
|
||||
channel_manager_->UnregisterChannelForEndpoint(endpoint_id);
|
||||
}
|
||||
latch.CountDown();
|
||||
});
|
||||
latch.Await();
|
||||
NEARBY_LOG(INFO, "Bringing down worker threads");
|
||||
|
||||
// Stop all the ongoing Runnables (as gracefully as possible).
|
||||
// Order matters: bring worker pools down first; serial_executor_ thread
|
||||
// should go last, since workers schedule jobs there even during shutdown.
|
||||
handlers_executor_.Shutdown();
|
||||
keep_alive_executor_.Shutdown();
|
||||
NEARBY_LOG(INFO, "Bringing down control thread");
|
||||
serial_executor_.Shutdown();
|
||||
NEARBY_LOG(INFO, "EndpointManager is down");
|
||||
}
|
||||
|
||||
const EndpointManager::FrameProcessor::Handle
|
||||
EndpointManager::RegisterFrameProcessor(
|
||||
V1Frame::FrameType frame_type, EndpointManager::FrameProcessor* processor) {
|
||||
const FrameProcessor::Handle handle = processor;
|
||||
CountDownLatch latch(1);
|
||||
RunOnEndpointManagerThread([this, frame_type, &latch, processor]() {
|
||||
auto it = frame_processors_.find(frame_type);
|
||||
if (it != frame_processors_.end()) {
|
||||
// TODO(tracyzhou): Add logging.
|
||||
it->second = processor;
|
||||
} else {
|
||||
frame_processors_.emplace(frame_type, processor);
|
||||
}
|
||||
latch.CountDown();
|
||||
});
|
||||
latch.Await();
|
||||
return handle;
|
||||
}
|
||||
|
||||
void EndpointManager::UnregisterFrameProcessor(V1Frame::FrameType frame_type,
|
||||
const void* handle) {
|
||||
RunOnEndpointManagerThread([this, frame_type, handle]() {
|
||||
auto it = frame_processors_.find(frame_type);
|
||||
if (it == frame_processors_.end()) return;
|
||||
if (it->second != handle) {
|
||||
NEARBY_LOG(INFO,
|
||||
"Failed to unregister: type=%d; handle mismatch: passed=%p, "
|
||||
"expected=%p",
|
||||
frame_type, handle, it->second);
|
||||
return;
|
||||
}
|
||||
|
||||
frame_processors_.erase(it);
|
||||
NEARBY_LOG(INFO, "unregistered: type=%d", frame_type);
|
||||
});
|
||||
}
|
||||
|
||||
EndpointManager::FrameProcessor* EndpointManager::GetFrameProcessor(
|
||||
V1Frame::FrameType frame_type) {
|
||||
EndpointManager::FrameProcessor* processor = nullptr;
|
||||
CountDownLatch latch(1);
|
||||
RunOnEndpointManagerThread([this, frame_type, &processor, &latch]() {
|
||||
auto it = frame_processors_.find(frame_type);
|
||||
if (it != frame_processors_.end()) {
|
||||
processor = it->second;
|
||||
}
|
||||
latch.CountDown();
|
||||
});
|
||||
latch.Await();
|
||||
return processor;
|
||||
}
|
||||
|
||||
void EndpointManager::EnsureWorkersTerminated(const std::string& endpoint_id) {
|
||||
auto item = endpoints_.find(endpoint_id);
|
||||
if (item != endpoints_.end()) {
|
||||
// If another instance of data and keep-alive handlers is running, it will
|
||||
// terminate soon; we should block until it happens.
|
||||
EndpointState& endpoint_state = item->second;
|
||||
NEARBY_LOG(INFO, "Waiting for workers to terminate for endpoint_id='%s'",
|
||||
endpoint_id.c_str());
|
||||
endpoint_state.barrier.Await();
|
||||
endpoints_.erase(item);
|
||||
}
|
||||
}
|
||||
|
||||
void EndpointManager::RegisterEndpoint(ClientProxy* client,
|
||||
const std::string& endpoint_id,
|
||||
const ConnectionResponseInfo& info,
|
||||
std::unique_ptr<EndpointChannel> channel,
|
||||
const ConnectionListener& listener) {
|
||||
CountDownLatch latch(1);
|
||||
|
||||
// NOTE (unique_ptr<> capture):
|
||||
// std::unique_ptr<> is not copyable, so we can not pass it to
|
||||
// lambda capture, because lambda eventually is converted to std::function<>.
|
||||
// Instead, we release() a pointer, and pass a raw pointer, which is copyalbe.
|
||||
// We ignore the risk of job not scheduled (and an associated risk of memory
|
||||
// leak), because this may only happen during service shutdown.
|
||||
RunOnEndpointManagerThread([this, client, channel = channel.release(),
|
||||
&endpoint_id, &info, &listener, &latch]() {
|
||||
// Pass ownership of channel to EndpointChannelManager
|
||||
NEARBY_LOG(INFO, "Registering endpoint with channel manager: id=%s",
|
||||
endpoint_id.c_str());
|
||||
channel_manager_->RegisterChannelForEndpoint(
|
||||
client, endpoint_id, std::unique_ptr<EndpointChannel>(channel));
|
||||
|
||||
EnsureWorkersTerminated(endpoint_id);
|
||||
EndpointState& endpoint_state =
|
||||
endpoints_.emplace(endpoint_id, EndpointState()).first->second;
|
||||
endpoint_state.client = client;
|
||||
|
||||
NEARBY_LOG(INFO, "Starting workers: id=%s", endpoint_id.c_str());
|
||||
// For every endpoint, there's normally only one Read handler instance
|
||||
// running on the handlers_executor_ pool. This instance reads data from the
|
||||
// endpoint and delegates incoming frames to various FrameProcessors.
|
||||
// Once the frame has been properly handled, it starts reading again for
|
||||
// the next frame. If the handler fails its read and no other
|
||||
// EndpointChannels are available for this endpoint, a disconnection
|
||||
// will be initiated.
|
||||
StartEndpointReader(
|
||||
[this, client, endpoint_id, barrier = &endpoint_state.barrier]() {
|
||||
EndpointChannelLoopRunnable(
|
||||
"Read", client, endpoint_id, barrier,
|
||||
[this, client, endpoint_id](EndpointChannel* channel) {
|
||||
return HandleData(endpoint_id, client, channel);
|
||||
});
|
||||
});
|
||||
|
||||
// For every endpoint, there's only one KeepAliveManager instance
|
||||
// running on the keep_alive_executor_ pool. This instance will
|
||||
// periodically send out a ping* to the endpoint while listening for an
|
||||
// incoming pong**. If it fails to send the ping, or if no pong is heard
|
||||
// within kKeepAliveReadTimeoutMillis milliseconds, it initiates a
|
||||
// disconnection.
|
||||
//
|
||||
// (*) Bluetooth requires a constant outgoing stream of messages. If
|
||||
// there's silence, Android will break the socket. This is why we ping.
|
||||
// (**) Wifi Hotspots can fail to notice a connection has been lost, and
|
||||
// they will happily keep writing to /dev/null. This is why we listen
|
||||
// for the pong.
|
||||
StartEndpointKeepAliveManager([this, client, endpoint_id,
|
||||
barrier = &endpoint_state.barrier]() {
|
||||
EndpointChannelLoopRunnable("KeepAliveManager", client, endpoint_id,
|
||||
barrier, [this](EndpointChannel* channel) {
|
||||
return HandleKeepAlive(channel);
|
||||
});
|
||||
});
|
||||
// TODO(tracyzhou): Add logging.
|
||||
|
||||
// It's now time to let the client know of this new connection so that
|
||||
// they can accept or reject it.
|
||||
client->OnConnectionInitiated(endpoint_id, info, listener);
|
||||
latch.CountDown();
|
||||
});
|
||||
latch.Await();
|
||||
}
|
||||
|
||||
void EndpointManager::UnregisterEndpoint(ClientProxy* client,
|
||||
const std::string& endpoint_id) {
|
||||
CountDownLatch latch(1);
|
||||
RunOnEndpointManagerThread([this, client, endpoint_id, &latch]() {
|
||||
channel_manager_->UnregisterChannelForEndpoint(endpoint_id);
|
||||
RemoveEndpoint(client, endpoint_id, /*notify=*/false);
|
||||
latch.CountDown();
|
||||
});
|
||||
latch.Await();
|
||||
}
|
||||
|
||||
// Designed to run asynchronously. It is called from IO thread pools, and
|
||||
// jobs in these pools may be waited for from the EndpointManager thread. If we
|
||||
// allow synchronous behavior here it will cause a live lock.
|
||||
void EndpointManager::DiscardEndpoint(ClientProxy* client,
|
||||
const std::string& endpoint_id) {
|
||||
RunOnEndpointManagerThread([this, client, endpoint_id]() {
|
||||
channel_manager_->UnregisterChannelForEndpoint(endpoint_id);
|
||||
RemoveEndpoint(client, endpoint_id,
|
||||
/*notify=*/
|
||||
client->IsConnectedToEndpoint(endpoint_id));
|
||||
});
|
||||
}
|
||||
|
||||
std::vector<std::string> EndpointManager::SendPayloadChunk(
|
||||
const PayloadTransferFrame::PayloadHeader& payload_header,
|
||||
const PayloadTransferFrame::PayloadChunk& payload_chunk,
|
||||
const std::vector<std::string>& endpoint_ids) {
|
||||
ByteArray bytes =
|
||||
parser::ForDataPayloadTransfer(payload_header, payload_chunk);
|
||||
|
||||
return SendTransferFrameBytes(endpoint_ids, bytes, payload_header.id(),
|
||||
/*offset=*/payload_chunk.offset(),
|
||||
/*packet_type=*/"DATA");
|
||||
}
|
||||
|
||||
std::vector<std::string> EndpointManager::SendControlMessage(
|
||||
const PayloadTransferFrame::PayloadHeader& header,
|
||||
const PayloadTransferFrame::ControlMessage& control,
|
||||
const std::vector<std::string>& endpoint_ids) {
|
||||
ByteArray bytes = parser::ForControlPayloadTransfer(header, control);
|
||||
|
||||
return SendTransferFrameBytes(endpoint_ids, bytes, header.id(),
|
||||
/*offset=*/control.offset(),
|
||||
/*packet_type=*/"CONTROL");
|
||||
}
|
||||
|
||||
// @EndpointManagerThread
|
||||
void EndpointManager::RemoveEndpoint(ClientProxy* client,
|
||||
const std::string& endpoint_id,
|
||||
bool notify) {
|
||||
// Unregistering from channel_manager_ will also serve to terminate
|
||||
// the dedicated handler and KeepAlive threads we started when we registered
|
||||
// this endpoint.
|
||||
if (channel_manager_->UnregisterChannelForEndpoint(endpoint_id)) {
|
||||
// Notify all frame processors of the disconnection immediately and wait
|
||||
// for them to clean up state. Only once all processors are done cleaning
|
||||
// up, we can remove the endpoint from ClientProxy after which there
|
||||
// should be no further interactions with the endpoint.
|
||||
// (See b/37352254 for history)
|
||||
WaitForEndpointDisconnectionProcessing(client, endpoint_id);
|
||||
EnsureWorkersTerminated(endpoint_id);
|
||||
|
||||
client->OnDisconnected(endpoint_id, notify);
|
||||
// TODO(tracyzhou): Add logging.
|
||||
}
|
||||
}
|
||||
|
||||
// @EndpointManagerThread
|
||||
void EndpointManager::WaitForEndpointDisconnectionProcessing(
|
||||
ClientProxy* client, const std::string& endpoint_id) {
|
||||
CountDownLatch barrier(frame_processors_.size());
|
||||
|
||||
for (auto& item : frame_processors_) {
|
||||
auto& processor = item.second;
|
||||
processor->OnEndpointDisconnect(client, endpoint_id, &barrier);
|
||||
}
|
||||
|
||||
barrier.Await(kProcessEndpointDisconnectionTimeout);
|
||||
}
|
||||
|
||||
std::vector<std::string> EndpointManager::SendTransferFrameBytes(
|
||||
const std::vector<std::string>& endpoint_ids, const ByteArray& bytes,
|
||||
std::int64_t payload_id, std::int64_t offset,
|
||||
const std::string& packet_type) {
|
||||
std::vector<std::string> failed_endpoint_ids;
|
||||
for (const std::string& endpoint_id : endpoint_ids) {
|
||||
std::shared_ptr<EndpointChannel> channel =
|
||||
channel_manager_->GetChannelForEndpoint(endpoint_id);
|
||||
|
||||
if (channel == nullptr) {
|
||||
// We no longer know about this endpoint (it was either explicitly
|
||||
// unregistered, or a read/write error made us unregister it internally).
|
||||
NEARBY_LOG(INFO, "Channel not available; id=%s", endpoint_id.c_str());
|
||||
failed_endpoint_ids.push_back(endpoint_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
Exception write_exception = channel->Write(bytes);
|
||||
if (!write_exception.Ok()) {
|
||||
failed_endpoint_ids.push_back(endpoint_id);
|
||||
NEARBY_LOG(INFO, "Failed to send packet; endpoint_id=%s",
|
||||
endpoint_id.c_str());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return failed_endpoint_ids;
|
||||
}
|
||||
|
||||
void EndpointManager::StartEndpointReader(Runnable runnable) {
|
||||
handlers_executor_.Execute(std::move(runnable));
|
||||
}
|
||||
|
||||
void EndpointManager::StartEndpointKeepAliveManager(Runnable runnable) {
|
||||
keep_alive_executor_.Execute(std::move(runnable));
|
||||
}
|
||||
|
||||
void EndpointManager::RunOnEndpointManagerThread(Runnable runnable) {
|
||||
serial_executor_.Execute(std::move(runnable));
|
||||
}
|
||||
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,218 @@
|
||||
#ifndef CORE_V2_INTERNAL_ENDPOINT_MANAGER_H_
|
||||
#define CORE_V2_INTERNAL_ENDPOINT_MANAGER_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
#include "core_v2/internal/client_proxy.h"
|
||||
#include "core_v2/internal/endpoint_channel.h"
|
||||
#include "core_v2/internal/endpoint_channel_manager.h"
|
||||
#include "core_v2/listeners.h"
|
||||
#include "proto/connections/offline_wire_formats.pb.h"
|
||||
#include "platform_v2/base/byte_array.h"
|
||||
#include "platform_v2/base/runnable.h"
|
||||
#include "platform_v2/public/count_down_latch.h"
|
||||
#include "platform_v2/public/multi_thread_executor.h"
|
||||
#include "platform_v2/public/single_thread_executor.h"
|
||||
#include "platform_v2/public/system_clock.h"
|
||||
#include "proto/connections_enums.pb.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
#include "absl/time/time.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
|
||||
// Manages all operations related to the remote endpoints with which we are
|
||||
// interacting.
|
||||
//
|
||||
// All processing of incoming and outgoing payloads is spread across this and
|
||||
// the PayloadManager as described below.
|
||||
//
|
||||
// The sending of outgoing payloads originates in
|
||||
// PayloadManager::SendPayload() before control is transferred over to
|
||||
// EndpointManager::SendPayloadChunk(). This work happens on one of three
|
||||
// dedicated writer threads belonging to the PayloadManager. The writer thread
|
||||
// that is used depends on the Payload::Type.
|
||||
//
|
||||
// The EndpointManager has one dedicated reader thread for each registered
|
||||
// endpoint, and the receiving of every incoming payload (and its subsequent
|
||||
// chunks) originates on one of those threads before control is transferred over
|
||||
// to PayloadManager::ProcessFrame() (still running on that
|
||||
// same dedicated reader thread).
|
||||
|
||||
class EndpointManager {
|
||||
public:
|
||||
class FrameProcessor {
|
||||
public:
|
||||
using Handle = void*;
|
||||
|
||||
virtual ~FrameProcessor() = default;
|
||||
|
||||
// @EndpointManagerReaderThread
|
||||
virtual void OnIncomingFrame(const OfflineFrame& offline_frame,
|
||||
const std::string& from_endpoint_id,
|
||||
ClientProxy* to_client,
|
||||
proto::connections::Medium current_medium) = 0;
|
||||
|
||||
// Implementations must call barrier.CountDown() once
|
||||
// they're done. This parallelizes the disconnection event across all frame
|
||||
// processors.
|
||||
//
|
||||
// @EndpointManagerThread
|
||||
virtual void OnEndpointDisconnect(ClientProxy* client,
|
||||
const std::string& endpoint_id,
|
||||
CountDownLatch* barrier) = 0;
|
||||
};
|
||||
|
||||
explicit EndpointManager(EndpointChannelManager* manager);
|
||||
~EndpointManager();
|
||||
|
||||
// Invoked from the constructors of the various *Manager components that make
|
||||
// up the OfflineServiceController implementation.
|
||||
// FrameProcessor* instances are of dynamic duration and survive all sessions.
|
||||
// returns unique handle to be used for unregistering.
|
||||
// Blocks until registration is complete.
|
||||
const FrameProcessor::Handle RegisterFrameProcessor(
|
||||
V1Frame::FrameType frame_type, FrameProcessor* processor);
|
||||
void UnregisterFrameProcessor(V1Frame::FrameType frame_type,
|
||||
const void* handle);
|
||||
|
||||
// Invoked from the different PcpHandler implementations (of which there can
|
||||
// be only one at a time).
|
||||
// Blocks until registration is complete.
|
||||
void RegisterEndpoint(ClientProxy* client, const std::string& endpoint_id,
|
||||
const ConnectionResponseInfo& info,
|
||||
std::unique_ptr<EndpointChannel> channel,
|
||||
const ConnectionListener& listener);
|
||||
// Called when a client explicitly asks to disconnect from this endpoint. In
|
||||
// this case, we do not notify the client of onDisconnected().
|
||||
void UnregisterEndpoint(ClientProxy* client, const std::string& endpoint_id);
|
||||
|
||||
// Returns the list of endpoints to which sending this chunk failed.
|
||||
//
|
||||
// Invoked from the PayloadManager's sendPayload() method.
|
||||
std::vector<std::string> SendPayloadChunk(
|
||||
const PayloadTransferFrame::PayloadHeader& payload_header,
|
||||
const PayloadTransferFrame::PayloadChunk& payload_chunk,
|
||||
const std::vector<std::string>& endpoint_ids);
|
||||
std::vector<std::string> SendControlMessage(
|
||||
const PayloadTransferFrame::PayloadHeader& payload_header,
|
||||
const PayloadTransferFrame::ControlMessage& control_message,
|
||||
const std::vector<std::string>& endpoint_ids);
|
||||
|
||||
// Called when we internally want to get rid of the endpoint, without the
|
||||
// client directly telling us to. For example...
|
||||
// a) We failed to read from the endpoint in its dedicated reader thread.
|
||||
// b) We failed to write to the endpoint in PayloadManager.
|
||||
// c) The connection was rejected in PCPHandler.
|
||||
// d) The dedicated KeepAlive thread exceeded its period of inactivity.
|
||||
// Or in the numerous other cases where a failure occurred and we no longer
|
||||
// believe the endpoint is in a healthy state.
|
||||
//
|
||||
// Note: This must not block. Otherwise we can get into a deadlock where we
|
||||
// ask everyone who's registered an FrameProcessor to
|
||||
// processEndpointDisconnection() while the caller of DiscardEndpoint() is
|
||||
// blocked here.
|
||||
void DiscardEndpoint(ClientProxy* client, const std::string& endpoint_id);
|
||||
|
||||
private:
|
||||
struct EndpointState {
|
||||
// ClientProxy object associated with this endpoint.
|
||||
ClientProxy* client;
|
||||
// Execution barrier, used to ensure that all workers associated with an
|
||||
// endpoint on handlers_executor_ and keep_alive_executor_ are terminated.
|
||||
CountDownLatch barrier{2};
|
||||
};
|
||||
|
||||
FrameProcessor* GetFrameProcessor(V1Frame::FrameType frame_type);
|
||||
|
||||
ExceptionOr<bool> HandleData(const std::string& endpoint_id,
|
||||
ClientProxy* client_proxy,
|
||||
EndpointChannel* endpoint_channel);
|
||||
|
||||
ExceptionOr<bool> HandleKeepAlive(EndpointChannel* endpoint_channel);
|
||||
|
||||
// Waits for a given endpoint EndpointChannelLoopRunnable() workers to
|
||||
// terminate.
|
||||
// Is called from RegisterEndpoint to avoid races; also called from
|
||||
// RemoveEndpoint as part of proper endpoint shutdown sequence.
|
||||
// @EndpointManagerThread
|
||||
void EnsureWorkersTerminated(const std::string& endpoint_id);
|
||||
|
||||
void EndpointChannelLoopRunnable(
|
||||
const std::string& runnable_name, ClientProxy* client_proxy,
|
||||
const std::string& endpoint_id, CountDownLatch* barrier,
|
||||
std::function<ExceptionOr<bool>(EndpointChannel*)> handler);
|
||||
|
||||
static void WaitForLatch(const std::string& method_name,
|
||||
CountDownLatch* latch);
|
||||
static void WaitForLatch(const std::string& method_name,
|
||||
CountDownLatch* latch, std::int32_t timeout_millis);
|
||||
|
||||
static constexpr absl::Duration kKeepAliveWriteInterval =
|
||||
absl::Milliseconds(5000);
|
||||
static constexpr absl::Duration kKeepAliveReadTimeout =
|
||||
absl::Milliseconds(30000);
|
||||
static constexpr absl::Duration kProcessEndpointDisconnectionTimeout =
|
||||
absl::Milliseconds(2000);
|
||||
static constexpr std::int32_t kMaxConcurrentEndpoints = 50;
|
||||
static constexpr absl::Time kInvalidTimestamp = absl::InfinitePast();
|
||||
|
||||
// It should be noted that this method may be called multiple times (because
|
||||
// invoking this method closes the endpoint channel, which causes the
|
||||
// dedicated reader and KeepAlive threads to terminate, which in turn leads to
|
||||
// this method being called), but that's alright because the implementation of
|
||||
// this method is idempotent.
|
||||
// @EndpointManagerThread
|
||||
void RemoveEndpoint(ClientProxy* client, const std::string& endpoint_id,
|
||||
bool notify);
|
||||
|
||||
void WaitForEndpointDisconnectionProcessing(ClientProxy* client,
|
||||
const std::string& endpoint_id);
|
||||
|
||||
std::vector<std::string> SendTransferFrameBytes(
|
||||
const std::vector<std::string>& endpoint_ids,
|
||||
const ByteArray& payload_transfer_frame_bytes, std::int64_t payload_id,
|
||||
std::int64_t offset, const std::string& packet_type);
|
||||
|
||||
// Executes data-handing jobs on a separate thread for each endpoint, on a
|
||||
// handlers_executor_.
|
||||
// If amount of concurrent connections is less the pool capacity, it is
|
||||
// possible that while a channel is being replaced, two jobs are trying to
|
||||
// run for the same endpoint (for a short time).
|
||||
// TODO (apolyudov): do not let extra job start.
|
||||
void StartEndpointReader(Runnable runnable);
|
||||
|
||||
// Executes keep-alive jobs on a separate thread for each endpoint on a
|
||||
// keep_alive_executor_.
|
||||
void StartEndpointKeepAliveManager(Runnable runnable);
|
||||
|
||||
// Executes all jobs sequentially, on a serial_executor_.
|
||||
void RunOnEndpointManagerThread(Runnable runnable);
|
||||
|
||||
EndpointChannelManager* channel_manager_;
|
||||
|
||||
absl::flat_hash_map<V1Frame::FrameType, FrameProcessor*>
|
||||
frame_processors_;
|
||||
|
||||
// We keep track of all registered channel endpoints here.
|
||||
absl::flat_hash_map<std::string, EndpointState> endpoints_;
|
||||
|
||||
MultiThreadExecutor keep_alive_executor_{kMaxConcurrentEndpoints};
|
||||
MultiThreadExecutor handlers_executor_{kMaxConcurrentEndpoints};
|
||||
SingleThreadExecutor serial_executor_;
|
||||
};
|
||||
|
||||
// Operator overloads when comparing FrameProcessor*.
|
||||
bool operator==(const EndpointManager::FrameProcessor& lhs,
|
||||
const EndpointManager::FrameProcessor& rhs);
|
||||
bool operator<(const EndpointManager::FrameProcessor& lhs,
|
||||
const EndpointManager::FrameProcessor& rhs);
|
||||
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
|
||||
#endif // CORE_V2_INTERNAL_ENDPOINT_MANAGER_H_
|
||||
@@ -0,0 +1,242 @@
|
||||
#include "core_v2/internal/endpoint_manager.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
|
||||
#include "core_v2/internal/client_proxy.h"
|
||||
#include "core_v2/internal/endpoint_channel_manager.h"
|
||||
#include "core_v2/internal/offline_frames.h"
|
||||
#include "platform_v2/base/byte_array.h"
|
||||
#include "platform_v2/base/exception.h"
|
||||
#include "platform_v2/public/count_down_latch.h"
|
||||
#include "platform_v2/public/logging.h"
|
||||
#include "platform_v2/public/pipe.h"
|
||||
#include "proto/connections_enums.pb.h"
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "absl/synchronization/mutex.h"
|
||||
#include "absl/time/clock.h"
|
||||
#include "absl/time/time.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace {
|
||||
|
||||
using ::location::nearby::proto::connections::DisconnectionReason;
|
||||
using ::location::nearby::proto::connections::Medium;
|
||||
using ::securegcm::D2DConnectionContextV1;
|
||||
using ::testing::_;
|
||||
using ::testing::MockFunction;
|
||||
using ::testing::Return;
|
||||
using ::testing::StrictMock;
|
||||
|
||||
class MockEndpointChannel : public EndpointChannel {
|
||||
public:
|
||||
MOCK_METHOD(ExceptionOr<ByteArray>, Read, (), (override));
|
||||
MOCK_METHOD(Exception, Write, (const ByteArray& data), (override));
|
||||
MOCK_METHOD(void, Close, (), (override));
|
||||
MOCK_METHOD(void, Close, (DisconnectionReason reason), (override));
|
||||
MOCK_METHOD(std::string, GetType, (), (const override));
|
||||
MOCK_METHOD(std::string, GetName, (), (const override));
|
||||
MOCK_METHOD(Medium, GetMedium, (), (const override));
|
||||
MOCK_METHOD(void, EnableEncryption,
|
||||
(D2DConnectionContextV1 * connection_context),
|
||||
(override));
|
||||
MOCK_METHOD(bool, IsPaused, (), (const override));
|
||||
MOCK_METHOD(void, Pause, (), (override));
|
||||
MOCK_METHOD(void, Resume, (), (override));
|
||||
MOCK_METHOD(absl::Time, GetLastReadTimestamp, (), (const override));
|
||||
|
||||
bool IsClosed() const {
|
||||
absl::MutexLock lock(&mutex_);
|
||||
return closed_;
|
||||
}
|
||||
void DoClose() {
|
||||
absl::MutexLock lock(&mutex_);
|
||||
closed_ = true;
|
||||
}
|
||||
|
||||
private:
|
||||
mutable absl::Mutex mutex_;
|
||||
bool closed_ = false;
|
||||
};
|
||||
|
||||
class MockFrameProcessor : public EndpointManager::FrameProcessor {
|
||||
public:
|
||||
MOCK_METHOD(void, OnIncomingFrame,
|
||||
(const OfflineFrame& offline_frame,
|
||||
const std::string& from_endpoint_id, ClientProxy* to_client,
|
||||
Medium current_medium),
|
||||
(override));
|
||||
|
||||
MOCK_METHOD(void, OnEndpointDisconnect,
|
||||
(ClientProxy * client, const std::string& endpoint_id,
|
||||
CountDownLatch* barrier),
|
||||
(override));
|
||||
};
|
||||
|
||||
class EndpointManagerTest : public ::testing::Test {
|
||||
protected:
|
||||
void RegisterEndpoint(std::unique_ptr<MockEndpointChannel> channel,
|
||||
bool should_close = true) {
|
||||
CountDownLatch done(1);
|
||||
if (should_close) {
|
||||
ON_CALL(*channel, Close(_))
|
||||
.WillByDefault(
|
||||
[&done](DisconnectionReason reason) { done.CountDown(); });
|
||||
}
|
||||
EXPECT_CALL(*channel, GetMedium()).WillRepeatedly(Return(Medium::BLE));
|
||||
EXPECT_CALL(*channel, GetLastReadTimestamp())
|
||||
.WillRepeatedly(Return(start_time_));
|
||||
EXPECT_CALL(mock_listener_.initiated_cb, Call).Times(1);
|
||||
em_.RegisterEndpoint(&client_, endpoint_id_, info_, std::move(channel),
|
||||
listener_);
|
||||
if (should_close) {
|
||||
EXPECT_TRUE(done.Await(absl::Milliseconds(1000)).result());
|
||||
}
|
||||
}
|
||||
|
||||
ClientProxy client_;
|
||||
std::vector<std::unique_ptr<EndpointManager::FrameProcessor>> processors_;
|
||||
EndpointChannelManager ecm_;
|
||||
EndpointManager em_{&ecm_};
|
||||
std::string endpoint_id_ = "endpoint_id";
|
||||
ConnectionResponseInfo info_ = {
|
||||
.remote_endpoint_name = "name",
|
||||
.authentication_token = "auth_token",
|
||||
.raw_authentication_token = ByteArray("auth_token"),
|
||||
.is_incoming_connection = true,
|
||||
};
|
||||
struct MockConnectionListener {
|
||||
StrictMock<MockFunction<void(const std::string& endpoint_id,
|
||||
const ConnectionResponseInfo& info)>>
|
||||
initiated_cb;
|
||||
StrictMock<MockFunction<void(const std::string& endpoint_id)>> accepted_cb;
|
||||
StrictMock<MockFunction<void(const std::string& endpoint_id,
|
||||
const Status& status)>>
|
||||
rejected_cb;
|
||||
StrictMock<MockFunction<void(const std::string& endpoint_id)>>
|
||||
disconnected_cb;
|
||||
StrictMock<MockFunction<void(const std::string& endpoint_id,
|
||||
std::int32_t quality)>>
|
||||
bandwidth_changed_cb;
|
||||
} mock_listener_;
|
||||
ConnectionListener listener_{
|
||||
.initiated_cb = mock_listener_.initiated_cb.AsStdFunction(),
|
||||
.accepted_cb = mock_listener_.accepted_cb.AsStdFunction(),
|
||||
.rejected_cb = mock_listener_.rejected_cb.AsStdFunction(),
|
||||
.disconnected_cb = mock_listener_.disconnected_cb.AsStdFunction(),
|
||||
.bandwidth_changed_cb =
|
||||
mock_listener_.bandwidth_changed_cb.AsStdFunction(),
|
||||
};
|
||||
absl::Time start_time_{absl::Now()};
|
||||
};
|
||||
|
||||
TEST_F(EndpointManagerTest, ConstructorDestructorWorks) { SUCCEED(); }
|
||||
|
||||
TEST_F(EndpointManagerTest, RegisterEndpointCallsOnConnectionInitiated) {
|
||||
auto endpoint_channel = std::make_unique<MockEndpointChannel>();
|
||||
EXPECT_CALL(*endpoint_channel, Read())
|
||||
.WillRepeatedly(Return(ExceptionOr<ByteArray>(Exception::kIo)));
|
||||
EXPECT_CALL(*endpoint_channel, Close(_)).Times(1);
|
||||
RegisterEndpoint(std::move(endpoint_channel));
|
||||
}
|
||||
|
||||
TEST_F(EndpointManagerTest, UnregisterEndpointCallsOnDisconnected) {
|
||||
auto endpoint_channel = std::make_unique<MockEndpointChannel>();
|
||||
EXPECT_CALL(*endpoint_channel, Read())
|
||||
.WillRepeatedly(Return(ExceptionOr<ByteArray>(Exception::kIo)));
|
||||
RegisterEndpoint(std::make_unique<MockEndpointChannel>());
|
||||
// NOTE: disconnect_cb is not called, because we did not reach fully connected
|
||||
// state. On top of that, UnregisterEndpoint is suppressing this notification.
|
||||
// (IMO, it should be called as long as any connection callback was called
|
||||
// before. (in this case initiated_cb is called)).
|
||||
// Test captures current protocol behavior.
|
||||
em_.UnregisterEndpoint(&client_, endpoint_id_);
|
||||
}
|
||||
|
||||
TEST_F(EndpointManagerTest, RegisterFrameProcessorWorks) {
|
||||
auto endpoint_channel = std::make_unique<MockEndpointChannel>();
|
||||
auto connect_request = std::make_unique<MockFrameProcessor>();
|
||||
auto read_data = parser::ForConnectionRequest("endpoint_id", "endpoint_name",
|
||||
1234, std::vector{Medium::BLE});
|
||||
EXPECT_CALL(*connect_request, OnIncomingFrame);
|
||||
EXPECT_CALL(*connect_request, OnEndpointDisconnect);
|
||||
EXPECT_CALL(*endpoint_channel, Read())
|
||||
.WillOnce(Return(ExceptionOr<ByteArray>(read_data)))
|
||||
.WillRepeatedly(Return(ExceptionOr<ByteArray>(Exception::kIo)));
|
||||
EXPECT_CALL(*endpoint_channel, Write(_))
|
||||
.WillRepeatedly(Return(Exception{Exception::kSuccess}));
|
||||
// Register frame processor, then register endpoint.
|
||||
// Endpoint will read one frame, then fail to read more and terminate.
|
||||
// On disconnection, it will notify frame processor and we verify that.
|
||||
const void* handle = em_.RegisterFrameProcessor(V1Frame::CONNECTION_REQUEST,
|
||||
connect_request.get());
|
||||
processors_.emplace_back(std::move(connect_request));
|
||||
EXPECT_NE(handle, nullptr);
|
||||
RegisterEndpoint(std::move(endpoint_channel));
|
||||
}
|
||||
|
||||
TEST_F(EndpointManagerTest, UnregisterFrameProcessorWorks) {
|
||||
auto endpoint_channel = std::make_unique<MockEndpointChannel>();
|
||||
EXPECT_CALL(*endpoint_channel, Read())
|
||||
.WillRepeatedly(Return(ExceptionOr<ByteArray>(Exception::kIo)));
|
||||
EXPECT_CALL(*endpoint_channel, Write(_))
|
||||
.WillRepeatedly(Return(Exception{Exception::kSuccess}));
|
||||
|
||||
// We should not receive any notifications to frame processor.
|
||||
auto connect_request = std::make_unique<StrictMock<MockFrameProcessor>>();
|
||||
|
||||
// Register frame processor and immediately unregister it.
|
||||
const void* handle = em_.RegisterFrameProcessor(V1Frame::CONNECTION_REQUEST,
|
||||
connect_request.get());
|
||||
processors_.emplace_back(std::move(connect_request));
|
||||
EXPECT_NE(handle, nullptr);
|
||||
em_.UnregisterFrameProcessor(V1Frame::CONNECTION_REQUEST, handle);
|
||||
// Endpoint will not send OnDisconnect notification to frame processor.
|
||||
RegisterEndpoint(std::move(endpoint_channel), false);
|
||||
em_.UnregisterEndpoint(&client_, endpoint_id_);
|
||||
}
|
||||
|
||||
TEST_F(EndpointManagerTest, SendControlMessageWorks) {
|
||||
auto endpoint_channel = std::make_unique<MockEndpointChannel>();
|
||||
PayloadTransferFrame::PayloadHeader header;
|
||||
PayloadTransferFrame::ControlMessage control;
|
||||
header.set_id(12345);
|
||||
header.set_type(PayloadTransferFrame::PayloadHeader::BYTES);
|
||||
header.set_total_size(1024);
|
||||
control.set_offset(150);
|
||||
control.set_event(PayloadTransferFrame::ControlMessage::PAYLOAD_CANCELED);
|
||||
|
||||
ON_CALL(*endpoint_channel, Read())
|
||||
.WillByDefault([channel = endpoint_channel.get()]() {
|
||||
if (channel->IsClosed()) return ExceptionOr<ByteArray>(Exception::kIo);
|
||||
NEARBY_LOG(INFO, "Simulate read delay: wait");
|
||||
absl::SleepFor(absl::Milliseconds(100));
|
||||
NEARBY_LOG(INFO, "Simulate read delay: done");
|
||||
if (channel->IsClosed()) return ExceptionOr<ByteArray>(Exception::kIo);
|
||||
return ExceptionOr<ByteArray>(ByteArray{});
|
||||
});
|
||||
ON_CALL(*endpoint_channel, Close(_))
|
||||
.WillByDefault(
|
||||
[channel = endpoint_channel.get()](DisconnectionReason reason) {
|
||||
channel->DoClose();
|
||||
NEARBY_LOG(INFO, "Channel closed");
|
||||
});
|
||||
EXPECT_CALL(*endpoint_channel, Write(_))
|
||||
.WillRepeatedly(Return(Exception{Exception::kSuccess}));
|
||||
|
||||
RegisterEndpoint(std::move(endpoint_channel), false);
|
||||
auto failed_ids =
|
||||
em_.SendControlMessage(header, control, std::vector{endpoint_id_});
|
||||
EXPECT_EQ(failed_ids, std::vector<std::string>{});
|
||||
NEARBY_LOG(INFO, "Will unregister endpoint now");
|
||||
em_.UnregisterEndpoint(&client_, endpoint_id_);
|
||||
NEARBY_LOG(INFO, "Will call destructors now");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,70 @@
|
||||
cc_library(
|
||||
name = "mediums",
|
||||
srcs = [
|
||||
"advertisement_read_result.cc",
|
||||
"ble_advertisement.cc",
|
||||
"ble_advertisement_header.cc",
|
||||
"ble_packet.cc",
|
||||
"bluetooth_radio.cc",
|
||||
"uuid.cc",
|
||||
],
|
||||
hdrs = [
|
||||
"advertisement_read_result.h",
|
||||
"ble_advertisement.h",
|
||||
"ble_advertisement_header.h",
|
||||
"ble_packet.h",
|
||||
"ble_peripheral.h",
|
||||
"bluetooth_radio.h",
|
||||
"lost_entity_tracker.h",
|
||||
"uuid.h",
|
||||
],
|
||||
visibility = [
|
||||
"//core_v2/internal:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//platform_v2/base",
|
||||
"//platform_v2/public",
|
||||
"//platform_v2/public:logging",
|
||||
"//absl/container:flat_hash_map",
|
||||
"//absl/container:flat_hash_set",
|
||||
"//absl/strings",
|
||||
"//absl/time",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "utils",
|
||||
srcs = ["utils.cc"],
|
||||
hdrs = ["utils.h"],
|
||||
visibility = [
|
||||
"//core_v2/internal/mediums/webrtc:__pkg__",
|
||||
],
|
||||
deps = [
|
||||
"//platform_v2/base",
|
||||
"//platform_v2/public",
|
||||
],
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "core_v2_internal_mediums_test",
|
||||
srcs = [
|
||||
"advertisement_read_result_test.cc",
|
||||
"ble_advertisement_header_test.cc",
|
||||
"ble_advertisement_test.cc",
|
||||
"ble_packet_test.cc",
|
||||
"ble_peripheral_test.cc",
|
||||
"bluetooth_radio_test.cc",
|
||||
"lost_entity_tracker_test.cc",
|
||||
"uuid_test.cc",
|
||||
],
|
||||
shard_count = 16,
|
||||
deps = [
|
||||
":mediums",
|
||||
"//platform_v2/base",
|
||||
"//platform_v2/impl/g3", # build_cleaner: keep
|
||||
"//platform_v2/public",
|
||||
"//platform_v2/public:logging",
|
||||
"//testing/base/public:gunit_main",
|
||||
"//absl/time",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,125 @@
|
||||
#include "core_v2/internal/mediums/advertisement_read_result.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
#include "platform_v2/public/mutex_lock.h"
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
#include "absl/time/clock.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace mediums {
|
||||
|
||||
const AdvertisementReadResult::Config AdvertisementReadResult::kDefaultConfig{
|
||||
.backoff_multiplier = 2.0,
|
||||
.base_backoff_duration = absl::Seconds(1),
|
||||
.max_backoff_duration = absl::Minutes(5),
|
||||
};
|
||||
|
||||
// Adds a successfully read advertisement for the specified slot to this read
|
||||
// result. This is fundamentally different from RecordLastReadStatus() because
|
||||
// we can report a read failure, but still manage to read some advertisements.
|
||||
void AdvertisementReadResult::AddAdvertisement(std::int32_t slot,
|
||||
const ByteArray& advertisement) {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
// Blindly remove from the advertisements map to make sure any existing
|
||||
// key-value pair is destroyed.
|
||||
advertisements_.emplace(slot, advertisement);
|
||||
}
|
||||
|
||||
// Determines whether or not an advertisement was successfully read at the
|
||||
// specified slot.
|
||||
bool AdvertisementReadResult::HasAdvertisement(std::int32_t slot) const {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
return advertisements_.contains(slot);
|
||||
}
|
||||
|
||||
// Retrieves all raw advertisements that were successfully read.
|
||||
std::vector<const ByteArray*> AdvertisementReadResult::GetAdvertisements()
|
||||
const {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
std::vector<const ByteArray*> all_advertisements;
|
||||
all_advertisements.reserve(advertisements_.size());
|
||||
for (const auto& item : advertisements_) {
|
||||
all_advertisements.emplace_back(&item.second);
|
||||
}
|
||||
|
||||
return all_advertisements;
|
||||
}
|
||||
|
||||
// Determines what stage we're in for retrying a read from an advertisement
|
||||
// GATT server.
|
||||
AdvertisementReadResult::RetryStatus
|
||||
AdvertisementReadResult::EvaluateRetryStatus() const {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
// Check if we have already succeeded reading this advertisement.
|
||||
if (status_ == Status::kSuccess) {
|
||||
return RetryStatus::kPreviouslySucceeded;
|
||||
}
|
||||
|
||||
// Check if we have recently failed to read this advertisement.
|
||||
if (GetDurationSinceReadLocked() < backoff_duration_) {
|
||||
return RetryStatus::kTooSoon;
|
||||
}
|
||||
|
||||
return RetryStatus::kRetry;
|
||||
}
|
||||
|
||||
// Records the status of the latest read, and updates the next backoff
|
||||
// duration for subsequent reads. Be sure to also call
|
||||
// AddAdvertisement() if any advertisements were read.
|
||||
void AdvertisementReadResult::RecordLastReadStatus(bool is_success) {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
// Update the last read timestamp.
|
||||
last_read_timestamp_ = SystemClock::ElapsedRealtime();
|
||||
|
||||
// Update the backoff duration.
|
||||
if (is_success) {
|
||||
// Reset the backoff duration now that we had a successful read.
|
||||
backoff_duration_ = config_.base_backoff_duration;
|
||||
} else {
|
||||
// Determine whether or not we were already failing before. If we were, we
|
||||
// should increase the backoff duration.
|
||||
if (status_ == Status::kFailure) {
|
||||
// Use exponential backoff to determine the next backoff duration. This
|
||||
// simply involves multiplying our current backoff duration by some
|
||||
// multiplier.
|
||||
absl::Duration next_backoff_duration =
|
||||
config_.backoff_multiplier * backoff_duration_;
|
||||
// Update the backoff duration, making sure not to blow past the
|
||||
// ceiling.
|
||||
backoff_duration_ =
|
||||
std::min(next_backoff_duration, config_.max_backoff_duration);
|
||||
} else {
|
||||
// This is our first time failing, so we should only backoff for the
|
||||
// initial duration.
|
||||
backoff_duration_ = config_.base_backoff_duration;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the internal result.
|
||||
status_ = is_success ? Status::kSuccess : Status::kFailure;
|
||||
}
|
||||
|
||||
// Returns how much time has passed since we last tried reading from an
|
||||
// advertisement GATT server.
|
||||
absl::Duration AdvertisementReadResult::GetDurationSinceRead() const {
|
||||
MutexLock lock(&mutex_);
|
||||
return GetDurationSinceReadLocked();
|
||||
}
|
||||
|
||||
absl::Duration AdvertisementReadResult::GetDurationSinceReadLocked() const {
|
||||
return SystemClock::ElapsedRealtime() - last_read_timestamp_;
|
||||
}
|
||||
|
||||
} // namespace mediums
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,90 @@
|
||||
#ifndef CORE_V2_INTERNAL_MEDIUMS_ADVERTISEMENT_READ_RESULT_H_
|
||||
#define CORE_V2_INTERNAL_MEDIUMS_ADVERTISEMENT_READ_RESULT_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "platform_v2/base/byte_array.h"
|
||||
#include "platform_v2/public/mutex.h"
|
||||
#include "platform_v2/public/system_clock.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
#include "absl/time/clock.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace mediums {
|
||||
|
||||
// Representation of a GATT advertisement read result. This object helps us
|
||||
// determine whether or not we need to retry GATT reads.
|
||||
class AdvertisementReadResult {
|
||||
public:
|
||||
// We need a long enough duration such that we always trigger a read
|
||||
// retry AND we always connect to it without delay. The former case
|
||||
// helps us initialize an AdvertisementReadResult so that we
|
||||
// unconditionally try reading on the first sighting. And the latter
|
||||
// case helps us connect immediately when we initialize a dummy read
|
||||
// result for fast advertisements (which don't use the GATT server).
|
||||
|
||||
struct Config {
|
||||
// How much to multiply the backoff duration by with every failure to read
|
||||
// from the advertisement GATT server. This should never be below 1!
|
||||
float backoff_multiplier;
|
||||
// The initial backoff duration when we fail to read from an advertisement
|
||||
// GATT server.
|
||||
absl::Duration base_backoff_duration;
|
||||
// The maximum backoff duration allowed between advertisement GATT server
|
||||
// reads.
|
||||
absl::Duration max_backoff_duration;
|
||||
};
|
||||
|
||||
static const Config kDefaultConfig;
|
||||
explicit AdvertisementReadResult(const Config& config = kDefaultConfig)
|
||||
: config_(config) {}
|
||||
~AdvertisementReadResult() = default;
|
||||
|
||||
enum class RetryStatus {
|
||||
kUnknown = 0,
|
||||
kRetry = 1,
|
||||
kPreviouslySucceeded = 2,
|
||||
kTooSoon = 3,
|
||||
};
|
||||
|
||||
void AddAdvertisement(std::int32_t slot, const ByteArray& advertisement)
|
||||
ABSL_LOCKS_EXCLUDED(mutex_);
|
||||
bool HasAdvertisement(std::int32_t slot) const ABSL_LOCKS_EXCLUDED(mutex_);
|
||||
std::vector<const ByteArray*> GetAdvertisements() const
|
||||
ABSL_LOCKS_EXCLUDED(mutex_);
|
||||
RetryStatus EvaluateRetryStatus() const ABSL_LOCKS_EXCLUDED(mutex_);
|
||||
void RecordLastReadStatus(bool is_success) ABSL_LOCKS_EXCLUDED(mutex_);
|
||||
absl::Duration GetDurationSinceRead() const ABSL_LOCKS_EXCLUDED(mutex_);
|
||||
|
||||
private:
|
||||
enum class Status {
|
||||
kUnknown = 0,
|
||||
kSuccess = 1,
|
||||
kFailure = 2,
|
||||
};
|
||||
|
||||
absl::Duration GetDurationSinceReadLocked() const
|
||||
ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
|
||||
|
||||
mutable Mutex mutex_;
|
||||
|
||||
// Maps slot numbers to the GATT advertisement found in that slot.
|
||||
absl::flat_hash_map<std::int32_t, ByteArray> advertisements_
|
||||
ABSL_GUARDED_BY(mutex_);
|
||||
|
||||
Config config_;
|
||||
absl::Duration backoff_duration_ ABSL_GUARDED_BY(mutex_);
|
||||
absl::Time last_read_timestamp_ ABSL_GUARDED_BY(mutex_);
|
||||
Status status_ ABSL_GUARDED_BY(mutex_) = Status::kUnknown;
|
||||
};
|
||||
|
||||
} // namespace mediums
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
|
||||
#endif // CORE_V2_INTERNAL_MEDIUMS_ADVERTISEMENT_READ_RESULT_H_
|
||||
@@ -0,0 +1,129 @@
|
||||
#include "core_v2/internal/mediums/advertisement_read_result.h"
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
#include "absl/time/clock.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace mediums {
|
||||
namespace {
|
||||
|
||||
constexpr char kAdvertisementBytes[] = "\x0A\x0B\x0C";
|
||||
|
||||
// Default values may be too big and impractical to wait for in the test.
|
||||
// For the test platform, we redefine them to some reasonable values.
|
||||
const absl::Duration kAdvertisementBaseBackoffDuration = absl::Seconds(1);
|
||||
const absl::Duration kAdvertisementMaxBackoffDuration = absl::Seconds(6);
|
||||
|
||||
const AdvertisementReadResult::Config test_config{
|
||||
.backoff_multiplier =
|
||||
AdvertisementReadResult::kDefaultConfig.backoff_multiplier,
|
||||
.base_backoff_duration = kAdvertisementBaseBackoffDuration,
|
||||
.max_backoff_duration = kAdvertisementMaxBackoffDuration,
|
||||
};
|
||||
|
||||
TEST(AdvertisementReadResultTest, AdvertisementExists) {
|
||||
AdvertisementReadResult advertisement_read_result(test_config);
|
||||
advertisement_read_result.RecordLastReadStatus(/* is_success= */ true);
|
||||
|
||||
std::int32_t slot = 6;
|
||||
advertisement_read_result.AddAdvertisement(slot,
|
||||
ByteArray(kAdvertisementBytes));
|
||||
|
||||
EXPECT_TRUE(advertisement_read_result.HasAdvertisement(slot));
|
||||
}
|
||||
|
||||
TEST(AdvertisementReadResultTest, AdvertisementNonExistent) {
|
||||
AdvertisementReadResult advertisement_read_result(test_config);
|
||||
advertisement_read_result.RecordLastReadStatus(/* is_success= */ true);
|
||||
|
||||
std::int32_t slot = 6;
|
||||
|
||||
EXPECT_FALSE(advertisement_read_result.HasAdvertisement(slot));
|
||||
}
|
||||
|
||||
TEST(AdvertisementReadResultTest, EvaluateRetryStatusInitialized) {
|
||||
AdvertisementReadResult advertisement_read_result(test_config);
|
||||
|
||||
EXPECT_EQ(advertisement_read_result.EvaluateRetryStatus(),
|
||||
AdvertisementReadResult::RetryStatus::kRetry);
|
||||
}
|
||||
|
||||
TEST(AdvertisementReadResultTest, EvaluateRetryStatusSuccess) {
|
||||
AdvertisementReadResult advertisement_read_result(test_config);
|
||||
advertisement_read_result.RecordLastReadStatus(/* is_success= */ true);
|
||||
|
||||
EXPECT_EQ(advertisement_read_result.EvaluateRetryStatus(),
|
||||
AdvertisementReadResult::RetryStatus::kPreviouslySucceeded);
|
||||
}
|
||||
|
||||
TEST(AdvertisementReadResultTest, EvaluateRetryStatusTooSoon) {
|
||||
AdvertisementReadResult advertisement_read_result(test_config);
|
||||
advertisement_read_result.RecordLastReadStatus(/* is_success= */ false);
|
||||
|
||||
// Sleep for some time, but not long enough to warrant a retry.
|
||||
absl::SleepFor(kAdvertisementBaseBackoffDuration / 2);
|
||||
|
||||
EXPECT_EQ(advertisement_read_result.EvaluateRetryStatus(),
|
||||
AdvertisementReadResult::RetryStatus::kTooSoon);
|
||||
}
|
||||
|
||||
TEST(AdvertisementReadResultTest, EvaluateRetryStatusRetry) {
|
||||
AdvertisementReadResult advertisement_read_result(test_config);
|
||||
advertisement_read_result.RecordLastReadStatus(/* is_success= */ false);
|
||||
|
||||
// Sleep long enough to warrant a retry.
|
||||
absl::SleepFor(kAdvertisementBaseBackoffDuration);
|
||||
|
||||
EXPECT_EQ(advertisement_read_result.EvaluateRetryStatus(),
|
||||
AdvertisementReadResult::RetryStatus::kRetry);
|
||||
}
|
||||
|
||||
TEST(AdvertisementReadResultTest, ReportStatusExponentialBackoff) {
|
||||
AdvertisementReadResult advertisement_read_result(test_config);
|
||||
advertisement_read_result.RecordLastReadStatus(/* is_success= */ false);
|
||||
|
||||
// Record an additional failure so our backoff duration increases.
|
||||
advertisement_read_result.RecordLastReadStatus(/* is_success= */ false);
|
||||
|
||||
// Sleep for the backoff duration. We shouldn't trigger a retry because the
|
||||
// backoff should have increased from failing a second time.
|
||||
absl::SleepFor(kAdvertisementBaseBackoffDuration);
|
||||
|
||||
EXPECT_EQ(advertisement_read_result.EvaluateRetryStatus(),
|
||||
AdvertisementReadResult::RetryStatus::kTooSoon);
|
||||
}
|
||||
|
||||
TEST(AdvertisementReadResultTest, ReportStatusExponentialBackoffMax) {
|
||||
AdvertisementReadResult advertisement_read_result(test_config);
|
||||
advertisement_read_result.RecordLastReadStatus(/* is_success= */ false);
|
||||
|
||||
// Record an absurd amount of failures so we hit the maximum backoff duration.
|
||||
for (std::int32_t i = 0; i < 1000; i++) {
|
||||
advertisement_read_result.RecordLastReadStatus(/* is_success= */ false);
|
||||
}
|
||||
|
||||
// Sleep for the maximum backoff duration. This should be enough to warrant a
|
||||
// retry.
|
||||
absl::SleepFor(kAdvertisementMaxBackoffDuration);
|
||||
|
||||
EXPECT_EQ(advertisement_read_result.EvaluateRetryStatus(),
|
||||
AdvertisementReadResult::RetryStatus::kRetry);
|
||||
}
|
||||
|
||||
TEST(AdvertisementReadResultTest, GetDurationSinceRead) {
|
||||
AdvertisementReadResult advertisement_read_result(test_config);
|
||||
advertisement_read_result.RecordLastReadStatus(/* is_success= */ true);
|
||||
|
||||
absl::Duration sleepTime = absl::Milliseconds(420);
|
||||
absl::SleepFor(sleepTime);
|
||||
|
||||
EXPECT_GE(advertisement_read_result.GetDurationSinceRead(), sleepTime);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mediums
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,201 @@
|
||||
#include "core_v2/internal/mediums/ble_advertisement.h"
|
||||
|
||||
#include <inttypes.h>
|
||||
|
||||
#include "platform_v2/public/logging.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace mediums {
|
||||
|
||||
BleAdvertisement::BleAdvertisement(Version version,
|
||||
SocketVersion socket_version,
|
||||
const ByteArray &service_id_hash,
|
||||
const ByteArray &data) {
|
||||
// Check that the given input is valid.
|
||||
if (!IsSupportedVersion(version) ||
|
||||
!IsSupportedSocketVersion(socket_version) ||
|
||||
service_id_hash.size() != kServiceIdHashLength ||
|
||||
data.size() > kMaxDataSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
version_ = version;
|
||||
socket_version_ = socket_version;
|
||||
service_id_hash_ = service_id_hash;
|
||||
data_ = data;
|
||||
}
|
||||
|
||||
BleAdvertisement::BleAdvertisement(const ByteArray &ble_advertisement_bytes) {
|
||||
if (ble_advertisement_bytes.Empty()) {
|
||||
NEARBY_LOG(INFO,
|
||||
"Cannot deserialize BleAdvertisement: null bytes passed in.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ble_advertisement_bytes.size() < kMinAdvertisementLength) {
|
||||
NEARBY_LOG(INFO,
|
||||
"Cannot deserialize BleAdvertisement: expecting min %d raw "
|
||||
"bytes, got %" PRIu64,
|
||||
kMinAdvertisementLength, ble_advertisement_bytes.size());
|
||||
return;
|
||||
}
|
||||
|
||||
// Now, time to read the bytes!
|
||||
const auto *read_ptr = ble_advertisement_bytes.data();
|
||||
|
||||
// 1. Version.
|
||||
version_ = static_cast<Version>((*read_ptr & kVersionBitmask) >> 5);
|
||||
if (!IsSupportedVersion(version_)) {
|
||||
NEARBY_LOG(INFO,
|
||||
"Cannot deserialize BleAdvertisement: unsupported Version %u",
|
||||
version_);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Socket Version.
|
||||
socket_version_ =
|
||||
static_cast<SocketVersion>((*read_ptr & kSocketVersionBitmask) >> 2);
|
||||
if (!IsSupportedSocketVersion(socket_version_)) {
|
||||
NEARBY_LOG(
|
||||
INFO,
|
||||
"Cannot deserialize BLEAdvertisement: unsupported SocketVersion %u",
|
||||
socket_version_);
|
||||
version_ = Version::kUndefined;
|
||||
return;
|
||||
}
|
||||
read_ptr += kVersionLength;
|
||||
|
||||
// 3. Service ID hash.
|
||||
service_id_hash_ = ByteArray(read_ptr, kServiceIdHashLength);
|
||||
read_ptr += kServiceIdHashLength;
|
||||
|
||||
// 4.1. Data size.
|
||||
size_t expected_data_size = DeserializeDataSize(read_ptr);
|
||||
if (expected_data_size < 0) {
|
||||
NEARBY_LOG(
|
||||
INFO,
|
||||
"Cannot deserialize BleAdvertisement: negative data size %" PRIu64,
|
||||
expected_data_size);
|
||||
version_ = Version::kUndefined;
|
||||
return;
|
||||
}
|
||||
read_ptr += kDataSizeLength;
|
||||
|
||||
// Check that the stated data size is the same as what we received.
|
||||
size_t actual_data_size = ComputeDataSize(ble_advertisement_bytes);
|
||||
if (actual_data_size < expected_data_size) {
|
||||
NEARBY_LOG(INFO,
|
||||
"Cannot deserialize BLEAdvertisement: expected data to be %zu "
|
||||
"bytes, got %" PRIu64 " bytes",
|
||||
expected_data_size, actual_data_size);
|
||||
version_ = Version::kUndefined;
|
||||
return;
|
||||
}
|
||||
|
||||
// 4.2. Data.
|
||||
data_ = ByteArray(read_ptr, expected_data_size);
|
||||
read_ptr += expected_data_size;
|
||||
}
|
||||
|
||||
BleAdvertisement::operator ByteArray() const {
|
||||
if (!IsValid()) {
|
||||
return ByteArray{};
|
||||
}
|
||||
|
||||
std::string out;
|
||||
|
||||
// The first 3 bits are the Version.
|
||||
char version_and_socket_version_byte =
|
||||
(static_cast<char>(version_) << 5) & kVersionBitmask;
|
||||
// The next 3 bits are the Socket version. 2 bits left are reserved.
|
||||
version_and_socket_version_byte |=
|
||||
(static_cast<char>(socket_version_) << 2) & kSocketVersionBitmask;
|
||||
// Serialize Data size bytes(4).
|
||||
ByteArray data_size_bytes{kDataSizeLength};
|
||||
auto *data_size_bytes_write_ptr = data_size_bytes.data();
|
||||
SerializeDataSize(data_size_bytes_write_ptr, data_.size());
|
||||
|
||||
out.reserve(1 + service_id_hash_.size() + 1 + data_.size());
|
||||
out.append(1, version_and_socket_version_byte);
|
||||
out.append(std::string(service_id_hash_));
|
||||
out.append(std::string(data_size_bytes));
|
||||
out.append(std::string(data_));
|
||||
|
||||
return ByteArray{std::move(out)};
|
||||
}
|
||||
|
||||
bool BleAdvertisement::operator==(const BleAdvertisement &rhs) const {
|
||||
return this->GetVersion() == rhs.GetVersion() &&
|
||||
this->GetSocketVersion() == rhs.GetSocketVersion() &&
|
||||
this->GetServiceIdHash() == rhs.GetServiceIdHash() &&
|
||||
this->GetData() == rhs.GetData();
|
||||
}
|
||||
|
||||
bool BleAdvertisement::operator<(const BleAdvertisement &rhs) const {
|
||||
if (this->GetVersion() != rhs.GetVersion()) {
|
||||
return this->GetVersion() < rhs.GetVersion();
|
||||
}
|
||||
if (this->GetSocketVersion() != rhs.GetSocketVersion()) {
|
||||
return this->GetSocketVersion() < rhs.GetSocketVersion();
|
||||
}
|
||||
if (this->GetServiceIdHash() != rhs.GetServiceIdHash()) {
|
||||
return this->GetServiceIdHash() < rhs.GetServiceIdHash();
|
||||
}
|
||||
return this->GetData() < rhs.GetData();
|
||||
}
|
||||
|
||||
bool BleAdvertisement::IsSupportedVersion(Version version) const {
|
||||
return version >= Version::kV1 && version <= Version::kV2;
|
||||
}
|
||||
|
||||
bool BleAdvertisement::IsSupportedSocketVersion(
|
||||
SocketVersion socket_version) const {
|
||||
return socket_version >= SocketVersion::kV1 &&
|
||||
socket_version <= SocketVersion::kV2;
|
||||
}
|
||||
|
||||
void BleAdvertisement::SerializeDataSize(char *data_size_bytes_write_ptr,
|
||||
size_t data_size) const {
|
||||
// Get a raw representation of the data size bytes in memory.
|
||||
char *data_size_bytes = reinterpret_cast<char *>(&data_size);
|
||||
|
||||
// Append these raw bytes to advertisement bytes, keeping in mind that we need
|
||||
// to convert from Little Endian to Big Endian in the process.
|
||||
for (int i = 0; i < kDataSizeLength; ++i) {
|
||||
data_size_bytes_write_ptr[i] = data_size_bytes[kDataSizeLength - i - 1];
|
||||
}
|
||||
}
|
||||
|
||||
size_t BleAdvertisement::DeserializeDataSize(
|
||||
const char *data_size_bytes_read_ptr) const {
|
||||
// Allocate a chunk of memory to store our deserialized size.
|
||||
char data_size_bytes[kDataSizeLength];
|
||||
|
||||
// Assign the bits of our size from the given raw bytes, keeping in mind that
|
||||
// we need to convert from Big Endian to Little Endian in the process.
|
||||
for (int i = 0; i < kDataSizeLength; ++i) {
|
||||
data_size_bytes[i] = data_size_bytes_read_ptr[kDataSizeLength - i - 1];
|
||||
}
|
||||
|
||||
// Interpret the char array as a single int.
|
||||
return static_cast<size_t>(
|
||||
*(reinterpret_cast<std::uint32_t *>(&data_size_bytes)));
|
||||
}
|
||||
|
||||
size_t BleAdvertisement::ComputeDataSize(
|
||||
const ByteArray &ble_advertisement_bytes) const {
|
||||
return ble_advertisement_bytes.size() - kMinAdvertisementLength;
|
||||
}
|
||||
|
||||
size_t BleAdvertisement::ComputeAdvertisementLength(
|
||||
const ByteArray &data) const {
|
||||
// The advertisement length is the minimum length + the length of the data.
|
||||
return kMinAdvertisementLength + data.size();
|
||||
}
|
||||
|
||||
} // namespace mediums
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,100 @@
|
||||
#ifndef CORE_V2_INTERNAL_MEDIUMS_BLE_ADVERTISEMENT_H_
|
||||
#define CORE_V2_INTERNAL_MEDIUMS_BLE_ADVERTISEMENT_H_
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "platform_v2/base/byte_array.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace mediums {
|
||||
|
||||
// Represents the format of the Mediums Ble Advertisement used in advertising
|
||||
// and discovery.
|
||||
//
|
||||
// [VERSION][SOCKET_VERSION][2_RESERVED_BITS][SERVICE_ID_HASH][DATA_SIZE][DATA]
|
||||
//
|
||||
// See go/nearby-ble-design for more information.
|
||||
class BleAdvertisement {
|
||||
public:
|
||||
// Versions of the BleAdvertisement.
|
||||
enum class Version {
|
||||
kUndefined = 0,
|
||||
kV1 = 1,
|
||||
kV2 = 2,
|
||||
// Version is only allocated 3 bits in the BleAdvertisement, so this can
|
||||
// never go beyond V7.
|
||||
};
|
||||
|
||||
// Versions of the BLESocket.
|
||||
enum class SocketVersion {
|
||||
kUndefined = 0,
|
||||
kV1 = 1,
|
||||
kV2 = 2,
|
||||
// SocketVersion is only allocated 3 bits in the BleAdvertisement, so this
|
||||
// can never go beyond V7.
|
||||
};
|
||||
|
||||
static constexpr int kServiceIdHashLength = 3;
|
||||
|
||||
BleAdvertisement() = default;
|
||||
BleAdvertisement(Version version, SocketVersion socket_version,
|
||||
const ByteArray &service_id_hash, const ByteArray &data);
|
||||
explicit BleAdvertisement(const ByteArray &ble_advertisement_bytes);
|
||||
BleAdvertisement(const BleAdvertisement &) = default;
|
||||
BleAdvertisement &operator=(const BleAdvertisement &) = default;
|
||||
BleAdvertisement(BleAdvertisement &&) = default;
|
||||
BleAdvertisement &operator=(BleAdvertisement &&) = default;
|
||||
~BleAdvertisement() = default;
|
||||
|
||||
explicit operator ByteArray() const;
|
||||
// Operator overloads when comparing BleAdvertisement.
|
||||
bool operator==(const BleAdvertisement &rhs) const;
|
||||
bool operator<(const BleAdvertisement &rhs) const;
|
||||
|
||||
bool IsValid() const { return IsSupportedVersion(version_); }
|
||||
Version GetVersion() const { return version_; }
|
||||
SocketVersion GetSocketVersion() const { return socket_version_; }
|
||||
ByteArray GetServiceIdHash() const { return service_id_hash_; }
|
||||
ByteArray &GetData() & { return data_; }
|
||||
const ByteArray &GetData() const & { return data_; }
|
||||
ByteArray &&GetData() && { return std::move(data_); }
|
||||
const ByteArray &&GetData() const && { return std::move(data_); }
|
||||
|
||||
private:
|
||||
bool IsSupportedVersion(Version version) const;
|
||||
bool IsSupportedSocketVersion(SocketVersion socket_version) const;
|
||||
void SerializeDataSize(char *data_size_bytes_write_ptr,
|
||||
size_t data_size) const;
|
||||
size_t DeserializeDataSize(const char *data_size_bytes_read_ptr) const;
|
||||
size_t ComputeDataSize(const ByteArray &ble_advertisement_bytes) const;
|
||||
size_t ComputeAdvertisementLength(const ByteArray &data) const;
|
||||
|
||||
static constexpr int kVersionLength = 1;
|
||||
// Length of one int. Be sure to re-evaluate how we compute data size in this
|
||||
// class if this constant ever changes!
|
||||
static constexpr int kDataSizeLength = 4;
|
||||
static constexpr int kMinAdvertisementLength =
|
||||
kVersionLength + kServiceIdHashLength + kDataSizeLength;
|
||||
// The maximum length for a Gatt characteristic value is 512 bytes, so make
|
||||
// sure the entire advertisement is less than that. The data can take up
|
||||
// whatever space is remaining after the bytes preceding it.
|
||||
static constexpr int kMaxGattCharacteristicValueSize = 512;
|
||||
static constexpr int kMaxDataSize =
|
||||
kMaxGattCharacteristicValueSize - kMinAdvertisementLength;
|
||||
static constexpr int kVersionBitmask = 0x0E0;
|
||||
static constexpr int kSocketVersionBitmask = 0x01C;
|
||||
|
||||
Version version_{Version::kUndefined};
|
||||
SocketVersion socket_version_{SocketVersion::kUndefined};
|
||||
ByteArray service_id_hash_;
|
||||
ByteArray data_;
|
||||
};
|
||||
|
||||
} // namespace mediums
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
|
||||
#endif // CORE_V2_INTERNAL_MEDIUMS_BLE_ADVERTISEMENT_H_
|
||||
@@ -0,0 +1,118 @@
|
||||
#include "core_v2/internal/mediums/ble_advertisement_header.h"
|
||||
|
||||
#include <inttypes.h>
|
||||
|
||||
#include "platform_v2/base/base64_utils.h"
|
||||
#include "platform_v2/public/logging.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace mediums {
|
||||
|
||||
BleAdvertisementHeader::BleAdvertisementHeader(
|
||||
Version version, int num_slots, const ByteArray &service_id_bloom_filter,
|
||||
const ByteArray &advertisement_hash) {
|
||||
// TODO(edwinwu): Checks if num_slots needs to be >= 0
|
||||
if (version != Version::kV2 ||
|
||||
service_id_bloom_filter.size() != kServiceIdBloomFilterLength ||
|
||||
advertisement_hash.size() != kAdvertisementHashLength) {
|
||||
return;
|
||||
}
|
||||
|
||||
version_ = version;
|
||||
num_slots_ = num_slots;
|
||||
service_id_bloom_filter_ = service_id_bloom_filter;
|
||||
advertisement_hash_ = advertisement_hash;
|
||||
}
|
||||
|
||||
BleAdvertisementHeader::BleAdvertisementHeader(
|
||||
const std::string &ble_advertisement_header_string) {
|
||||
ByteArray ble_advertisement_header_bytes =
|
||||
Base64Utils::Decode(ble_advertisement_header_string);
|
||||
|
||||
if (ble_advertisement_header_bytes.Empty()) {
|
||||
NEARBY_LOG(
|
||||
ERROR,
|
||||
"Cannot deserialize BLEAdvertisementHeader: failed Base64 decoding");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ble_advertisement_header_bytes.size() < kMinAdvertisementHeaderLength) {
|
||||
NEARBY_LOG(ERROR,
|
||||
"Cannot deserialize BleAdvertisementHeader: expecting min %u "
|
||||
"raw bytes, got %" PRIu64 " instead",
|
||||
kMinAdvertisementHeaderLength,
|
||||
ble_advertisement_header_bytes.size());
|
||||
return;
|
||||
}
|
||||
|
||||
// Start reading the bytes.
|
||||
auto *ble_advertisement_header_read_ptr =
|
||||
ble_advertisement_header_bytes.data();
|
||||
|
||||
// The first 3 bits are supposed to be the version.
|
||||
version_ = static_cast<Version>(
|
||||
(*ble_advertisement_header_read_ptr & kVersionBitmask) >> 5);
|
||||
if (version_ != Version::kV2) {
|
||||
NEARBY_LOG(
|
||||
ERROR,
|
||||
"Cannot deserialize BleAdvertisementHeader: unsupported Version %d",
|
||||
version_);
|
||||
return;
|
||||
}
|
||||
// The last 5 bits of the first byte represent the number of slots.
|
||||
num_slots_ = static_cast<std::uint32_t>(*ble_advertisement_header_read_ptr &
|
||||
kNumSlotsBitmask);
|
||||
ble_advertisement_header_read_ptr++;
|
||||
|
||||
// Service ID bloom filter.
|
||||
service_id_bloom_filter_ =
|
||||
ByteArray(ble_advertisement_header_read_ptr, kServiceIdBloomFilterLength);
|
||||
ble_advertisement_header_read_ptr += kServiceIdBloomFilterLength;
|
||||
|
||||
// Advertisement hash.
|
||||
advertisement_hash_ =
|
||||
ByteArray(ble_advertisement_header_read_ptr, kAdvertisementHashLength);
|
||||
ble_advertisement_header_read_ptr += kAdvertisementHashLength;
|
||||
}
|
||||
|
||||
BleAdvertisementHeader::operator std::string() const {
|
||||
if (!IsValid()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string out;
|
||||
|
||||
// The first 3 bits are the Version.
|
||||
char version_and_num_slots_byte =
|
||||
(static_cast<char>(version_) << 5) & kVersionBitmask;
|
||||
// The next 5 bits are the number of slots.
|
||||
version_and_num_slots_byte |=
|
||||
static_cast<char>(num_slots_) & kNumSlotsBitmask;
|
||||
out.reserve(1 + service_id_bloom_filter_.size() + advertisement_hash_.size());
|
||||
out.append(1, version_and_num_slots_byte);
|
||||
out.append(std::string(service_id_bloom_filter_));
|
||||
out.append(std::string(advertisement_hash_));
|
||||
|
||||
return Base64Utils::Encode(ByteArray(std::move(out)));
|
||||
}
|
||||
|
||||
bool BleAdvertisementHeader::operator<(
|
||||
const BleAdvertisementHeader &rhs) const {
|
||||
if (this->GetVersion() != rhs.GetVersion()) {
|
||||
return this->GetVersion() < rhs.GetVersion();
|
||||
}
|
||||
if (this->GetNumSlots() != rhs.GetNumSlots()) {
|
||||
return this->GetNumSlots() < rhs.GetNumSlots();
|
||||
}
|
||||
if (this->GetServiceIdBloomFilter() != rhs.GetServiceIdBloomFilter()) {
|
||||
return this->GetServiceIdBloomFilter() < rhs.GetServiceIdBloomFilter();
|
||||
}
|
||||
return this->GetAdvertisementHash() < rhs.GetAdvertisementHash();
|
||||
}
|
||||
|
||||
} // namespace mediums
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,84 @@
|
||||
#ifndef CORE_V2_INTERNAL_MEDIUMS_BLE_ADVERTISEMENT_HEADER_H_
|
||||
#define CORE_V2_INTERNAL_MEDIUMS_BLE_ADVERTISEMENT_HEADER_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "platform_v2/base/byte_array.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace mediums {
|
||||
|
||||
// Represents the format of the Mediums BLE Advertisement Header used in
|
||||
// Advertising + Discovery.
|
||||
//
|
||||
// [VERSION][NUM_SLOTS][SERVICE_ID_BLOOM_FILTER][ADVERTISEMENT_HASH]
|
||||
//
|
||||
// See go/nearby-ble-design for more information.
|
||||
//
|
||||
// Note. The object constructed by default constructor or the parameterized
|
||||
// constructor with invalid value(s) is treated as invalid instance. Caller
|
||||
// should be responsible to call IsValid() to check the instance is invalid in
|
||||
// advance before continue on.
|
||||
class BleAdvertisementHeader {
|
||||
public:
|
||||
// Versions of the BleAdvertisementHeader.
|
||||
enum class Version {
|
||||
kUndefined = 0,
|
||||
kV1 = 1,
|
||||
kV2 = 2,
|
||||
// Version is only allocated 3 bits in the BleAdvertisementHeader, so this
|
||||
// can never go beyond V7.
|
||||
//
|
||||
// V1 is not present because it's an old format used in Nearby Connections
|
||||
// before this logic was pushed down into Nearby Mediums. V1 put
|
||||
// everything in the service data, while V2 puts the data inside a GATT
|
||||
// characteristic so the two are not compatible.
|
||||
};
|
||||
|
||||
BleAdvertisementHeader() = default;
|
||||
BleAdvertisementHeader(Version version, int num_slots,
|
||||
const ByteArray &service_id_bloom_filter,
|
||||
const ByteArray &advertisement_hash);
|
||||
explicit BleAdvertisementHeader(
|
||||
const std::string &ble_advertisement_header_string);
|
||||
~BleAdvertisementHeader() = default;
|
||||
|
||||
BleAdvertisementHeader(const BleAdvertisementHeader &) = default;
|
||||
BleAdvertisementHeader &operator=(const BleAdvertisementHeader &) = default;
|
||||
BleAdvertisementHeader(BleAdvertisementHeader &&) = default;
|
||||
BleAdvertisementHeader &operator=(BleAdvertisementHeader &&) = default;
|
||||
|
||||
// Produces an encoded binary string which can be decoded by the explicit
|
||||
// constructor. The returned string is empty if BleAdvertisementHeader is not
|
||||
// valid - false on IsValid().
|
||||
explicit operator std::string() const;
|
||||
bool operator<(const BleAdvertisementHeader &rhs) const;
|
||||
|
||||
bool IsValid() const { return version_ == Version::kV2; }
|
||||
Version GetVersion() const { return version_; }
|
||||
int GetNumSlots() const { return num_slots_; }
|
||||
ByteArray GetServiceIdBloomFilter() const { return service_id_bloom_filter_; }
|
||||
ByteArray GetAdvertisementHash() const { return advertisement_hash_; }
|
||||
|
||||
private:
|
||||
static constexpr int kServiceIdBloomFilterLength = 10;
|
||||
static constexpr int kAdvertisementHashLength = 4;
|
||||
static constexpr int kMinAdvertisementHeaderLength =
|
||||
1 + kServiceIdBloomFilterLength + kAdvertisementHashLength;
|
||||
static constexpr int kVersionBitmask = 0x0E0;
|
||||
static constexpr int kNumSlotsBitmask = 0x01F;
|
||||
|
||||
Version version_ = Version::kUndefined;
|
||||
int num_slots_;
|
||||
ByteArray service_id_bloom_filter_;
|
||||
ByteArray advertisement_hash_;
|
||||
};
|
||||
|
||||
} // namespace mediums
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
|
||||
#endif // CORE_V2_INTERNAL_MEDIUMS_BLE_ADVERTISEMENT_HEADER_H_
|
||||
@@ -0,0 +1,176 @@
|
||||
#include "core_v2/internal/mediums/ble_advertisement_header.h"
|
||||
|
||||
#include "platform_v2/base/base64_utils.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace mediums {
|
||||
namespace {
|
||||
constexpr BleAdvertisementHeader::Version kVersion =
|
||||
BleAdvertisementHeader::Version::kV2;
|
||||
constexpr int kNumSlots = 2;
|
||||
constexpr char kServiceIDBloomFilter[] =
|
||||
"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a";
|
||||
constexpr char kAdvertisementHash[] = "\x0a\x0b\x0c\x0d";
|
||||
|
||||
TEST(BleAdvertisementHeaderTest, ConstructionWorks) {
|
||||
ByteArray service_id_bloom_filter(kServiceIDBloomFilter);
|
||||
ByteArray advertisement_hash(kAdvertisementHash);
|
||||
|
||||
BleAdvertisementHeader ble_advertisement_header(
|
||||
kVersion, kNumSlots, service_id_bloom_filter, advertisement_hash);
|
||||
|
||||
EXPECT_TRUE(ble_advertisement_header.IsValid());
|
||||
EXPECT_EQ(kVersion, ble_advertisement_header.GetVersion());
|
||||
EXPECT_EQ(kNumSlots, ble_advertisement_header.GetNumSlots());
|
||||
EXPECT_EQ(service_id_bloom_filter,
|
||||
ble_advertisement_header.GetServiceIdBloomFilter());
|
||||
EXPECT_EQ(advertisement_hash,
|
||||
ble_advertisement_header.GetAdvertisementHash());
|
||||
}
|
||||
|
||||
TEST(BleAdvertisementHeaderTest, ConstructionFailsWithBadVersion) {
|
||||
auto bad_version = static_cast<BleAdvertisementHeader::Version>(666);
|
||||
|
||||
ByteArray service_id_bloom_filter(kServiceIDBloomFilter);
|
||||
ByteArray advertisement_hash(kAdvertisementHash);
|
||||
|
||||
BleAdvertisementHeader ble_advertisement_header(
|
||||
bad_version, kNumSlots, service_id_bloom_filter, advertisement_hash);
|
||||
|
||||
EXPECT_FALSE(ble_advertisement_header.IsValid());
|
||||
}
|
||||
|
||||
TEST(BleAdvertisementHeaderTest,
|
||||
ConstructionFailsWithShortServiceIdBloomFilter) {
|
||||
char short_service_id_bloom_filter[] = "\x01\x02\x03\x04\x05\x06\x07\x08\x09";
|
||||
|
||||
ByteArray short_service_id_bloom_filter_bytes(short_service_id_bloom_filter);
|
||||
ByteArray advertisement_hash(kAdvertisementHash);
|
||||
|
||||
BleAdvertisementHeader ble_advertisement_header(
|
||||
kVersion, kNumSlots, short_service_id_bloom_filter_bytes,
|
||||
advertisement_hash);
|
||||
|
||||
EXPECT_FALSE(ble_advertisement_header.IsValid());
|
||||
}
|
||||
|
||||
TEST(BleAdvertisementHeaderTest,
|
||||
ConstructionFailsWithLongServiceIdBloomFilter) {
|
||||
char long_service_id_bloom_filter[] =
|
||||
"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b";
|
||||
|
||||
ByteArray service_id_bloom_filter(long_service_id_bloom_filter);
|
||||
ByteArray advertisement_hash(kAdvertisementHash);
|
||||
|
||||
BleAdvertisementHeader ble_advertisement_header(
|
||||
kVersion, kNumSlots, service_id_bloom_filter, advertisement_hash);
|
||||
|
||||
EXPECT_FALSE(ble_advertisement_header.IsValid());
|
||||
}
|
||||
|
||||
TEST(BleAdvertisementHeaderTest, ConstructionFailsWithShortAdvertisementHash) {
|
||||
char short_advertisement_hash[] = "\x0a\x0b\x0c";
|
||||
|
||||
ByteArray service_id_bloom_filter(kServiceIDBloomFilter);
|
||||
ByteArray advertisement_hash(short_advertisement_hash);
|
||||
|
||||
BleAdvertisementHeader ble_advertisement_header(
|
||||
kVersion, kNumSlots, service_id_bloom_filter, advertisement_hash);
|
||||
|
||||
EXPECT_FALSE(ble_advertisement_header.IsValid());
|
||||
}
|
||||
|
||||
TEST(BleAdvertisementHeaderTest, ConstructionFailsWithLongAdvertisementHash) {
|
||||
char long_advertisement_hash[] = "\x0a\x0b\x0c\x0d\0x0e";
|
||||
|
||||
ByteArray service_id_bloom_filter(kServiceIDBloomFilter);
|
||||
ByteArray advertisement_hash(long_advertisement_hash,
|
||||
sizeof(long_advertisement_hash) / sizeof(char));
|
||||
BleAdvertisementHeader ble_advertisement_header(
|
||||
kVersion, kNumSlots, service_id_bloom_filter, advertisement_hash);
|
||||
|
||||
EXPECT_FALSE(ble_advertisement_header.IsValid());
|
||||
}
|
||||
|
||||
TEST(BleAdvertisementHeaderTest, ConstructionFromSerializedStringWorks) {
|
||||
ByteArray service_id_bloom_filter(kServiceIDBloomFilter);
|
||||
ByteArray advertisement_hash(kAdvertisementHash);
|
||||
|
||||
BleAdvertisementHeader org_ble_advertisement_header(
|
||||
kVersion, kNumSlots, service_id_bloom_filter, advertisement_hash);
|
||||
auto ble_advertisement_header_string =
|
||||
std::string(org_ble_advertisement_header);
|
||||
|
||||
auto ble_advertisement_header =
|
||||
BleAdvertisementHeader(ble_advertisement_header_string);
|
||||
|
||||
EXPECT_TRUE(ble_advertisement_header.IsValid());
|
||||
EXPECT_EQ(kVersion, ble_advertisement_header.GetVersion());
|
||||
EXPECT_EQ(kNumSlots, ble_advertisement_header.GetNumSlots());
|
||||
EXPECT_EQ(service_id_bloom_filter,
|
||||
ble_advertisement_header.GetServiceIdBloomFilter());
|
||||
EXPECT_EQ(advertisement_hash,
|
||||
ble_advertisement_header.GetAdvertisementHash());
|
||||
}
|
||||
|
||||
TEST(BleAdvertisementHeaderTest, ConstructionFromExtraBytesWorks) {
|
||||
ByteArray service_id_bloom_filter(kServiceIDBloomFilter);
|
||||
ByteArray advertisement_hash(kAdvertisementHash);
|
||||
|
||||
BleAdvertisementHeader ble_advertisement_header(
|
||||
kVersion, kNumSlots, service_id_bloom_filter, advertisement_hash);
|
||||
auto ble_advertisement_header_string = std::string(ble_advertisement_header);
|
||||
|
||||
// Base64 decode the string, add a character, and then re-encode it.
|
||||
ByteArray ble_advertisement_header_bytes =
|
||||
Base64Utils::Decode(ble_advertisement_header_string);
|
||||
ByteArray long_ble_advertisement_header_bytes(
|
||||
ble_advertisement_header_bytes.size() + 1);
|
||||
long_ble_advertisement_header_bytes.CopyAt(0, ble_advertisement_header_bytes);
|
||||
std::string long_ble_advertisement_header_string =
|
||||
Base64Utils::Encode(long_ble_advertisement_header_bytes);
|
||||
|
||||
auto long_ble_advertisement_header =
|
||||
BleAdvertisementHeader(long_ble_advertisement_header_string);
|
||||
|
||||
EXPECT_TRUE(long_ble_advertisement_header.IsValid());
|
||||
EXPECT_EQ(kVersion, long_ble_advertisement_header.GetVersion());
|
||||
EXPECT_EQ(kNumSlots, long_ble_advertisement_header.GetNumSlots());
|
||||
EXPECT_EQ(service_id_bloom_filter,
|
||||
long_ble_advertisement_header.GetServiceIdBloomFilter());
|
||||
EXPECT_EQ(advertisement_hash,
|
||||
long_ble_advertisement_header.GetAdvertisementHash());
|
||||
}
|
||||
|
||||
TEST(BleAdvertisementHeaderTest, ConstructionFromShortLengthFails) {
|
||||
ByteArray service_id_bloom_filter(kServiceIDBloomFilter);
|
||||
ByteArray advertisement_hash(kAdvertisementHash);
|
||||
|
||||
BleAdvertisementHeader ble_advertisement_header(
|
||||
kVersion, kNumSlots, service_id_bloom_filter, advertisement_hash);
|
||||
auto ble_advertisement_header_string = std::string(ble_advertisement_header);
|
||||
|
||||
// Base64 decode the string, remove a character, and then re-encode it.
|
||||
ByteArray ble_advertisement_header_bytes =
|
||||
Base64Utils::Decode(ble_advertisement_header_string);
|
||||
ByteArray short_ble_advertisement_header_bytes(
|
||||
ble_advertisement_header_bytes.size() - 1);
|
||||
short_ble_advertisement_header_bytes.CopyAt(0,
|
||||
ble_advertisement_header_bytes);
|
||||
std::string short_ble_advertisement_header_string =
|
||||
Base64Utils::Encode(short_ble_advertisement_header_bytes);
|
||||
|
||||
auto short_ble_advertisement_header =
|
||||
BleAdvertisementHeader(short_ble_advertisement_header_string);
|
||||
|
||||
EXPECT_FALSE(short_ble_advertisement_header.IsValid());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mediums
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,223 @@
|
||||
#include "core_v2/internal/mediums/ble_advertisement.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace mediums {
|
||||
namespace {
|
||||
|
||||
const BleAdvertisement::Version kVersion = BleAdvertisement::Version::kV2;
|
||||
const BleAdvertisement::SocketVersion kSocketVersion =
|
||||
BleAdvertisement::SocketVersion::kV2;
|
||||
const char kServiceIDHashBytes[] = "\x0a\x0b\x0c";
|
||||
const char kData[] =
|
||||
"How much wood can a woodchuck chuck if a wood chuck would chuck wood?";
|
||||
// This corresponds to the length of a specific BleAdvertisement packed with the
|
||||
// kData given above. Be sure to update this if kData ever changes.
|
||||
const size_t kAdvertisementLength = 77;
|
||||
const size_t kLongAdvertisementLength = kAdvertisementLength + 1000;
|
||||
|
||||
TEST(BleAdvertisementTest, ConstructionWorksV1) {
|
||||
ByteArray service_id_hash{kServiceIDHashBytes};
|
||||
ByteArray data{kData};
|
||||
|
||||
BleAdvertisement ble_advertisement{BleAdvertisement::Version::kV1,
|
||||
BleAdvertisement::SocketVersion::kV1,
|
||||
service_id_hash, data};
|
||||
|
||||
EXPECT_TRUE(ble_advertisement.IsValid());
|
||||
EXPECT_EQ(BleAdvertisement::Version::kV1, ble_advertisement.GetVersion());
|
||||
EXPECT_EQ(BleAdvertisement::SocketVersion::kV1,
|
||||
ble_advertisement.GetSocketVersion());
|
||||
EXPECT_EQ(service_id_hash, ble_advertisement.GetServiceIdHash());
|
||||
EXPECT_EQ(data.size(), ble_advertisement.GetData().size());
|
||||
EXPECT_EQ(data, ble_advertisement.GetData());
|
||||
}
|
||||
|
||||
TEST(BleAdvertisementTest, ConstructionFailsWithBadVersion) {
|
||||
BleAdvertisement::Version bad_version =
|
||||
static_cast<BleAdvertisement::Version>(666);
|
||||
|
||||
ByteArray service_id_hash{kServiceIDHashBytes};
|
||||
ByteArray data{kData};
|
||||
|
||||
BleAdvertisement ble_advertisement{bad_version, kSocketVersion,
|
||||
service_id_hash, data};
|
||||
|
||||
EXPECT_FALSE(ble_advertisement.IsValid());
|
||||
}
|
||||
|
||||
TEST(BleAdvertisementTest, ConstructionFailsWithBadSocketVersion) {
|
||||
BleAdvertisement::SocketVersion bad_socket_version =
|
||||
static_cast<BleAdvertisement::SocketVersion>(666);
|
||||
|
||||
ByteArray service_id_hash{kServiceIDHashBytes};
|
||||
ByteArray data{kData};
|
||||
|
||||
BleAdvertisement ble_advertisement{kVersion, bad_socket_version,
|
||||
service_id_hash, data};
|
||||
|
||||
EXPECT_FALSE(ble_advertisement.IsValid());
|
||||
}
|
||||
|
||||
TEST(BleAdvertisementTest, ConstructionFailsWithShortServiceIdHash) {
|
||||
char short_service_id_hash_bytes[] = "\x0a\x0b";
|
||||
|
||||
ByteArray bad_service_id_hash{short_service_id_hash_bytes};
|
||||
ByteArray data{kData};
|
||||
|
||||
BleAdvertisement ble_advertisement{kVersion, kSocketVersion,
|
||||
bad_service_id_hash, data};
|
||||
|
||||
EXPECT_FALSE(ble_advertisement.IsValid());
|
||||
}
|
||||
|
||||
TEST(BleAdvertisementTest, ConstructionFailsWithLongServiceIdHash) {
|
||||
char long_service_id_hash_bytes[] = "\x0a\x0b\x0c\x0d";
|
||||
|
||||
ByteArray bad_service_id_hash{long_service_id_hash_bytes};
|
||||
ByteArray data{kData};
|
||||
|
||||
BleAdvertisement ble_advertisement{kVersion, kSocketVersion,
|
||||
bad_service_id_hash, data};
|
||||
|
||||
EXPECT_FALSE(ble_advertisement.IsValid());
|
||||
}
|
||||
|
||||
TEST(BleAdvertisementTest, ConstructionFailsWithLongData) {
|
||||
// BleAdvertisement shouldn't be able to support data with the max GATT
|
||||
// attribute length because it needs some room for the preceding fields.
|
||||
char long_data[512]{};
|
||||
|
||||
ByteArray service_id_hash{kServiceIDHashBytes};
|
||||
ByteArray bad_data{long_data, 512};
|
||||
|
||||
BleAdvertisement ble_advertisement{kVersion, kSocketVersion, service_id_hash,
|
||||
bad_data};
|
||||
|
||||
EXPECT_FALSE(ble_advertisement.IsValid());
|
||||
}
|
||||
|
||||
TEST(BleAdvertisementTest, ConstructionFromSerializedBytesWorks) {
|
||||
ByteArray service_id_hash{kServiceIDHashBytes};
|
||||
ByteArray data{kData};
|
||||
|
||||
BleAdvertisement org_ble_advertisement{kVersion, kSocketVersion,
|
||||
service_id_hash, data};
|
||||
ByteArray ble_advertisement_bytes{org_ble_advertisement};
|
||||
BleAdvertisement ble_advertisement{ble_advertisement_bytes};
|
||||
|
||||
EXPECT_TRUE(ble_advertisement.IsValid());
|
||||
EXPECT_EQ(kVersion, ble_advertisement.GetVersion());
|
||||
EXPECT_EQ(kSocketVersion, ble_advertisement.GetSocketVersion());
|
||||
EXPECT_EQ(service_id_hash, ble_advertisement.GetServiceIdHash());
|
||||
EXPECT_EQ(data.size(), ble_advertisement.GetData().size());
|
||||
EXPECT_EQ(data, ble_advertisement.GetData());
|
||||
}
|
||||
|
||||
TEST(BleAdvertisementTest, ConstructionFromSerializedBytesWithEmptyDataWorks) {
|
||||
char empty_data[0]{};
|
||||
|
||||
ByteArray service_id_hash{kServiceIDHashBytes};
|
||||
ByteArray data{empty_data};
|
||||
|
||||
BleAdvertisement org_ble_advertisement{kVersion, kSocketVersion,
|
||||
service_id_hash, data};
|
||||
ByteArray ble_advertisement_bytes{org_ble_advertisement};
|
||||
BleAdvertisement ble_advertisement{ble_advertisement_bytes};
|
||||
|
||||
EXPECT_TRUE(ble_advertisement.IsValid());
|
||||
EXPECT_EQ(kVersion, ble_advertisement.GetVersion());
|
||||
EXPECT_EQ(kSocketVersion, ble_advertisement.GetSocketVersion());
|
||||
EXPECT_EQ(service_id_hash, ble_advertisement.GetServiceIdHash());
|
||||
EXPECT_EQ(data.size(), ble_advertisement.GetData().size());
|
||||
EXPECT_EQ(data, ble_advertisement.GetData());
|
||||
}
|
||||
|
||||
TEST(BleAdvertisementTest, ConstructionFromExtraSerializedBytesWorks) {
|
||||
ByteArray service_id_hash{kServiceIDHashBytes};
|
||||
ByteArray data{kData};
|
||||
|
||||
BleAdvertisement org_ble_advertisement{kVersion, kSocketVersion,
|
||||
service_id_hash, data};
|
||||
ByteArray org_ble_advertisement_bytes{org_ble_advertisement};
|
||||
|
||||
// Copy the bytes into a new array with extra bytes. We must explicitly
|
||||
// define how long our array is because we can't use variable length arrays.
|
||||
char raw_ble_advertisement_bytes[kLongAdvertisementLength]{};
|
||||
memcpy(raw_ble_advertisement_bytes, org_ble_advertisement_bytes.data(),
|
||||
std::min(sizeof(raw_ble_advertisement_bytes),
|
||||
org_ble_advertisement_bytes.size()));
|
||||
|
||||
// Re-parse the Ble advertisement using our extra long advertisement bytes.
|
||||
ByteArray long_ble_advertisement_bytes{raw_ble_advertisement_bytes,
|
||||
kLongAdvertisementLength};
|
||||
BleAdvertisement long_ble_advertisement{long_ble_advertisement_bytes};
|
||||
|
||||
EXPECT_TRUE(long_ble_advertisement.IsValid());
|
||||
EXPECT_EQ(kVersion, long_ble_advertisement.GetVersion());
|
||||
EXPECT_EQ(kSocketVersion, long_ble_advertisement.GetSocketVersion());
|
||||
EXPECT_EQ(service_id_hash, long_ble_advertisement.GetServiceIdHash());
|
||||
EXPECT_EQ(data.size(), long_ble_advertisement.GetData().size());
|
||||
EXPECT_EQ(data, long_ble_advertisement.GetData());
|
||||
}
|
||||
|
||||
TEST(BleAdvertisementTest, ConstructionFromNullBytesFails) {
|
||||
BleAdvertisement ble_advertisement{ByteArray{}};
|
||||
|
||||
EXPECT_FALSE(ble_advertisement.IsValid());
|
||||
}
|
||||
|
||||
TEST(BleAdvertisementTest, ConstructionFromShortLengthSerializedBytesFails) {
|
||||
ByteArray service_id_hash{kServiceIDHashBytes};
|
||||
ByteArray data{kData};
|
||||
|
||||
BleAdvertisement org_ble_advertisement{kVersion, kSocketVersion,
|
||||
service_id_hash, data};
|
||||
ByteArray org_ble_advertisement_bytes{org_ble_advertisement};
|
||||
|
||||
// Cut off the advertisement so that it's too short.
|
||||
ByteArray short_ble_advertisement_bytes{org_ble_advertisement_bytes.data(),
|
||||
7};
|
||||
BleAdvertisement short_ble_advertisement{short_ble_advertisement_bytes};
|
||||
|
||||
EXPECT_FALSE(short_ble_advertisement.IsValid());
|
||||
}
|
||||
|
||||
TEST(BleAdvertisementTest,
|
||||
ConstructionFromSerializedBytesWithInvalidDataLengthFails) {
|
||||
ByteArray service_id_hash{kServiceIDHashBytes};
|
||||
ByteArray data{kData};
|
||||
|
||||
BleAdvertisement org_ble_advertisement{kVersion, kSocketVersion,
|
||||
service_id_hash, data};
|
||||
ByteArray org_ble_advertisement_bytes{org_ble_advertisement};
|
||||
|
||||
// Corrupt the DATA_SIZE bits. Start by making a raw copy of the Ble
|
||||
// advertisement bytes so we can modify it. We must explicitly define how
|
||||
// long our array is because we can't use variable length arrays.
|
||||
char raw_ble_advertisement_bytes[kAdvertisementLength];
|
||||
memcpy(raw_ble_advertisement_bytes, org_ble_advertisement_bytes.data(),
|
||||
kAdvertisementLength);
|
||||
|
||||
// The data size field lives in indices 4-7. Corrupt it.
|
||||
memset(raw_ble_advertisement_bytes + 4, 0xFF, 4);
|
||||
|
||||
// Try to parse the Ble advertisement using our corrupted advertisement bytes.
|
||||
ByteArray corrupted_ble_advertisement_bytes{raw_ble_advertisement_bytes,
|
||||
kAdvertisementLength};
|
||||
BleAdvertisement corrupted_ble_advertisement{
|
||||
corrupted_ble_advertisement_bytes};
|
||||
|
||||
EXPECT_FALSE(corrupted_ble_advertisement.IsValid());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mediums
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,59 @@
|
||||
#include "core_v2/internal/mediums/ble_packet.h"
|
||||
|
||||
#include "platform_v2/public/logging.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace mediums {
|
||||
|
||||
BlePacket::BlePacket(const ByteArray& service_id_hash, const ByteArray& data) {
|
||||
if (service_id_hash.size() != kServiceIdHashLength ||
|
||||
data.size() > kMaxDataSize) {
|
||||
return;
|
||||
}
|
||||
service_id_hash_ = service_id_hash;
|
||||
data_ = data;
|
||||
}
|
||||
|
||||
BlePacket::BlePacket(const ByteArray& ble_packet_bytes) {
|
||||
if (ble_packet_bytes.Empty()) {
|
||||
NEARBY_LOG(ERROR, "Cannot deserialize BlePacket: null bytes passed in");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ble_packet_bytes.size() < kServiceIdHashLength) {
|
||||
NEARBY_LOG(
|
||||
INFO,
|
||||
"Cannot deserialize BlePacket: expecting min %u raw bytes, got %zu",
|
||||
kServiceIdHashLength, ble_packet_bytes.size());
|
||||
return;
|
||||
}
|
||||
|
||||
const char *ble_packet_bytes_read_ptr = ble_packet_bytes.data();
|
||||
service_id_hash_ =
|
||||
ByteArray(ble_packet_bytes_read_ptr, kServiceIdHashLength);
|
||||
ble_packet_bytes_read_ptr += kServiceIdHashLength;
|
||||
|
||||
data_ = ByteArray(ble_packet_bytes_read_ptr,
|
||||
ble_packet_bytes.size() - kServiceIdHashLength);
|
||||
}
|
||||
|
||||
BlePacket::operator ByteArray() const {
|
||||
if (!IsValid()) {
|
||||
return ByteArray();
|
||||
}
|
||||
|
||||
std::string out;
|
||||
|
||||
out.reserve(service_id_hash_.size() + data_.size());
|
||||
out.append(std::string(service_id_hash_));
|
||||
out.append(std::string(data_));
|
||||
|
||||
return ByteArray(std::move(out));
|
||||
}
|
||||
|
||||
} // namespace mediums
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,51 @@
|
||||
#ifndef CORE_V2_INTERNAL_MEDIUMS_BLE_PACKET_H_
|
||||
#define CORE_V2_INTERNAL_MEDIUMS_BLE_PACKET_H_
|
||||
|
||||
#include <limits>
|
||||
|
||||
#include "platform_v2/base/byte_array.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace mediums {
|
||||
|
||||
// Represents the format of data sent over Ble sockets.
|
||||
//
|
||||
// [SERVICE_ID_HASH][DATA]
|
||||
//
|
||||
// See go/nearby-ble-design for more information.
|
||||
class BlePacket {
|
||||
public:
|
||||
static const std::uint32_t kServiceIdHashLength = 3;
|
||||
|
||||
BlePacket() = default;
|
||||
BlePacket(const ByteArray& service_id_hash, const ByteArray& data);
|
||||
explicit BlePacket(const ByteArray& ble_packet_byte);
|
||||
~BlePacket() = default;
|
||||
|
||||
BlePacket(const BlePacket&) = default;
|
||||
BlePacket& operator=(const BlePacket&) = default;
|
||||
BlePacket(BlePacket&&) = default;
|
||||
BlePacket& operator=(BlePacket&&) = default;
|
||||
|
||||
explicit operator ByteArray() const;
|
||||
|
||||
bool IsValid() const { return !service_id_hash_.Empty(); }
|
||||
ByteArray GetServiceIdHash() const { return service_id_hash_; }
|
||||
ByteArray GetData() const { return data_; }
|
||||
|
||||
private:
|
||||
static const std::uint32_t kMaxDataSize =
|
||||
std::numeric_limits<int32_t>::max() - kServiceIdHashLength;
|
||||
|
||||
ByteArray service_id_hash_;
|
||||
ByteArray data_;
|
||||
};
|
||||
|
||||
} // namespace mediums
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
|
||||
#endif // CORE_V2_INTERNAL_MEDIUMS_BLE_PACKET_H_
|
||||
@@ -0,0 +1,97 @@
|
||||
#include "core_v2/internal/mediums/ble_packet.h"
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace mediums {
|
||||
|
||||
constexpr char kServiceIDHash[] = "\x0a\x0b\x0c";
|
||||
constexpr char kData[] = "\x01\x02\x03\x04\x05";
|
||||
|
||||
TEST(BlePacketTest, ConstructionWorks) {
|
||||
ByteArray service_id_hash(kServiceIDHash);
|
||||
ByteArray data(kData);
|
||||
|
||||
BlePacket ble_packet(service_id_hash, data);
|
||||
|
||||
EXPECT_TRUE(ble_packet.IsValid());
|
||||
EXPECT_EQ(service_id_hash, ble_packet.GetServiceIdHash());
|
||||
EXPECT_EQ(data, ble_packet.GetData());
|
||||
}
|
||||
|
||||
TEST(BlePacketTest, ConstructionWorksWithEmptyData) {
|
||||
char empty_data[] = {};
|
||||
|
||||
ByteArray service_id_hash(kServiceIDHash);
|
||||
ByteArray data(empty_data);
|
||||
|
||||
BlePacket ble_packet(service_id_hash, data);
|
||||
|
||||
EXPECT_TRUE(ble_packet.IsValid());
|
||||
EXPECT_EQ(service_id_hash, ble_packet.GetServiceIdHash());
|
||||
EXPECT_EQ(data, ble_packet.GetData());
|
||||
}
|
||||
|
||||
TEST(BlePacketTest, ConstructionFailsWithShortServiceIdHash) {
|
||||
char short_service_id_hash[] = "\x0a\x0b";
|
||||
|
||||
ByteArray service_id_hash(short_service_id_hash);
|
||||
ByteArray data(kData);
|
||||
|
||||
BlePacket ble_packet(service_id_hash, data);
|
||||
|
||||
EXPECT_FALSE(ble_packet.IsValid());
|
||||
}
|
||||
|
||||
TEST(BlePacketTest, ConstructionFailsWithLongServiceIdHash) {
|
||||
char long_service_id_hash[] = "\x0a\x0b\x0c\x0d";
|
||||
|
||||
ByteArray service_id_hash(long_service_id_hash);
|
||||
ByteArray data(kData);
|
||||
|
||||
BlePacket ble_packet(service_id_hash, data);
|
||||
|
||||
EXPECT_FALSE(ble_packet.IsValid());
|
||||
}
|
||||
|
||||
TEST(BlePacketTest, ConstructionFromSerializedBytesWorks) {
|
||||
ByteArray service_id_hash(kServiceIDHash);
|
||||
ByteArray data(kData);
|
||||
|
||||
BlePacket org_ble_packet(service_id_hash, data);
|
||||
ByteArray ble_packet_bytes(org_ble_packet);
|
||||
|
||||
BlePacket ble_packet(ble_packet_bytes);
|
||||
|
||||
EXPECT_TRUE(ble_packet.IsValid());
|
||||
EXPECT_EQ(service_id_hash, ble_packet.GetServiceIdHash());
|
||||
EXPECT_EQ(data, ble_packet.GetData());
|
||||
}
|
||||
|
||||
TEST(BlePacketTest, ConstructionFromNullBytesFails) {
|
||||
BlePacket ble_packet(ByteArray{});
|
||||
|
||||
EXPECT_FALSE(ble_packet.IsValid());
|
||||
}
|
||||
|
||||
TEST(BlePacketTest, ConstructionFromShortLengthDataFails) {
|
||||
ByteArray service_id_hash(kServiceIDHash);
|
||||
ByteArray data(kData);
|
||||
|
||||
BlePacket org_ble_packet(service_id_hash, data);
|
||||
ByteArray org_ble_packet_bytes(org_ble_packet);
|
||||
|
||||
// Cut off the packet so that it's too short
|
||||
ByteArray short_ble_packet_bytes(ByteArray(org_ble_packet_bytes.data(), 2));
|
||||
|
||||
BlePacket short_ble_packet(short_ble_packet_bytes);
|
||||
|
||||
EXPECT_FALSE(short_ble_packet.IsValid());
|
||||
}
|
||||
|
||||
} // namespace mediums
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,36 @@
|
||||
#ifndef CORE_V2_INTERNAL_MEDIUMS_BLE_PERIPHERAL_H_
|
||||
#define CORE_V2_INTERNAL_MEDIUMS_BLE_PERIPHERAL_H_
|
||||
|
||||
#include "platform_v2/base/byte_array.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace mediums {
|
||||
|
||||
class BlePeripheral {
|
||||
public:
|
||||
BlePeripheral() = default;
|
||||
explicit BlePeripheral(const ByteArray& id) : id_(id) {}
|
||||
~BlePeripheral() = default;
|
||||
|
||||
BlePeripheral(const BlePeripheral&) = default;
|
||||
BlePeripheral& operator=(const BlePeripheral&) = default;
|
||||
BlePeripheral(BlePeripheral&&) = default;
|
||||
BlePeripheral& operator=(BlePeripheral&&) = default;
|
||||
|
||||
bool IsValid() const { return !id_.Empty(); }
|
||||
ByteArray GetId() const { return id_; }
|
||||
|
||||
private:
|
||||
// A unique identifier for this peripheral. It can be the BLE advertisement it
|
||||
// was found on, or even simply the BLE MAC address.
|
||||
ByteArray id_;
|
||||
};
|
||||
|
||||
} // namespace mediums
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
|
||||
#endif // CORE_V2_INTERNAL_MEDIUMS_BLE_PERIPHERAL_H_
|
||||
@@ -0,0 +1,33 @@
|
||||
#include "core_v2/internal/mediums/ble_peripheral.h"
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace mediums {
|
||||
namespace {
|
||||
|
||||
const char kId[] = "AB12";
|
||||
|
||||
TEST(BlePeripheralTest, ConstructionWorks) {
|
||||
ByteArray id(kId);
|
||||
|
||||
BlePeripheral ble_peripheral(id);
|
||||
|
||||
EXPECT_TRUE(ble_peripheral.IsValid());
|
||||
EXPECT_EQ(id, ble_peripheral.GetId());
|
||||
}
|
||||
|
||||
TEST(BlePeripheralTest, ConstructionEmptyFails) {
|
||||
BlePeripheral ble_peripheral;
|
||||
|
||||
EXPECT_FALSE(ble_peripheral.IsValid());
|
||||
EXPECT_TRUE(ble_peripheral.GetId().Empty());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mediums
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,104 @@
|
||||
#include "core_v2/internal/mediums/bluetooth_radio.h"
|
||||
|
||||
#include "platform_v2/base/exception.h"
|
||||
#include "platform_v2/public/logging.h"
|
||||
#include "platform_v2/public/system_clock.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
|
||||
BluetoothRadio::BluetoothRadio() {
|
||||
if (!IsAdapterValid()) {
|
||||
NEARBY_LOG(ERROR, "Bluetooth adapter is not valid: BT is not supported");
|
||||
}
|
||||
}
|
||||
|
||||
BluetoothRadio::~BluetoothRadio() {
|
||||
// We never enabled Bluetooth, nothing to do.
|
||||
if (!ever_saved_state_.Get()) {
|
||||
NEARBY_LOG(INFO, "BT adapter was not used. Not touching HW.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle Bluetooth regardless of our original state. Some devices/chips can
|
||||
// start to freak out after some time (e.g. b/37775337), and this helps to
|
||||
// ensure BT resets properly.
|
||||
NEARBY_LOG(INFO, "Toggle BT adapter state before releasing adapter.");
|
||||
Toggle();
|
||||
|
||||
NEARBY_LOG(INFO, "Bring BT adapter to original state");
|
||||
if (!SetBluetoothState(originally_enabled_.Get())) {
|
||||
NEARBY_LOG(INFO, "Failed to restore BT adapter original state.");
|
||||
}
|
||||
}
|
||||
|
||||
bool BluetoothRadio::Enable() {
|
||||
if (!SaveOriginalState()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return SetBluetoothState(true);
|
||||
}
|
||||
|
||||
bool BluetoothRadio::Disable() {
|
||||
if (!SaveOriginalState()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return SetBluetoothState(false);
|
||||
}
|
||||
|
||||
bool BluetoothRadio::IsEnabled() const {
|
||||
return IsAdapterValid() && IsInDesiredState(true);
|
||||
}
|
||||
|
||||
bool BluetoothRadio::Toggle() {
|
||||
if (!SaveOriginalState()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SetBluetoothState(false)) {
|
||||
NEARBY_LOG(INFO, "BT Toggle: Failed to turn BT off.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (SystemClock::Sleep(kPauseBetweenToggle).Raised(Exception::kInterrupted)) {
|
||||
NEARBY_LOG(INFO, "BT Toggle: interrupted before turing on.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SetBluetoothState(true)) {
|
||||
NEARBY_LOG(INFO, "BT Toggle: Failed to turn BT on.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BluetoothRadio::SetBluetoothState(bool enable) {
|
||||
return bluetooth_adapter_.SetStatus(
|
||||
enable ? BluetoothAdapter::Status::kEnabled
|
||||
: BluetoothAdapter::Status::kDisabled);
|
||||
}
|
||||
|
||||
bool BluetoothRadio::IsInDesiredState(bool should_be_enabled) const {
|
||||
return bluetooth_adapter_.IsEnabled() == should_be_enabled;
|
||||
}
|
||||
|
||||
bool BluetoothRadio::SaveOriginalState() {
|
||||
if (!IsAdapterValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we haven't saved the original state of the radio, save it.
|
||||
if (!ever_saved_state_.Set(true)) {
|
||||
originally_enabled_.Set(bluetooth_adapter_.IsEnabled());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,80 @@
|
||||
#ifndef CORE_V2_INTERNAL_MEDIUMS_BLUETOOTH_RADIO_H_
|
||||
#define CORE_V2_INTERNAL_MEDIUMS_BLUETOOTH_RADIO_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "platform_v2/public/atomic_boolean.h"
|
||||
#include "platform_v2/public/bluetooth_adapter.h"
|
||||
#include "absl/time/clock.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
|
||||
// Provides the operations that can be performed on the Bluetooth radio.
|
||||
class BluetoothRadio {
|
||||
public:
|
||||
BluetoothRadio();
|
||||
BluetoothRadio(BluetoothRadio&&) = default;
|
||||
BluetoothRadio& operator=(BluetoothRadio&&) = default;
|
||||
|
||||
// Reverts the Bluetooth radio to its original state.
|
||||
~BluetoothRadio();
|
||||
|
||||
// Enables Bluetooth.
|
||||
//
|
||||
// This must be called before attempting to invoke any other methods of
|
||||
// this class.
|
||||
//
|
||||
// Returns true if enabled successfully.
|
||||
bool Enable();
|
||||
|
||||
// Disables Bluetooth.
|
||||
//
|
||||
// Returns true if disabled successfully.
|
||||
bool Disable();
|
||||
|
||||
// Returns true if the Bluetooth radio is currently enabled.
|
||||
bool IsEnabled() const;
|
||||
|
||||
// Turn BT radio Off, delay for kPauseBetweenToggle and then turn it On.
|
||||
// This will block calling thread for at least kPauseBetweenToggle duration.
|
||||
bool Toggle();
|
||||
|
||||
// Returns result of BluetoothAdapter::IsValid() for private adapter instance.
|
||||
bool IsAdapterValid() const {
|
||||
return bluetooth_adapter_.IsValid();
|
||||
}
|
||||
|
||||
BluetoothAdapter& GetBluetoothAdapter() {
|
||||
return bluetooth_adapter_;
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr absl::Duration kPauseBetweenToggle = absl::Seconds(3);
|
||||
|
||||
bool SetBluetoothState(bool enable);
|
||||
bool IsInDesiredState(bool should_be_enabled) const;
|
||||
// To be called in enable(), disable(), and toggle(). This will remember the
|
||||
// original state of the radio before any radio state has been modified.
|
||||
// Returns false if Bluetooth doesn't exist on the device and the state cannot
|
||||
// be obtained.
|
||||
bool SaveOriginalState();
|
||||
|
||||
// BluetoothAdapter::IsValid() will return false if BT is not supported.
|
||||
BluetoothAdapter bluetooth_adapter_;
|
||||
|
||||
// The Bluetooth radio's original state, before we modified it. True if
|
||||
// originally enabled, false if originally disabled.
|
||||
// We restore the radio to its original state in the destructor.
|
||||
|
||||
AtomicBoolean originally_enabled_{false};
|
||||
// false if we never modified the radio state, true otherwise.
|
||||
AtomicBoolean ever_saved_state_{false};
|
||||
};
|
||||
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
|
||||
#endif // CORE_V2_INTERNAL_MEDIUMS_BLUETOOTH_RADIO_H_
|
||||
@@ -0,0 +1,45 @@
|
||||
#include "core_v2/internal/mediums/bluetooth_radio.h"
|
||||
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace {
|
||||
|
||||
TEST(BluetoothRadioTest, ConstructorDestructorWorks) {
|
||||
BluetoothRadio radio;
|
||||
EXPECT_TRUE(radio.IsAdapterValid());
|
||||
}
|
||||
|
||||
TEST(BluetoothRadioTest, CanEnable) {
|
||||
BluetoothRadio radio;
|
||||
EXPECT_TRUE(radio.IsAdapterValid());
|
||||
EXPECT_FALSE(radio.IsEnabled());
|
||||
EXPECT_TRUE(radio.Enable());
|
||||
EXPECT_TRUE(radio.IsEnabled());
|
||||
}
|
||||
|
||||
TEST(BluetoothRadioTest, CanDisable) {
|
||||
BluetoothRadio radio;
|
||||
EXPECT_TRUE(radio.IsAdapterValid());
|
||||
EXPECT_FALSE(radio.IsEnabled());
|
||||
EXPECT_TRUE(radio.Enable());
|
||||
EXPECT_TRUE(radio.IsEnabled());
|
||||
EXPECT_TRUE(radio.Disable());
|
||||
EXPECT_FALSE(radio.IsEnabled());
|
||||
}
|
||||
|
||||
TEST(BluetoothRadioTest, CanToggle) {
|
||||
BluetoothRadio radio;
|
||||
EXPECT_TRUE(radio.IsAdapterValid());
|
||||
EXPECT_FALSE(radio.IsEnabled());
|
||||
EXPECT_TRUE(radio.Toggle());
|
||||
EXPECT_TRUE(radio.IsEnabled());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,80 @@
|
||||
#ifndef CORE_V2_INTERNAL_MEDIUMS_LOST_ENTITY_TRACKER_H_
|
||||
#define CORE_V2_INTERNAL_MEDIUMS_LOST_ENTITY_TRACKER_H_
|
||||
|
||||
#include "platform_v2/public/mutex.h"
|
||||
#include "platform_v2/public/mutex_lock.h"
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace mediums {
|
||||
|
||||
// Tracks "lost" entities based on a manual update/compute model. Used by
|
||||
// mediums that only report found devices. Lost entities are computed based off
|
||||
// of whether a specific entity was rediscovered since the last call to
|
||||
// ComputeLostEntities.
|
||||
//
|
||||
// Note: Entity must overload the < and == operators.
|
||||
template <typename Entity>
|
||||
class LostEntityTracker {
|
||||
public:
|
||||
using EntitySet = absl::flat_hash_set<Entity>;
|
||||
|
||||
LostEntityTracker();
|
||||
~LostEntityTracker();
|
||||
|
||||
// Records the given entity as being recently found, whether or not this is
|
||||
// our first time discovering the entity.
|
||||
void RecordFoundEntity(const Entity& entity) ABSL_LOCKS_EXCLUDED(mutex_);
|
||||
|
||||
// Computes and returns the set of entities considered lost since the last
|
||||
// time this method was called.
|
||||
EntitySet ComputeLostEntities() ABSL_LOCKS_EXCLUDED(mutex_);
|
||||
|
||||
private:
|
||||
Mutex mutex_;
|
||||
EntitySet current_entities_ ABSL_GUARDED_BY(mutex_);
|
||||
EntitySet previously_found_entities_ ABSL_GUARDED_BY(mutex_);
|
||||
};
|
||||
|
||||
template <typename Entity>
|
||||
LostEntityTracker<Entity>::LostEntityTracker()
|
||||
: current_entities_{}, previously_found_entities_{} {}
|
||||
|
||||
template <typename Entity>
|
||||
LostEntityTracker<Entity>::~LostEntityTracker() {
|
||||
previously_found_entities_.clear();
|
||||
current_entities_.clear();
|
||||
}
|
||||
|
||||
template <typename Entity>
|
||||
void LostEntityTracker<Entity>::RecordFoundEntity(const Entity& entity) {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
current_entities_.insert(entity);
|
||||
}
|
||||
|
||||
template <typename Entity>
|
||||
typename LostEntityTracker<Entity>::EntitySet
|
||||
LostEntityTracker<Entity>::ComputeLostEntities() {
|
||||
MutexLock lock(&mutex_);
|
||||
|
||||
// The set of lost entities is the previously found set MINUS the currently
|
||||
// found set.
|
||||
for (const auto& item : current_entities_) {
|
||||
previously_found_entities_.erase(item);
|
||||
}
|
||||
auto lost_entities = std::move(previously_found_entities_);
|
||||
previously_found_entities_ = std::move(current_entities_);
|
||||
current_entities_ = {};
|
||||
|
||||
return lost_entities;
|
||||
}
|
||||
|
||||
} // namespace mediums
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
|
||||
#endif // CORE_V2_INTERNAL_MEDIUMS_LOST_ENTITY_TRACKER_H_
|
||||
@@ -0,0 +1,123 @@
|
||||
#include "core_v2/internal/mediums/lost_entity_tracker.h"
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace mediums {
|
||||
namespace {
|
||||
|
||||
struct TestEntity {
|
||||
int id;
|
||||
|
||||
template <typename H>
|
||||
friend H AbslHashValue(H h, const TestEntity& test_entity) {
|
||||
return H::combine(std::move(h), test_entity.id);
|
||||
}
|
||||
|
||||
bool operator==(const TestEntity& other) const { return id == other.id; }
|
||||
bool operator<(const TestEntity& other) const { return id < other.id; }
|
||||
};
|
||||
|
||||
TEST(LostEntityTrackerTest, NoEntitiesLost) {
|
||||
LostEntityTracker<TestEntity> lost_entity_tracker;
|
||||
TestEntity entity_1{1};
|
||||
TestEntity entity_2{2};
|
||||
TestEntity entity_3{3};
|
||||
|
||||
// Discover some entities.
|
||||
lost_entity_tracker.RecordFoundEntity(entity_1);
|
||||
lost_entity_tracker.RecordFoundEntity(entity_2);
|
||||
lost_entity_tracker.RecordFoundEntity(entity_3);
|
||||
|
||||
// Make sure none are lost on the first round.
|
||||
ASSERT_TRUE(lost_entity_tracker.ComputeLostEntities().empty());
|
||||
|
||||
// Rediscover the same entities.
|
||||
lost_entity_tracker.RecordFoundEntity(entity_1);
|
||||
lost_entity_tracker.RecordFoundEntity(entity_2);
|
||||
lost_entity_tracker.RecordFoundEntity(entity_3);
|
||||
|
||||
// Make sure we still didn't lose any entities.
|
||||
EXPECT_TRUE(lost_entity_tracker.ComputeLostEntities().empty());
|
||||
}
|
||||
|
||||
TEST(LostEntityTrackerTest, AllEntitiesLost) {
|
||||
LostEntityTracker<TestEntity> lost_entity_tracker;
|
||||
TestEntity entity_1{1};
|
||||
TestEntity entity_2{2};
|
||||
TestEntity entity_3{3};
|
||||
|
||||
// Discover some entities.
|
||||
lost_entity_tracker.RecordFoundEntity(entity_1);
|
||||
lost_entity_tracker.RecordFoundEntity(entity_2);
|
||||
lost_entity_tracker.RecordFoundEntity(entity_3);
|
||||
|
||||
// Make sure none are lost on the first round.
|
||||
EXPECT_TRUE(lost_entity_tracker.ComputeLostEntities().empty());
|
||||
|
||||
// Go through a round without rediscovering any entities.
|
||||
typename LostEntityTracker<TestEntity>::EntitySet lost_entities =
|
||||
lost_entity_tracker.ComputeLostEntities();
|
||||
EXPECT_TRUE(lost_entities.find(entity_1) != lost_entities.end());
|
||||
EXPECT_TRUE(lost_entities.find(entity_2) != lost_entities.end());
|
||||
EXPECT_TRUE(lost_entities.find(entity_3) != lost_entities.end());
|
||||
}
|
||||
|
||||
TEST(LostEntityTrackerTest, SomeEntitiesLost) {
|
||||
LostEntityTracker<TestEntity> lost_entity_tracker;
|
||||
TestEntity entity_1{1};
|
||||
TestEntity entity_2{2};
|
||||
TestEntity entity_3{3};
|
||||
|
||||
// Discover some entities.
|
||||
lost_entity_tracker.RecordFoundEntity(entity_1);
|
||||
lost_entity_tracker.RecordFoundEntity(entity_2);
|
||||
|
||||
// Make sure none are lost on the first round.
|
||||
EXPECT_TRUE(lost_entity_tracker.ComputeLostEntities().empty());
|
||||
|
||||
// Go through the next round only rediscovering one of our entities and
|
||||
// discovering an additional entity as well. Then, verify that only one entity
|
||||
// was lost after the check.
|
||||
lost_entity_tracker.RecordFoundEntity(entity_1);
|
||||
lost_entity_tracker.RecordFoundEntity(entity_3);
|
||||
typename LostEntityTracker<TestEntity>::EntitySet lost_entities =
|
||||
lost_entity_tracker.ComputeLostEntities();
|
||||
EXPECT_TRUE(lost_entities.find(entity_1) == lost_entities.end());
|
||||
EXPECT_TRUE(lost_entities.find(entity_2) != lost_entities.end());
|
||||
EXPECT_TRUE(lost_entities.find(entity_3) == lost_entities.end());
|
||||
}
|
||||
|
||||
TEST(LostEntityTrackerTest, SameEntityMultipleCopies) {
|
||||
LostEntityTracker<TestEntity> lost_entity_tracker;
|
||||
TestEntity entity_1{1};
|
||||
TestEntity entity_1_copy{1};
|
||||
|
||||
// Discover an entity.
|
||||
lost_entity_tracker.RecordFoundEntity(entity_1);
|
||||
|
||||
// Make sure none are lost on the first round.
|
||||
EXPECT_TRUE(lost_entity_tracker.ComputeLostEntities().empty());
|
||||
|
||||
// Rediscover the same entity, but through a copy of it.
|
||||
lost_entity_tracker.RecordFoundEntity(entity_1_copy);
|
||||
|
||||
// Make sure none are lost on the second round.
|
||||
EXPECT_TRUE(lost_entity_tracker.ComputeLostEntities().empty());
|
||||
|
||||
// Go through a round without rediscovering any entities and verify that we
|
||||
// lost an entity equivalent to both copies of it.
|
||||
typename LostEntityTracker<TestEntity>::EntitySet lost_entities =
|
||||
lost_entity_tracker.ComputeLostEntities();
|
||||
EXPECT_EQ(lost_entities.size(), 1);
|
||||
EXPECT_TRUE(lost_entities.find(entity_1) != lost_entities.end());
|
||||
EXPECT_TRUE(lost_entities.find(entity_1_copy) != lost_entities.end());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mediums
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,41 @@
|
||||
#include "core_v2/internal/mediums/utils.h"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "platform_v2/base/prng.h"
|
||||
#include "platform_v2/public/crypto.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
|
||||
ByteArray Utils::GenerateRandomBytes(size_t length) {
|
||||
Prng rng;
|
||||
std::string data;
|
||||
data.reserve(length);
|
||||
|
||||
// Adds 4 random bytes per iteration.
|
||||
while (length > 0) {
|
||||
std::uint32_t val = rng.NextUint32();
|
||||
for (int i = 0; i < 4; i++) {
|
||||
data += val & 0xFF;
|
||||
val >>= 8;
|
||||
length--;
|
||||
|
||||
if (!length) break;
|
||||
}
|
||||
}
|
||||
|
||||
return ByteArray(data);
|
||||
}
|
||||
|
||||
ByteArray Utils::Sha256Hash(const ByteArray& source, size_t length) {
|
||||
ByteArray full_hash(length);
|
||||
full_hash.CopyAt(0, Crypto::Sha256(std::string(source)));
|
||||
return full_hash;
|
||||
}
|
||||
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,22 @@
|
||||
#ifndef CORE_V2_INTERNAL_MEDIUMS_UTILS_H_
|
||||
#define CORE_V2_INTERNAL_MEDIUMS_UTILS_H_
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "platform_v2/base/byte_array.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
|
||||
class Utils {
|
||||
public:
|
||||
static ByteArray GenerateRandomBytes(size_t length);
|
||||
static ByteArray Sha256Hash(const ByteArray& source, size_t length);
|
||||
};
|
||||
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
|
||||
#endif // CORE_V2_INTERNAL_MEDIUMS_UTILS_H_
|
||||
@@ -0,0 +1,75 @@
|
||||
#include "core_v2/internal/mediums/uuid.h"
|
||||
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
#include "platform_v2/public/crypto.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace {
|
||||
std::ostream& write_hex(std::ostream& os, absl::string_view data) {
|
||||
for (const auto b : data) {
|
||||
os << std::setfill('0')
|
||||
<< std::setw(2)
|
||||
<< std::hex
|
||||
<< (static_cast<unsigned int>(b) & 0x0ff);
|
||||
}
|
||||
return os;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Uuid::Uuid(absl::string_view data) : data_(Crypto::Md5(data)) {
|
||||
// Based on the Java counterpart at
|
||||
// http://androidxref.com/8.0.0_r4/xref/libcore/ojluni/src/main/java/java/util/UUID.java#162.
|
||||
data_[6] &= 0x0f; // Clear version.
|
||||
data_[6] |= 0x30; // Set to version 3.
|
||||
data_[8] &= 0x3f; // Clear variant.
|
||||
data_[8] |= 0x80; // Set to IETF variant.
|
||||
}
|
||||
|
||||
Uuid::Uuid(std::uint64_t most_sig_bits, std::uint64_t least_sig_bits) {
|
||||
// Base on the Java counterpart at
|
||||
// http://androidxref.com/8.0.0_r4/xref/libcore/ojluni/src/main/java/java/util/UUID.java#104.
|
||||
data_.reserve(sizeof(most_sig_bits) + sizeof(least_sig_bits));
|
||||
|
||||
data_[0] = static_cast<char>((most_sig_bits >> 56) & 0x0ff);
|
||||
data_[1] = static_cast<char>((most_sig_bits >> 48) & 0x0ff);
|
||||
data_[2] = static_cast<char>((most_sig_bits >> 40) & 0x0ff);
|
||||
data_[3] = static_cast<char>((most_sig_bits >> 32) & 0x0ff);
|
||||
data_[4] = static_cast<char>((most_sig_bits >> 24) & 0x0ff);
|
||||
data_[5] = static_cast<char>((most_sig_bits >> 16) & 0x0ff);
|
||||
data_[6] = static_cast<char>((most_sig_bits >> 8) & 0x0ff);
|
||||
data_[7] = static_cast<char>((most_sig_bits >> 0) & 0x0ff);
|
||||
|
||||
data_[8] = static_cast<char>((least_sig_bits >> 56) & 0x0ff);
|
||||
data_[9] = static_cast<char>((least_sig_bits >> 48) & 0x0ff);
|
||||
data_[10] = static_cast<char>((least_sig_bits >> 40) & 0x0ff);
|
||||
data_[11] = static_cast<char>((least_sig_bits >> 32) & 0x0ff);
|
||||
data_[12] = static_cast<char>((least_sig_bits >> 24) & 0x0ff);
|
||||
data_[13] = static_cast<char>((least_sig_bits >> 16) & 0x0ff);
|
||||
data_[14] = static_cast<char>((least_sig_bits >> 8) & 0x0ff);
|
||||
data_[15] = static_cast<char>((least_sig_bits >> 0) & 0x0ff);
|
||||
}
|
||||
|
||||
Uuid::operator std::string() const {
|
||||
// Based on the Java counterpart at
|
||||
// http://androidxref.com/8.0.0_r4/xref/libcore/ojluni/src/main/java/java/util/UUID.java#375.
|
||||
std::ostringstream md5_hex;
|
||||
write_hex(md5_hex, absl::string_view(&data_[0], 4));
|
||||
md5_hex << "-";
|
||||
write_hex(md5_hex, absl::string_view(&data_[4], 2));
|
||||
md5_hex << "-";
|
||||
write_hex(md5_hex, absl::string_view(&data_[6], 2));
|
||||
md5_hex << "-";
|
||||
write_hex(md5_hex, absl::string_view(&data_[8], 2));
|
||||
md5_hex << "-";
|
||||
write_hex(md5_hex, absl::string_view(&data_[10], 6));
|
||||
|
||||
return md5_hex.str();
|
||||
}
|
||||
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,45 @@
|
||||
#ifndef CORE_V2_INTERNAL_MEDIUMS_UUID_H_
|
||||
#define CORE_V2_INTERNAL_MEDIUMS_UUID_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "absl/strings/string_view.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
|
||||
// A type 3 name-based
|
||||
// (https://en.wikipedia.org/wiki/Universally_unique_identifier#Versions_3_and_5_(namespace_name-based))
|
||||
// UUID.
|
||||
//
|
||||
// https://developer.android.com/reference/java/util/UUID.html
|
||||
class Uuid final {
|
||||
public:
|
||||
Uuid() : Uuid("uuid") {}
|
||||
explicit Uuid(absl::string_view data);
|
||||
Uuid(std::uint64_t most_sig_bits, std::uint64_t least_sig_bits);
|
||||
Uuid(const Uuid&) = default;
|
||||
Uuid& operator=(const Uuid&) = default;
|
||||
Uuid(Uuid&&) = default;
|
||||
Uuid& operator=(Uuid&&) = default;
|
||||
~Uuid() = default;
|
||||
|
||||
// Returns the canonical textual representation
|
||||
// (https://en.wikipedia.org/wiki/Universally_unique_identifier#Format) of the
|
||||
// UUID.
|
||||
explicit operator std::string() const;
|
||||
std::string data() const {
|
||||
return data_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::string data_;
|
||||
};
|
||||
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
|
||||
#endif // CORE_V2_INTERNAL_MEDIUMS_UUID_H_
|
||||
@@ -0,0 +1,56 @@
|
||||
#include "core_v2/internal/mediums/uuid.h"
|
||||
|
||||
#include "platform_v2/public/crypto.h"
|
||||
#include "platform_v2/public/logging.h"
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace {
|
||||
|
||||
constexpr char kString[] = "some string";
|
||||
constexpr std::uint64_t kNum1 = 0x123456789abcdef0;
|
||||
constexpr std::uint64_t kNum2 = 0x21436587a9cbed0f;
|
||||
|
||||
TEST(UuidTest, CreateFromStringWithMd5) {
|
||||
Uuid uuid(kString);
|
||||
std::string uuid_str(uuid);
|
||||
std::string uuid_data(uuid.data());
|
||||
std::string md5_data(Crypto::Md5(kString));
|
||||
NEARBY_LOG(INFO, "MD5-based UUID: '%s'", uuid_str.c_str());
|
||||
uuid_data[6] = 0;
|
||||
uuid_data[8] = 0;
|
||||
md5_data[6] = 0;
|
||||
md5_data[8] = 0;
|
||||
EXPECT_EQ(md5_data, uuid_data);
|
||||
}
|
||||
|
||||
TEST(UuidTest, CreateFromBinary) {
|
||||
Uuid uuid(kNum1, kNum2);
|
||||
std::string uuid_data(uuid.data());
|
||||
std::string uuid_str(uuid);
|
||||
NEARBY_LOG(INFO, "UUID: '%s'", uuid_str.c_str());
|
||||
EXPECT_EQ(uuid_data[0], (kNum1 >> 56) & 0xFF);
|
||||
EXPECT_EQ(uuid_data[1], (kNum1 >> 48) & 0xFF);
|
||||
EXPECT_EQ(uuid_data[2], (kNum1 >> 40) & 0xFF);
|
||||
EXPECT_EQ(uuid_data[3], (kNum1 >> 32) & 0xFF);
|
||||
EXPECT_EQ(uuid_data[4], (kNum1 >> 24) & 0xFF);
|
||||
EXPECT_EQ(uuid_data[5], (kNum1 >> 16) & 0xFF);
|
||||
EXPECT_EQ(uuid_data[6], (kNum1 >> 8) & 0xFF);
|
||||
EXPECT_EQ(uuid_data[7], (kNum1 >> 0) & 0xFF);
|
||||
EXPECT_EQ(uuid_data[8], (kNum2 >> 56) & 0xFF);
|
||||
EXPECT_EQ(uuid_data[9], (kNum2 >> 48) & 0xFF);
|
||||
EXPECT_EQ(uuid_data[10], (kNum2 >> 40) & 0xFF);
|
||||
EXPECT_EQ(uuid_data[11], (kNum2 >> 32) & 0xFF);
|
||||
EXPECT_EQ(uuid_data[12], (kNum2 >> 24) & 0xFF);
|
||||
EXPECT_EQ(uuid_data[13], (kNum2 >> 16) & 0xFF);
|
||||
EXPECT_EQ(uuid_data[14], (kNum2 >> 8) & 0xFF);
|
||||
EXPECT_EQ(uuid_data[15], (kNum2 >> 0) & 0xFF);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,76 @@
|
||||
cc_library(
|
||||
name = "webrtc",
|
||||
srcs = [
|
||||
"webrtc_socket.cc",
|
||||
],
|
||||
hdrs = [
|
||||
"webrtc_socket.h",
|
||||
],
|
||||
deps = [
|
||||
"//core_v2:core_types",
|
||||
"//platform_v2/base",
|
||||
"//platform_v2/public",
|
||||
"//platform_v2/public:logging",
|
||||
"//webrtc/api:libjingle_peerconnection_api",
|
||||
],
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "webrtc_test",
|
||||
srcs = ["webrtc_socket_test.cc"],
|
||||
deps = [
|
||||
":webrtc",
|
||||
"//platform_v2/base",
|
||||
"//platform_v2/impl/g3", # buildcleaner: keep
|
||||
"//testing/base/public:gunit_main",
|
||||
"//webrtc/api:libjingle_peerconnection_api",
|
||||
],
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "peer_id_test",
|
||||
srcs = ["peer_id_test.cc"],
|
||||
deps = [
|
||||
":peer_id",
|
||||
"//platform_v2/base",
|
||||
"//platform_v2/impl/g3", #buildcleaner: keep
|
||||
"//platform_v2/public",
|
||||
"//testing/base/public:gunit_main",
|
||||
],
|
||||
)
|
||||
|
||||
cc_test(
|
||||
name = "signaling_frames_test",
|
||||
srcs = ["signaling_frames_test.cc"],
|
||||
deps = [
|
||||
":peer_id",
|
||||
":signaling_frames",
|
||||
"//platform_v2/impl/g3", # buildcleaner: keep
|
||||
"//net/proto2/public:proto2",
|
||||
"//testing/base/public:gunit_main",
|
||||
"//webrtc/pc:peerconnection", # buildcleaner: keep
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "peer_id",
|
||||
srcs = ["peer_id.cc"],
|
||||
hdrs = ["peer_id.h"],
|
||||
deps = [
|
||||
"//core_v2/internal/mediums:utils",
|
||||
"//platform_v2/base",
|
||||
"//absl/strings",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "signaling_frames",
|
||||
srcs = ["signaling_frames.cc"],
|
||||
hdrs = ["signaling_frames.h"],
|
||||
deps = [
|
||||
":peer_id",
|
||||
"//platform_v2/base",
|
||||
"//location/nearby/mediums/proto:web_rtc_signaling_frames_cc_proto",
|
||||
"//webrtc/api:libjingle_peerconnection_api",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
#include "core_v2/internal/mediums/webrtc/peer_id.h"
|
||||
|
||||
#include <sstream>
|
||||
|
||||
#include "core_v2/internal/mediums/utils.h"
|
||||
#include "absl/strings/ascii.h"
|
||||
#include "absl/strings/escaping.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace mediums {
|
||||
|
||||
namespace {
|
||||
constexpr int kPeerIdLength = 64;
|
||||
|
||||
std::string BytesToStringUppercase(const ByteArray& bytes) {
|
||||
std::string hex_string(
|
||||
absl::BytesToHexString(std::string(bytes.data(), bytes.size())));
|
||||
absl::AsciiStrToUpper(&hex_string);
|
||||
return hex_string;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
PeerId PeerId::FromRandom() {
|
||||
return FromSeed(Utils::GenerateRandomBytes(kPeerIdLength));
|
||||
}
|
||||
|
||||
PeerId PeerId::FromSeed(const ByteArray& seed) {
|
||||
ByteArray full_hash(Utils::Sha256Hash(seed, kPeerIdLength));
|
||||
ByteArray hashed_seed(full_hash.data(), kPeerIdLength / 2);
|
||||
return PeerId(BytesToStringUppercase(hashed_seed));
|
||||
}
|
||||
|
||||
} // namespace mediums
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,35 @@
|
||||
#ifndef CORE_V2_INTERNAL_MEDIUMS_WEBRTC_PEER_ID_H_
|
||||
#define CORE_V2_INTERNAL_MEDIUMS_WEBRTC_PEER_ID_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "platform_v2/base/byte_array.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace mediums {
|
||||
|
||||
// PeerId is used as an identifier to exchange SDP messages to establish WebRTC
|
||||
// p2p connection.
|
||||
class PeerId {
|
||||
public:
|
||||
explicit PeerId(const string& id) : id_(id) {}
|
||||
~PeerId() = default;
|
||||
|
||||
static PeerId FromRandom();
|
||||
static PeerId FromSeed(const ByteArray& seed);
|
||||
|
||||
const string& GetId() const { return id_; }
|
||||
|
||||
private:
|
||||
const string id_;
|
||||
};
|
||||
|
||||
} // namespace mediums
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
|
||||
#endif // CORE_V2_INTERNAL_MEDIUMS_WEBRTC_PEER_ID_H_
|
||||
@@ -0,0 +1,42 @@
|
||||
#include "core_v2/internal/mediums/webrtc/peer_id.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "platform_v2/base/byte_array.h"
|
||||
#include "platform_v2/public/crypto.h"
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace mediums {
|
||||
|
||||
TEST(PeerIdTest, GenerateRandomPeerId) {
|
||||
PeerId peer_id = PeerId::FromRandom();
|
||||
EXPECT_EQ(64, peer_id.GetId().size());
|
||||
}
|
||||
|
||||
TEST(PeerIdTest, GenerateFromSeed) {
|
||||
// Values calculated by running actual SHA-256 hash on |seed|.
|
||||
std::string seed = "seed";
|
||||
std::string expected_peer_id =
|
||||
"19B25856E1C150CA834CFFC8B59B23ADBD0EC0389E58EB22B3B64768098D002B";
|
||||
|
||||
ByteArray seed_bytes(seed);
|
||||
PeerId peer_id = PeerId::FromSeed(seed_bytes);
|
||||
|
||||
EXPECT_EQ(64, peer_id.GetId().size());
|
||||
EXPECT_EQ(expected_peer_id, peer_id.GetId());
|
||||
}
|
||||
|
||||
TEST(PeerIdTest, GetId) {
|
||||
const std::string id = "this_is_a_test";
|
||||
PeerId peer_id(id);
|
||||
EXPECT_EQ(id, peer_id.GetId());
|
||||
}
|
||||
|
||||
} // namespace mediums
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,120 @@
|
||||
#include "core_v2/internal/mediums/webrtc/signaling_frames.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace mediums {
|
||||
namespace webrtc_frames {
|
||||
using WebRtcSignalingFrame = location::nearby::mediums::WebRtcSignalingFrame;
|
||||
|
||||
namespace {
|
||||
|
||||
ByteArray FrameToByteArray(const WebRtcSignalingFrame& signaling_frame) {
|
||||
std::string message;
|
||||
signaling_frame.SerializeToString(&message);
|
||||
return ByteArray(message.c_str(), message.size());
|
||||
}
|
||||
|
||||
void SetSenderId(const PeerId& sender_id, WebRtcSignalingFrame& frame) {
|
||||
frame.mutable_sender_id()->set_id(sender_id.GetId());
|
||||
}
|
||||
|
||||
std::unique_ptr<webrtc::IceCandidateInterface> DecodeIceCandidate(
|
||||
location::nearby::mediums::IceCandidate ice_candidate_proto) {
|
||||
webrtc::SdpParseError error;
|
||||
return std::unique_ptr<webrtc::IceCandidateInterface>(
|
||||
webrtc::CreateIceCandidate(ice_candidate_proto.sdp_mid(),
|
||||
ice_candidate_proto.sdp_m_line_index(),
|
||||
ice_candidate_proto.sdp(), &error));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ByteArray EncodeReadyForSignalingPoke(const PeerId& sender_id) {
|
||||
WebRtcSignalingFrame signaling_frame;
|
||||
signaling_frame.set_type(WebRtcSignalingFrame::READY_FOR_SIGNALING_POKE_TYPE);
|
||||
SetSenderId(sender_id, signaling_frame);
|
||||
signaling_frame.set_allocated_ready_for_signaling_poke(
|
||||
new location::nearby::mediums::ReadyForSignalingPoke());
|
||||
return FrameToByteArray(std::move(signaling_frame));
|
||||
}
|
||||
|
||||
ByteArray EncodeOffer(const PeerId& sender_id,
|
||||
const webrtc::SessionDescriptionInterface& offer) {
|
||||
WebRtcSignalingFrame signaling_frame;
|
||||
signaling_frame.set_type(WebRtcSignalingFrame::OFFER_TYPE);
|
||||
SetSenderId(sender_id, signaling_frame);
|
||||
std::string offer_str;
|
||||
offer.ToString(&offer_str);
|
||||
signaling_frame.mutable_offer()
|
||||
->mutable_session_description()
|
||||
->set_description(offer_str);
|
||||
return FrameToByteArray(std::move(signaling_frame));
|
||||
}
|
||||
|
||||
ByteArray EncodeAnswer(const PeerId& sender_id,
|
||||
const webrtc::SessionDescriptionInterface& answer) {
|
||||
WebRtcSignalingFrame signaling_frame;
|
||||
signaling_frame.set_type(WebRtcSignalingFrame::ANSWER_TYPE);
|
||||
SetSenderId(sender_id, signaling_frame);
|
||||
std::string answer_str;
|
||||
answer.ToString(&answer_str);
|
||||
signaling_frame.mutable_answer()
|
||||
->mutable_session_description()
|
||||
->set_description(answer_str);
|
||||
return FrameToByteArray(std::move(signaling_frame));
|
||||
}
|
||||
|
||||
ByteArray EncodeIceCandidates(
|
||||
const PeerId& sender_id,
|
||||
const std::vector<location::nearby::mediums::IceCandidate>&
|
||||
ice_candidates) {
|
||||
WebRtcSignalingFrame signaling_frame;
|
||||
signaling_frame.set_type(WebRtcSignalingFrame::ICE_CANDIDATES_TYPE);
|
||||
SetSenderId(sender_id, signaling_frame);
|
||||
for (const auto& ice_candidate : ice_candidates) {
|
||||
*signaling_frame.mutable_ice_candidates()->add_ice_candidates() =
|
||||
ice_candidate;
|
||||
}
|
||||
return FrameToByteArray(std::move(signaling_frame));
|
||||
}
|
||||
|
||||
std::unique_ptr<webrtc::SessionDescriptionInterface> DecodeOffer(
|
||||
const WebRtcSignalingFrame& frame) {
|
||||
return webrtc::CreateSessionDescription(
|
||||
webrtc::SdpType::kOffer,
|
||||
frame.offer().session_description().description());
|
||||
}
|
||||
|
||||
std::unique_ptr<webrtc::SessionDescriptionInterface> DecodeAnswer(
|
||||
const WebRtcSignalingFrame& frame) {
|
||||
return webrtc::CreateSessionDescription(
|
||||
webrtc::SdpType::kAnswer,
|
||||
frame.answer().session_description().description());
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<webrtc::IceCandidateInterface>> DecodeIceCandidates(
|
||||
const WebRtcSignalingFrame& frame) {
|
||||
std::vector<std::unique_ptr<webrtc::IceCandidateInterface>> ice_candidates;
|
||||
for (const auto& candidate : frame.ice_candidates().ice_candidates()) {
|
||||
ice_candidates.push_back(DecodeIceCandidate(candidate));
|
||||
}
|
||||
return ice_candidates;
|
||||
}
|
||||
|
||||
location::nearby::mediums::IceCandidate EncodeIceCandidate(
|
||||
const webrtc::IceCandidateInterface& ice_candidate) {
|
||||
std::string sdp;
|
||||
ice_candidate.ToString(&sdp);
|
||||
location::nearby::mediums::IceCandidate ice_candidate_proto;
|
||||
ice_candidate_proto.set_sdp(sdp);
|
||||
ice_candidate_proto.set_sdp_mid(ice_candidate.sdp_mid());
|
||||
ice_candidate_proto.set_sdp_m_line_index(ice_candidate.sdp_mline_index());
|
||||
return ice_candidate_proto;
|
||||
}
|
||||
|
||||
} // namespace webrtc_frames
|
||||
} // namespace mediums
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,44 @@
|
||||
#ifndef CORE_V2_INTERNAL_MEDIUMS_WEBRTC_SIGNALING_FRAMES_H_
|
||||
#define CORE_V2_INTERNAL_MEDIUMS_WEBRTC_SIGNALING_FRAMES_H_
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "core_v2/internal/mediums/webrtc/peer_id.h"
|
||||
#include "platform_v2/base/byte_array.h"
|
||||
#include "location/nearby/mediums/proto/web_rtc_signaling_frames.pb.h"
|
||||
#include "webrtc/api/peer_connection_interface.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace mediums {
|
||||
namespace webrtc_frames {
|
||||
|
||||
ByteArray EncodeReadyForSignalingPoke(const PeerId& sender_id);
|
||||
|
||||
ByteArray EncodeOffer(const PeerId& sender_id,
|
||||
const webrtc::SessionDescriptionInterface& offer);
|
||||
ByteArray EncodeAnswer(const PeerId& sender_id,
|
||||
const webrtc::SessionDescriptionInterface& answer);
|
||||
|
||||
ByteArray EncodeIceCandidates(
|
||||
const PeerId& sender_id,
|
||||
const std::vector<location::nearby::mediums::IceCandidate>& ice_candidates);
|
||||
location::nearby::mediums::IceCandidate EncodeIceCandidate(
|
||||
const webrtc::IceCandidateInterface& ice_candidate);
|
||||
|
||||
std::unique_ptr<webrtc::SessionDescriptionInterface> DecodeOffer(
|
||||
const location::nearby::mediums::WebRtcSignalingFrame& frame);
|
||||
std::unique_ptr<webrtc::SessionDescriptionInterface> DecodeAnswer(
|
||||
const location::nearby::mediums::WebRtcSignalingFrame& frame);
|
||||
|
||||
std::vector<std::unique_ptr<webrtc::IceCandidateInterface>> DecodeIceCandidates(
|
||||
const location::nearby::mediums::WebRtcSignalingFrame& frame);
|
||||
|
||||
} // namespace webrtc_frames
|
||||
} // namespace mediums
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
|
||||
#endif // CORE_V2_INTERNAL_MEDIUMS_WEBRTC_SIGNALING_FRAMES_H_
|
||||
@@ -0,0 +1,182 @@
|
||||
#include "core_v2/internal/mediums/webrtc/signaling_frames.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "core_v2/internal/mediums/webrtc/peer_id.h"
|
||||
#include "net/proto2/public/text_format.h"
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace mediums {
|
||||
namespace webrtc_frames {
|
||||
|
||||
namespace {
|
||||
|
||||
const char kSampleSdp[] =
|
||||
"v=0\r\no=- 7859371131 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 "
|
||||
"0\r\na=msid-semantic: WMS\r\n";
|
||||
|
||||
const char kIceCandidateSdp1[] =
|
||||
"a=candidate:1 1 UDP 2130706431 10.0.1.1 8998 typ host";
|
||||
const char kIceCandidateSdp2[] =
|
||||
"a=candidate:2 1 UDP 1694498815 192.0.2.3 45664 typ srflx raddr";
|
||||
|
||||
const char kIceSdpMid[] = "data";
|
||||
const int kIceSdpMLineIndex = 0;
|
||||
|
||||
const char kOfferProto[] = R"(
|
||||
sender_id { id: "abc" }
|
||||
type: OFFER_TYPE
|
||||
offer {
|
||||
session_description {
|
||||
description: "v=0\r\no=- 7859371131 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=msid-semantic: WMS\r\n"
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
const char kAnswerProto[] = R"(
|
||||
sender_id { id: "abc" }
|
||||
type: ANSWER_TYPE
|
||||
answer {
|
||||
session_description {
|
||||
description: "v=0\r\no=- 7859371131 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=msid-semantic: WMS\r\n"
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
const char kIceCandidatesProto[] = R"(
|
||||
sender_id { id: "abc" }
|
||||
type: ICE_CANDIDATES_TYPE
|
||||
ice_candidates {
|
||||
ice_candidates {
|
||||
sdp: "candidate:1 1 udp 2130706431 10.0.1.1 8998 typ host generation 0"
|
||||
sdp_mid: "data"
|
||||
sdp_m_line_index: 0
|
||||
}
|
||||
ice_candidates {
|
||||
sdp: "candidate:2 1 udp 1694498815 192.0.2.3 45664 typ srflx generation 0"
|
||||
sdp_mid: "data"
|
||||
sdp_m_line_index: 0
|
||||
}
|
||||
}
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
TEST(SignalingFramesTest, SignalingPoke) {
|
||||
PeerId sender_id("abc");
|
||||
ByteArray encoded_poke = EncodeReadyForSignalingPoke(sender_id);
|
||||
|
||||
location::nearby::mediums::WebRtcSignalingFrame frame;
|
||||
frame.ParseFromString(std::string(encoded_poke.data(), encoded_poke.size()));
|
||||
|
||||
EXPECT_THAT(frame, testing::EqualsProto(R"(
|
||||
sender_id { id: "abc" }
|
||||
type: READY_FOR_SIGNALING_POKE_TYPE
|
||||
ready_for_signaling_poke {}
|
||||
)"));
|
||||
}
|
||||
|
||||
TEST(SignalingFramesTest, EncodeValidOffer) {
|
||||
PeerId sender_id("abc");
|
||||
std::unique_ptr<webrtc::SessionDescriptionInterface> offer =
|
||||
webrtc::CreateSessionDescription(webrtc::SdpType::kOffer, kSampleSdp);
|
||||
ByteArray encoded_offer = EncodeOffer(sender_id, *offer);
|
||||
|
||||
location::nearby::mediums::WebRtcSignalingFrame frame;
|
||||
frame.ParseFromString(
|
||||
std::string(encoded_offer.data(), encoded_offer.size()));
|
||||
|
||||
EXPECT_THAT(frame, testing::EqualsProto(kOfferProto));
|
||||
}
|
||||
|
||||
TEST(SignaingFramesTest, DecodeValidOffer) {
|
||||
location::nearby::mediums::WebRtcSignalingFrame frame;
|
||||
proto2::TextFormat::ParseFromString(kOfferProto, &frame);
|
||||
std::unique_ptr<webrtc::SessionDescriptionInterface> decoded_offer =
|
||||
DecodeOffer(frame);
|
||||
|
||||
EXPECT_EQ(webrtc::SdpType::kOffer, decoded_offer->GetType());
|
||||
std::string description;
|
||||
decoded_offer->ToString(&description);
|
||||
EXPECT_EQ(kSampleSdp, description);
|
||||
}
|
||||
|
||||
TEST(SignalingFramesTest, EncodeValidAnswer) {
|
||||
PeerId sender_id("abc");
|
||||
std::unique_ptr<webrtc::SessionDescriptionInterface> answer(
|
||||
webrtc::CreateSessionDescription(webrtc::SdpType::kAnswer, kSampleSdp));
|
||||
ByteArray encoded_answer = EncodeAnswer(sender_id, *answer);
|
||||
|
||||
location::nearby::mediums::WebRtcSignalingFrame frame;
|
||||
frame.ParseFromString(
|
||||
std::string(encoded_answer.data(), encoded_answer.size()));
|
||||
|
||||
EXPECT_THAT(frame, testing::EqualsProto(kAnswerProto));
|
||||
}
|
||||
|
||||
TEST(SignalingFramesTest, DecodeValidAnswer) {
|
||||
location::nearby::mediums::WebRtcSignalingFrame frame;
|
||||
proto2::TextFormat::ParseFromString(kAnswerProto, &frame);
|
||||
std::unique_ptr<webrtc::SessionDescriptionInterface> decoded_answer =
|
||||
DecodeAnswer(frame);
|
||||
|
||||
EXPECT_EQ(webrtc::SdpType::kAnswer, decoded_answer->GetType());
|
||||
std::string description;
|
||||
decoded_answer->ToString(&description);
|
||||
EXPECT_EQ(kSampleSdp, description);
|
||||
}
|
||||
|
||||
TEST(SignalingFramesTest, EncodeValidIceCandidates) {
|
||||
PeerId sender_id("abc");
|
||||
webrtc::SdpParseError error;
|
||||
|
||||
std::vector<std::unique_ptr<webrtc::IceCandidateInterface>> ice_candidates;
|
||||
ice_candidates.emplace_back(webrtc::CreateIceCandidate(
|
||||
kIceSdpMid, kIceSdpMLineIndex, kIceCandidateSdp1, &error));
|
||||
ice_candidates.emplace_back(webrtc::CreateIceCandidate(
|
||||
kIceSdpMid, kIceSdpMLineIndex, kIceCandidateSdp2, &error));
|
||||
std::vector<location::nearby::mediums::IceCandidate> encoded_candidates_vec;
|
||||
for (const auto& ice_candidate : ice_candidates) {
|
||||
encoded_candidates_vec.push_back(EncodeIceCandidate(*ice_candidate));
|
||||
}
|
||||
ByteArray encoded_candidates =
|
||||
EncodeIceCandidates(sender_id, encoded_candidates_vec);
|
||||
|
||||
location::nearby::mediums::WebRtcSignalingFrame frame;
|
||||
frame.ParseFromString(
|
||||
std::string(encoded_candidates.data(), encoded_candidates.size()));
|
||||
|
||||
EXPECT_THAT(frame, testing::EqualsProto(kIceCandidatesProto));
|
||||
}
|
||||
|
||||
TEST(SignalingFramesTest, DecodeValidIceCandidates) {
|
||||
webrtc::SdpParseError error;
|
||||
std::vector<std::unique_ptr<webrtc::IceCandidateInterface>> ice_candidates;
|
||||
ice_candidates.emplace_back(webrtc::CreateIceCandidate(
|
||||
kIceSdpMid, kIceSdpMLineIndex, kIceCandidateSdp1, &error));
|
||||
ice_candidates.emplace_back(webrtc::CreateIceCandidate(
|
||||
kIceSdpMid, kIceSdpMLineIndex, kIceCandidateSdp2, &error));
|
||||
|
||||
location::nearby::mediums::WebRtcSignalingFrame frame;
|
||||
proto2::TextFormat::ParseFromString(kIceCandidatesProto, &frame);
|
||||
std::vector<std::unique_ptr<webrtc::IceCandidateInterface>>
|
||||
decoded_candidates = DecodeIceCandidates(frame);
|
||||
|
||||
ASSERT_EQ(2u, decoded_candidates.size());
|
||||
for (int i = 0; i < static_cast<int>(decoded_candidates.size()); i++) {
|
||||
EXPECT_TRUE(ice_candidates[i]->candidate().IsEquivalent(
|
||||
decoded_candidates[i]->candidate()));
|
||||
EXPECT_EQ(ice_candidates[i]->sdp_mid(), decoded_candidates[i]->sdp_mid());
|
||||
EXPECT_EQ(ice_candidates[i]->sdp_mline_index(),
|
||||
decoded_candidates[i]->sdp_mline_index());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace webrtc_frames
|
||||
} // namespace mediums
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,101 @@
|
||||
#include "core_v2/internal/mediums/webrtc/webrtc_socket.h"
|
||||
|
||||
#include "platform_v2/public/logging.h"
|
||||
#include "platform_v2/public/mutex_lock.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace mediums {
|
||||
|
||||
// OutputStreamImpl
|
||||
Exception WebRtcSocket::OutputStreamImpl::Write(const ByteArray& data) {
|
||||
if (data.size() > kMaxDataSize) {
|
||||
NEARBY_LOG(WARNING, "Sending data larger than 1MB");
|
||||
return {Exception::kIo};
|
||||
}
|
||||
|
||||
socket_->BlockUntilSufficientSpaceInBuffer(data.size());
|
||||
|
||||
if (socket_->IsClosed()) {
|
||||
NEARBY_LOG(WARNING, "Tried sending message while socket is closed");
|
||||
return {Exception::kIo};
|
||||
}
|
||||
|
||||
if (!socket_->SendMessage(data)) {
|
||||
return {Exception::kIo};
|
||||
}
|
||||
return {Exception::kSuccess};
|
||||
}
|
||||
|
||||
Exception WebRtcSocket::OutputStreamImpl::Flush() {
|
||||
// Java implementation is empty.
|
||||
return {Exception::kSuccess};
|
||||
}
|
||||
|
||||
Exception WebRtcSocket::OutputStreamImpl::Close() {
|
||||
socket_->Close();
|
||||
return {Exception::kSuccess};
|
||||
}
|
||||
|
||||
// WebRtcSocket
|
||||
WebRtcSocket::WebRtcSocket(
|
||||
const string& name,
|
||||
rtc::scoped_refptr<webrtc::DataChannelInterface> data_channel)
|
||||
: name_(name), data_channel_(std::move(data_channel)) {}
|
||||
|
||||
InputStream& WebRtcSocket::GetInputStream() { return pipe_.GetInputStream(); }
|
||||
|
||||
OutputStream& WebRtcSocket::GetOutputStream() { return output_stream_; }
|
||||
|
||||
void WebRtcSocket::Close() {
|
||||
if (IsClosed()) return;
|
||||
|
||||
closed_.Set(true);
|
||||
pipe_.GetInputStream().Close();
|
||||
pipe_.GetOutputStream().Close();
|
||||
data_channel_->Close();
|
||||
WakeUpWriter();
|
||||
socket_closed_listener_.socket_closed_cb();
|
||||
}
|
||||
|
||||
void WebRtcSocket::NotifyDataChannelMsgReceived(const ByteArray& message) {
|
||||
if (!pipe_.GetOutputStream().Write(message).Ok()) {
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pipe_.GetOutputStream().Flush().Ok()) Close();
|
||||
}
|
||||
|
||||
void WebRtcSocket::NotifyDataChannelBufferedAmountChanged() { WakeUpWriter(); }
|
||||
|
||||
bool WebRtcSocket::SendMessage(const ByteArray& data) {
|
||||
return data_channel_->Send(
|
||||
webrtc::DataBuffer(std::string(data.data(), data.size())));
|
||||
}
|
||||
|
||||
bool WebRtcSocket::IsClosed() { return closed_.Get(); }
|
||||
|
||||
void WebRtcSocket::WakeUpWriter() {
|
||||
MutexLock lock(&backpressure_mutex_);
|
||||
buffer_variable_.Notify();
|
||||
}
|
||||
|
||||
void WebRtcSocket::SetOnSocketClosedListener(SocketClosedListener&& listener) {
|
||||
socket_closed_listener_ = std::move(listener);
|
||||
}
|
||||
|
||||
void WebRtcSocket::BlockUntilSufficientSpaceInBuffer(int length) {
|
||||
MutexLock lock(&backpressure_mutex_);
|
||||
while (!IsClosed() &&
|
||||
(data_channel_->buffered_amount() + length > kMaxDataSize)) {
|
||||
// TODO(himanshujaju): Add wait with timeout.
|
||||
buffer_variable_.Wait();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace mediums
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,101 @@
|
||||
#ifndef CORE_V2_INTERNAL_MEDIUMS_WEBRTC_WEBRTC_SOCKET_H_
|
||||
#define CORE_V2_INTERNAL_MEDIUMS_WEBRTC_WEBRTC_SOCKET_H_
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "core_v2/listeners.h"
|
||||
#include "platform_v2/base/input_stream.h"
|
||||
#include "platform_v2/base/output_stream.h"
|
||||
#include "platform_v2/base/socket.h"
|
||||
#include "platform_v2/public/atomic_boolean.h"
|
||||
#include "platform_v2/public/condition_variable.h"
|
||||
#include "platform_v2/public/mutex.h"
|
||||
#include "platform_v2/public/pipe.h"
|
||||
#include "webrtc/api/data_channel_interface.h"
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace mediums {
|
||||
|
||||
// Maximum data size: 1 MB
|
||||
constexpr int kMaxDataSize = 1 * 1024 * 1024;
|
||||
|
||||
// Defines the Socket implementation specific to WebRTC, which uses the WebRTC
|
||||
// data channel to send and receive messages.
|
||||
//
|
||||
// Messages are buffered here to prevent the data channel from overflowing,
|
||||
// which could lead to data loss.
|
||||
class WebRtcSocket : public Socket {
|
||||
public:
|
||||
WebRtcSocket(const string& name,
|
||||
rtc::scoped_refptr<webrtc::DataChannelInterface> data_channel);
|
||||
~WebRtcSocket() override = default;
|
||||
|
||||
WebRtcSocket(const WebRtcSocket& other) = delete;
|
||||
WebRtcSocket& operator=(const WebRtcSocket& other) = delete;
|
||||
|
||||
// Overrides for location::nearby::Socket:
|
||||
InputStream& GetInputStream() override;
|
||||
OutputStream& GetOutputStream() override;
|
||||
void Close() override;
|
||||
|
||||
// Callback from WebRTC data channel when new message has been received from
|
||||
// the remote.
|
||||
void NotifyDataChannelMsgReceived(const ByteArray& message);
|
||||
|
||||
// Callback from WebRTC data channel that the buffered data amount has
|
||||
// changed.
|
||||
void NotifyDataChannelBufferedAmountChanged();
|
||||
|
||||
// Listener class the gets called when the socket is closed.
|
||||
struct SocketClosedListener {
|
||||
std::function<void()> socket_closed_cb = DefaultCallback<>();
|
||||
};
|
||||
|
||||
void SetOnSocketClosedListener(SocketClosedListener&& listener);
|
||||
|
||||
private:
|
||||
class OutputStreamImpl : public OutputStream {
|
||||
public:
|
||||
explicit OutputStreamImpl(WebRtcSocket* const socket) : socket_(socket) {}
|
||||
~OutputStreamImpl() override = default;
|
||||
|
||||
OutputStreamImpl(const OutputStreamImpl& other) = delete;
|
||||
OutputStreamImpl& operator=(const OutputStreamImpl& other) = delete;
|
||||
|
||||
// OutputStream:
|
||||
Exception Write(const ByteArray& data) override;
|
||||
Exception Flush() override;
|
||||
Exception Close() override;
|
||||
|
||||
private:
|
||||
// |this| OutputStreamImpl is owned by |socket_|.
|
||||
WebRtcSocket* const socket_;
|
||||
};
|
||||
|
||||
void WakeUpWriter();
|
||||
bool IsClosed();
|
||||
bool SendMessage(const ByteArray& data);
|
||||
void BlockUntilSufficientSpaceInBuffer(int length);
|
||||
|
||||
string name_;
|
||||
rtc::scoped_refptr<webrtc::DataChannelInterface> data_channel_;
|
||||
|
||||
Pipe pipe_;
|
||||
|
||||
OutputStreamImpl output_stream_{this};
|
||||
|
||||
AtomicBoolean closed_{false};
|
||||
|
||||
SocketClosedListener socket_closed_listener_;
|
||||
|
||||
mutable Mutex backpressure_mutex_;
|
||||
ConditionVariable buffer_variable_{&backpressure_mutex_};
|
||||
};
|
||||
|
||||
} // namespace mediums
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
|
||||
#endif // CORE_V2_INTERNAL_MEDIUMS_WEBRTC_WEBRTC_SOCKET_H_
|
||||
@@ -0,0 +1,154 @@
|
||||
#include "core_v2/internal/mediums/webrtc/webrtc_socket.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "platform_v2/base/byte_array.h"
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "webrtc/api/data_channel_interface.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace mediums {
|
||||
|
||||
namespace {
|
||||
|
||||
// using TestPlatform = platform::ImplementationPlatform;
|
||||
|
||||
const char kSocketName[] = "TestSocket";
|
||||
|
||||
class MockDataChannel
|
||||
: public rtc::RefCountedObject<webrtc::DataChannelInterface> {
|
||||
public:
|
||||
MOCK_METHOD(void, RegisterObserver, (webrtc::DataChannelObserver*));
|
||||
MOCK_METHOD(void, UnregisterObserver, ());
|
||||
|
||||
MOCK_METHOD(std::string, label, (), (const));
|
||||
|
||||
MOCK_METHOD(bool, reliable, (), (const));
|
||||
MOCK_METHOD(int, id, (), (const));
|
||||
MOCK_METHOD(DataState, state, (), (const));
|
||||
MOCK_METHOD(uint32_t, messages_sent, (), (const));
|
||||
MOCK_METHOD(uint64_t, bytes_sent, (), (const));
|
||||
MOCK_METHOD(uint32_t, messages_received, (), (const));
|
||||
MOCK_METHOD(uint64_t, bytes_received, (), (const));
|
||||
|
||||
MOCK_METHOD(uint64_t, buffered_amount, (), (const));
|
||||
|
||||
MOCK_METHOD(void, Close, ());
|
||||
|
||||
MOCK_METHOD(bool, Send, (const webrtc::DataBuffer&));
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(WebRtcSocketTest, ReadFromSocket) {
|
||||
const ByteArray kMessage{"Message"};
|
||||
rtc::scoped_refptr<MockDataChannel> mock_data_channel = new MockDataChannel();
|
||||
WebRtcSocket webrtc_socket(kSocketName, mock_data_channel);
|
||||
|
||||
webrtc_socket.NotifyDataChannelMsgReceived(kMessage);
|
||||
ExceptionOr<ByteArray> result = webrtc_socket.GetInputStream().Read(7);
|
||||
EXPECT_TRUE(result.ok());
|
||||
EXPECT_EQ(result.result(), kMessage);
|
||||
}
|
||||
|
||||
TEST(WebRtcSocketTest, ReadMultipleMessages) {
|
||||
rtc::scoped_refptr<MockDataChannel> mock_data_channel = new MockDataChannel();
|
||||
WebRtcSocket webrtc_socket(kSocketName, mock_data_channel);
|
||||
|
||||
webrtc_socket.NotifyDataChannelMsgReceived(ByteArray{"Me"});
|
||||
webrtc_socket.NotifyDataChannelMsgReceived(ByteArray{"ssa"});
|
||||
webrtc_socket.NotifyDataChannelMsgReceived(ByteArray{"ge"});
|
||||
|
||||
ExceptionOr<ByteArray> result;
|
||||
|
||||
// This behaviour is different from the Java code
|
||||
result = webrtc_socket.GetInputStream().Read(7);
|
||||
EXPECT_TRUE(result.ok());
|
||||
EXPECT_EQ(result.result(), ByteArray{"Me"});
|
||||
|
||||
result = webrtc_socket.GetInputStream().Read(7);
|
||||
EXPECT_TRUE(result.ok());
|
||||
EXPECT_EQ(result.result(), ByteArray{"ssa"});
|
||||
|
||||
result = webrtc_socket.GetInputStream().Read(7);
|
||||
EXPECT_TRUE(result.ok());
|
||||
EXPECT_EQ(result.result(), ByteArray{"ge"});
|
||||
}
|
||||
|
||||
TEST(WebRtcSocketTest, WriteToSocket) {
|
||||
const ByteArray kMessage{"Message"};
|
||||
rtc::scoped_refptr<MockDataChannel> mock_data_channel = new MockDataChannel();
|
||||
WebRtcSocket webrtc_socket(kSocketName, mock_data_channel);
|
||||
|
||||
EXPECT_CALL(*mock_data_channel, Send(testing::_))
|
||||
.WillRepeatedly(testing::Return(true));
|
||||
EXPECT_TRUE(webrtc_socket.GetOutputStream().Write(kMessage).Ok());
|
||||
}
|
||||
|
||||
TEST(WebRtcSocketTest, SendDataBiggerThanMax) {
|
||||
const ByteArray kMessage{kMaxDataSize + 1};
|
||||
rtc::scoped_refptr<MockDataChannel> mock_data_channel = new MockDataChannel();
|
||||
WebRtcSocket webrtc_socket(kSocketName, mock_data_channel);
|
||||
|
||||
EXPECT_CALL(*mock_data_channel, Send(testing::_)).Times(0);
|
||||
EXPECT_EQ(webrtc_socket.GetOutputStream().Write(kMessage),
|
||||
Exception{Exception::kIo});
|
||||
}
|
||||
|
||||
TEST(WebRtcSocketTest, WriteToDataChannelFails) {
|
||||
ByteArray kMessage{"Message"};
|
||||
rtc::scoped_refptr<MockDataChannel> mock_data_channel = new MockDataChannel();
|
||||
WebRtcSocket webrtc_socket(kSocketName, mock_data_channel);
|
||||
|
||||
ON_CALL(*mock_data_channel, Send(testing::_))
|
||||
.WillByDefault(testing::Return(false));
|
||||
EXPECT_EQ(webrtc_socket.GetOutputStream().Write(kMessage),
|
||||
Exception{Exception::kIo});
|
||||
}
|
||||
|
||||
TEST(WebRtcSocketTest, Close) {
|
||||
rtc::scoped_refptr<MockDataChannel> mock_data_channel = new MockDataChannel();
|
||||
WebRtcSocket webrtc_socket(kSocketName, mock_data_channel);
|
||||
|
||||
EXPECT_CALL(*mock_data_channel, Close());
|
||||
|
||||
int socket_closed_cb_called = 0;
|
||||
|
||||
webrtc_socket.SetOnSocketClosedListener(
|
||||
{.socket_closed_cb = [&]() { socket_closed_cb_called++; }});
|
||||
webrtc_socket.Close();
|
||||
|
||||
EXPECT_EQ(socket_closed_cb_called, 1);
|
||||
}
|
||||
|
||||
TEST(WebRtcSocketTest, WriteOnClosedChannel) {
|
||||
ByteArray kMessage{"Message"};
|
||||
rtc::scoped_refptr<MockDataChannel> mock_data_channel = new MockDataChannel();
|
||||
WebRtcSocket webrtc_socket(kSocketName, mock_data_channel);
|
||||
webrtc_socket.Close();
|
||||
|
||||
EXPECT_CALL(*mock_data_channel, Send(testing::_)).Times(0);
|
||||
EXPECT_EQ(webrtc_socket.GetOutputStream().Write(kMessage),
|
||||
Exception{Exception::kIo});
|
||||
}
|
||||
|
||||
TEST(WebRtcSocketTest, ReadFromClosedChannel) {
|
||||
ByteArray kMessage{"Message"};
|
||||
rtc::scoped_refptr<MockDataChannel> mock_data_channel = new MockDataChannel();
|
||||
WebRtcSocket webrtc_socket(kSocketName, mock_data_channel);
|
||||
ON_CALL(*mock_data_channel, Send(testing::_))
|
||||
.WillByDefault(testing::Return(true));
|
||||
|
||||
webrtc_socket.GetOutputStream().Write(kMessage);
|
||||
webrtc_socket.Close();
|
||||
|
||||
EXPECT_EQ(webrtc_socket.GetInputStream().Read(7).exception(), Exception::kIo);
|
||||
}
|
||||
|
||||
} // namespace mediums
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,71 @@
|
||||
#ifndef CORE_V2_INTERNAL_MOCK_SERVICE_CONTROLLER_H_
|
||||
#define CORE_V2_INTERNAL_MOCK_SERVICE_CONTROLLER_H_
|
||||
|
||||
#include "core_v2/internal/service_controller.h"
|
||||
#include "gmock/gmock.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
|
||||
/* Mock implementation for ServiceController:
|
||||
* All methods execute asynchronously (in a private executor thread).
|
||||
* To synchronise, two approaches may be used:
|
||||
* 1. For methods that have result callback, we use it to unblock main thread.
|
||||
* 2. For methods that do not have callbacks, we provide a mock implementation
|
||||
* that unblocks main thread.
|
||||
*/
|
||||
class MockServiceController : public ServiceController {
|
||||
public:
|
||||
MOCK_METHOD(Status, StartAdvertising,
|
||||
(ClientProxy * client, const std::string& service_id,
|
||||
const ConnectionOptions& options,
|
||||
const ConnectionRequestInfo& info),
|
||||
(override));
|
||||
|
||||
MOCK_METHOD(void, StopAdvertising, (ClientProxy * client), (override));
|
||||
|
||||
MOCK_METHOD(Status, StartDiscovery,
|
||||
(ClientProxy * client, const std::string& service_id,
|
||||
const ConnectionOptions& options,
|
||||
const DiscoveryListener& listener),
|
||||
(override));
|
||||
|
||||
MOCK_METHOD(void, StopDiscovery, (ClientProxy * client), (override));
|
||||
|
||||
MOCK_METHOD(Status, RequestConnection,
|
||||
(ClientProxy * client, const std::string& endpoint_id,
|
||||
const ConnectionRequestInfo& info),
|
||||
(override));
|
||||
|
||||
MOCK_METHOD(Status, AcceptConnection,
|
||||
(ClientProxy * client, const std::string& endpoint_id,
|
||||
const PayloadListener& listener),
|
||||
(override));
|
||||
|
||||
MOCK_METHOD(Status, RejectConnection,
|
||||
(ClientProxy * client, const std::string& endpoint_id),
|
||||
(override));
|
||||
|
||||
MOCK_METHOD(void, InitiateBandwidthUpgrade,
|
||||
(ClientProxy * client, const std::string& endpoint_id),
|
||||
(override));
|
||||
|
||||
MOCK_METHOD(void, SendPayload,
|
||||
(ClientProxy * client,
|
||||
const std::vector<std::string>& endpoint_ids, Payload payload),
|
||||
(override));
|
||||
|
||||
MOCK_METHOD(Status, CancelPayload,
|
||||
(ClientProxy * client, std::int64_t payload_id), (override));
|
||||
|
||||
MOCK_METHOD(void, DisconnectFromEndpoint,
|
||||
(ClientProxy * client, const std::string& endpoint_id),
|
||||
(override));
|
||||
};
|
||||
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
|
||||
#endif // CORE_V2_INTERNAL_MOCK_SERVICE_CONTROLLER_H_
|
||||
@@ -0,0 +1,251 @@
|
||||
#include "core_v2/internal/offline_frames.h"
|
||||
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#include "core/internal/message_lite.h"
|
||||
#include "platform_v2/base/byte_array.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace parser {
|
||||
namespace {
|
||||
|
||||
using ExceptionOrOfflineFrame = ExceptionOr<OfflineFrame>;
|
||||
using Medium = proto::connections::Medium;
|
||||
using MessageLite = ::google3_proto_compat::MessageLite;
|
||||
|
||||
ByteArray ToBytes(OfflineFrame&& frame) {
|
||||
ByteArray bytes(frame.ByteSizeLong());
|
||||
frame.set_version(OfflineFrame::V1);
|
||||
frame.SerializeToArray(bytes.data(), bytes.size());
|
||||
return bytes;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ExceptionOrOfflineFrame FromBytes(const ByteArray& bytes) {
|
||||
OfflineFrame frame;
|
||||
|
||||
if (frame.ParseFromString(std::string(bytes))) {
|
||||
return ExceptionOrOfflineFrame(std::move(frame));
|
||||
} else {
|
||||
return ExceptionOrOfflineFrame(Exception::kInvalidProtocolBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
V1Frame::FrameType GetFrameType(const OfflineFrame& frame) {
|
||||
if ((frame.version() == OfflineFrame::V1) && frame.has_v1()) {
|
||||
return frame.v1().type();
|
||||
}
|
||||
|
||||
return V1Frame::UNKNOWN_FRAME_TYPE;
|
||||
}
|
||||
|
||||
ByteArray ForConnectionRequest(const std::string& endpoint_id,
|
||||
const std::string& endpoint_name,
|
||||
std::int32_t nonce,
|
||||
const std::vector<Medium>& mediums) {
|
||||
OfflineFrame frame;
|
||||
|
||||
frame.set_version(OfflineFrame::V1);
|
||||
auto* v1_frame = frame.mutable_v1();
|
||||
v1_frame->set_type(V1Frame::CONNECTION_REQUEST);
|
||||
auto* connection_request = v1_frame->mutable_connection_request();
|
||||
connection_request->set_endpoint_id(endpoint_id);
|
||||
connection_request->set_endpoint_name(endpoint_name);
|
||||
connection_request->set_endpoint_info(endpoint_name);
|
||||
connection_request->set_nonce(nonce);
|
||||
for (const auto& medium : mediums) {
|
||||
connection_request->add_mediums(MediumToConnectionRequestMedium(medium));
|
||||
}
|
||||
|
||||
return ToBytes(std::move(frame));
|
||||
}
|
||||
|
||||
ByteArray ForConnectionResponse(std::int32_t status) {
|
||||
OfflineFrame frame;
|
||||
|
||||
frame.set_version(OfflineFrame::V1);
|
||||
auto* v1_frame = frame.mutable_v1();
|
||||
v1_frame->set_type(V1Frame::CONNECTION_RESPONSE);
|
||||
auto* sub_frame = v1_frame->mutable_connection_response();
|
||||
sub_frame->set_status(status);
|
||||
|
||||
return ToBytes(std::move(frame));
|
||||
}
|
||||
|
||||
ByteArray ForDataPayloadTransfer(
|
||||
const PayloadTransferFrame::PayloadHeader& header,
|
||||
const PayloadTransferFrame::PayloadChunk& chunk) {
|
||||
OfflineFrame frame;
|
||||
|
||||
frame.set_version(OfflineFrame::V1);
|
||||
auto* v1_frame = frame.mutable_v1();
|
||||
v1_frame->set_type(V1Frame::PAYLOAD_TRANSFER);
|
||||
auto* sub_frame = v1_frame->mutable_payload_transfer();
|
||||
sub_frame->set_packet_type(PayloadTransferFrame::DATA);
|
||||
*sub_frame->mutable_payload_header() = header;
|
||||
*sub_frame->mutable_payload_chunk() = chunk;
|
||||
|
||||
return ToBytes(std::move(frame));
|
||||
}
|
||||
|
||||
ByteArray ForControlPayloadTransfer(
|
||||
const PayloadTransferFrame::PayloadHeader& header,
|
||||
const PayloadTransferFrame::ControlMessage& control) {
|
||||
OfflineFrame frame;
|
||||
|
||||
frame.set_version(OfflineFrame::V1);
|
||||
auto* v1_frame = frame.mutable_v1();
|
||||
v1_frame->set_type(V1Frame::PAYLOAD_TRANSFER);
|
||||
auto* sub_frame = v1_frame->mutable_payload_transfer();
|
||||
sub_frame->set_packet_type(PayloadTransferFrame::CONTROL);
|
||||
*sub_frame->mutable_payload_header() = header;
|
||||
*sub_frame->mutable_control_message() = control;
|
||||
|
||||
return ToBytes(std::move(frame));
|
||||
}
|
||||
|
||||
ByteArray ForBandwidthUpgradeWifiHotspot(const std::string& ssid,
|
||||
const std::string& password,
|
||||
std::int32_t port) {
|
||||
OfflineFrame frame;
|
||||
|
||||
frame.set_version(OfflineFrame::V1);
|
||||
auto* v1_frame = frame.mutable_v1();
|
||||
v1_frame->set_type(V1Frame::BANDWIDTH_UPGRADE_NEGOTIATION);
|
||||
auto* sub_frame = v1_frame->mutable_bandwidth_upgrade_negotiation();
|
||||
sub_frame->set_event_type(
|
||||
BandwidthUpgradeNegotiationFrame::UPGRADE_PATH_AVAILABLE);
|
||||
auto* upgrade_path_info = sub_frame->mutable_upgrade_path_info();
|
||||
upgrade_path_info->set_medium(
|
||||
BandwidthUpgradeNegotiationFrame::UpgradePathInfo::WIFI_HOTSPOT);
|
||||
auto* wifi_hotspot_credentials =
|
||||
upgrade_path_info->mutable_wifi_hotspot_credentials();
|
||||
wifi_hotspot_credentials->set_ssid(ssid);
|
||||
wifi_hotspot_credentials->set_password(password);
|
||||
wifi_hotspot_credentials->set_port(port);
|
||||
|
||||
return ToBytes(std::move(frame));
|
||||
}
|
||||
|
||||
ByteArray ForBandwidthUpgradeLastWrite() {
|
||||
OfflineFrame frame;
|
||||
|
||||
frame.set_version(OfflineFrame::V1);
|
||||
auto* v1_frame = frame.mutable_v1();
|
||||
v1_frame->set_type(V1Frame::BANDWIDTH_UPGRADE_NEGOTIATION);
|
||||
auto* sub_frame = v1_frame->mutable_bandwidth_upgrade_negotiation();
|
||||
sub_frame->set_event_type(
|
||||
BandwidthUpgradeNegotiationFrame::LAST_WRITE_TO_PRIOR_CHANNEL);
|
||||
|
||||
return ToBytes(std::move(frame));
|
||||
}
|
||||
|
||||
ByteArray ForBandwidthUpgradeSafeToClose() {
|
||||
OfflineFrame frame;
|
||||
|
||||
frame.set_version(OfflineFrame::V1);
|
||||
auto* v1_frame = frame.mutable_v1();
|
||||
v1_frame->set_type(V1Frame::BANDWIDTH_UPGRADE_NEGOTIATION);
|
||||
auto* sub_frame = v1_frame->mutable_bandwidth_upgrade_negotiation();
|
||||
sub_frame->set_event_type(
|
||||
BandwidthUpgradeNegotiationFrame::SAFE_TO_CLOSE_PRIOR_CHANNEL);
|
||||
|
||||
return ToBytes(std::move(frame));
|
||||
}
|
||||
|
||||
ByteArray ForBandwidthUpgradeIntroduction(const std::string& endpoint_id) {
|
||||
OfflineFrame frame;
|
||||
|
||||
frame.set_version(OfflineFrame::V1);
|
||||
auto* v1_frame = frame.mutable_v1();
|
||||
v1_frame->set_type(V1Frame::BANDWIDTH_UPGRADE_NEGOTIATION);
|
||||
auto* sub_frame = v1_frame->mutable_bandwidth_upgrade_negotiation();
|
||||
sub_frame->set_event_type(
|
||||
BandwidthUpgradeNegotiationFrame::CLIENT_INTRODUCTION);
|
||||
auto* client_introduction = sub_frame->mutable_client_introduction();
|
||||
client_introduction->set_endpoint_id(endpoint_id);
|
||||
|
||||
return ToBytes(std::move(frame));
|
||||
}
|
||||
|
||||
ByteArray ForKeepAlive() {
|
||||
OfflineFrame frame;
|
||||
|
||||
frame.set_version(OfflineFrame::V1);
|
||||
auto* v1_frame = frame.mutable_v1();
|
||||
v1_frame->set_type(V1Frame::KEEP_ALIVE);
|
||||
v1_frame->mutable_keep_alive();
|
||||
|
||||
return ToBytes(std::move(frame));
|
||||
}
|
||||
|
||||
ConnectionRequestFrame::Medium MediumToConnectionRequestMedium(
|
||||
proto::connections::Medium medium) {
|
||||
switch (medium) {
|
||||
case Medium::MDNS:
|
||||
return ConnectionRequestFrame::MDNS;
|
||||
case Medium::BLUETOOTH:
|
||||
return ConnectionRequestFrame::BLUETOOTH;
|
||||
case Medium::WIFI_HOTSPOT:
|
||||
return ConnectionRequestFrame::WIFI_HOTSPOT;
|
||||
case Medium::BLE:
|
||||
return ConnectionRequestFrame::BLE;
|
||||
case Medium::WIFI_LAN:
|
||||
return ConnectionRequestFrame::WIFI_LAN;
|
||||
case Medium::WIFI_AWARE:
|
||||
return ConnectionRequestFrame::WIFI_AWARE;
|
||||
case Medium::NFC:
|
||||
return ConnectionRequestFrame::NFC;
|
||||
case Medium::WIFI_DIRECT:
|
||||
return ConnectionRequestFrame::WIFI_DIRECT;
|
||||
case Medium::WEB_RTC:
|
||||
return ConnectionRequestFrame::WEB_RTC;
|
||||
default:
|
||||
return ConnectionRequestFrame::UNKNOWN_MEDIUM;
|
||||
}
|
||||
}
|
||||
|
||||
proto::connections::Medium ConnectionRequestMediumToMedium(
|
||||
ConnectionRequestFrame::Medium medium) {
|
||||
switch (medium) {
|
||||
case ConnectionRequestFrame::MDNS:
|
||||
return Medium::MDNS;
|
||||
case ConnectionRequestFrame::BLUETOOTH:
|
||||
return Medium::BLUETOOTH;
|
||||
case ConnectionRequestFrame::WIFI_HOTSPOT:
|
||||
return Medium::WIFI_HOTSPOT;
|
||||
case ConnectionRequestFrame::BLE:
|
||||
return Medium::BLE;
|
||||
case ConnectionRequestFrame::WIFI_LAN:
|
||||
return Medium::WIFI_LAN;
|
||||
case ConnectionRequestFrame::WIFI_AWARE:
|
||||
return Medium::WIFI_AWARE;
|
||||
case ConnectionRequestFrame::NFC:
|
||||
return Medium::NFC;
|
||||
case ConnectionRequestFrame::WIFI_DIRECT:
|
||||
return Medium::WIFI_DIRECT;
|
||||
case ConnectionRequestFrame::WEB_RTC:
|
||||
return Medium::WEB_RTC;
|
||||
default:
|
||||
return Medium::UNKNOWN_MEDIUM;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<proto::connections::Medium> ConnectionRequestMediumsToMediums(
|
||||
const ConnectionRequestFrame& frame) {
|
||||
std::vector<proto::connections::Medium> result;
|
||||
for (const auto& int_medium : frame.mediums()) {
|
||||
result.push_back(ConnectionRequestMediumToMedium(
|
||||
static_cast<ConnectionRequestFrame::Medium>(int_medium)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace parser
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,61 @@
|
||||
#ifndef CORE_V2_INTERNAL_OFFLINE_FRAMES_H_
|
||||
#define CORE_V2_INTERNAL_OFFLINE_FRAMES_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "proto/connections/offline_wire_formats.pb.h"
|
||||
#include "platform_v2/base/byte_array.h"
|
||||
#include "platform_v2/base/exception.h"
|
||||
#include "proto/connections_enums.pb.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace parser {
|
||||
|
||||
// Serialize/Deserialize Nearby Connections Protocol messages.
|
||||
|
||||
// Parses incoming message.
|
||||
// Returns OfflineFrame if parser was able to understand it, or
|
||||
// Exception::kInvalidProtocolBuffer, if parser failed.
|
||||
ExceptionOr<OfflineFrame> FromBytes(const ByteArray& offline_frame_bytes);
|
||||
|
||||
// Returns FrameType of a parsed message, or
|
||||
// V1Frame::UNKNOWN_FRAME_TYPE, if frame contents is not recognized.
|
||||
V1Frame::FrameType GetFrameType(const OfflineFrame& offline_frame);
|
||||
|
||||
// Build ConnectionRequest message.
|
||||
ByteArray ForConnectionRequest(
|
||||
const std::string& endpoint_id, const std::string& endpoint_name,
|
||||
std::int32_t nonce, const std::vector<proto::connections::Medium>& mediums);
|
||||
ByteArray ForConnectionResponse(std::int32_t status);
|
||||
|
||||
ByteArray ForDataPayloadTransfer(
|
||||
const PayloadTransferFrame::PayloadHeader& header,
|
||||
const PayloadTransferFrame::PayloadChunk& chunk);
|
||||
ByteArray ForControlPayloadTransfer(
|
||||
const PayloadTransferFrame::PayloadHeader& header,
|
||||
const PayloadTransferFrame::ControlMessage& control);
|
||||
|
||||
ByteArray ForBandwidthUpgradeWifiHotspot(
|
||||
const std::string& ssid, const std::string& password, std::int32_t port);
|
||||
ByteArray ForBandwidthUpgradeLastWrite();
|
||||
ByteArray ForBandwidthUpgradeSafeToClose();
|
||||
ByteArray ForBandwidthUpgradeIntroduction(const std::string& endpoint_id);
|
||||
|
||||
ByteArray ForKeepAlive();
|
||||
|
||||
ConnectionRequestFrame::Medium MediumToConnectionRequestMedium(
|
||||
proto::connections::Medium medium);
|
||||
proto::connections::Medium ConnectionRequestMediumToMedium(
|
||||
ConnectionRequestFrame::Medium medium);
|
||||
std::vector<proto::connections::Medium> ConnectionRequestMediumsToMediums(
|
||||
const ConnectionRequestFrame& connection_request_frame);
|
||||
|
||||
} // namespace parser
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
|
||||
#endif // CORE_V2_INTERNAL_OFFLINE_FRAMES_H_
|
||||
@@ -0,0 +1,252 @@
|
||||
#include "core_v2/internal/offline_frames.h"
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "proto/connections/offline_wire_formats.pb.h"
|
||||
#include "platform_v2/base/byte_array.h"
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace parser {
|
||||
namespace {
|
||||
|
||||
using Medium = proto::connections::Medium;
|
||||
using ::testing::EqualsProto;
|
||||
|
||||
constexpr char kEndpointId[] = "ABC";
|
||||
constexpr char kEndpointName[] = "XYZ";
|
||||
constexpr int kNonce = 1234;
|
||||
constexpr std::array<Medium, 9> kMediums = {
|
||||
Medium::MDNS, Medium::BLUETOOTH, Medium::WIFI_HOTSPOT,
|
||||
Medium::BLE, Medium::WIFI_LAN, Medium::WIFI_AWARE,
|
||||
Medium::NFC, Medium::WIFI_DIRECT, Medium::WEB_RTC,
|
||||
};
|
||||
|
||||
TEST(OfflineFramesTest, CanParseMessageFromBytes) {
|
||||
OfflineFrame tx_message;
|
||||
|
||||
{
|
||||
tx_message.set_version(OfflineFrame::V1);
|
||||
auto* v1_frame = tx_message.mutable_v1();
|
||||
auto* sub_frame = v1_frame->mutable_connection_request();
|
||||
|
||||
v1_frame->set_type(V1Frame::CONNECTION_REQUEST);
|
||||
sub_frame->set_endpoint_id(kEndpointId);
|
||||
sub_frame->set_endpoint_name(kEndpointName);
|
||||
sub_frame->set_endpoint_info(kEndpointName);
|
||||
sub_frame->set_nonce(kNonce);
|
||||
for (auto& medium : kMediums) {
|
||||
sub_frame->add_mediums(MediumToConnectionRequestMedium(medium));
|
||||
}
|
||||
}
|
||||
auto serialized_bytes = ByteArray(tx_message.SerializeAsString());
|
||||
auto ret_value = FromBytes(serialized_bytes);
|
||||
ASSERT_TRUE(ret_value.ok());
|
||||
const auto& rx_message = ret_value.result();
|
||||
EXPECT_THAT(rx_message, EqualsProto(tx_message));
|
||||
EXPECT_EQ(GetFrameType(rx_message), V1Frame::CONNECTION_REQUEST);
|
||||
EXPECT_EQ(
|
||||
ConnectionRequestMediumsToMediums(rx_message.v1().connection_request()),
|
||||
std::vector(kMediums.begin(), kMediums.end()));
|
||||
}
|
||||
|
||||
TEST(OfflineFramesTest, CanGenerateConnectionRequest) {
|
||||
constexpr char kExpected[] =
|
||||
R"pb(
|
||||
version: V1
|
||||
v1: <
|
||||
type: CONNECTION_REQUEST
|
||||
connection_request: <
|
||||
endpoint_id: "ABC"
|
||||
endpoint_name: "XYZ"
|
||||
endpoint_info: "XYZ"
|
||||
nonce: 1234
|
||||
mediums: MDNS
|
||||
mediums: BLUETOOTH
|
||||
mediums: WIFI_HOTSPOT
|
||||
mediums: BLE
|
||||
mediums: WIFI_LAN
|
||||
mediums: WIFI_AWARE
|
||||
mediums: NFC
|
||||
mediums: WIFI_DIRECT
|
||||
mediums: WEB_RTC
|
||||
>
|
||||
>)pb";
|
||||
ByteArray bytes =
|
||||
ForConnectionRequest(kEndpointId, kEndpointName, kNonce,
|
||||
std::vector(kMediums.begin(), kMediums.end()));
|
||||
auto response = FromBytes(bytes);
|
||||
ASSERT_TRUE(response.ok());
|
||||
OfflineFrame message = FromBytes(bytes).result();
|
||||
EXPECT_THAT(message, EqualsProto(kExpected));
|
||||
}
|
||||
|
||||
TEST(OfflineFramesTest, CanGenerateConnectionResponse) {
|
||||
constexpr char kExpected[] =
|
||||
R"pb(
|
||||
version: V1
|
||||
v1: <
|
||||
type: CONNECTION_RESPONSE
|
||||
connection_response: < status: 1 >
|
||||
>)pb";
|
||||
ByteArray bytes = ForConnectionResponse(1);
|
||||
auto response = FromBytes(bytes);
|
||||
ASSERT_TRUE(response.ok());
|
||||
OfflineFrame message = FromBytes(bytes).result();
|
||||
EXPECT_THAT(message, EqualsProto(kExpected));
|
||||
}
|
||||
|
||||
TEST(OfflineFramesTest, CanGenerateControlPayloadTransfer) {
|
||||
PayloadTransferFrame::PayloadHeader header;
|
||||
PayloadTransferFrame::ControlMessage control;
|
||||
header.set_id(12345);
|
||||
header.set_type(PayloadTransferFrame::PayloadHeader::BYTES);
|
||||
header.set_total_size(1024);
|
||||
control.set_event(PayloadTransferFrame::ControlMessage::PAYLOAD_CANCELED);
|
||||
control.set_offset(150);
|
||||
|
||||
constexpr char kExpected[] =
|
||||
R"pb(
|
||||
version: V1
|
||||
v1: <
|
||||
type: PAYLOAD_TRANSFER
|
||||
payload_transfer: <
|
||||
packet_type: CONTROL,
|
||||
payload_header: < type: BYTES id: 12345 total_size: 1024 >
|
||||
control_message: < event: PAYLOAD_CANCELED offset: 150 >
|
||||
>
|
||||
>)pb";
|
||||
ByteArray bytes = ForControlPayloadTransfer(header, control);
|
||||
auto response = FromBytes(bytes);
|
||||
ASSERT_TRUE(response.ok());
|
||||
OfflineFrame message = FromBytes(bytes).result();
|
||||
EXPECT_THAT(message, EqualsProto(kExpected));
|
||||
}
|
||||
|
||||
TEST(OfflineFramesTest, CanGenerateDataPayloadTransfer) {
|
||||
PayloadTransferFrame::PayloadHeader header;
|
||||
PayloadTransferFrame::PayloadChunk chunk;
|
||||
header.set_id(12345);
|
||||
header.set_type(PayloadTransferFrame::PayloadHeader::BYTES);
|
||||
header.set_total_size(1024);
|
||||
chunk.set_body("payload data");
|
||||
chunk.set_offset(150);
|
||||
chunk.set_flags(1);
|
||||
|
||||
constexpr char kExpected[] =
|
||||
R"pb(
|
||||
version: V1
|
||||
v1: <
|
||||
type: PAYLOAD_TRANSFER
|
||||
payload_transfer: <
|
||||
packet_type: DATA,
|
||||
payload_header: < type: BYTES id: 12345 total_size: 1024 >
|
||||
payload_chunk: < flags: 1 offset: 150 body: "payload data" >
|
||||
>
|
||||
>)pb";
|
||||
ByteArray bytes = ForDataPayloadTransfer(header, chunk);
|
||||
auto response = FromBytes(bytes);
|
||||
ASSERT_TRUE(response.ok());
|
||||
OfflineFrame message = FromBytes(bytes).result();
|
||||
EXPECT_THAT(message, EqualsProto(kExpected));
|
||||
}
|
||||
|
||||
TEST(OfflineFramesTest, CanGenerateBandwidthUpgradeWifiHotspot) {
|
||||
constexpr char kExpected[] =
|
||||
R"pb(
|
||||
version: V1
|
||||
v1: <
|
||||
type: BANDWIDTH_UPGRADE_NEGOTIATION
|
||||
bandwidth_upgrade_negotiation: <
|
||||
event_type: UPGRADE_PATH_AVAILABLE
|
||||
upgrade_path_info: <
|
||||
medium: WIFI_HOTSPOT
|
||||
wifi_hotspot_credentials: <
|
||||
ssid: "ssid"
|
||||
password: "password"
|
||||
port: 1234
|
||||
>
|
||||
>
|
||||
>
|
||||
>)pb";
|
||||
ByteArray bytes = ForBandwidthUpgradeWifiHotspot("ssid", "password", 1234);
|
||||
auto response = FromBytes(bytes);
|
||||
ASSERT_TRUE(response.ok());
|
||||
OfflineFrame message = FromBytes(bytes).result();
|
||||
EXPECT_THAT(message, EqualsProto(kExpected));
|
||||
}
|
||||
|
||||
TEST(OfflineFramesTest, CanGenerateBandwidthUpgradeLastWrite) {
|
||||
constexpr char kExpected[] =
|
||||
R"pb(
|
||||
version: V1
|
||||
v1: <
|
||||
type: BANDWIDTH_UPGRADE_NEGOTIATION
|
||||
bandwidth_upgrade_negotiation: < event_type: LAST_WRITE_TO_PRIOR_CHANNEL >
|
||||
>)pb";
|
||||
ByteArray bytes = ForBandwidthUpgradeLastWrite();
|
||||
auto response = FromBytes(bytes);
|
||||
ASSERT_TRUE(response.ok());
|
||||
OfflineFrame message = FromBytes(bytes).result();
|
||||
EXPECT_THAT(message, EqualsProto(kExpected));
|
||||
}
|
||||
|
||||
TEST(OfflineFramesTest, CanGenerateBandwidthUpgradeSafeToClose) {
|
||||
constexpr char kExpected[] =
|
||||
R"pb(
|
||||
version: V1
|
||||
v1: <
|
||||
type: BANDWIDTH_UPGRADE_NEGOTIATION
|
||||
bandwidth_upgrade_negotiation: < event_type: SAFE_TO_CLOSE_PRIOR_CHANNEL >
|
||||
>)pb";
|
||||
ByteArray bytes = ForBandwidthUpgradeSafeToClose();
|
||||
auto response = FromBytes(bytes);
|
||||
ASSERT_TRUE(response.ok());
|
||||
OfflineFrame message = FromBytes(bytes).result();
|
||||
EXPECT_THAT(message, EqualsProto(kExpected));
|
||||
}
|
||||
|
||||
TEST(OfflineFramesTest, CanGenerateBandwidthUpgradeIntroduction) {
|
||||
constexpr char kExpected[] =
|
||||
R"pb(
|
||||
version: V1
|
||||
v1: <
|
||||
type: BANDWIDTH_UPGRADE_NEGOTIATION
|
||||
bandwidth_upgrade_negotiation: <
|
||||
event_type: CLIENT_INTRODUCTION
|
||||
client_introduction: < endpoint_id: "ABC" >
|
||||
>
|
||||
>)pb";
|
||||
ByteArray bytes = ForBandwidthUpgradeIntroduction(kEndpointId);
|
||||
auto response = FromBytes(bytes);
|
||||
ASSERT_TRUE(response.ok());
|
||||
OfflineFrame message = FromBytes(bytes).result();
|
||||
EXPECT_THAT(message, EqualsProto(kExpected));
|
||||
}
|
||||
|
||||
TEST(OfflineFramesTest, CanGenerateKeepAlive) {
|
||||
constexpr char kExpected[] =
|
||||
R"pb(
|
||||
version: V1
|
||||
v1: <
|
||||
type: KEEP_ALIVE
|
||||
keep_alive: <>
|
||||
>)pb";
|
||||
ByteArray bytes = ForKeepAlive();
|
||||
auto response = FromBytes(bytes);
|
||||
ASSERT_TRUE(response.ok());
|
||||
OfflineFrame message = FromBytes(bytes).result();
|
||||
EXPECT_THAT(message, EqualsProto(kExpected));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace parser
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,26 @@
|
||||
#ifndef CORE_V2_INTERNAL_PCP_H_
|
||||
#define CORE_V2_INTERNAL_PCP_H_
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
|
||||
// The PreConnectionProtocol (PCP) defines the combinations of interactions
|
||||
// between the techniques (ultrasound audio, Bluetooth device names, BLE
|
||||
// advertisements) used for offline Advertisement + Discovery, and identifies
|
||||
// the steps to go through on each device.
|
||||
//
|
||||
// See go/nearby-offline-data-interchange-formats for more.
|
||||
enum class Pcp {
|
||||
kUnknown = 0,
|
||||
kP2pStar = 1,
|
||||
kP2pCluster = 2,
|
||||
kP2pPointToPoint = 3,
|
||||
// PCP is only allocated 5 bits in our data interchange formats, so there can
|
||||
// never be more than 31 PCP values.
|
||||
};
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
|
||||
#endif // CORE_V2_INTERNAL_PCP_H_
|
||||
@@ -0,0 +1,88 @@
|
||||
#ifndef CORE_V2_INTERNAL_PCP_HANDLER_H_
|
||||
#define CORE_V2_INTERNAL_PCP_HANDLER_H_
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "core_v2/internal/client_proxy.h"
|
||||
#include "core_v2/internal/pcp.h"
|
||||
#include "core_v2/listeners.h"
|
||||
#include "core_v2/options.h"
|
||||
#include "core_v2/params.h"
|
||||
#include "core_v2/status.h"
|
||||
#include "core_v2/strategy.h"
|
||||
#include "proto/connections_enums.pb.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
|
||||
// Defines the set of methods that need to be implemented to handle the
|
||||
// per-PCP-specific operations in the OfflineServiceController.
|
||||
//
|
||||
// These methods are all meant to be synchronous, and should return only after
|
||||
// knowing they've done what they were supposed to do (or unequivocally failed
|
||||
// to do so).
|
||||
//
|
||||
// See details here:
|
||||
// https://source.corp.google.com/piper///depot/google3/core_v2/core.h
|
||||
class PcpHandler {
|
||||
public:
|
||||
virtual ~PcpHandler() = default;
|
||||
|
||||
// Return strategy supported by this protocol.
|
||||
virtual Strategy GetStrategy() = 0;
|
||||
|
||||
// Return concrete variant of protocol.
|
||||
virtual Pcp GetPcp() = 0;
|
||||
|
||||
// We have been asked by the client to start advertising. Once we successfully
|
||||
// start advertising, we'll change the ClientProxy's state.
|
||||
// ConnectionListener (info.listener) will be notified in case of any event.
|
||||
// See
|
||||
// https://source.corp.google.com/piper///depot/google3/core_v2/listeners.h;bpv=1;bpt=1;l=71?gsn=ConnectionListener
|
||||
virtual Status StartAdvertising(ClientProxy* client,
|
||||
const std::string& service_id,
|
||||
const ConnectionOptions& options,
|
||||
const ConnectionRequestInfo& info) = 0;
|
||||
|
||||
// If Advertising is active, stop it, and change CLientProxy state,
|
||||
// otherwise do nothing.
|
||||
virtual void StopAdvertising(ClientProxy* client) = 0;
|
||||
|
||||
// Start discovery of endpoints that may be advertising.
|
||||
// Update ClientProxy state once discovery started.
|
||||
// DiscoveryListener will get called in case of any event.
|
||||
virtual Status StartDiscovery(ClientProxy* client,
|
||||
const std::string& service_id,
|
||||
const ConnectionOptions& options,
|
||||
const DiscoveryListener& listener) = 0;
|
||||
|
||||
// If Discovery is active, stop it, and change CLientProxy state,
|
||||
// otherwise do nothing.
|
||||
virtual void StopDiscovery(ClientProxy* client) = 0;
|
||||
|
||||
// If remote endpoint has been successfully discovered, request it to form a
|
||||
// connection, update state on ClientProxy.
|
||||
virtual Status RequestConnection(ClientProxy* client,
|
||||
const std::string& endpoint_id,
|
||||
const ConnectionRequestInfo& info) = 0;
|
||||
|
||||
// Either party may call this to accept connection on their part.
|
||||
// Until both parties call it, connection will not reach a data phase.
|
||||
// Update state in ClientProxy.
|
||||
virtual Status AcceptConnection(ClientProxy* clientProxy,
|
||||
const std::string& endpoint_id,
|
||||
const PayloadListener& payload_listener) = 0;
|
||||
|
||||
// Either party may call this to reject connection on their part before
|
||||
// connection reaches data phase. If either party does call it, connection
|
||||
// will terminate. Update state in ClientProxy.
|
||||
virtual Status RejectConnection(ClientProxy* client,
|
||||
const std::string& endpoint_id) = 0;
|
||||
};
|
||||
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
|
||||
#endif // CORE_V2_INTERNAL_PCP_HANDLER_H_
|
||||
@@ -0,0 +1,77 @@
|
||||
#ifndef CORE_V2_INTERNAL_SERVICE_CONTROLLER_H_
|
||||
#define CORE_V2_INTERNAL_SERVICE_CONTROLLER_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core_v2/internal/client_proxy.h"
|
||||
#include "core_v2/listeners.h"
|
||||
#include "core_v2/options.h"
|
||||
#include "core_v2/params.h"
|
||||
#include "core_v2/payload.h"
|
||||
#include "core_v2/status.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
|
||||
// Interface defines the core functionality of Nearby Connections Service.
|
||||
//
|
||||
// In every method, ClientProxy* represents the client app which receives
|
||||
// notifications from Nearby Connections service and forwards them to the app.
|
||||
// ResultCallback arguments are not provided for this class, because all methods
|
||||
// are called synchronously.
|
||||
// The rest of arguments have the same meaning as the corresponding
|
||||
// methods in the definition of location::nearby::Core API.
|
||||
//
|
||||
// See details here:
|
||||
// https://source.corp.google.com/piper///depot/google3/core_v2/core.h
|
||||
class ServiceController {
|
||||
public:
|
||||
virtual ~ServiceController() = default;
|
||||
ServiceController() = default;
|
||||
ServiceController(const ServiceController&) = delete;
|
||||
ServiceController& operator=(const ServiceController&) = delete;
|
||||
|
||||
// Starts advertising an endpoint for a local app.
|
||||
virtual Status StartAdvertising(ClientProxy* client_proxy,
|
||||
const std::string& service_id,
|
||||
const ConnectionOptions& options,
|
||||
const ConnectionRequestInfo& info) = 0;
|
||||
virtual void StopAdvertising(ClientProxy* client_proxy) = 0;
|
||||
|
||||
virtual Status StartDiscovery(ClientProxy* client_proxy,
|
||||
const std::string& service_id,
|
||||
const ConnectionOptions& options,
|
||||
const DiscoveryListener& listener) = 0;
|
||||
virtual void StopDiscovery(ClientProxy* client_proxy) = 0;
|
||||
|
||||
virtual Status RequestConnection(ClientProxy* client_proxy,
|
||||
const std::string& endpoint_id,
|
||||
const ConnectionRequestInfo& info) = 0;
|
||||
virtual Status AcceptConnection(ClientProxy* client_proxy,
|
||||
const std::string& endpoint_id,
|
||||
const PayloadListener& listener) = 0;
|
||||
virtual Status RejectConnection(ClientProxy* client_proxy,
|
||||
const std::string& endpoint_id) = 0;
|
||||
|
||||
virtual void InitiateBandwidthUpgrade(ClientProxy* client_proxy,
|
||||
const std::string& endpoint_id) = 0;
|
||||
|
||||
virtual void SendPayload(ClientProxy* client_proxy,
|
||||
const std::vector<std::string>& endpoint_ids,
|
||||
Payload payload) = 0;
|
||||
|
||||
virtual Status CancelPayload(ClientProxy* client_proxy,
|
||||
std::int64_t payload_id) = 0;
|
||||
|
||||
virtual void DisconnectFromEndpoint(ClientProxy* client_proxy,
|
||||
const std::string& endpoint_id) = 0;
|
||||
};
|
||||
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
|
||||
#endif // CORE_V2_INTERNAL_SERVICE_CONTROLLER_H_
|
||||
@@ -0,0 +1,383 @@
|
||||
#include "core_v2/internal/service_controller_router.h"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "core_v2/internal/client_proxy.h"
|
||||
#include "core_v2/listeners.h"
|
||||
#include "core_v2/options.h"
|
||||
#include "core_v2/params.h"
|
||||
#include "core_v2/payload.h"
|
||||
#include "absl/time/clock.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
|
||||
ServiceControllerRouter::~ServiceControllerRouter() {
|
||||
// TODO(tracyzhou): Add logging.
|
||||
|
||||
// And make sure that cleanup is the last thing we do.
|
||||
serializer_.Shutdown();
|
||||
}
|
||||
|
||||
void ServiceControllerRouter::StartAdvertising(
|
||||
ClientProxy* client, absl::string_view service_id,
|
||||
const ConnectionOptions& options, const ConnectionRequestInfo& info,
|
||||
const ResultCallback& callback) {
|
||||
RouteToServiceController([this, client, service_id = std::string(service_id),
|
||||
options, info, callback]() {
|
||||
Status status = AcquireServiceControllerForClient(client, options.strategy);
|
||||
if (!status.Ok()) {
|
||||
callback.result_cb(status);
|
||||
return;
|
||||
}
|
||||
|
||||
if (client->IsAdvertising()) {
|
||||
callback.result_cb({Status::kAlreadyAdvertising});
|
||||
return;
|
||||
}
|
||||
|
||||
status = service_controller_->StartAdvertising(client, service_id, options,
|
||||
info);
|
||||
callback.result_cb(status);
|
||||
});
|
||||
}
|
||||
|
||||
void ServiceControllerRouter::StopAdvertising(ClientProxy* client,
|
||||
const ResultCallback& callback) {
|
||||
RouteToServiceController([this, client, callback]() {
|
||||
if (ClientHasAcquiredServiceController(client) && client->IsAdvertising()) {
|
||||
service_controller_->StopAdvertising(client);
|
||||
}
|
||||
callback.result_cb({Status::kSuccess});
|
||||
});
|
||||
}
|
||||
|
||||
void ServiceControllerRouter::StartDiscovery(ClientProxy* client,
|
||||
absl::string_view service_id,
|
||||
const ConnectionOptions& options,
|
||||
const DiscoveryListener& listener,
|
||||
const ResultCallback& callback) {
|
||||
RouteToServiceController([this, client, service_id = std::string(service_id),
|
||||
options, listener, callback]() {
|
||||
Status status = AcquireServiceControllerForClient(client, options.strategy);
|
||||
if (!status.Ok()) {
|
||||
callback.result_cb(status);
|
||||
return;
|
||||
}
|
||||
|
||||
if (client->IsDiscovering()) {
|
||||
callback.result_cb({Status::kAlreadyDiscovering});
|
||||
return;
|
||||
}
|
||||
|
||||
status = service_controller_->StartDiscovery(client, service_id, options,
|
||||
listener);
|
||||
callback.result_cb(status);
|
||||
});
|
||||
}
|
||||
|
||||
void ServiceControllerRouter::StopDiscovery(ClientProxy* client,
|
||||
const ResultCallback& callback) {
|
||||
RouteToServiceController([this, client, callback]() {
|
||||
if (ClientHasAcquiredServiceController(client) && client->IsDiscovering()) {
|
||||
service_controller_->StopDiscovery(client);
|
||||
}
|
||||
callback.result_cb({Status::kSuccess});
|
||||
});
|
||||
}
|
||||
|
||||
void ServiceControllerRouter::RequestConnection(
|
||||
ClientProxy* client, absl::string_view endpoint_id,
|
||||
const ConnectionRequestInfo& info, const ResultCallback& callback) {
|
||||
RouteToServiceController(
|
||||
[this, client, endpoint_id = std::string(endpoint_id), info, callback]() {
|
||||
if (!ClientHasAcquiredServiceController(client)) {
|
||||
callback.result_cb({Status::kOutOfOrderApiCall});
|
||||
return;
|
||||
}
|
||||
|
||||
if (client->HasPendingConnectionToEndpoint(endpoint_id) ||
|
||||
client->IsConnectedToEndpoint(endpoint_id)) {
|
||||
callback.result_cb({Status::kAlreadyConnectedToEndpoint});
|
||||
return;
|
||||
}
|
||||
|
||||
callback.result_cb(
|
||||
service_controller_->RequestConnection(client, endpoint_id, info));
|
||||
});
|
||||
}
|
||||
|
||||
void ServiceControllerRouter::AcceptConnection(ClientProxy* client,
|
||||
absl::string_view endpoint_id,
|
||||
const PayloadListener& listener,
|
||||
const ResultCallback& callback) {
|
||||
RouteToServiceController([this, client,
|
||||
endpoint_id = std::string(endpoint_id), listener,
|
||||
callback]() {
|
||||
if (!ClientHasAcquiredServiceController(client)) {
|
||||
callback.result_cb({Status::kOutOfOrderApiCall});
|
||||
return;
|
||||
}
|
||||
|
||||
if (client->IsConnectedToEndpoint(endpoint_id)) {
|
||||
callback.result_cb({Status::kAlreadyConnectedToEndpoint});
|
||||
return;
|
||||
}
|
||||
|
||||
if (client->HasLocalEndpointResponded(endpoint_id)) {
|
||||
// TODO(tracyzhou): logging
|
||||
callback.result_cb({Status::kOutOfOrderApiCall});
|
||||
return;
|
||||
}
|
||||
|
||||
callback.result_cb(
|
||||
service_controller_->AcceptConnection(client, endpoint_id, listener));
|
||||
});
|
||||
}
|
||||
|
||||
void ServiceControllerRouter::RejectConnection(ClientProxy* client,
|
||||
absl::string_view endpoint_id,
|
||||
const ResultCallback& callback) {
|
||||
RouteToServiceController(
|
||||
[this, client, endpoint_id = std::string(endpoint_id), callback]() {
|
||||
if (!ClientHasAcquiredServiceController(client)) {
|
||||
callback.result_cb({Status::kOutOfOrderApiCall});
|
||||
return;
|
||||
}
|
||||
|
||||
if (client->IsConnectedToEndpoint(endpoint_id)) {
|
||||
callback.result_cb({Status::kAlreadyConnectedToEndpoint});
|
||||
return;
|
||||
}
|
||||
|
||||
if (client->HasLocalEndpointResponded(endpoint_id)) {
|
||||
// TODO(tracyzhou): logging
|
||||
callback.result_cb({Status::kOutOfOrderApiCall});
|
||||
return;
|
||||
}
|
||||
|
||||
callback.result_cb(
|
||||
service_controller_->RejectConnection(client, endpoint_id));
|
||||
});
|
||||
}
|
||||
|
||||
void ServiceControllerRouter::InitiateBandwidthUpgrade(
|
||||
ClientProxy* client, absl::string_view endpoint_id,
|
||||
const ResultCallback& callback) {
|
||||
RouteToServiceController(
|
||||
[this, client, endpoint_id = std::string(endpoint_id), callback]() {
|
||||
if (!ClientHasAcquiredServiceController(client) ||
|
||||
!client->IsConnectedToEndpoint(endpoint_id)) {
|
||||
callback.result_cb({Status::kOutOfOrderApiCall});
|
||||
return;
|
||||
}
|
||||
|
||||
service_controller_->InitiateBandwidthUpgrade(client, endpoint_id);
|
||||
|
||||
// Operation is triggered; the caller can listen to
|
||||
// ConnectionListener::OnBandwidthChanged() to determine its success.
|
||||
callback.result_cb({Status::kSuccess});
|
||||
});
|
||||
}
|
||||
|
||||
void ServiceControllerRouter::SendPayload(
|
||||
ClientProxy* client, absl::Span<const std::string> endpoint_ids,
|
||||
Payload payload, const ResultCallback& callback) {
|
||||
// Payload is a move-only type.
|
||||
// We have to capture it by value inside the lambda, and pass it over to
|
||||
// the executor as an std::function<void()> instance.
|
||||
// Lambda must be copyable, in order ot satisfy std::function<> requirements.
|
||||
// To make it so, we need Payload wrapped by a copyable wrapper.
|
||||
// std::shared_ptr<> is used, because it is copyable.
|
||||
auto shared_payload = std::make_shared<Payload>(std::move(payload));
|
||||
RouteToServiceController(
|
||||
[this, client, shared_payload,
|
||||
endpoint_ids = std::vector(endpoint_ids.begin(), endpoint_ids.end()),
|
||||
&callback]() {
|
||||
if (!ClientHasAcquiredServiceController(client)) {
|
||||
callback.result_cb({Status::kOutOfOrderApiCall});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ClientHasConnectionToAtLeastOneEndpoint(client, endpoint_ids)) {
|
||||
callback.result_cb({Status::kEndpointUnknown});
|
||||
return;
|
||||
}
|
||||
|
||||
service_controller_->SendPayload(client, endpoint_ids,
|
||||
std::move(*shared_payload));
|
||||
|
||||
// At this point, we've queued up the send Payload request with the
|
||||
// ServiceController; any further failures (e.g. one of the endpoints is
|
||||
// unknown, goes away, or otherwise fails) will be returned to the
|
||||
// client as a PayloadTransferUpdate.
|
||||
callback.result_cb({Status::kSuccess});
|
||||
});
|
||||
}
|
||||
|
||||
void ServiceControllerRouter::CancelPayload(ClientProxy* client,
|
||||
std::uint64_t payload_id,
|
||||
const ResultCallback& callback) {
|
||||
RouteToServiceController([this, client, payload_id, callback]() {
|
||||
if (!ClientHasAcquiredServiceController(client)) {
|
||||
callback.result_cb({Status::kOutOfOrderApiCall});
|
||||
return;
|
||||
}
|
||||
|
||||
callback.result_cb(service_controller_->CancelPayload(client, payload_id));
|
||||
});
|
||||
}
|
||||
|
||||
void ServiceControllerRouter::DisconnectFromEndpoint(
|
||||
ClientProxy* client, absl::string_view endpoint_id,
|
||||
const ResultCallback& callback) {
|
||||
RouteToServiceController(
|
||||
[this, client, endpoint_id = std::string(endpoint_id), callback]() {
|
||||
if (ClientHasAcquiredServiceController(client)) {
|
||||
if (!client->IsConnectedToEndpoint(endpoint_id) &&
|
||||
!client->HasPendingConnectionToEndpoint(endpoint_id)) {
|
||||
callback.result_cb({Status::kOutOfOrderApiCall});
|
||||
return;
|
||||
}
|
||||
service_controller_->DisconnectFromEndpoint(client, endpoint_id);
|
||||
callback.result_cb({Status::kSuccess});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ServiceControllerRouter::StopAllEndpoints(ClientProxy* client,
|
||||
const ResultCallback& callback) {
|
||||
RouteToServiceController([this, client, callback]() {
|
||||
if (ClientHasAcquiredServiceController(client)) {
|
||||
DoneWithStrategySessionForClient(client);
|
||||
}
|
||||
callback.result_cb({Status::kSuccess});
|
||||
});
|
||||
}
|
||||
|
||||
void ServiceControllerRouter::ClientDisconnecting(
|
||||
ClientProxy* client, const ResultCallback& callback) {
|
||||
RouteToServiceController([this, client, callback]() {
|
||||
if (ClientHasAcquiredServiceController(client)) {
|
||||
DoneWithStrategySessionForClient(client);
|
||||
// Log the completion of this client's connection.
|
||||
// TODO(tracyzhou): Add logging.
|
||||
}
|
||||
callback.result_cb({Status::kSuccess});
|
||||
});
|
||||
}
|
||||
|
||||
Status ServiceControllerRouter::AcquireServiceControllerForClient(
|
||||
ClientProxy* client, Strategy strategy) {
|
||||
if (current_strategy_.IsNone()) {
|
||||
// Case 1: There is no existing Strategy at all.
|
||||
|
||||
// Set everything up for the first time.
|
||||
Status status = UpdateCurrentServiceControllerAndStrategy(strategy);
|
||||
if (!status.Ok()) {
|
||||
return status;
|
||||
}
|
||||
clients_.insert(client);
|
||||
return {Status::kSuccess};
|
||||
} else if (strategy == current_strategy_) {
|
||||
// Case 2: The existing Strategy matches.
|
||||
|
||||
// The new client just needs to be added to the set of clients using the
|
||||
// current ServiceController.
|
||||
clients_.insert(client);
|
||||
return {Status::kSuccess};
|
||||
} else {
|
||||
// Case 3: The existing Strategy doesn't match.
|
||||
|
||||
// It's only safe for a client to cause a switch if it's the only client
|
||||
// using the current ServiceController.
|
||||
bool is_the_only_client_of_service_controller =
|
||||
clients_.size() == 1 && ClientHasAcquiredServiceController(client);
|
||||
if (!is_the_only_client_of_service_controller) {
|
||||
// TODO(tracyzhou): logging
|
||||
return {Status::kAlreadyHaveActiveStrategy};
|
||||
}
|
||||
|
||||
// If the client still has connected endpoints, they must disconnect before
|
||||
// they can switch.
|
||||
if (!client->GetConnectedEndpoints().empty()) {
|
||||
// TODO(tracyzhou): logging
|
||||
return {Status::kOutOfOrderApiCall};
|
||||
}
|
||||
|
||||
// By this point, it's safe to switch the Strategy and ServiceController
|
||||
// (and since it's the only client, there's no need to add it to the set of
|
||||
// clients using the current ServiceController).
|
||||
return UpdateCurrentServiceControllerAndStrategy(strategy);
|
||||
}
|
||||
}
|
||||
|
||||
bool ServiceControllerRouter::ClientHasAcquiredServiceController(
|
||||
ClientProxy* client) const {
|
||||
return clients_.contains(client);
|
||||
}
|
||||
|
||||
void ServiceControllerRouter::ReleaseServiceControllerForClient(
|
||||
ClientProxy* client) {
|
||||
clients_.erase(client);
|
||||
|
||||
if (clients_.empty()) {
|
||||
service_controller_.reset();
|
||||
current_strategy_ = Strategy{};
|
||||
}
|
||||
}
|
||||
|
||||
/** Clean up all state for this client. The client is now free to switch
|
||||
* strategies. */
|
||||
void ServiceControllerRouter::DoneWithStrategySessionForClient(
|
||||
ClientProxy* client) {
|
||||
// Disconnect from all the connected endpoints tied to this clientProxy.
|
||||
for (auto& endpoint_id : client->GetPendingConnectedEndpoints()) {
|
||||
service_controller_->DisconnectFromEndpoint(client, endpoint_id);
|
||||
}
|
||||
|
||||
for (auto& endpoint_id : client->GetConnectedEndpoints()) {
|
||||
service_controller_->DisconnectFromEndpoint(client, endpoint_id);
|
||||
}
|
||||
|
||||
// Stop any advertising and discovery that may be underway due to this
|
||||
// clientProxy.
|
||||
service_controller_->StopAdvertising(client);
|
||||
service_controller_->StopDiscovery(client);
|
||||
|
||||
ReleaseServiceControllerForClient(client);
|
||||
}
|
||||
|
||||
void ServiceControllerRouter::RouteToServiceController(Runnable runnable) {
|
||||
serializer_.Execute(std::move(runnable));
|
||||
}
|
||||
|
||||
bool ServiceControllerRouter::ClientHasConnectionToAtLeastOneEndpoint(
|
||||
ClientProxy* client, const std::vector<std::string>& remote_endpoint_ids) {
|
||||
for (auto& endpoint_id : remote_endpoint_ids) {
|
||||
if (client->IsConnectedToEndpoint(endpoint_id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Status ServiceControllerRouter::UpdateCurrentServiceControllerAndStrategy(
|
||||
Strategy strategy) {
|
||||
if (!strategy.IsValid()) {
|
||||
// TODO(tracyzhou): logging
|
||||
return {Status::kError};
|
||||
}
|
||||
|
||||
service_controller_.reset(service_controller_factory_());
|
||||
current_strategy_ = strategy;
|
||||
|
||||
return {Status::kSuccess};
|
||||
}
|
||||
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,111 @@
|
||||
#ifndef CORE_V2_INTERNAL_SERVICE_CONTROLLER_ROUTER_H_
|
||||
#define CORE_V2_INTERNAL_SERVICE_CONTROLLER_ROUTER_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core_v2/internal/client_proxy.h"
|
||||
#include "core_v2/internal/service_controller.h"
|
||||
#include "core_v2/options.h"
|
||||
#include "core_v2/params.h"
|
||||
#include "platform_v2/base/runnable.h"
|
||||
#include "platform_v2/public/single_thread_executor.h"
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "absl/types/span.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
|
||||
// ServiceControllerRouter: this class is an implementation detail of a
|
||||
// location::nearby::Core class. The latter delegates all of its activities to
|
||||
// the former.
|
||||
//
|
||||
// All the activities are documented in the public API class:
|
||||
// https://source.corp.google.com/piper///depot/google3/core_v2/core.h
|
||||
//
|
||||
// In every method, ClientProxy* represents the client app which receives
|
||||
// notifications from Nearby Connections service and forwards them to the app.
|
||||
// The rest of arguments have the same meaning as the corresponding
|
||||
// methods in the definition of location::nearby::Core API.
|
||||
//
|
||||
// Every activity is handled the same way:
|
||||
// 1) all the arguments to the call are captured by value;
|
||||
// 2) the actual processing is scheduled on a private single-threaded executor,
|
||||
// which makes locking unnecessary, when internal data is being manipulated.
|
||||
// 3) activity handlers are delegating much of their work to an implementation
|
||||
// of a ServiceController interface, which does the actual job.
|
||||
class ServiceControllerRouter {
|
||||
public:
|
||||
explicit ServiceControllerRouter(std::function<ServiceController*()> factory)
|
||||
: service_controller_factory_(std::move(factory)) {}
|
||||
~ServiceControllerRouter();
|
||||
ServiceControllerRouter(ServiceControllerRouter&&) = default;
|
||||
ServiceControllerRouter& operator=(ServiceControllerRouter&&) = default;
|
||||
|
||||
void StartAdvertising(ClientProxy* client, absl::string_view service_id,
|
||||
const ConnectionOptions& options,
|
||||
const ConnectionRequestInfo& info,
|
||||
const ResultCallback& callback);
|
||||
void StopAdvertising(ClientProxy* client, const ResultCallback& callback);
|
||||
|
||||
void StartDiscovery(ClientProxy* client, absl::string_view service_id,
|
||||
const ConnectionOptions& options,
|
||||
const DiscoveryListener& listener,
|
||||
const ResultCallback& callback);
|
||||
void StopDiscovery(ClientProxy* client, const ResultCallback& callback);
|
||||
|
||||
void RequestConnection(ClientProxy* client, absl::string_view endpoint_id,
|
||||
const ConnectionRequestInfo& info,
|
||||
const ResultCallback& callback);
|
||||
void AcceptConnection(ClientProxy* client, absl::string_view endpoint_id,
|
||||
const PayloadListener& listener,
|
||||
const ResultCallback& callback);
|
||||
void RejectConnection(ClientProxy* client, absl::string_view endpoint_id,
|
||||
const ResultCallback& callback);
|
||||
|
||||
void InitiateBandwidthUpgrade(ClientProxy* client,
|
||||
absl::string_view endpoint_id,
|
||||
const ResultCallback& callback);
|
||||
|
||||
void SendPayload(ClientProxy* client,
|
||||
absl::Span<const std::string> endpoint_ids, Payload payload,
|
||||
const ResultCallback& callback);
|
||||
void CancelPayload(ClientProxy* client, std::uint64_t payload_id,
|
||||
const ResultCallback& callback);
|
||||
|
||||
void DisconnectFromEndpoint(ClientProxy* client,
|
||||
absl::string_view endpoint_id,
|
||||
const ResultCallback& callback);
|
||||
void StopAllEndpoints(ClientProxy* client, const ResultCallback& callback);
|
||||
|
||||
void ClientDisconnecting(ClientProxy* client, const ResultCallback& callback);
|
||||
|
||||
private:
|
||||
friend class ServiceControllerRouterTest;
|
||||
static bool ClientHasConnectionToAtLeastOneEndpoint(
|
||||
ClientProxy* client, const std::vector<std::string>& remote_endpoint_ids);
|
||||
|
||||
void RouteToServiceController(Runnable runnable);
|
||||
|
||||
Status AcquireServiceControllerForClient(ClientProxy* client,
|
||||
Strategy strategy);
|
||||
bool ClientHasAcquiredServiceController(ClientProxy* client) const;
|
||||
void ReleaseServiceControllerForClient(ClientProxy* client);
|
||||
void DoneWithStrategySessionForClient(ClientProxy* client);
|
||||
Status UpdateCurrentServiceControllerAndStrategy(Strategy strategy);
|
||||
|
||||
absl::flat_hash_set<ClientProxy*> clients_;
|
||||
std::function<ServiceController*()> service_controller_factory_;
|
||||
std::unique_ptr<ServiceController> service_controller_;
|
||||
Strategy current_strategy_;
|
||||
SingleThreadExecutor serializer_;
|
||||
};
|
||||
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
|
||||
#endif // CORE_V2_INTERNAL_SERVICE_CONTROLLER_ROUTER_H_
|
||||
@@ -0,0 +1,376 @@
|
||||
#include "core_v2/internal/service_controller_router.h"
|
||||
|
||||
#include <cinttypes>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "core_v2/internal/client_proxy.h"
|
||||
#include "core_v2/internal/mock_service_controller.h"
|
||||
#include "core_v2/internal/service_controller.h"
|
||||
#include "core_v2/listeners.h"
|
||||
#include "core_v2/options.h"
|
||||
#include "core_v2/params.h"
|
||||
#include "platform_v2/base/byte_array.h"
|
||||
#include "platform_v2/public/condition_variable.h"
|
||||
#include "platform_v2/public/mutex.h"
|
||||
#include "platform_v2/public/mutex_lock.h"
|
||||
#include "gmock/gmock.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
#include "absl/time/clock.h"
|
||||
#include "absl/types/span.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
|
||||
namespace {
|
||||
using ::testing::Return;
|
||||
} // namespace
|
||||
|
||||
// This class must be in the same namespace as ServiceControllerRouter for
|
||||
// friend class to work.
|
||||
class ServiceControllerRouterTest : public testing::Test {
|
||||
public:
|
||||
ServiceControllerRouterTest() = default;
|
||||
~ServiceControllerRouterTest() override {
|
||||
router_.service_controller_.release();
|
||||
}
|
||||
|
||||
void StartAdvertising(ClientProxy* client, std::string service_id,
|
||||
ConnectionOptions options, ConnectionRequestInfo info,
|
||||
ResultCallback callback) {
|
||||
EXPECT_CALL(mock_, StartAdvertising)
|
||||
.WillOnce(Return(Status{Status::kSuccess}));
|
||||
{
|
||||
MutexLock lock(&mutex_);
|
||||
complete_ = false;
|
||||
router_.StartAdvertising(client, service_id, options, info, callback);
|
||||
while (!complete_) cond_.Wait();
|
||||
EXPECT_EQ(result_, Status{Status::kSuccess});
|
||||
}
|
||||
client->StartedAdvertising(kServiceId, options.strategy, info.listener,
|
||||
absl::MakeSpan(mediums_));
|
||||
EXPECT_TRUE(client->IsAdvertising());
|
||||
}
|
||||
|
||||
void StopAdvertising(ClientProxy* client, ResultCallback callback) {
|
||||
EXPECT_CALL(mock_, StopAdvertising).Times(1);
|
||||
{
|
||||
MutexLock lock(&mutex_);
|
||||
complete_ = false;
|
||||
router_.StopAdvertising(client, callback);
|
||||
while (!complete_) cond_.Wait();
|
||||
}
|
||||
client->StoppedAdvertising();
|
||||
EXPECT_FALSE(client->IsAdvertising());
|
||||
}
|
||||
|
||||
void StartDiscovery(ClientProxy* client, std::string service_id,
|
||||
ConnectionOptions options,
|
||||
const DiscoveryListener& listener,
|
||||
const ResultCallback& callback) {
|
||||
EXPECT_CALL(mock_, StartDiscovery)
|
||||
.WillOnce(Return(Status{Status::kSuccess}));
|
||||
{
|
||||
MutexLock lock(&mutex_);
|
||||
complete_ = false;
|
||||
router_.StartDiscovery(client, kServiceId, options, listener, callback);
|
||||
while (!complete_) cond_.Wait();
|
||||
EXPECT_EQ(result_, Status{Status::kSuccess});
|
||||
}
|
||||
client->StartedDiscovery(service_id, options.strategy, listener,
|
||||
absl::MakeSpan(mediums_));
|
||||
EXPECT_TRUE(client->IsDiscovering());
|
||||
}
|
||||
|
||||
void StopDiscovery(ClientProxy* client, ResultCallback callback) {
|
||||
EXPECT_CALL(mock_, StopDiscovery).Times(1);
|
||||
{
|
||||
MutexLock lock(&mutex_);
|
||||
complete_ = false;
|
||||
router_.StopDiscovery(client, callback);
|
||||
while (!complete_) cond_.Wait();
|
||||
}
|
||||
client->StoppedDiscovery();
|
||||
EXPECT_FALSE(client->IsDiscovering());
|
||||
}
|
||||
|
||||
void RequestConnection(ClientProxy* client, const std::string& endpoint_id,
|
||||
const ConnectionRequestInfo& request_info,
|
||||
ResultCallback callback) {
|
||||
EXPECT_CALL(mock_, RequestConnection)
|
||||
.WillOnce(Return(Status{Status::kSuccess}));
|
||||
{
|
||||
MutexLock lock(&mutex_);
|
||||
complete_ = false;
|
||||
router_.RequestConnection(client, endpoint_id, request_info, callback);
|
||||
while (!complete_) cond_.Wait();
|
||||
EXPECT_EQ(result_, Status{Status::kSuccess});
|
||||
}
|
||||
ConnectionResponseInfo response_info{
|
||||
.remote_endpoint_name = "endpoint_name",
|
||||
.authentication_token = "auth_token",
|
||||
.raw_authentication_token = ByteArray("auth_token"),
|
||||
.is_incoming_connection = true,
|
||||
};
|
||||
client->OnConnectionInitiated(endpoint_id, response_info,
|
||||
request_info.listener);
|
||||
EXPECT_TRUE(client->HasPendingConnectionToEndpoint(endpoint_id));
|
||||
}
|
||||
|
||||
void AcceptConnection(ClientProxy* client, const std::string endpoint_id,
|
||||
const PayloadListener& listener,
|
||||
const ResultCallback& callback) {
|
||||
EXPECT_CALL(mock_, AcceptConnection)
|
||||
.WillOnce(Return(Status{Status::kSuccess}));
|
||||
// Pre-condition for successful Accept is: connection must exist.
|
||||
EXPECT_TRUE(client->HasPendingConnectionToEndpoint(endpoint_id));
|
||||
{
|
||||
MutexLock lock(&mutex_);
|
||||
complete_ = false;
|
||||
router_.AcceptConnection(client, endpoint_id, listener, callback);
|
||||
while (!complete_) cond_.Wait();
|
||||
EXPECT_EQ(result_, Status{Status::kSuccess});
|
||||
}
|
||||
client->LocalEndpointAcceptedConnection(endpoint_id, listener);
|
||||
client->RemoteEndpointAcceptedConnection(endpoint_id);
|
||||
EXPECT_TRUE(client->IsConnectionAccepted(endpoint_id));
|
||||
client->OnConnectionAccepted(endpoint_id);
|
||||
EXPECT_TRUE(client->IsConnectedToEndpoint(endpoint_id));
|
||||
}
|
||||
|
||||
void RejectConnection(ClientProxy* client, const std::string endpoint_id,
|
||||
ResultCallback callback) {
|
||||
EXPECT_CALL(mock_, RejectConnection)
|
||||
.WillOnce(Return(Status{Status::kSuccess}));
|
||||
// Pre-condition for successful Accept is: connection must exist.
|
||||
EXPECT_TRUE(client->HasPendingConnectionToEndpoint(endpoint_id));
|
||||
{
|
||||
MutexLock lock(&mutex_);
|
||||
complete_ = false;
|
||||
router_.RejectConnection(client, endpoint_id, callback);
|
||||
while (!complete_) cond_.Wait();
|
||||
EXPECT_EQ(result_, Status{Status::kSuccess});
|
||||
}
|
||||
client->LocalEndpointRejectedConnection(endpoint_id);
|
||||
EXPECT_TRUE(client->IsConnectionRejected(endpoint_id));
|
||||
}
|
||||
|
||||
void InitiateBandwidthUpgrade(ClientProxy* client,
|
||||
const std::string endpoint_id,
|
||||
ResultCallback callback) {
|
||||
EXPECT_CALL(mock_, InitiateBandwidthUpgrade).Times(1);
|
||||
EXPECT_TRUE(client->IsConnectedToEndpoint(endpoint_id));
|
||||
{
|
||||
MutexLock lock(&mutex_);
|
||||
complete_ = false;
|
||||
router_.InitiateBandwidthUpgrade(client, endpoint_id, callback);
|
||||
while (!complete_) cond_.Wait();
|
||||
EXPECT_EQ(result_, Status{Status::kSuccess});
|
||||
}
|
||||
}
|
||||
|
||||
void SendPayload(ClientProxy* client,
|
||||
const std::vector<std::string>& endpoint_ids,
|
||||
Payload payload, ResultCallback callback) {
|
||||
EXPECT_CALL(mock_, SendPayload).Times(1);
|
||||
|
||||
bool connected = false;
|
||||
for (const auto& endpoint_id : endpoint_ids) {
|
||||
connected = connected || client->IsConnectedToEndpoint(endpoint_id);
|
||||
}
|
||||
EXPECT_TRUE(connected);
|
||||
{
|
||||
MutexLock lock(&mutex_);
|
||||
complete_ = false;
|
||||
router_.SendPayload(client, absl::MakeSpan(endpoint_ids),
|
||||
std::move(payload), callback);
|
||||
while (!complete_) cond_.Wait();
|
||||
EXPECT_EQ(result_, Status{Status::kSuccess});
|
||||
}
|
||||
}
|
||||
|
||||
void CancelPayload(ClientProxy* client, std::int64_t payload_id,
|
||||
ResultCallback callback) {
|
||||
EXPECT_CALL(mock_, CancelPayload)
|
||||
.WillOnce(Return(Status{Status::kSuccess}));
|
||||
{
|
||||
MutexLock lock(&mutex_);
|
||||
complete_ = false;
|
||||
router_.CancelPayload(client, payload_id, callback);
|
||||
while (!complete_) cond_.Wait();
|
||||
EXPECT_EQ(result_, Status{Status::kSuccess});
|
||||
}
|
||||
}
|
||||
|
||||
void DisconnectFromEndpoint(ClientProxy* client,
|
||||
const std::string endpoint_id,
|
||||
ResultCallback callback) {
|
||||
EXPECT_CALL(mock_, DisconnectFromEndpoint).Times(1);
|
||||
EXPECT_TRUE(client->IsConnectedToEndpoint(endpoint_id));
|
||||
{
|
||||
MutexLock lock(&mutex_);
|
||||
complete_ = false;
|
||||
router_.DisconnectFromEndpoint(client, endpoint_id, callback);
|
||||
while (!complete_) cond_.Wait();
|
||||
}
|
||||
client->OnDisconnected(endpoint_id, false);
|
||||
EXPECT_FALSE(client->IsConnectedToEndpoint(endpoint_id));
|
||||
}
|
||||
|
||||
protected:
|
||||
const ResultCallback kCallback{
|
||||
.result_cb =
|
||||
[this](Status status) {
|
||||
MutexLock lock(&mutex_);
|
||||
result_ = status;
|
||||
complete_ = true;
|
||||
cond_.Notify();
|
||||
},
|
||||
};
|
||||
const std::string kServiceId = "service id";
|
||||
const std::string kRequestorName = "requestor name";
|
||||
const std::string kRemoteEndpointId = "remote endpoint id";
|
||||
const std::int64_t kPayloadId = UINT64_C(0x123456789ABCDEF0);
|
||||
const ConnectionOptions kConnectionOptions{
|
||||
.strategy = Strategy::kP2pPointToPoint,
|
||||
.auto_upgrade_bandwidth = true,
|
||||
.enforce_topology_constraints = true,
|
||||
};
|
||||
|
||||
std::vector<proto::connections::Medium> mediums_{
|
||||
proto::connections::Medium::BLUETOOTH};
|
||||
const ConnectionRequestInfo kConnectionRequestInfo{
|
||||
.name = kRequestorName,
|
||||
.listener = ConnectionListener(),
|
||||
};
|
||||
|
||||
DiscoveryListener discovery_listener_;
|
||||
PayloadListener payload_listener_;
|
||||
|
||||
Mutex mutex_;
|
||||
ConditionVariable cond_{&mutex_};
|
||||
Status result_ ABSL_GUARDED_BY(mutex_) = {Status::kError};
|
||||
bool complete_ ABSL_GUARDED_BY(mutex_) = false;
|
||||
MockServiceController mock_;
|
||||
ClientProxy client_;
|
||||
|
||||
ServiceControllerRouter router_{
|
||||
[this]() -> ServiceController* { return &mock_; }};
|
||||
};
|
||||
|
||||
namespace {
|
||||
TEST_F(ServiceControllerRouterTest, CostructorDestructorWorks) { SUCCEED(); }
|
||||
|
||||
TEST_F(ServiceControllerRouterTest, StartAdvertisingCalled) {
|
||||
StartAdvertising(&client_, kServiceId, kConnectionOptions,
|
||||
kConnectionRequestInfo, kCallback);
|
||||
}
|
||||
|
||||
TEST_F(ServiceControllerRouterTest, StopAdvertisingCalled) {
|
||||
StartAdvertising(&client_, kServiceId, kConnectionOptions,
|
||||
kConnectionRequestInfo, kCallback);
|
||||
StopAdvertising(&client_, kCallback);
|
||||
}
|
||||
|
||||
TEST_F(ServiceControllerRouterTest, StartDiscoveryCalled) {
|
||||
StartDiscovery(&client_, kServiceId, kConnectionOptions, discovery_listener_,
|
||||
kCallback);
|
||||
}
|
||||
|
||||
TEST_F(ServiceControllerRouterTest, StopDiscoveryCalled) {
|
||||
StartDiscovery(&client_, kServiceId, kConnectionOptions, discovery_listener_,
|
||||
kCallback);
|
||||
StopDiscovery(&client_, kCallback);
|
||||
}
|
||||
|
||||
TEST_F(ServiceControllerRouterTest, RequestConnectionCalled) {
|
||||
// Either Advertising, or Discovery should be ongoing.
|
||||
StartDiscovery(&client_, kServiceId, kConnectionOptions, discovery_listener_,
|
||||
kCallback);
|
||||
RequestConnection(&client_, kRemoteEndpointId, kConnectionRequestInfo,
|
||||
kCallback);
|
||||
}
|
||||
|
||||
TEST_F(ServiceControllerRouterTest, AcceptConnectionCalled) {
|
||||
// Either Adviertisng, or Discovery should be ongoing.
|
||||
StartDiscovery(&client_, kServiceId, kConnectionOptions, discovery_listener_,
|
||||
kCallback);
|
||||
// Establish connection.
|
||||
RequestConnection(&client_, kRemoteEndpointId, kConnectionRequestInfo,
|
||||
kCallback);
|
||||
// Now, we can accept connection.
|
||||
AcceptConnection(&client_, kRemoteEndpointId, payload_listener_, kCallback);
|
||||
}
|
||||
|
||||
TEST_F(ServiceControllerRouterTest, RejectConnectionCalled) {
|
||||
// Either Adviertisng, or Discovery should be ongoing.
|
||||
StartDiscovery(&client_, kServiceId, kConnectionOptions, discovery_listener_,
|
||||
kCallback);
|
||||
// Establish connection.
|
||||
RequestConnection(&client_, kRemoteEndpointId, kConnectionRequestInfo,
|
||||
kCallback);
|
||||
// Now, we can reject connection.
|
||||
RejectConnection(&client_, kRemoteEndpointId, kCallback);
|
||||
}
|
||||
|
||||
TEST_F(ServiceControllerRouterTest, InitiateBandwidthUpgradeCalled) {
|
||||
// Either Adviertisng, or Discovery should be ongoing.
|
||||
StartDiscovery(&client_, kServiceId, kConnectionOptions, discovery_listener_,
|
||||
kCallback);
|
||||
// Establish connection.
|
||||
RequestConnection(&client_, kRemoteEndpointId, kConnectionRequestInfo,
|
||||
kCallback);
|
||||
// Now, we can accept connection.
|
||||
AcceptConnection(&client_, kRemoteEndpointId, payload_listener_, kCallback);
|
||||
// Now we can change connection bandwidth.
|
||||
InitiateBandwidthUpgrade(&client_, kRemoteEndpointId, kCallback);
|
||||
}
|
||||
|
||||
TEST_F(ServiceControllerRouterTest, SendPayloadCalled) {
|
||||
// Either Adviertisng, or Discovery should be ongoing.
|
||||
StartDiscovery(&client_, kServiceId, kConnectionOptions, discovery_listener_,
|
||||
kCallback);
|
||||
// Establish connection.
|
||||
RequestConnection(&client_, kRemoteEndpointId, kConnectionRequestInfo,
|
||||
kCallback);
|
||||
// Now, we can accept connection.
|
||||
AcceptConnection(&client_, kRemoteEndpointId, payload_listener_, kCallback);
|
||||
// Now we can send payload.
|
||||
SendPayload(&client_, std::vector<std::string>{kRemoteEndpointId},
|
||||
Payload{ByteArray("data")}, kCallback);
|
||||
}
|
||||
|
||||
TEST_F(ServiceControllerRouterTest, CancelPayloadCalled) {
|
||||
// Either Adviertisng, or Discovery should be ongoing.
|
||||
StartDiscovery(&client_, kServiceId, kConnectionOptions, discovery_listener_,
|
||||
kCallback);
|
||||
// Establish connection.
|
||||
RequestConnection(&client_, kRemoteEndpointId, kConnectionRequestInfo,
|
||||
kCallback);
|
||||
// Now, we can accept connection.
|
||||
AcceptConnection(&client_, kRemoteEndpointId, payload_listener_, kCallback);
|
||||
// We have to know payload id, before we can cancel payload transfer.
|
||||
// It is either after a call to SendPayload, or after receiving
|
||||
// PayloadProgress callback. Let's assume we have it, and proceed.
|
||||
CancelPayload(&client_, kPayloadId, kCallback);
|
||||
}
|
||||
|
||||
TEST_F(ServiceControllerRouterTest, DisconnectFromEndpointCalled) {
|
||||
// Either Adviertisng, or Discovery should be ongoing.
|
||||
StartDiscovery(&client_, kServiceId, kConnectionOptions, discovery_listener_,
|
||||
kCallback);
|
||||
// Establish connection.
|
||||
RequestConnection(&client_, kRemoteEndpointId, kConnectionRequestInfo,
|
||||
kCallback);
|
||||
// Now, we can accept connection.
|
||||
AcceptConnection(&client_, kRemoteEndpointId, payload_listener_, kCallback);
|
||||
// We can disconnect at any time after RequestConnection.
|
||||
DisconnectFromEndpoint(&client_, kRemoteEndpointId, kCallback);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,180 @@
|
||||
#include "core_v2/internal/wifi_lan_service_info.h"
|
||||
|
||||
#include <inttypes.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <utility>
|
||||
|
||||
#include "platform_v2/base/base64_utils.h"
|
||||
#include "platform_v2/public/logging.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
|
||||
WifiLanServiceInfo::WifiLanServiceInfo(Version version, Pcp pcp,
|
||||
absl::string_view endpoint_id,
|
||||
const ByteArray& service_id_hash,
|
||||
absl::string_view endpoint_name) {
|
||||
if (version != Version::kV1 || endpoint_id.empty() ||
|
||||
endpoint_id.length() != kEndpointIdLength ||
|
||||
service_id_hash.size() != kServiceIdHashLength) {
|
||||
return;
|
||||
}
|
||||
switch (pcp) {
|
||||
case Pcp::kP2pCluster: // Fall through
|
||||
case Pcp::kP2pStar: // Fall through
|
||||
case Pcp::kP2pPointToPoint:
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
version_ = version;
|
||||
pcp_ = pcp;
|
||||
service_id_hash_ = service_id_hash;
|
||||
endpoint_id_ = endpoint_id;
|
||||
}
|
||||
|
||||
WifiLanServiceInfo::WifiLanServiceInfo(absl::string_view service_info_string) {
|
||||
ByteArray service_info_bytes = Base64Utils::Decode(service_info_string);
|
||||
|
||||
if (service_info_bytes.Empty()) {
|
||||
NEARBY_LOG(
|
||||
ERROR,
|
||||
"Cannot deserialize WifiLanServiceInfo: failed Base64 decoding of %s",
|
||||
std::string(service_info_string).c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
if (service_info_bytes.size() > kMaxLanServiceNameLength) {
|
||||
NEARBY_LOG(ERROR,
|
||||
"Cannot deserialize WifiLanServiceInfo: expecting max %d raw "
|
||||
"bytes, got %" PRIu64,
|
||||
kMaxLanServiceNameLength, service_info_bytes.size());
|
||||
return;
|
||||
}
|
||||
|
||||
if (service_info_bytes.size() < kMinLanServiceNameLength) {
|
||||
NEARBY_LOG(ERROR,
|
||||
"Cannot deserialize WifiLanServiceInfo: expecting min %d raw "
|
||||
"bytes, got %" PRIu64,
|
||||
kMinLanServiceNameLength, service_info_bytes.size());
|
||||
return;
|
||||
}
|
||||
|
||||
// The upper 3 bits are supposed to be the version.
|
||||
version_ = static_cast<Version>(
|
||||
(service_info_bytes.data()[0] & kVersionBitmask) >> kVersionShift);
|
||||
const char* service_info_bytes_read_ptr = service_info_bytes.data();
|
||||
switch (version_) {
|
||||
case Version::kV1:
|
||||
// The lower 5 bits of the V1 payload are supposed to be the Pcp.
|
||||
pcp_ = static_cast<Pcp>(*service_info_bytes_read_ptr & kPcpBitmask);
|
||||
service_info_bytes_read_ptr++;
|
||||
switch (pcp_) {
|
||||
case Pcp::kP2pCluster: // Fall through
|
||||
case Pcp::kP2pStar: // Fall through
|
||||
case Pcp::kP2pPointToPoint:
|
||||
// The next 32 bits are supposed to be the endpoint_id.
|
||||
endpoint_id_ =
|
||||
std::string(service_info_bytes_read_ptr, kEndpointIdLength);
|
||||
service_info_bytes_read_ptr += kEndpointIdLength;
|
||||
|
||||
// The next 24 bits are supposed to be the service_id_hash.
|
||||
service_id_hash_ =
|
||||
ByteArray(service_info_bytes_read_ptr, kServiceIdHashLength);
|
||||
service_info_bytes_read_ptr += kServiceIdHashLength;
|
||||
|
||||
// The next bits are supposed to be endpoint_name.
|
||||
// TODO(edwinwu): Implements it. Temp to set "found_device".
|
||||
endpoint_name_ = "found_device";
|
||||
break;
|
||||
|
||||
default:
|
||||
// TODO(edwinwu): [ANALYTICIZE] This either represents corruption over
|
||||
// the air, or older versions of GmsCore intermingling with newer
|
||||
// ones.
|
||||
NEARBY_LOG(
|
||||
ERROR,
|
||||
"Cannot deserialize WifiLanServiceInfo: unsupported V1 PCP %d",
|
||||
pcp_);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// TODO(edwinwu): [ANALYTICIZE] This either represents corruption over
|
||||
// the air, or older versions of GmsCore intermingling with newer ones.
|
||||
NEARBY_LOG(
|
||||
ERROR,
|
||||
"Cannot deserialize WifiLanServiceInfo: unsupported Version %d",
|
||||
version_);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
WifiLanServiceInfo::operator std::string() const {
|
||||
if (!IsValid()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
ByteArray wifi_lan_service_info_name_bytes(kMinLanServiceNameLength);
|
||||
auto* wifi_lan_service_info_name_bytes_write_ptr =
|
||||
wifi_lan_service_info_name_bytes.data();
|
||||
|
||||
// The upper 3 bits are the Version.
|
||||
auto version_and_pcp_byte = static_cast<char>(
|
||||
(static_cast<uint32_t>(Version::kV1) << 5) & kVersionBitmask);
|
||||
// The lower 5 bits are the PCP.
|
||||
version_and_pcp_byte |=
|
||||
static_cast<char>(static_cast<uint32_t>(pcp_) & kPcpBitmask);
|
||||
*wifi_lan_service_info_name_bytes_write_ptr = version_and_pcp_byte;
|
||||
wifi_lan_service_info_name_bytes_write_ptr++;
|
||||
|
||||
switch (pcp_) {
|
||||
case Pcp::kP2pCluster: // Fall through
|
||||
case Pcp::kP2pStar: // Fall through
|
||||
case Pcp::kP2pPointToPoint:
|
||||
// The next 32 bits are the endpoint_id.
|
||||
if (endpoint_id_.size() != kEndpointIdLength) {
|
||||
NEARBY_LOG(
|
||||
ERROR,
|
||||
"Cannot serialize WifiLanServiceInfo: V1 Endpoint ID %s (%" PRIu64
|
||||
" bytes) should be exactly %d bytes",
|
||||
endpoint_id_.c_str(), endpoint_id_.size(), kEndpointIdLength);
|
||||
return "";
|
||||
}
|
||||
memcpy(wifi_lan_service_info_name_bytes_write_ptr, endpoint_id_.data(),
|
||||
kEndpointIdLength);
|
||||
wifi_lan_service_info_name_bytes_write_ptr += kEndpointIdLength;
|
||||
|
||||
// The next 24 bits are the service_id_hash.
|
||||
if (service_id_hash_.size() != kServiceIdHashLength) {
|
||||
NEARBY_LOG(
|
||||
ERROR,
|
||||
"Cannot serialize WifiLanServiceInfo: V1 ServiceID hash (%" PRIu64
|
||||
" bytes) should be exactly %d bytes",
|
||||
service_id_hash_.size(), kServiceIdHashLength);
|
||||
return "";
|
||||
}
|
||||
memcpy(wifi_lan_service_info_name_bytes_write_ptr,
|
||||
service_id_hash_.data(), kServiceIdHashLength);
|
||||
wifi_lan_service_info_name_bytes_write_ptr += kServiceIdHashLength;
|
||||
|
||||
// The next bits are the endpoint_name.
|
||||
// TODO(edwinwu): Implements to parse endpoint_name.
|
||||
break;
|
||||
default:
|
||||
NEARBY_LOG(ERROR,
|
||||
"Cannot serialize WifiLanServiceInfo: unsupported V1 PCP %d",
|
||||
pcp_);
|
||||
return "";
|
||||
}
|
||||
|
||||
return Base64Utils::Encode(wifi_lan_service_info_name_bytes);
|
||||
}
|
||||
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
@@ -0,0 +1,81 @@
|
||||
#ifndef CORE_V2_INTERNAL_WIFI_LAN_SERVICE_INFO_H_
|
||||
#define CORE_V2_INTERNAL_WIFI_LAN_SERVICE_INFO_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "core_v2/internal/pcp.h"
|
||||
#include "platform_v2/base/byte_array.h"
|
||||
#include "absl/strings/string_view.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
|
||||
// Represents the format of the WifiLan service info used in Advertising +
|
||||
// Discovery.
|
||||
//
|
||||
// See go/nearby-offline-data-interchange-formats for the specification.
|
||||
class WifiLanServiceInfo {
|
||||
public:
|
||||
// Versions of the WifiLanServiceInfo.
|
||||
enum class Version {
|
||||
kUndefined = 0,
|
||||
kV1 = 1,
|
||||
};
|
||||
|
||||
static constexpr std::uint32_t kServiceIdHashLength = 3;
|
||||
|
||||
WifiLanServiceInfo() = default;
|
||||
WifiLanServiceInfo(Version version, Pcp pcp, absl::string_view endpoint_id,
|
||||
const ByteArray& service_id_hash,
|
||||
absl::string_view endpoint_name);
|
||||
explicit WifiLanServiceInfo(absl::string_view service_info_string);
|
||||
~WifiLanServiceInfo() = default;
|
||||
|
||||
WifiLanServiceInfo(const WifiLanServiceInfo&) = default;
|
||||
WifiLanServiceInfo& operator=(const WifiLanServiceInfo&) = default;
|
||||
WifiLanServiceInfo(WifiLanServiceInfo&&) = default;
|
||||
WifiLanServiceInfo& operator=(WifiLanServiceInfo&&) = default;
|
||||
|
||||
explicit operator std::string() const;
|
||||
|
||||
inline bool IsValid() const { return !endpoint_id_.empty(); }
|
||||
inline Version GetVersion() const { return version_; }
|
||||
inline Pcp GetPcp() const { return pcp_; }
|
||||
inline std::string GetEndpointId() const { return endpoint_id_; }
|
||||
inline std::string GetEndpointName() const { return endpoint_name_; }
|
||||
inline ByteArray GetServiceIdHash() const { return service_id_hash_; }
|
||||
|
||||
private:
|
||||
// The maximum length of encrypted WifiLanServiceInfo string.
|
||||
static constexpr int kMaxLanServiceNameLength = 47;
|
||||
// The minimum length of encrypted WifiLanServiceInfo string.
|
||||
static constexpr int kMinLanServiceNameLength = 9;
|
||||
// The length for endpoint id in encrypted WifiLanServiceInfo string.
|
||||
static constexpr int kEndpointIdLength = 4;
|
||||
// The maximum length for endpoint id in encrypted WifiLanServiceInfo string.
|
||||
static constexpr int kMaxEndpointNameLength = 131;
|
||||
|
||||
static constexpr int kVersionBitmask = 0x0E0;
|
||||
static constexpr int kPcpBitmask = 0x01F;
|
||||
static constexpr int kVersionShift = 5;
|
||||
|
||||
// WifiLanServiceInfo version.
|
||||
Version version_ = Version::kUndefined;
|
||||
// Pre-Connection Protocols version.
|
||||
Pcp pcp_ = Pcp::kUnknown;
|
||||
// Connected endpoint id.
|
||||
std::string endpoint_id_;
|
||||
// Connected hash service id.
|
||||
ByteArray service_id_hash_;
|
||||
// TODO(edwinwu): Replaces endpointName as endPointInfo eventually;
|
||||
// it is not in this version yet for endpointName.
|
||||
// Connected endpoint name.
|
||||
std::string endpoint_name_;
|
||||
};
|
||||
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
|
||||
#endif // CORE_V2_INTERNAL_WIFI_LAN_SERVICE_INFO_H_
|
||||
@@ -0,0 +1,143 @@
|
||||
#include "core_v2/internal/wifi_lan_service_info.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
|
||||
#include "platform_v2/base/base64_utils.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
namespace location {
|
||||
namespace nearby {
|
||||
namespace connections {
|
||||
namespace {
|
||||
|
||||
const WifiLanServiceInfo::Version kVersion = WifiLanServiceInfo::Version::kV1;
|
||||
const Pcp kPcp = Pcp::kP2pCluster;
|
||||
const char kEndPointID[] = "AB12";
|
||||
const char kServiceIDHashBytes[] = {0x0A, 0x0B, 0x0C};
|
||||
// TODO(edwinwu): Temp to set empty string for endpoint_name.
|
||||
const char kEndPointName[] = "";
|
||||
|
||||
TEST(WifiLanServiceInfoTest, ConstructionWorks) {
|
||||
auto service_id_hash = ByteArray(kServiceIDHashBytes,
|
||||
sizeof(kServiceIDHashBytes) / sizeof(char));
|
||||
auto wifi_lan_service_info = WifiLanServiceInfo(
|
||||
kVersion, kPcp, kEndPointID, service_id_hash, kEndPointName);
|
||||
auto is_valid = wifi_lan_service_info.IsValid();
|
||||
|
||||
EXPECT_TRUE(is_valid);
|
||||
EXPECT_EQ(kPcp, wifi_lan_service_info.GetPcp());
|
||||
EXPECT_EQ(kVersion, wifi_lan_service_info.GetVersion());
|
||||
EXPECT_EQ(kEndPointID, wifi_lan_service_info.GetEndpointId());
|
||||
EXPECT_EQ(service_id_hash, wifi_lan_service_info.GetServiceIdHash());
|
||||
}
|
||||
|
||||
TEST(WifiLanServiceInfoTest, ConstructionFromSerializedStringWorks) {
|
||||
auto service_id_hash = ByteArray(kServiceIDHashBytes,
|
||||
sizeof(kServiceIDHashBytes) / sizeof(char));
|
||||
auto org_wifi_lan_service_info = WifiLanServiceInfo(
|
||||
kVersion, kPcp, kEndPointID, service_id_hash, kEndPointName);
|
||||
auto wifi_lan_service_info_string = std::string(org_wifi_lan_service_info);
|
||||
|
||||
auto wifi_lan_service_info = WifiLanServiceInfo(wifi_lan_service_info_string);
|
||||
auto is_valid = wifi_lan_service_info.IsValid();
|
||||
|
||||
EXPECT_TRUE(is_valid);
|
||||
EXPECT_EQ(kPcp, wifi_lan_service_info.GetPcp());
|
||||
EXPECT_EQ(kVersion, wifi_lan_service_info.GetVersion());
|
||||
EXPECT_EQ(kEndPointID, wifi_lan_service_info.GetEndpointId());
|
||||
EXPECT_EQ(service_id_hash, wifi_lan_service_info.GetServiceIdHash());
|
||||
}
|
||||
|
||||
TEST(WifiLanServiceInfoTest, ConstructionFailsWithBadVersion) {
|
||||
auto bad_version = static_cast<WifiLanServiceInfo::Version>(666);
|
||||
|
||||
auto service_id_hash = ByteArray(kServiceIDHashBytes,
|
||||
sizeof(kServiceIDHashBytes) / sizeof(char));
|
||||
auto wifi_lan_service_info = WifiLanServiceInfo(
|
||||
bad_version, kPcp, kEndPointID, service_id_hash, kEndPointName);
|
||||
|
||||
auto is_valid = wifi_lan_service_info.IsValid();
|
||||
|
||||
EXPECT_FALSE(is_valid);
|
||||
}
|
||||
|
||||
TEST(WifiLanServiceInfoTest, ConstructionFailsWithBadPCP) {
|
||||
auto bad_pcp = static_cast<Pcp>(666);
|
||||
|
||||
auto service_id_hash = ByteArray(kServiceIDHashBytes,
|
||||
sizeof(kServiceIDHashBytes) / sizeof(char));
|
||||
auto wifi_lan_service_info = WifiLanServiceInfo(
|
||||
kVersion, bad_pcp, kEndPointID, service_id_hash, kEndPointName);
|
||||
auto is_valid = wifi_lan_service_info.IsValid();
|
||||
|
||||
EXPECT_FALSE(is_valid);
|
||||
}
|
||||
|
||||
TEST(WifiLanServiceInfoTest, ConstructionFailsWithShortEndpointId) {
|
||||
std::string short_endpoint_id("AB1");
|
||||
|
||||
auto service_id_hash = ByteArray(kServiceIDHashBytes,
|
||||
sizeof(kServiceIDHashBytes) / sizeof(char));
|
||||
auto wifi_lan_service_info = WifiLanServiceInfo(
|
||||
kVersion, kPcp, short_endpoint_id, service_id_hash, kEndPointName);
|
||||
auto is_valid = wifi_lan_service_info.IsValid();
|
||||
|
||||
EXPECT_FALSE(is_valid);
|
||||
}
|
||||
|
||||
TEST(WifiLanServiceInfoTest, ConstructionFailsWithLongEndpointId) {
|
||||
std::string long_endpoint_id("AB12X");
|
||||
|
||||
auto service_id_hash = ByteArray(kServiceIDHashBytes,
|
||||
sizeof(kServiceIDHashBytes) / sizeof(char));
|
||||
auto wifi_lan_service_info = WifiLanServiceInfo(
|
||||
kVersion, kPcp, long_endpoint_id, service_id_hash, kEndPointName);
|
||||
auto is_valid = wifi_lan_service_info.IsValid();
|
||||
|
||||
EXPECT_FALSE(is_valid);
|
||||
}
|
||||
|
||||
TEST(WifiLanServiceInfoTest, ConstructionFailsWithShortServiceIdHash) {
|
||||
char short_service_id_hash_bytes[] = {0x0A, 0x0B};
|
||||
|
||||
auto short_service_id_hash =
|
||||
ByteArray(short_service_id_hash_bytes,
|
||||
sizeof(short_service_id_hash_bytes) / sizeof(char));
|
||||
auto wifi_lan_service_info = WifiLanServiceInfo(
|
||||
kVersion, kPcp, kEndPointID, short_service_id_hash, kEndPointName);
|
||||
auto is_valid = wifi_lan_service_info.IsValid();
|
||||
|
||||
EXPECT_FALSE(is_valid);
|
||||
}
|
||||
|
||||
TEST(WifiLanServiceInfoTest, ConstructionFailsWithLongServiceIdHash) {
|
||||
char long_service_id_hash_bytes[] = {0x0A, 0x0B, 0x0C, 0x0D};
|
||||
|
||||
auto long_service_id_hash =
|
||||
ByteArray(long_service_id_hash_bytes,
|
||||
sizeof(long_service_id_hash_bytes) / sizeof(char));
|
||||
auto wifi_lan_service_info = WifiLanServiceInfo(
|
||||
kVersion, kPcp, kEndPointID, long_service_id_hash, kEndPointName);
|
||||
auto is_valid = wifi_lan_service_info.IsValid();
|
||||
|
||||
EXPECT_FALSE(is_valid);
|
||||
}
|
||||
|
||||
TEST(WifiLanServiceInfoTest, ConstructionFailsWithShortStringLength) {
|
||||
char wifi_lan_service_info_string[] = {'X'};
|
||||
|
||||
auto wifi_lan_service_info_bytes =
|
||||
ByteArray(wifi_lan_service_info_string,
|
||||
sizeof(wifi_lan_service_info_string) / sizeof(char));
|
||||
auto wifi_lan_service_info =
|
||||
WifiLanServiceInfo(Base64Utils::Encode(wifi_lan_service_info_bytes));
|
||||
auto is_valid = wifi_lan_service_info.IsValid();
|
||||
|
||||
EXPECT_FALSE(is_valid);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace connections
|
||||
} // namespace nearby
|
||||
} // namespace location
|
||||
Reference in New Issue
Block a user