Files
nearby/connections/implementation/mediums/ble/discovered_peripheral_tracker.cc
T
2026-07-07 03:15:15 -07:00

1127 lines
44 KiB
C++

// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "connections/implementation/mediums/ble/discovered_peripheral_tracker.h"
#include <algorithm>
#include <iterator>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "absl/container/flat_hash_map.h"
#include "absl/status/statusor.h"
#include "absl/strings/escaping.h"
#include "absl/time/time.h"
#include "connections/implementation/ble_advertisement.h"
#include "connections/implementation/flags/nearby_connections_feature_flags.h"
#include "connections/implementation/mediums/advertisements/advertisement_util.h"
#include "connections/implementation/mediums/advertisements/dct_advertisement.h"
#include "connections/implementation/mediums/ble/advertisement_read_result.h"
#include "connections/implementation/mediums/ble/ble_advertisement.h"
#include "connections/implementation/mediums/ble/ble_advertisement_header.h"
#include "connections/implementation/mediums/ble/ble_utils.h"
#include "connections/implementation/mediums/ble/bloom_filter.h"
#include "connections/implementation/mediums/ble/discovered_peripheral_callback.h"
#include "connections/implementation/mediums/ble/instant_on_lost_advertisement.h"
#include "connections/implementation/mediums/lost_entity_tracker.h"
#include "connections/implementation/pcp.h"
#include "connections/implementation/webrtc_state.h"
#include "internal/flags/nearby_flags.h"
#include "internal/platform/ble.h"
#include "internal/platform/byte_array.h"
#include "internal/platform/feature_flags.h"
#include "internal/platform/implementation/ble.h"
#include "internal/platform/implementation/system_clock.h"
#include "internal/platform/logging.h"
#include "internal/platform/multi_thread_executor.h"
#include "internal/platform/mutex_lock.h"
#include "internal/platform/uuid.h"
using ::nearby::api::ble::BleAdvertisementData;
namespace nearby {
namespace connections {
namespace mediums {
namespace {
constexpr int kGattThreadCount = 1;
constexpr absl::Duration kInstantLostAdvertisementTimeout = absl::Seconds(60);
constexpr absl::Duration kExtendedAdvertisementHeaderDelay = absl::Seconds(3);
constexpr absl::Duration kAdvertisementHeaderExpiry = absl::Seconds(15);
} // namespace
// Private c'tor for testing.
DiscoveredPeripheralTracker::DiscoveredPeripheralTracker(
bool is_extended_advertisement_available, bool start_fetch_executor)
: is_extended_advertisement_available_(is_extended_advertisement_available),
start_fetch_executor_(start_fetch_executor) {}
DiscoveredPeripheralTracker::DiscoveredPeripheralTracker(
bool is_extended_advertisement_available)
: DiscoveredPeripheralTracker(is_extended_advertisement_available,
/*start_fetch_executor=*/true) {}
DiscoveredPeripheralTracker::~DiscoveredPeripheralTracker() { Shutdown(); }
void DiscoveredPeripheralTracker::StartFetchExecutorIfNeeded() {
if (executor_ != nullptr) {
return;
}
executor_ = std::make_unique<MultiThreadExecutor>(kGattThreadCount);
executor_->Execute([this]() { GattFetchingLoop(); });
}
void DiscoveredPeripheralTracker::Shutdown() {
if (shutting_down_.Set(true)) {
return;
}
std::unique_ptr<MultiThreadExecutor> executor_to_shutdown;
{
MutexLock lock(&mutex_);
executor_to_shutdown = std::move(executor_);
}
{
MutexLock lock(&task_mutex_);
cond_.Notify();
}
if (executor_to_shutdown != nullptr) {
executor_to_shutdown->Shutdown();
}
}
void DiscoveredPeripheralTracker::StartTracking(
const std::string& service_id, bool include_dct_advertisement, Pcp pcp,
DiscoveredPeripheralCallback discovered_peripheral_callback,
const Uuid& fast_advertisement_service_uuid) {
MutexLock lock(&mutex_);
if (start_fetch_executor_) {
StartFetchExecutorIfNeeded();
}
ServiceIdInfo service_id_info = {
.discovered_peripheral_callback =
std::move(discovered_peripheral_callback),
.lost_entity_tracker =
std::make_unique<LostEntityTracker<BleAdvertisement>>(),
.fast_advertisement_service_uuid = fast_advertisement_service_uuid,
.include_dct_advertisement = include_dct_advertisement,
.pcp = pcp};
if (service_id_infos_.empty()) {
medium_start_scanning_time_ = SystemClock::ElapsedRealtime();
VLOG(1) << "Medium start scanning time: " << medium_start_scanning_time_;
}
// Replace if key exists.
service_id_infos_.insert_or_assign(service_id, std::move(service_id_info));
// Add service id hash to service id map for dct advertisement.
if (include_dct_advertisement) {
dct_service_id_hash_to_service_id_map_.insert_or_assign(
advertisements::ble::DctAdvertisement::ComputeServiceIdHash(service_id),
service_id);
}
// Clear all of the GATT read results. With this cleared, we will now attempt
// to reconnect to every peripheral we see, giving us a chance to search for
// the new service we're now tracking.
// See the documentation of advertisementReadResult for more information.
advertisement_read_results_.clear();
// Remove stale data from any previous sessions.
ClearDataForServiceId(service_id);
}
void DiscoveredPeripheralTracker::StopTracking(const std::string& service_id) {
MutexLock lock(&mutex_);
dct_service_id_hash_to_service_id_map_.erase(
advertisements::ble::DctAdvertisement::ComputeServiceIdHash(service_id));
service_id_infos_.erase(service_id);
}
void DiscoveredPeripheralTracker::ProcessFoundBleAdvertisement(
BlePeripheral peripheral, BleAdvertisementData advertisement_data,
AdvertisementFetcher advertisement_fetcher) {
MutexLock lock(&mutex_);
if (shutting_down_.Get()) {
return;
}
if (service_id_infos_.empty()) {
LOG(INFO) << "Ignoring BLE advertisement header because we are not "
"tracking any service IDs.";
return;
}
if (!peripheral.IsValid() || advertisement_data.service_data.empty()) {
LOG(INFO) << "Ignoring BLE advertisement header because the peripheral is "
"invalid or the given service data is empty.";
return;
}
if (HandleOnLostAdvertisementLocked(advertisement_data)) {
return;
}
if (IsSkippableGattAdvertisement(advertisement_data)) {
VLOG(1) << "Ignore GATT advertisement and wait for extended advertisement.";
return;
}
if (IsLegacyDeviceAdvertisementData(advertisement_data)) {
if (nearby::FeatureFlags::GetInstance()
.GetFlags()
.enable_invoking_legacy_device_discovered_cb) {
for (auto& itor : service_id_infos_) {
itor.second.discovered_peripheral_callback
.legacy_device_discovered_cb();
}
}
return;
}
if (!dct_service_id_hash_to_service_id_map_.empty() &&
advertisement_data.service_data.contains(bleutils::kDctServiceUuid)) {
std::optional<BleAdvertisementData> dct_advertisement_data =
HandleDctAdvertisement(advertisement_data);
if (!dct_advertisement_data.has_value()) {
return;
}
advertisement_data = std::move(*dct_advertisement_data);
}
HandleAdvertisement(peripheral, advertisement_data);
HandleAdvertisementHeader(peripheral, advertisement_data,
std::move(advertisement_fetcher));
}
bool DiscoveredPeripheralTracker::HandleOnLostAdvertisementLocked(
const BleAdvertisementData& advertisement_data) {
auto service_data =
advertisement_data.service_data.find(bleutils::kCopresenceServiceUuid);
if (service_data == advertisement_data.service_data.end()) {
return false;
}
absl::StatusOr<InstantOnLostAdvertisement> on_lost_advertisement =
InstantOnLostAdvertisement::CreateFromBytes(
service_data->second.AsStringView());
if (!on_lost_advertisement.ok()) {
return false;
}
std::vector<BleAdvertisement> advertisements_to_clear;
for (const auto& hash : on_lost_advertisement->hashes()) {
for (const auto& it : gatt_advertisement_infos_) {
if (it.second.instant_on_lost_hash.string_data() == hash) {
auto discovery_cb_it = service_id_infos_.find(it.second.service_id);
if (discovery_cb_it == service_id_infos_.end()) {
LOG(INFO)
<< __func__
<< ": Discarding OnLost advertisement for untracked service_id";
break;
}
auto gatt_advertisements =
gatt_advertisements_[it.second.advertisement_header];
// Need to report OnLost for each gatt_advertisement.
for (const auto& gatt_advertisement : gatt_advertisements) {
BlePeripheral lost_peripheral = it.second.peripheral;
lost_peripheral.SetId(ByteArray(gatt_advertisement));
if (gatt_advertisement.IsValid()) {
AddInstantLostAdvertisement(it.second.advertisement_header);
discovery_cb_it->second.discovered_peripheral_callback
.instant_lost_cb(lost_peripheral, it.second.service_id,
gatt_advertisement.GetData(),
gatt_advertisement.IsFastAdvertisement());
LOG(INFO) << __func__ << ": OnLost triggered for service_id "
<< it.second.service_id;
}
advertisements_to_clear.push_back(gatt_advertisement);
}
break;
}
}
}
for (const auto& advertisement : advertisements_to_clear) {
ClearGattAdvertisement(advertisement);
}
return true;
}
void DiscoveredPeripheralTracker::ProcessLostGattAdvertisements() {
MutexLock lock(&mutex_);
int lost_count = 0;
for (auto& it : service_id_infos_) {
const std::string& service_id = it.first;
ServiceIdInfo& service_id_info = it.second;
BleAdvertisementSet lost_gatt_advertisements =
service_id_info.lost_entity_tracker->ComputeLostEntities();
// Clear the map state for each lost GATT advertisement and report it to
// the client.
for (const auto& gatt_advertisement : lost_gatt_advertisements) {
const auto it = gatt_advertisement_infos_.find(gatt_advertisement);
if (it != gatt_advertisement_infos_.end()) {
BlePeripheral lost_peripheral = it->second.peripheral;
if (lost_peripheral.IsValid()) {
lost_peripheral.SetId(ByteArray(gatt_advertisement));
service_id_info.discovered_peripheral_callback.peripheral_lost_cb(
std::move(lost_peripheral), service_id,
gatt_advertisement.GetData(),
gatt_advertisement.IsFastAdvertisement());
++lost_count;
}
}
ClearGattAdvertisement(gatt_advertisement);
}
}
LOG(INFO) << __func__ << ": Lost " << lost_count
<< " GATT advertisements due to peripheral timeout.";
}
void DiscoveredPeripheralTracker::ClearDataForServiceId(
const std::string& service_id) {
std::vector<BleAdvertisement> advertisement_list;
for (const auto& it : gatt_advertisement_infos_) {
if (it.second.service_id == service_id) {
advertisement_list.push_back(it.first);
}
}
for (auto const& advertisement : advertisement_list) {
ClearGattAdvertisement(advertisement);
}
}
bool DiscoveredPeripheralTracker::IsSkippableGattAdvertisement(
const api::ble::BleAdvertisementData& advertisement_data) {
if (!is_extended_advertisement_available_) {
// Don't skip any advertisement if the scanner doesn't support extended
// advertisement.
return false;
}
BleAdvertisementHeader advertisement_header(
ExtractAdvertisementHeaderBytes(advertisement_data));
if (!advertisement_header.IsValid()) {
// Don't skip any advertisement if the header is not valid. It may be one
// of the legacy advertisement formats that is dealt with later.
return false;
}
// Delay processing GATT advertisements that have corresponding extended
// advertisements for 3s. After that all GATT advertisements are processed.
if (advertisement_header.IsSupportExtendedAdvertisement() &&
(SystemClock::ElapsedRealtime() - medium_start_scanning_time_) <
kExtendedAdvertisementHeaderDelay) {
return true;
}
return false;
}
void DiscoveredPeripheralTracker::ClearGattAdvertisement(
const BleAdvertisement& gatt_advertisement) {
const auto gai_it = gatt_advertisement_infos_.find(gatt_advertisement);
if (gai_it == gatt_advertisement_infos_.end()) {
return;
}
auto item = gatt_advertisement_infos_.extract(gai_it);
GattAdvertisementInfo& gatt_advertisement_info = item.mapped();
const auto ga_it =
gatt_advertisements_.find(gatt_advertisement_info.advertisement_header);
if (ga_it != gatt_advertisements_.end()) {
// Remove the GATT advertisement from the advertisement header it's
// associated with.
BleAdvertisementSet& gatt_advertisement_set = ga_it->second;
gatt_advertisement_set.erase(gatt_advertisement);
// Unconditionally remove the header from advertisement_read_results_ so
// we can attempt to reread the GATT advertisement if they return.
advertisement_read_results_.erase(
gatt_advertisement_info.advertisement_header);
// If there are no more tracked GATT advertisements under this header, go
// ahead and remove it from gatt_advertisements_.
if (gatt_advertisement_set.empty()) {
gatt_advertisements_.erase(ga_it);
}
}
}
void DiscoveredPeripheralTracker::HandleAdvertisement(
BlePeripheral peripheral,
const nearby::api::ble::BleAdvertisementData& advertisement_data) {
ByteArray advertisement_bytes =
ExtractInterestingAdvertisementBytes(advertisement_data);
if (advertisement_bytes.Empty()) {
return;
}
// The UUID that the Fast/Regular Advertisement was found on. For regular
// advertisement, it's always kCopresenceServiceUuid. For fast
// advertisement, we may have below 2 UUIDs: 1. kCopresenceServiceUuid 2.
// Caller UUID.
// First filter out kCopresenceServiceUuid and see if any Caller UUID
// existed; if not then just take the kCopresenceServiceUuid as
// |service_uuid|.
absl::flat_hash_map<Uuid, nearby::ByteArray> extracted_uuids;
// Filter out kCoprsence service uuid.
std::remove_copy_if(advertisement_data.service_data.begin(),
advertisement_data.service_data.end(),
std::inserter(extracted_uuids, extracted_uuids.end()),
[](const std::pair<const Uuid, nearby::ByteArray>& pair) {
return pair.first == bleutils::kCopresenceServiceUuid;
});
Uuid service_uuid;
if (!extracted_uuids.empty()) {
service_uuid = extracted_uuids.begin()->first;
} else {
service_uuid = bleutils::kCopresenceServiceUuid;
}
// Create a header tied to this fast advertisement. This helps us track the
// advertisement when reporting it as lost or connecting.
BleAdvertisementHeader advertisement_header =
CreateAdvertisementHeader(advertisement_bytes);
// Process the fast advertisement like we would a GATT advertisement and
// insert a placeholder AdvertisementReadResult.
advertisement_read_results_.insert(
{advertisement_header, std::make_unique<AdvertisementReadResult>()});
BleAdvertisementHeader new_advertisement_header = HandleRawGattAdvertisements(
peripheral, advertisement_header, {&advertisement_bytes}, service_uuid);
UpdateCommonStateForFoundBleAdvertisement(new_advertisement_header);
}
ByteArray DiscoveredPeripheralTracker::ExtractInterestingAdvertisementBytes(
const nearby::api::ble::BleAdvertisementData& advertisement_data) {
// Iterate through all tracked service IDs to see if any of their fast
// advertisements are contained within this BLE advertisement.
for (const auto& item : service_id_infos_) {
const ServiceIdInfo& service_id_info = item.second;
// Check if there's service data for this fast advertisement service UUID.
// If so, we can short-circuit since all BLE advertisements can contain at
// most ONE fast advertisement.
const auto sd_it = advertisement_data.service_data.find(
service_id_info.fast_advertisement_service_uuid);
if (sd_it != advertisement_data.service_data.end()) {
return sd_it->second;
}
}
return {};
}
BleAdvertisementHeader DiscoveredPeripheralTracker::CreateAdvertisementHeader(
const ByteArray& advertisement_bytes) {
// Our end goal is to have a fully zeroed-out byte array of the correct
// length representing an empty bloom filter.
BloomFilter bloom_filter(
std::make_unique<BitSetImpl<
BleAdvertisementHeader::kServiceIdBloomFilterByteLength>>());
return BleAdvertisementHeader(
BleAdvertisementHeader::Version::kV2, /*extended_advertisement=*/false,
/*num_slots=*/1, ByteArray(bloom_filter),
bleutils::GenerateAdvertisementHash(advertisement_bytes),
/*psm=*/BleAdvertisementHeader::kDefaultPsmValue);
}
BleAdvertisementHeader DiscoveredPeripheralTracker::HandleRawGattAdvertisements(
BlePeripheral peripheral,
const BleAdvertisementHeader& advertisement_header,
const std::vector<const ByteArray*>& gatt_advertisement_bytes_list,
const Uuid& service_uuid) {
absl::flat_hash_map<std::string, BleAdvertisement>
parsed_gatt_advertisements = ParseRawGattAdvertisements(
gatt_advertisement_bytes_list, service_uuid);
// Update state for each GATT advertisement.
BleAdvertisementSet ble_advertisement_set;
BleAdvertisementHeader new_advertisement_header = advertisement_header;
for (const auto& item : parsed_gatt_advertisements) {
const std::string& service_id = item.first;
const BleAdvertisement& gatt_advertisement = item.second;
BleAdvertisementHeader old_advertisement_header;
const auto gai_it = gatt_advertisement_infos_.find(gatt_advertisement);
if (gai_it != gatt_advertisement_infos_.end()) {
old_advertisement_header = gai_it->second.advertisement_header;
}
ble_advertisement_set.insert(gatt_advertisement);
int new_psm = new_advertisement_header.GetPsm();
if (gatt_advertisement.GetPsm() !=
BleAdvertisementHeader::kDefaultPsmValue &&
gatt_advertisement.GetPsm() != new_psm) {
new_psm = gatt_advertisement.GetPsm();
// Also set header with new psm value to compare next time.
new_advertisement_header.SetPsm(new_psm);
}
// If the device received first fast advertisement is legacy one after
// then received extended one, should replace legacy with extended one
// which has psm value.
if (!old_advertisement_header.IsValid() ||
ShouldNotifyForNewPsm(old_advertisement_header.GetPsm(), new_psm)) {
// The GATT advertisement has never been seen before. Report it up to
// the client.
const auto sii_it = service_id_infos_.find(service_id);
if (sii_it == service_id_infos_.end()) {
LOG(WARNING) << "HandleRawGattAdvertisements, failed to find "
"callback for service_id="
<< service_id;
continue;
}
if (peripheral.IsValid()) {
peripheral.SetPsm(new_psm);
BlePeripheral discovered_peripheral = peripheral;
discovered_peripheral.SetId(ByteArray(gatt_advertisement));
if (IsInstantLostAdvertisement(new_advertisement_header)) {
LOG(INFO) << "Skip the advertisement header with hash "
<< absl::BytesToHexString(
new_advertisement_header.GetAdvertisementHash()
.AsStringView())
<< " due to it was reported lost.";
continue;
}
LOG(INFO) << "Found new GATT advertisement : "
<< gatt_advertisement.ToReadableString();
sii_it->second.discovered_peripheral_callback.peripheral_discovered_cb(
std::move(discovered_peripheral), service_id,
gatt_advertisement.GetData(),
gatt_advertisement.IsFastAdvertisement());
}
} else if (old_advertisement_header.GetPsm() !=
BleAdvertisementHeader::kDefaultPsmValue &&
new_advertisement_header.GetPsm() ==
BleAdvertisementHeader::kDefaultPsmValue) {
// Don't replace it in advertisementHeaders if this one without PSM but
// older has, it's the case that we received extended fast advertisement
// after then received legacy one.
continue;
} else if (ShouldRemoveHeader(old_advertisement_header,
new_advertisement_header)) {
// The GATT advertisement has been seen on a different advertisement
// header. Remove info about the old advertisement header since it's
// stale now.
advertisement_read_results_.erase(old_advertisement_header);
gatt_advertisements_.erase(old_advertisement_header);
}
GattAdvertisementInfo gatt_advertisement_info = {
.service_id = service_id,
.advertisement_header = new_advertisement_header,
.peripheral = peripheral,
.instant_on_lost_hash = bleutils::GenerateAdvertisementHash(
gatt_advertisement.ByteArrayWithExtraField())};
gatt_advertisement_infos_.insert_or_assign(
gatt_advertisement, std::move(gatt_advertisement_info));
}
// Insert the list of read GATT advertisements for this advertisement
// header.
gatt_advertisements_.insert(
{new_advertisement_header, std::move(ble_advertisement_set)});
return new_advertisement_header;
}
absl::flat_hash_map<std::string, BleAdvertisement>
DiscoveredPeripheralTracker::ParseRawGattAdvertisements(
const std::vector<const ByteArray*>& gatt_advertisement_bytes_list,
const Uuid& service_uuid) {
absl::flat_hash_map<std::string, BleAdvertisement>
parsed_gatt_advertisements = {};
// TODO(edwinwu): Refactor this big loop as subroutines.
for (const auto gatt_advertisement_bytes : gatt_advertisement_bytes_list) {
// First, parse the raw bytes into a BleAdvertisement.
auto gatt_advertisement_status_or =
BleAdvertisement::CreateBleAdvertisement(*gatt_advertisement_bytes);
if (!gatt_advertisement_status_or.ok()) {
VLOG(1) << gatt_advertisement_status_or.status();
continue;
}
auto gatt_advertisement = gatt_advertisement_status_or.value();
// Make sure the advertisement belongs to a service ID we're tracking.
for (const auto& item : service_id_infos_) {
const std::string& service_id = item.first;
// If we already found a higher version advertisement for this service
// ID, there's no point in comparing this advertisement against it.
const auto pga_it = parsed_gatt_advertisements.find(service_id);
if (pga_it != parsed_gatt_advertisements.end()) {
if (pga_it->second.GetVersion() > gatt_advertisement.GetVersion()) {
continue;
}
}
// service_id_hash is null here (mediums advertisement) because we
// already have a UUID in the fast advertisement.
if (gatt_advertisement.IsFastAdvertisement() && !service_uuid.IsEmpty()) {
const auto sii_it = service_id_infos_.find(service_id);
if (sii_it != service_id_infos_.end()) {
if (sii_it->second.fast_advertisement_service_uuid == service_uuid) {
VLOG(1) << "This GATT advertisement:"
<< absl::BytesToHexString(
gatt_advertisement_bytes->AsStringView())
<< " is a fast advertisement and matched UUID="
<< service_uuid.Get16BitAsString()
<< " in a map with service_id=" << service_id;
parsed_gatt_advertisements.insert({service_id, gatt_advertisement});
}
}
continue;
}
// Map the service ID to the advertisement if the service_id_hash match.
if (bleutils::GenerateServiceIdHash(service_id) ==
gatt_advertisement.GetServiceIdHash()) {
LOG(INFO) << "Matched service_id=" << service_id
<< " to GATT advertisement="
<< absl::BytesToHexString(
gatt_advertisement_bytes->AsStringView());
parsed_gatt_advertisements.insert({service_id, gatt_advertisement});
break;
}
}
}
return parsed_gatt_advertisements;
}
bool DiscoveredPeripheralTracker::ShouldNotifyForNewPsm(int old_psm,
int new_psm) const {
return new_psm != BleAdvertisementHeader::kDefaultPsmValue &&
old_psm != new_psm;
}
bool DiscoveredPeripheralTracker::ShouldRemoveHeader(
const BleAdvertisementHeader& old_advertisement_header,
const BleAdvertisementHeader& new_advertisement_header) {
if (old_advertisement_header == new_advertisement_header) {
return false;
}
// We received the physical from legacy advertisement and create a mock one
// when receive a regular advertisement from extended advertisements. Avoid
// to remove the physical header for the new incoming regular extended
// advertisement. Otherwise, it make the device to fetch advertisement when
// received a physical header again.
if (is_extended_advertisement_available_) {
if (!IsDummyAdvertisementHeader(old_advertisement_header) &&
IsDummyAdvertisementHeader(new_advertisement_header)) {
return false;
}
}
return true;
}
bool DiscoveredPeripheralTracker::IsDummyAdvertisementHeader(
const BleAdvertisementHeader& advertisement_header) {
// Do not count advertisementHash and psm value here, for L2CAP feature, the
// regular advertisement has different value, it will include PSM value if
// received it from extended advertisement protocol and it will not has PSM
// value if it fetched from GATT connection.
BloomFilter bloom_filter(
std::make_unique<BitSetImpl<
BleAdvertisementHeader::kServiceIdBloomFilterByteLength>>());
return advertisement_header.GetVersion() ==
BleAdvertisementHeader::Version::kV2 &&
advertisement_header.GetNumSlots() == 1 &&
advertisement_header.GetServiceIdBloomFilter() ==
ByteArray(bloom_filter);
}
std::optional<BleAdvertisementData>
DiscoveredPeripheralTracker::HandleDctAdvertisement(
const BleAdvertisementData& advertisement_data) {
// This is DCT advertisement. Build a new advertisement data base on DCT
// advertisement.
std::optional<advertisements::ble::DctAdvertisement> dct_advertisement =
advertisements::ble::DctAdvertisement::Parse(std::string(
advertisement_data.service_data.at(bleutils::kDctServiceUuid)));
if (!dct_advertisement.has_value()) {
LOG(WARNING) << "Failed to parse DCT advertisement.";
return std::nullopt;
}
std::optional<std::string> endpoint_id = dct_advertisement->GetEndpointId();
if (!endpoint_id.has_value()) {
LOG(WARNING) << "Failed to generate endpoint id.";
return std::nullopt;
}
const auto& it = dct_service_id_hash_to_service_id_map_.find(
dct_advertisement->GetServiceIdHash());
if (it == dct_service_id_hash_to_service_id_map_.end()) {
LOG(WARNING) << "Failed to find service id hash in the map.";
return std::nullopt;
}
std::string& service_id = it->second;
const auto& service_id_info_it = service_id_infos_.find(service_id);
if (service_id_info_it == service_id_infos_.end()) {
LOG(WARNING) << "Failed to find service id in the map.";
return std::nullopt;
}
ServiceIdInfo& service_id_info = service_id_info_it->second;
ByteArray service_id_hash = bleutils::GenerateServiceIdHash(service_id);
// Build the new BLE advertisement data.
std::string endpoint_info =
advertisements::BuildEndpointInfo(dct_advertisement->GetDeviceName());
connections::BleAdvertisement connections_advertisement(
connections::BleAdvertisement::Version::kV1, service_id_info.pcp,
/*service_id_hash=*/service_id_hash, *endpoint_id,
ByteArray(endpoint_info), /*bluetooth_mac_address=*/{},
/*uwb_address=*/ByteArray(),
/*web_rtc_state=*/WebRtcState::kUnconnectable);
BleAdvertisement medium_advertisement = {
mediums::BleAdvertisement::Version::kV2,
mediums::BleAdvertisement::SocketVersion::kV2,
/*service_id_hash=*/service_id_hash,
ByteArray(connections_advertisement),
ByteArray(dct_advertisement->GetDeviceToken()),
dct_advertisement->GetPsm()};
BleAdvertisementData new_advertisement_data{};
new_advertisement_data.service_data.insert(
{service_id_info.fast_advertisement_service_uuid,
medium_advertisement.ByteArrayWithExtraField()});
return new_advertisement_data;
}
void DiscoveredPeripheralTracker::HandleAdvertisementHeader(
BlePeripheral peripheral,
const nearby::api::ble::BleAdvertisementData& advertisement_data,
AdvertisementFetcher advertisement_fetcher) {
// Attempt to parse the advertisement header.
BleAdvertisementHeader advertisement_header(
ExtractAdvertisementHeaderBytes(advertisement_data));
if (!advertisement_header.IsValid()) {
VLOG(1) << "Failed to deserialize BLE advertisement header. Ignoring.";
return;
}
// Check if the advertisement header contains a service ID we're tracking.
if (!IsInterestingAdvertisementHeader(advertisement_header)) {
VLOG(1) << "Ignoring BLE advertisement header with hash"
<< absl::BytesToHexString(
advertisement_header.GetAdvertisementHash().AsStringView())
<< " because it does not contain any service IDs "
"we're interested in.";
return;
}
// Report a nearby legacy device is found when advertisement header doesn't
// support extended advertisement.
if (!advertisement_header.IsSupportExtendedAdvertisement()) {
for (auto& item : service_id_infos_) {
item.second.discovered_peripheral_callback.legacy_device_discovered_cb();
}
}
// Determine whether or not we need to read a fresh GATT advertisement.
VLOG(1) << "Handle GATT advertisement header with hash "
<< absl::BytesToHexString(
advertisement_header.GetAdvertisementHash().AsStringView())
<< " in thread";
if (!ShouldReadRawAdvertisementFromServer(advertisement_header)) {
UpdateCommonStateForFoundBleAdvertisement(advertisement_header);
return;
}
{
MutexLock lock(&task_mutex_);
// Check if the advertisement header is already in progress.
if (fetch_in_progress_header_.has_value() &&
fetch_in_progress_header_ == advertisement_header) {
UpdateCommonStateForFoundBleAdvertisement(advertisement_header);
return;
}
if (advertisement_header.IsSupportExtendedAdvertisement() &&
is_extended_advertisement_available_) {
for (auto& item : gatt_extended_fetch_tasks_) {
if (item.advertisement_header == advertisement_header) {
item.scheduled_time = SystemClock::ElapsedRealtime();
UpdateCommonStateForFoundBleAdvertisement(advertisement_header);
return;
}
}
gatt_extended_fetch_tasks_.push_back({
.peripheral = peripheral,
.advertisement_header = advertisement_header,
.advertisement_fetcher = std::move(advertisement_fetcher),
.scheduled_time = SystemClock::ElapsedRealtime(),
});
} else {
for (auto& item : gatt_fetch_tasks_) {
if (item.advertisement_header == advertisement_header) {
item.scheduled_time = SystemClock::ElapsedRealtime();
UpdateCommonStateForFoundBleAdvertisement(advertisement_header);
return;
}
}
gatt_fetch_tasks_.push_back({
.peripheral = peripheral,
.advertisement_header = advertisement_header,
.advertisement_fetcher = std::move(advertisement_fetcher),
.scheduled_time = SystemClock::ElapsedRealtime(),
});
}
cond_.Notify();
}
// Regardless of whether or not we read a new GATT advertisement, the maps
// should now be up-to-date. With this information, do some general
// housekeeping.
UpdateCommonStateForFoundBleAdvertisement(advertisement_header);
}
ByteArray DiscoveredPeripheralTracker::ExtractAdvertisementHeaderBytes(
const nearby::api::ble::BleAdvertisementData& advertisement_data) {
const auto it =
advertisement_data.service_data.find(bleutils::kCopresenceServiceUuid);
if (it != advertisement_data.service_data.end()) {
const ByteArray& advertisement_header_bytes = it->second;
if (!advertisement_header_bytes.Empty()) {
return advertisement_header_bytes;
}
}
return {};
}
bool DiscoveredPeripheralTracker::IsInterestingAdvertisementHeader(
const BleAdvertisementHeader& advertisement_header) {
BloomFilter bloom_filter(
std::make_unique<BitSetImpl<
BleAdvertisementHeader::kServiceIdBloomFilterByteLength>>(),
advertisement_header.GetServiceIdBloomFilter());
for (const auto& item : service_id_infos_) {
const std::string& service_id = item.first;
if (bloom_filter.PossiblyContains(service_id)) {
return true;
}
}
return false;
}
bool DiscoveredPeripheralTracker::ShouldReadRawAdvertisementFromServer(
const BleAdvertisementHeader& advertisement_header) {
// Check if we have never seen this header. New headers should always be
// read.
const auto it = advertisement_read_results_.find(advertisement_header);
if (it == advertisement_read_results_.end()) {
LOG(INFO) << "Received advertisement header with hash "
<< absl::BytesToHexString(
advertisement_header.GetAdvertisementHash().AsStringView())
<< ", but we have never seen it before. Caller should "
"try reading its GATT advertisement.";
return true;
}
// Extract the last read result for this particular header.
AdvertisementReadResult* advertisement_read_result = it->second.get();
// Now evaluate if we should retry reading.
switch (advertisement_read_result->EvaluateRetryStatus()) {
case AdvertisementReadResult::RetryStatus::kRetry:
LOG(INFO)
<< "Received advertisement header with hash "
<< absl::BytesToHexString(
advertisement_header.GetAdvertisementHash().AsStringView())
<< ". Caller should retry reading its GATT advertisement.";
return true;
case AdvertisementReadResult::RetryStatus::kPreviouslySucceeded:
VLOG(1) << "Received advertisement header with hash "
<< absl::BytesToHexString(
advertisement_header.GetAdvertisementHash().AsStringView())
<< ", but we have already read its GATT advertisement.";
return false;
case AdvertisementReadResult::RetryStatus::kTooSoon:
VLOG(1)
<< "Received advertisement header with hash "
<< absl::BytesToHexString(
advertisement_header.GetAdvertisementHash().AsStringView())
<< ", but we have recently failed to read its GATT advertisement.";
return false;
case AdvertisementReadResult::RetryStatus::kUnknown:
// Fall through.
break;
}
LOG(INFO) << "Received advertisement header with hash "
<< absl::BytesToHexString(
advertisement_header.GetAdvertisementHash().AsStringView())
<< ", but we do not know whether or not to retry reading "
"its GATT advertisement. Caller should retry to be safe.";
return true;
}
std::vector<const ByteArray*>
DiscoveredPeripheralTracker::FetchRawAdvertisements(
BlePeripheral peripheral,
const BleAdvertisementHeader& advertisement_header,
AdvertisementFetcher advertisement_fetcher) {
// Fetch the raw GATT advertisements and store the results.
auto& result = advertisement_read_results_[advertisement_header];
if (result == nullptr) {
result = std::make_unique<mediums::AdvertisementReadResult>();
}
std::vector<std::string> service_ids;
std::transform(service_id_infos_.begin(), service_id_infos_.end(),
std::back_inserter(service_ids),
[](auto& kv) { return kv.first; });
advertisement_fetcher(peripheral, advertisement_header.GetNumSlots(),
advertisement_header.GetPsm(), service_ids, *result);
// Take those results and return all the advertisements we were able to
// read.
return result->GetAdvertisements();
}
void DiscoveredPeripheralTracker::FetchRawAdvertisementsInThread(
BlePeripheral peripheral,
const BleAdvertisementHeader& advertisement_header,
AdvertisementFetcher advertisement_fetcher) {
std::vector<std::string> service_ids;
{
MutexLock lock(&mutex_);
if (!IsInterestingAdvertisementHeader(advertisement_header)) {
LOG(INFO) << ": Ignore to read raw advertisement from server due to it "
"is not interesting header now.";
return;
}
if (!ShouldReadRawAdvertisementFromServer(advertisement_header)) {
return;
}
std::transform(service_id_infos_.begin(), service_id_infos_.end(),
std::back_inserter(service_ids),
[](auto& kv) { return kv.first; });
}
auto result = std::make_unique<mediums::AdvertisementReadResult>();
advertisement_fetcher(peripheral, advertisement_header.GetNumSlots(),
advertisement_header.GetPsm(), service_ids, *result);
{
MutexLock lock(&mutex_);
// The fetching process might take a few seconds, and tracking settings
// could change during that time. We need to double-check if the result
// is still valid afterward.
if (!IsInterestingAdvertisementHeader(advertisement_header)) {
LOG(WARNING)
<< ": Ignore the fetched GATT advertisement from server due to it "
"is not interesting header now.";
return;
}
auto it = advertisement_read_results_.insert_or_assign(advertisement_header,
std::move(result));
std::vector<const ByteArray*> gatt_advertisement_bytes_list =
it.first->second->GetAdvertisements();
if (gatt_advertisement_bytes_list.empty()) {
VLOG(1) << ": Ignore the fetched GATT advertisement from server due to "
"it is empty.";
return;
}
HandleRawGattAdvertisements(peripheral, advertisement_header,
gatt_advertisement_bytes_list,
/*service_uuid=*/{});
UpdateCommonStateForFoundBleAdvertisement(advertisement_header);
VLOG(1) << ": Completed to handle GATT advertisement header with hash "
<< absl::BytesToHexString(
advertisement_header.GetAdvertisementHash().AsStringView())
<< " in thread";
}
}
void DiscoveredPeripheralTracker::GattFetchingLoop() {
while (true) {
GattFetchTask task;
bool found_task = false;
{
MutexLock lock(&task_mutex_);
if (shutting_down_) {
gatt_fetch_tasks_.clear();
return;
}
while (gatt_fetch_tasks_.empty() && gatt_extended_fetch_tasks_.empty() &&
!shutting_down_) {
cond_.Wait();
}
if (shutting_down_) {
gatt_fetch_tasks_.clear();
gatt_extended_fetch_tasks_.clear();
return;
}
// Non-extended advertisements are prioritized over extended
// advertisements.
if (!gatt_fetch_tasks_.empty()) {
task = std::move(gatt_fetch_tasks_.front());
gatt_fetch_tasks_.pop_front();
fetch_in_progress_header_ = task.advertisement_header;
found_task = true;
} else if (!gatt_extended_fetch_tasks_.empty()) {
task = std::move(gatt_extended_fetch_tasks_.front());
gatt_extended_fetch_tasks_.pop_front();
fetch_in_progress_header_ = task.advertisement_header;
found_task = true;
}
}
if (!found_task) {
LOG(WARNING) << "No task found, skip to fetch raw advertisement.";
continue;
}
// Check if the task is expired.
if (SystemClock::ElapsedRealtime() - task.scheduled_time >
kAdvertisementHeaderExpiry) {
VLOG(1) << "GATT advertisement with hash: "
<< absl::BytesToHexString(
task.advertisement_header.GetAdvertisementHash()
.AsStringView())
<< " is expired, skip to fetch raw advertisement.";
} else {
FetchRawAdvertisementsInThread(task.peripheral, task.advertisement_header,
std::move(task.advertisement_fetcher));
}
// Clear in progress header after the task is done.
{
MutexLock lock(&task_mutex_);
fetch_in_progress_header_.reset();
}
}
}
void DiscoveredPeripheralTracker::UpdateCommonStateForFoundBleAdvertisement(
const BleAdvertisementHeader& advertisement_header) {
const auto ga_it = gatt_advertisements_.find(advertisement_header);
if (ga_it == gatt_advertisements_.end()) {
VLOG(1)
<< "No GATT advertisements found for advertisement header with hash "
<< absl::BytesToHexString(
advertisement_header.GetAdvertisementHash().AsStringView());
return;
}
BleAdvertisementSet saved_gatt_advertisements = ga_it->second;
for (const auto& gatt_advertisement : saved_gatt_advertisements) {
const auto gai_it = gatt_advertisement_infos_.find(gatt_advertisement);
if (gai_it == gatt_advertisement_infos_.end()) {
continue;
}
const GattAdvertisementInfo& gatt_advertisement_info = gai_it->second;
const auto sii_it =
service_id_infos_.find(gatt_advertisement_info.service_id);
if (sii_it != service_id_infos_.end()) {
auto* lost_entity_tracker = sii_it->second.lost_entity_tracker.get();
if (!lost_entity_tracker) {
LOG(WARNING) << "UpdateCommonStateForFoundBleAdvertisement, "
"failed to find entity "
"tracker for service_id="
<< gatt_advertisement_info.service_id;
continue;
}
lost_entity_tracker->RecordFoundEntity(gatt_advertisement);
}
}
}
bool DiscoveredPeripheralTracker::IsLegacyDeviceAdvertisementData(
const BleAdvertisementData& advertisement_data) {
return !advertisement_data.is_extended_advertisement &&
advertisement_data.service_data.size() == 1 &&
advertisement_data.service_data.find(
bleutils::kCopresenceServiceUuid) !=
advertisement_data.service_data.end() &&
advertisement_data.service_data.at(bleutils::kCopresenceServiceUuid) ==
ByteArray(DiscoveredPeripheralTracker::kDummyAdvertisementValue);
}
bool DiscoveredPeripheralTracker::IsInstantLostAdvertisement(
const BleAdvertisementHeader& advertisement_header) {
RemoveExpiredInstantLostAdvertisements();
return lost_advertisment_infos_.contains(
std::string(advertisement_header.GetAdvertisementHash()));
}
void DiscoveredPeripheralTracker::AddInstantLostAdvertisement(
const BleAdvertisementHeader& advertisement_header) {
LOG(INFO) << "Add instant lost advertisement header with hash "
<< absl::BytesToHexString(
advertisement_header.GetAdvertisementHash().AsStringView());
lost_advertisment_infos_[std::string(
advertisement_header.GetAdvertisementHash())] =
SystemClock::ElapsedRealtime();
}
void DiscoveredPeripheralTracker::RemoveExpiredInstantLostAdvertisements() {
absl::Time now = SystemClock::ElapsedRealtime();
if (now - last_lost_info_update_time_ < kInstantLostAdvertisementTimeout) {
return;
}
auto it = lost_advertisment_infos_.begin(),
end = lost_advertisment_infos_.end();
LOG(INFO) << "Start to remove expired lost advertisements.";
int count = 0;
while (it != end) {
if (now - it->second >= kInstantLostAdvertisementTimeout) {
lost_advertisment_infos_.erase(it++);
++count;
} else {
++it;
}
}
last_lost_info_update_time_ = now;
LOG(INFO) << "Removed " << count << " expired lost advertisements.";
}
} // namespace mediums
} // namespace connections
} // namespace nearby