From 3050f9432c51f26c2ce4c57cf01bf9e26779b2a4 Mon Sep 17 00:00:00 2001 From: edwinwu Date: Wed, 13 Apr 2022 10:54:28 -0700 Subject: [PATCH] [BLE Refactor] Implements DiscoveredPeripheralTracker. PiperOrigin-RevId: 441524475 --- .../implementation/mediums/ble_v2/BUILD | 6 + .../mediums/ble_v2/ble_utils.cc | 31 +- .../implementation/mediums/ble_v2/ble_utils.h | 27 +- .../ble_v2/discovered_peripheral_tracker.cc | 638 ++++++++++++ .../ble_v2/discovered_peripheral_tracker.h | 282 ++++++ .../discovered_peripheral_tracker_test.cc | 940 ++++++++++++++++++ 6 files changed, 1921 insertions(+), 3 deletions(-) create mode 100644 connections/implementation/mediums/ble_v2/discovered_peripheral_tracker.cc create mode 100644 connections/implementation/mediums/ble_v2/discovered_peripheral_tracker.h create mode 100644 connections/implementation/mediums/ble_v2/discovered_peripheral_tracker_test.cc diff --git a/connections/implementation/mediums/ble_v2/BUILD b/connections/implementation/mediums/ble_v2/BUILD index 185db4f3..c52faecf 100644 --- a/connections/implementation/mediums/ble_v2/BUILD +++ b/connections/implementation/mediums/ble_v2/BUILD @@ -22,6 +22,7 @@ cc_library( "ble_packet.cc", "ble_utils.cc", "bloom_filter.cc", + "discovered_peripheral_tracker.cc", ], hdrs = [ "advertisement_read_result.h", @@ -32,6 +33,7 @@ cc_library( "ble_utils.h", "bloom_filter.h", "discovered_peripheral_callback.h", + "discovered_peripheral_tracker.h", ], copts = ["-DCORE_ADAPTER_DLL"], visibility = [ @@ -41,6 +43,7 @@ cc_library( "//connections:core_types", "//connections/implementation/mediums:utils", "//internal/platform:base", + "//internal/platform:comm", "//internal/platform:logging", "//internal/platform:types", "//internal/platform:util", @@ -49,6 +52,7 @@ cc_library( "@com_google_absl//absl/container:flat_hash_set", "@com_google_absl//absl/numeric:int128", "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:str_format", "@com_google_absl//absl/time", ], ) @@ -63,11 +67,13 @@ cc_test( "ble_peripheral_test.cc", "ble_utils_test.cc", "bloom_filter_test.cc", + "discovered_peripheral_tracker_test.cc", ], deps = [ ":ble_v2", "//internal/platform:base", "//internal/platform:comm", + "//internal/platform:types", "//internal/platform/implementation/g3", # buildcleaner: keep "@com_github_protobuf_matchers//protobuf-matchers", "@com_google_absl//absl/hash:hash_testing", diff --git a/connections/implementation/mediums/ble_v2/ble_utils.cc b/connections/implementation/mediums/ble_v2/ble_utils.cc index 4d6bb366..fc383d77 100644 --- a/connections/implementation/mediums/ble_v2/ble_utils.cc +++ b/connections/implementation/mediums/ble_v2/ble_utils.cc @@ -34,6 +34,20 @@ namespace { constexpr std::int64_t kAdvertisementUuidMsb = 0x0000000000003000; constexpr std::int64_t kAdvertisementUuidLsb = 0x8000000000000000; +// Creates a string as a space separated listing of hex bytes with [] at the +// beginning and the end. +// +// This is the legacy input for hash in version (kV1). It is for testing only. +std::string StringToPrintableHexString(const std::string& source) { + // Print out the byte array as a space separated listing of hex bytes. + std::string out = "[ "; + for (const char& c : source) { + absl::StrAppend(&out, absl::StrFormat("%#04x ", c)); + } + absl::StrAppend(&out, "]"); + return out; +} + } // namespace const absl::string_view kCopresenceServiceUuid = @@ -43,8 +57,21 @@ ByteArray GenerateHash(const std::string& source, size_t size) { return Utils::Sha256Hash(source, size); } -ByteArray GenerateServiceIdHash(const std::string& service_id) { - return Utils::Sha256Hash(service_id, BlePacket::kServiceIdHashLength); +ByteArray GenerateServiceIdHash(const std::string& service_id, + BleAdvertisement::Version version) { + switch (version) { + // legacy hash for testing only. + case BleAdvertisement::Version::kV1: + return Utils::Sha256Hash(StringToPrintableHexString(service_id), + BlePacket::kServiceIdHashLength); + case BleAdvertisement::Version::kV2: + [[fallthrough]]; + case BleAdvertisement::Version::kUndefined: + [[fallthrough]]; + default: + // Use the latest known hashing scheme. + return Utils::Sha256Hash(service_id, BlePacket::kServiceIdHashLength); + } } ByteArray GenerateDeviceToken() { diff --git a/connections/implementation/mediums/ble_v2/ble_utils.h b/connections/implementation/mediums/ble_v2/ble_utils.h index 0b5a2f75..b0cb17a4 100644 --- a/connections/implementation/mediums/ble_v2/ble_utils.h +++ b/connections/implementation/mediums/ble_v2/ble_utils.h @@ -17,6 +17,7 @@ #include +#include "absl/strings/str_format.h" #include "connections/implementation/mediums/ble_v2//ble_advertisement.h" #include "connections/implementation/mediums/ble_v2/ble_advertisement_header.h" #include "connections/implementation/mediums/ble_v2/ble_packet.h" @@ -32,10 +33,34 @@ namespace bleutils { ABSL_CONST_INIT extern const absl::string_view kCopresenceServiceUuid; +// Return SHA256 hash. +// +// source - the string to be hashed. +// size - size of returned byte array. ByteArray GenerateHash(const std::string& source, size_t size); -ByteArray GenerateServiceIdHash(const std::string& service_id); + +// Return SHA256 hash of service ID. +// +// source - service id. +// version - BleAdvertisement::Version. kV1 has been deprecated and just used +// for testing. +ByteArray GenerateServiceIdHash( + const std::string& service_id, + BleAdvertisement::Version version = BleAdvertisement::Version::kV2); + +// Returns device token generated by SHA256 hash in random uint32 and +// size of mediums::BleAdvertisement::kDeviceTokenLength ByteArray GenerateDeviceToken(); + +// Return SHA256 hash of advertisement byte array. +// +// advertisement_bytes - advertisement byte array. +// The generated hash size is determined by byte array size of input. ByteArray GenerateAdvertisementHash(const ByteArray& advertisement_bytes); + +// Generates a BLE characteristic UUID for an advertisement at the given slot. +// +// slot - the advertisement slot to generate a UUID for. std::string GenerateAdvertisementUuid(int slot); } // namespace bleutils diff --git a/connections/implementation/mediums/ble_v2/discovered_peripheral_tracker.cc b/connections/implementation/mediums/ble_v2/discovered_peripheral_tracker.cc new file mode 100644 index 00000000..cbf0b390 --- /dev/null +++ b/connections/implementation/mediums/ble_v2/discovered_peripheral_tracker.cc @@ -0,0 +1,638 @@ +// 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_v2/discovered_peripheral_tracker.h" + +#include +#include +#include +#include + +#include "absl/strings/escaping.h" +#include "connections/implementation/mediums/ble_v2/advertisement_read_result.h" +#include "connections/implementation/mediums/ble_v2/ble_advertisement.h" +#include "connections/implementation/mediums/ble_v2/ble_advertisement_header.h" +#include "connections/implementation/mediums/ble_v2/ble_utils.h" +#include "connections/implementation/mediums/ble_v2/bloom_filter.h" +#include "internal/platform/mutex_lock.h" + +namespace location { +namespace nearby { +namespace connections { +namespace mediums { + +void DiscoveredPeripheralTracker::StartTracking( + const std::string& service_id, + const DiscoveredPeripheralCallback& discovered_peripheral_callback, + const std::string& fast_advertisement_service_uuid) { + MutexLock lock(&mutex_); + + ServiceIdInfo service_id_info = { + .discovered_peripheral_callback = + std::move(discovered_peripheral_callback), + .lost_entity_tracker = + absl::make_unique>(), + .fast_advertisement_service_uuid = fast_advertisement_service_uuid}; + + // Replace if key exists. + service_id_infos_.insert_or_assign(service_id, std::move(service_id_info)); + + // 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_); + + service_id_infos_.erase(service_id); +} + +void DiscoveredPeripheralTracker::ProcessFoundBleAdvertisement( + BleV2Peripheral peripheral, + const ::location::nearby::api::ble_v2::BleAdvertisementData& + advertisement_data, + AdvertisementFetcher advertisement_fetcher) { + MutexLock lock(&mutex_); + + if (service_id_infos_.empty()) { + NEARBY_LOGS(INFO) << "Ignoring BLE advertisement header because we are not " + "tracking any service IDs."; + return; + } + + if (!peripheral.IsValid() || advertisement_data.service_data.empty()) { + NEARBY_LOGS(INFO) + << "Ignoring BLE advertisement header because the peripheral is " + "invalid or the given service data is empty."; + return; + } + + HandleAdvertisement(peripheral, advertisement_data); + HandleAdvertisementHeader(advertisement_data, /*mutated=*/peripheral, + std::move(advertisement_fetcher)); +} + +void DiscoveredPeripheralTracker::ProcessLostGattAdvertisements() { + MutexLock lock(&mutex_); + + for (const auto& it : service_id_infos_) { + const std::string& service_id = it.first; + const ServiceIdInfo& service_id_info = it.second; + DiscoveredPeripheralCallback discovered_peripheral_callback = + service_id_info.discovered_peripheral_callback; + + 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) { + ClearGattAdvertisement(gatt_advertisement); + + BlePeripheral peripheral = GenerateBlePeripheral(gatt_advertisement); + if (peripheral.IsValid()) { + discovered_peripheral_callback.peripheral_lost_cb(peripheral, + service_id); + } + } + } +} + +void DiscoveredPeripheralTracker::ClearDataForServiceId( + const std::string& service_id) { + for (const auto& it : gatt_advertisement_infos_) { + if (it.second.service_id != service_id) { + continue; + } + ClearGattAdvertisement(it.first); + } +} + +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( + const BleV2Peripheral& peripheral, + const location::nearby::api::ble_v2::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|. + std::vector extracted_uuids; + // Filter out kCoprsence service uuid. + std::remove_copy_if(advertisement_data.service_uuids.begin(), + advertisement_data.service_uuids.end(), + std::back_inserter(extracted_uuids), + [](const std::string& advertisement_data_service_uuid) { + return advertisement_data_service_uuid == + bleutils::kCopresenceServiceUuid; + }); + std::string service_uuid; + if (!extracted_uuids.empty()) { + service_uuid = extracted_uuids.front(); + } else { + service_uuid = std::string(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, absl::make_unique()}); + + BleAdvertisementHeader new_advertisement_header = HandleRawGattAdvertisements( + advertisement_header, {&advertisement_bytes}, service_uuid); + UpdateCommonStateForFoundBleAdvertisement(new_advertisement_header, + /*mac_address=*/peripheral.GetId()); +} + +ByteArray DiscoveredPeripheralTracker::ExtractInterestingAdvertisementBytes( + const location::nearby::api::ble_v2::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>()); + + return BleAdvertisementHeader( + BleAdvertisementHeader::Version::kV2, /*extended_advertisement=*/false, + /*num_slots=*/1, ByteArray(bloom_filter), + bleutils::GenerateAdvertisementHash(advertisement_bytes), + /*psm=*/BleAdvertisementHeader::kDefaultPsmValue); +} + +BleAdvertisementHeader DiscoveredPeripheralTracker::HandleRawGattAdvertisements( + const BleAdvertisementHeader& advertisement_header, + const std::vector& gatt_advertisement_bytes_list, + const std::string& service_uuid) { + absl::flat_hash_map + 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()) { + NEARBY_LOGS(WARNING) << "HandleRawGattAdvertisements, failed to find " + "callback for service_id=" + << service_id; + continue; + } + BlePeripheral discovered_ble_peripheral = + GenerateBlePeripheral(gatt_advertisement, new_psm); + if (discovered_ble_peripheral.IsValid()) { + sii_it->second.discovered_peripheral_callback.peripheral_discovered_cb( + discovered_ble_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, + .mac_address = {}}; + 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 +DiscoveredPeripheralTracker::ParseRawGattAdvertisements( + const std::vector& gatt_advertisement_bytes_list, + const std::string& service_uuid) { + absl::flat_hash_map + 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. + BleAdvertisement gatt_advertisement(*gatt_advertisement_bytes); + if (!gatt_advertisement.IsValid()) { + NEARBY_LOGS(INFO) << "Unable to parse raw GATT advertisement:" + << absl::BytesToHexString( + gatt_advertisement_bytes->data()); + continue; + } + + // 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.empty()) { + 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) { + NEARBY_LOGS(INFO) + << "This GATT advertisement:" + << absl::BytesToHexString(gatt_advertisement_bytes->data()) + << " is a fast advertisement and matched UUID=" << service_uuid + << " 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()) { + NEARBY_LOGS(INFO) << "Matched service_id=" << service_id + << " to GATT advertisement=" + << absl::BytesToHexString( + gatt_advertisement_bytes->data()); + 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. + // TODO(b/213835576) : Implement API to fetch the support for extended + // advertisement from platform impl. + bool is_extended_advertisement_available = false; + 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 fetcted from GATT connection. + BloomFilter bloom_filter( + std::make_unique>()); + return advertisement_header.GetVersion() == + BleAdvertisementHeader::Version::kV2 && + advertisement_header.GetNumSlots() == 1 && + advertisement_header.GetServiceIdBloomFilter() == + ByteArray(bloom_filter); +} + +void DiscoveredPeripheralTracker::HandleAdvertisementHeader( + const location::nearby::api::ble_v2::BleAdvertisementData& + advertisement_data, + BleV2Peripheral& peripheral, AdvertisementFetcher advertisement_fetcher) { + // Attempt to parse the advertisement header. + BleAdvertisementHeader advertisement_header( + ExtractAdvertisementHeaderBytes(advertisement_data)); + if (!advertisement_header.IsValid()) { + NEARBY_LOGS(INFO) + << "Failed to deserialize BLE advertisement header. Ignoring."; + return; + } + + // Check if the advertisement header contains a service ID we're tracking. + if (!IsInterestingAdvertisementHeader(advertisement_header)) { + NEARBY_LOGS(VERBOSE) << "Ignoring BLE advertisement header=" + << absl::BytesToHexString( + ByteArray(advertisement_header).data()) + << " because it does not contain any service IDs " + "we're interested in."; + return; + } + + // Determine whether or not we need to read a fresh GATT advertisement. + if (ShouldReadRawAdvertisementFromServer(advertisement_header)) { + // Determine whether or not we need to read a fresh GATT advertisement. + std::vector gatt_advertisement_bytes_list = + FetchRawAdvertisements(advertisement_header, + /*mutated=*/peripheral, + std::move(advertisement_fetcher)); + if (!gatt_advertisement_bytes_list.empty()) { + HandleRawGattAdvertisements(advertisement_header, + gatt_advertisement_bytes_list, ""); + } + } + + // 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, + /*mac_address=*/peripheral.GetId()); +} + +ByteArray DiscoveredPeripheralTracker::ExtractAdvertisementHeaderBytes( + const location::nearby::api::ble_v2::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>(), + 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. + ByteArray advertisement_header_bytes(advertisement_header); + const auto it = advertisement_read_results_.find(advertisement_header); + if (it == advertisement_read_results_.end()) { + NEARBY_LOGS(INFO) << "Received advertisement header=" + << absl::BytesToHexString( + advertisement_header_bytes.data()) + << ", 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: + NEARBY_LOGS(INFO) + << "Received advertisement header=" + << absl::BytesToHexString(advertisement_header_bytes.data()) + << ". Caller should retry reading its GATT advertisement."; + return true; + case AdvertisementReadResult::RetryStatus::kPreviouslySucceeded: + NEARBY_LOGS(INFO) << "Received advertisement header=" + << absl::BytesToHexString( + advertisement_header_bytes.data()) + << ", but we have already read its GATT advertisement."; + return false; + case AdvertisementReadResult::RetryStatus::kTooSoon: + NEARBY_LOGS(INFO) + << "Received advertisement header=" + << absl::BytesToHexString(advertisement_header_bytes.data()) + << ", but we have recently failed to read its GATT advertisement."; + return false; + case AdvertisementReadResult::RetryStatus::kUnknown: + // Fall through. + break; + } + + NEARBY_LOGS(INFO) + << "Received advertisement header=" + << absl::BytesToHexString(advertisement_header_bytes.data()) + << ", but we do not know whether or not to retry reading " + "its GATT advertisement. Caller should retry to be safe."; + return true; +} + +std::vector +DiscoveredPeripheralTracker::FetchRawAdvertisements( + const BleAdvertisementHeader& advertisement_header, + BleV2Peripheral& peripheral, AdvertisementFetcher advertisement_fetcher) { + // Fetch the raw GATT advertisements and store the results. + AdvertisementReadResult* advertisement_read_result = nullptr; + const auto it = advertisement_read_results_.find(advertisement_header); + if (it != advertisement_read_results_.end()) { + advertisement_read_result = it->second.get(); + } + + std::vector service_ids; + std::transform(service_id_infos_.begin(), service_id_infos_.end(), + std::back_inserter(service_ids), + [](auto& kv) { return kv.first; }); + std::unique_ptr read_result = + advertisement_fetcher.fetch_advertisements( + advertisement_header.GetNumSlots(), advertisement_header.GetPsm(), + service_ids, advertisement_read_result, + /*mutated=*/peripheral); + if (!read_result) { + return {}; + } + + auto iterator_and_result_pair = advertisement_read_results_.insert_or_assign( + advertisement_header, std::move(read_result)); + // Take those results and return all the advertisements we were able to read. + std::vector advertisement_bytes_list; + if (iterator_and_result_pair.second) { + advertisement_bytes_list = + iterator_and_result_pair.first->second->GetAdvertisements(); + } + return advertisement_bytes_list; +} + +void DiscoveredPeripheralTracker::UpdateCommonStateForFoundBleAdvertisement( + const BleAdvertisementHeader& advertisement_header, + const std::string& mac_address) { + const auto ga_it = gatt_advertisements_.find(advertisement_header); + if (ga_it == gatt_advertisements_.end()) { + NEARBY_LOGS(INFO) + << "No GATT advertisements found for advertisement header=" + << absl::BytesToHexString(ByteArray(advertisement_header).data()); + 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; + } + 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) { + NEARBY_LOGS(WARNING) << "UpdateCommonStateForFoundBleAdvertisement, " + "failed to find entity " + "tracker for service_id=" + << gatt_advertisement_info.service_id; + continue; + } + lost_entity_tracker->RecordFoundEntity(gatt_advertisement); + + gatt_advertisement_info.mac_address = mac_address; + gatt_advertisement_infos_[gatt_advertisement] = gatt_advertisement_info; + } + } +} + +BlePeripheral DiscoveredPeripheralTracker::GenerateBlePeripheral( + const BleAdvertisement& gatt_advertisement, int psm) { + return BlePeripheral(ByteArray(gatt_advertisement), psm); +} + +} // namespace mediums +} // namespace connections +} // namespace nearby +} // namespace location diff --git a/connections/implementation/mediums/ble_v2/discovered_peripheral_tracker.h b/connections/implementation/mediums/ble_v2/discovered_peripheral_tracker.h new file mode 100644 index 00000000..b90c8d47 --- /dev/null +++ b/connections/implementation/mediums/ble_v2/discovered_peripheral_tracker.h @@ -0,0 +1,282 @@ +// 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. + +#ifndef CORE_INTERNAL_MEDIUMS_BLE_V2_DISCOVERED_PERIPHERAL_TRACKER_H_ +#define CORE_INTERNAL_MEDIUMS_BLE_V2_DISCOVERED_PERIPHERAL_TRACKER_H_ + +#include +#include +#include + +#include "connections/implementation/mediums//lost_entity_tracker.h" +#include "connections/implementation/mediums/ble_v2/advertisement_read_result.h" +#include "connections/implementation/mediums/ble_v2/ble_advertisement.h" +#include "connections/implementation/mediums/ble_v2/ble_advertisement_header.h" +#include "connections/implementation/mediums/ble_v2/discovered_peripheral_callback.h" +#include "connections/implementation/mediums/lost_entity_tracker.h" +#include "internal/platform/bluetooth_adapter.h" +#include "internal/platform/byte_array.h" +#include "internal/platform/mutex.h" + +namespace location { +namespace nearby { +namespace connections { +namespace mediums { + +// Manages all discovered peripheral logic for {@link BluetoothLowEnergy}. This +// includes tracking found peripherals, lost peripherals, and MAC addresses +// associated with those peripherals. +// +// See go/ble-on-lost for more information. It includes the algorithms used to +// compute found and lost peripherals. +class DiscoveredPeripheralTracker { + public: + // GATT advertisement fetcher. + struct AdvertisementFetcher { + // Fetches relevant GATT advertisements for the peripheral found in {@link + // DiscoveredPeripheralTracker#ProcessFoundBleAdvertisement(}. + std::function( + int num_slots, int psm, + const std::vector& interesting_service_ids, + AdvertisementReadResult* advertisement_read_result, + BleV2Peripheral& peripheral)> + fetch_advertisements = [](int, int, const std::vector&, + AdvertisementReadResult*, BleV2Peripheral&) + -> std::unique_ptr { return nullptr; }; + }; + + // Starts tracking discoveries for a particular service Id. + // + // service_id - The service ID to track. + // discovered_peripheral_callback - The callback to invoke for discovery + // events. + // fast_advertisement_service_uuid - The service UUID to look for fast + // advertisements on. + // Note: fast_advertisement_service_uuid can be empty string to indicate + // that `fast_advertisement_service_uuid` will be ignored for regular + // advertisement. + void StartTracking( + const std::string& service_id, + const DiscoveredPeripheralCallback& discovered_peripheral_callback, + const std::string& fast_advertisement_service_uuid) + ABSL_LOCKS_EXCLUDED(mutex_); + + // Stops tracking discoveries for a particular service Id. + void StopTracking(const std::string& service_id) ABSL_LOCKS_EXCLUDED(mutex_); + + // Processes a found BLE advertisement. + // + // peripheral - To access the status of the operation. The ownership + // is moved into 'discovered_peripheral_tracker' object. + // advertisement_data - The found BLE advertisement data. + // advertisement_fetcher - The advertisement fetcher callback for regular + // advertisement if the advertisement header is been + // processed. + void ProcessFoundBleAdvertisement( + BleV2Peripheral peripheral, + const api::ble_v2::BleAdvertisementData& advertisement_data, + AdvertisementFetcher advertisement_fetcher) ABSL_LOCKS_EXCLUDED(mutex_); + + // Processes the set of lost GATT advertisements and notifies the client of + // any lost peripherals. + void ProcessLostGattAdvertisements() ABSL_LOCKS_EXCLUDED(mutex_); + + private: + using BleAdvertisementSet = absl::flat_hash_set; + + // A container to hold callback or other informations that bring from BLE + // medium when `StartTracking`. + struct ServiceIdInfo { + // Tracks what service IDs are currently active and gives us client + // callbacks to call. + DiscoveredPeripheralCallback discovered_peripheral_callback; + + // Used to periodically compute lost GATT advertisements. + std::unique_ptr> lost_entity_tracker; + + // Used to check for fast advertisements delivered through BLE advertisement + // service data, under the given UUID. + std::string fast_advertisement_service_uuid; + }; + + // A container to hold the related informations for a GATT advertisement. + struct GattAdvertisementInfo { + // GATT advertisement to the service ID it's associated with. Tracks what + // GATT advertisements are currently active. Used to determine which + // LostEntityTracker to invoke when advertisements are rediscovered. + std::string service_id; + + // Used to efficiently find advertisement headers to delete when GATT + // advertisements are updated. This is a reverse map of + // gatt_advertisements_. + BleAdvertisementHeader advertisement_header; + + // Used when we need to make a socket connection based off of the GATT + // advertisement alone. Entries are modified every time a GATT + // advertisement's advertisement header is seen. + std::string mac_address; + }; + + // Clears stale data from any previous sessions. + void ClearDataForServiceId(const std::string& service_id) + ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_); + + // Clears out all data related to the provided GATT advertisement. This + // includes: + // 1. Removing the corresponding GATT advertisement from + // gatt_advertisement_infos_. + // 2. Removing the corresponding advertisement header from + // advertisement_read_results. + // 3. Removing the corresponding advertisement header from + // gatt_advertisements_, only if there are no remaining GATT + // advertisements related to that header. + void ClearGattAdvertisement(const BleAdvertisement& gatt_advertisement) + ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_); + + // Handles the legacy fast advertisement or the extended fast/regular + // advertisement. + void HandleAdvertisement( + const BleV2Peripheral& peripheral, + const api::ble_v2::BleAdvertisementData& advertisement_data) + ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_); + + // Extracts the advertisement byte array from `AdvertisementData`. + ByteArray ExtractInterestingAdvertisementBytes( + const api::ble_v2::BleAdvertisementData& advertisement_data) + ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_); + + // Creates an advertisement header that's purely a hash of the fast/regular + // advertisement, since they come with no header. + BleAdvertisementHeader CreateAdvertisementHeader( + const ByteArray& advertisement_bytes); + + // Returns BleAdvertisementHeader, it may be replaced if the header is mock + // and there's a psm value in advertisement. + BleAdvertisementHeader HandleRawGattAdvertisements( + const BleAdvertisementHeader& advertisement_header, + const std::vector& gatt_advertisement_bytes_list, + const std::string& service_uuid) ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_); + + // Returns a map of service IDs to GATT advertisements who belong to a tracked + // service ID. + absl::flat_hash_map ParseRawGattAdvertisements( + const std::vector& gatt_advertisement_bytes_list, + const std::string& service_uuid) ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_); + + // Returns true if `new_psm` is not default value and different with + // `old_psm`. + bool ShouldNotifyForNewPsm(int old_psm, int new_psm) const; + + // Returns true if two headers are not the same. + bool ShouldRemoveHeader( + const BleAdvertisementHeader& old_advertisement_header, + const BleAdvertisementHeader& new_advertisement_header); + + // Returns true if it is a faked advertisement header which is purely a hash + // of the fast/regular advertisement, since they come with no header. + bool IsDummyAdvertisementHeader( + const BleAdvertisementHeader& advertisement_header); + + // Handles the advertisement header for regular advertisement. + void HandleAdvertisementHeader( + const api::ble_v2::BleAdvertisementData& advertisement_data, + BleV2Peripheral& peripheral, AdvertisementFetcher advertisement_fetcher) + ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_); + + // Extracts the advertisement header byte array from `AdvertisementData`. + ByteArray ExtractAdvertisementHeaderBytes( + const api::ble_v2::BleAdvertisementData& advertisement_data) + ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_); + + // Returns true if the advertisement header contains a service ID we're + // tracking. + bool IsInterestingAdvertisementHeader( + const BleAdvertisementHeader& advertisement_header) + ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_); + + // Returns true if the `advertisement_header` is allowd to callbck to fecth + // advertisement. + bool ShouldReadRawAdvertisementFromServer( + const BleAdvertisementHeader& advertisement_header) + ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_); + + // Fetches advertsiement from BLE medium if advertisement header is read in + // AdvertisementData. + // + // advertisement_fetcher : a fetcher passed from BLE medium to read the + // advertisemeent from BLE characteristics by GATT server. + std::vector FetchRawAdvertisements( + const BleAdvertisementHeader& advertisement_header, + BleV2Peripheral& peripheral, AdvertisementFetcher advertisement_fetcher) + ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_); + + // Updates `gatt_advertisement_infos_` map no matter whether we read a new + // GATT advertisement by the input `advertisement_header` and 'mac_address`. + void UpdateCommonStateForFoundBleAdvertisement( + const BleAdvertisementHeader& advertisement_header, + const std::string& mac_address) ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_); + + // Creates BlePeripheral based on the input of advertisement and psm value. + BlePeripheral GenerateBlePeripheral( + const BleAdvertisement& gatt_advertisement, + int psm = BleAdvertisementHeader::kDefaultPsmValue); + + Mutex mutex_; + + // ------------ SERVICE ID MAPS ------------ + // Entries in these maps all follow the same lifecycle. Entries are added in + // StartTracking, and removed in StopTracking. + absl::flat_hash_map service_id_infos_ + ABSL_GUARDED_BY(mutex_); + + // ------------ ADVERTISEMENT HEADER MAPS ------------ + // Maps advertisement headers to AdvertisementReadResult. Tells us when to + // retry reading a GATT advertisement. If no entry exists for a particular + // header, we should try reading a GATT advertisement. Entries are added + // whenever a GATT advertisement read is attempted, and removed when GATT + // advertisements are lost. Entries are also removed whenever + // gattAdvertisements removes its entry. + // + // The map is also cleared whenever StartTracking is called, due to client + // changes. For example, say clients A and B start scanning and discover + // advertisements A and B (for both clients) on advertisement header 1. Then, + // A restarts scanning, causing us to clear stale advertisement A. However, + // since B was still scanning, we don't remove advertisement header 1 from the + // map. This causes us to never re-read advertisement A. + absl::flat_hash_map> + advertisement_read_results_ ABSL_GUARDED_BY(mutex_); + + // Maps advertisement headers to a set of GATT advertisements from a single + // peripheral. Used to retrieve GATT advertisements that we need to reprocess + // every time a header is seen. Entries are added when GATT advertisements are + // read, removed when all associated GATT advertisements are lost or become + // stale, and replaced when the advertisement header is updated for a single + // remote peripheral. + absl::flat_hash_map + gatt_advertisements_ ABSL_GUARDED_BY(mutex_); + + // ------------ GATT ADVERTISEMENT MAPS ------------ + // Entries are added when Gatt advertisements are read, and removed when Gatt + // advertisements are lost or become stale. + absl::flat_hash_map + gatt_advertisement_infos_ ABSL_GUARDED_BY(mutex_); +}; + +} // namespace mediums +} // namespace connections +} // namespace nearby +} // namespace location + +#endif // CORE_INTERNAL_MEDIUMS_BLE_V2_DISCOVERED_PERIPHERAL_TRACKER_H_ diff --git a/connections/implementation/mediums/ble_v2/discovered_peripheral_tracker_test.cc b/connections/implementation/mediums/ble_v2/discovered_peripheral_tracker_test.cc new file mode 100644 index 00000000..f7ca0374 --- /dev/null +++ b/connections/implementation/mediums/ble_v2/discovered_peripheral_tracker_test.cc @@ -0,0 +1,940 @@ +// 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_v2/discovered_peripheral_tracker.h" + +#include +#include + +#include "gmock/gmock.h" +#include "protobuf-matchers/protocol-buffer-matchers.h" +#include "gtest/gtest.h" +#include "connections/implementation/mediums/ble_v2/ble_utils.h" +#include "connections/implementation/mediums/ble_v2/bloom_filter.h" +#include "internal/platform/bluetooth_adapter.h" +#include "internal/platform/count_down_latch.h" +#include "internal/platform/mutex.h" +#include "internal/platform/mutex_lock.h" + +namespace location { +namespace nearby { +namespace connections { +namespace mediums { + +namespace { + +constexpr absl::Duration kWaitDuration = absl::Milliseconds(1000); +constexpr absl::string_view kCopresenceServiceUuid = + "0000FEF3-0000-1000-8000-00805F9B34FB"; +constexpr absl::string_view kFastAdvertisementServiceUuid = + "0000FE2C-0000-1000-8000-00805F9B34FB"; +constexpr absl::string_view kServiceIdA = "A"; +constexpr absl::string_view kServiceIdB = "B"; +constexpr absl::string_view kMacAddress1 = "4C:8B:1D:CE:BA:D1"; +constexpr absl::string_view kData = "\x04\x02\x00"; +constexpr absl::string_view kData2 = "\x07\x00\x07"; +constexpr absl::string_view kDeviceToken = "\x04\x20"; + +ByteArray CreateFastBleAdvertisement(const ByteArray& data, + const ByteArray& device_token) { + return ByteArray(BleAdvertisement( + BleAdvertisement::Version::kV2, BleAdvertisement::SocketVersion::kV2, + /*service_id_hash=*/ByteArray{}, data, device_token, + BleAdvertisementHeader::kDefaultPsmValue)); +} + +ByteArray CreateBleAdvertisement(const std::string& service_id, + const ByteArray& data, + const ByteArray& device_token) { + return ByteArray(BleAdvertisement( + BleAdvertisement::Version::kV2, BleAdvertisement::SocketVersion::kV2, + bleutils::GenerateServiceIdHash(service_id), data, device_token, + BleAdvertisementHeader::kDefaultPsmValue)); +} + +// Legacy advertisement is not supported any more but we fake the legacy one +// by setting version and socket version as kV1 and using legacy hash function. +ByteArray CreateLegacyBleAdvertisement(const std::string& service_id, + const ByteArray& data, + const ByteArray& device_token) { + return ByteArray(BleAdvertisement( + BleAdvertisement::Version::kV1, BleAdvertisement::SocketVersion::kV1, + bleutils::GenerateServiceIdHash(service_id, + BleAdvertisement::Version::kV1), + data, device_token, BleAdvertisementHeader::kDefaultPsmValue)); +} + +ByteArray CreateBleAdvertisementHeader(const ByteArray& advertisement_hash, + int psm, + std::vector& service_ids) { + BloomFilter service_id_bloom_filter( + std::make_unique>()); + + for (const std::string& service_id : service_ids) { + service_id_bloom_filter.Add(service_id); + } + + return ByteArray(BleAdvertisementHeader(BleAdvertisementHeader::Version::kV2, + /*extended_advertisement=*/false, + /*num_slots=*/service_ids.size(), + ByteArray(service_id_bloom_filter), + advertisement_hash, psm)); +} + +ByteArray CreateBleAdvertisementHeader(const ByteArray& advertisement_hash, + std::vector& service_ids) { + return CreateBleAdvertisementHeader(advertisement_hash, + BleAdvertisementHeader::kDefaultPsmValue, + service_ids); +} + +ByteArray GenerateRandomAdvertisementHash() { + ByteArray random_advertisement_hash = Utils::GenerateRandomBytes( + BleAdvertisementHeader::kAdvertisementHashByteLength); + + return random_advertisement_hash; +} + +// A stub BlePeripheral implementation. +class BlePeripheralStub : public api::ble_v2::BlePeripheral { + public: + explicit BlePeripheralStub(absl::string_view mac_address) { + mac_address_ = mac_address; + } + + std::string GetId() const override { return mac_address_; } + + private: + std::string mac_address_; +}; + +class DiscoveredPeripheralTrackerTest : public testing::Test { + public: + void SetUp() override {} + + BleV2Peripheral CreateBlePeripheral(absl::string_view mac_address) { + ble_peripheral_ = std::make_unique(mac_address); + return BleV2Peripheral(ble_peripheral_.get()); + } + + // Simulates to see a fast advertisement. + void FindFastAdvertisement( + const api::ble_v2::BleAdvertisementData& advertisement_data, + const std::vector& advertisement_bytes_list, + CountDownLatch& fetch_latch) { + BleV2Peripheral peripheral = CreateBlePeripheral(kMacAddress1); + + discovered_peripheral_tracker_.ProcessFoundBleAdvertisement( + peripheral, advertisement_data, + GetAdvertisementFetcher(fetch_latch, advertisement_bytes_list)); + } + + // Simulates to see a regular advertisement. + void FindAdvertisement( + const api::ble_v2::BleAdvertisementData& advertisement_data, + const std::vector& advertisement_bytes_list, + CountDownLatch& fetch_latch) { + BleV2Peripheral peripheral = CreateBlePeripheral(kMacAddress1); + + discovered_peripheral_tracker_.ProcessFoundBleAdvertisement( + peripheral, advertisement_data, + GetAdvertisementFetcher(fetch_latch, advertisement_bytes_list)); + } + + int GetFetchAdvertisementCallbackCount() const { + MutexLock lock(&mutex_); + return fetch_count_; + } + + protected: + // A stub Advertisement fetcher. + DiscoveredPeripheralTracker::AdvertisementFetcher GetAdvertisementFetcher( + CountDownLatch& fetch_latch, + const std::vector& advertisement_bytes_list) { + return { + .fetch_advertisements = + [this, &fetch_latch, &advertisement_bytes_list]( + int num_slots, int psm, + const std::vector& interesting_service_ids, + AdvertisementReadResult* arr, BleV2Peripheral& peripheral) + -> std::unique_ptr { + MutexLock lock(&mutex_); + fetch_count_++; + auto advertisement_read_result = + std::make_unique(); + int slot = 0; + for (const auto& advertisement_bytes : advertisement_bytes_list) { + advertisement_read_result->AddAdvertisement(slot++, + advertisement_bytes); + } + advertisement_read_result->RecordLastReadStatus(/*isSuccess=*/true); + fetch_latch.CountDown(); + return advertisement_read_result; + }, + }; + } + + mutable Mutex mutex_; + int fetch_count_ ABSL_GUARDED_BY(mutex_) = 0; + std::unique_ptr ble_peripheral_; + DiscoveredPeripheralTracker discovered_peripheral_tracker_; +}; + +TEST_F(DiscoveredPeripheralTrackerTest, + FoundFastAdvertisementPeripheralDiscovered) { + ByteArray fast_advertisement_bytes = CreateFastBleAdvertisement( + ByteArray(std::string(kData)), ByteArray(std::string(kDeviceToken))); + CountDownLatch found_latch(1); + CountDownLatch fetch_latch(1); + + discovered_peripheral_tracker_.StartTracking( + std::string(kServiceIdA), + { + .peripheral_discovered_cb = + [&found_latch](BlePeripheral& peripheral, + const std::string& service_id, + const ByteArray& advertisement_bytes, + bool fast_advertisement) { + EXPECT_EQ(advertisement_bytes, ByteArray(std::string(kData))); + EXPECT_TRUE(fast_advertisement); + found_latch.CountDown(); + }, + }, + std::string(kFastAdvertisementServiceUuid)); + + api::ble_v2::BleAdvertisementData advertisement_data; + if (!fast_advertisement_bytes.Empty()) { + advertisement_data.service_uuids.insert( + std::string(kFastAdvertisementServiceUuid)); + advertisement_data.service_data.insert( + {std::string(kFastAdvertisementServiceUuid), fast_advertisement_bytes}); + } + + FindFastAdvertisement(advertisement_data, {}, fetch_latch); + + // We should receive a client callback of a peripheral discovery without a + // GATT read. + fetch_latch.Await(kWaitDuration); + EXPECT_TRUE(found_latch.Await(kWaitDuration).result()); + EXPECT_EQ(GetFetchAdvertisementCallbackCount(), 0); +} + +TEST_F(DiscoveredPeripheralTrackerTest, + FoundFastAdvertisementDuplicateAdvertisements) { + ByteArray fast_advertisement_bytes = CreateFastBleAdvertisement( + ByteArray(std::string(kData)), ByteArray(std::string(kDeviceToken))); + CountDownLatch found_latch(3); + CountDownLatch fetch_latch(3); + int callback_times = 0; + + discovered_peripheral_tracker_.StartTracking( + std::string(kServiceIdA), + { + .peripheral_discovered_cb = + [&callback_times, &found_latch]( + BlePeripheral& peripheral, const std::string& service_id, + const ByteArray& advertisement_bytes, + bool fast_advertisement) { + callback_times++; + EXPECT_EQ(advertisement_bytes, ByteArray(std::string(kData))); + EXPECT_TRUE(fast_advertisement); + found_latch.CountDown(); + }, + }, + std::string(kFastAdvertisementServiceUuid)); + + api::ble_v2::BleAdvertisementData advertisement_data; + if (!fast_advertisement_bytes.Empty()) { + advertisement_data.service_uuids.insert( + std::string(kFastAdvertisementServiceUuid)); + advertisement_data.service_data.insert( + {std::string(kFastAdvertisementServiceUuid), fast_advertisement_bytes}); + } + + FindFastAdvertisement(advertisement_data, {}, fetch_latch); + FindFastAdvertisement(advertisement_data, {}, fetch_latch); + FindFastAdvertisement(advertisement_data, {}, fetch_latch); + + // We should only receive ONE client callback of a peripheral discovery with + // ZERO GATT reads. + fetch_latch.Await(kWaitDuration * 3); + EXPECT_EQ(callback_times, 1); + // The count down won't be finished since the callback only been called once. + // Let it time out. + EXPECT_FALSE(found_latch.Await(3 * kWaitDuration).result()); + EXPECT_EQ(GetFetchAdvertisementCallbackCount(), 0); +} + +TEST_F(DiscoveredPeripheralTrackerTest, + FoundFastAdvertisementUntrackedFastAdvertisementServiceUuid) { + ByteArray fast_advertisement_bytes = CreateFastBleAdvertisement( + ByteArray(std::string(kData)), ByteArray(std::string(kDeviceToken))); + CountDownLatch found_latch(1); + CountDownLatch fetch_latch(1); + + // Start tracking a service ID and then process a discovery containing a valid + // fast advertisement, but under a different service UUID. + discovered_peripheral_tracker_.StartTracking( + std::string(kServiceIdA), + { + .peripheral_discovered_cb = + [&found_latch](BlePeripheral& peripheral, + const std::string& service_id, + const ByteArray& advertisement_bytes, + bool fast_advertisement) { + EXPECT_EQ(advertisement_bytes, ByteArray(std::string(kData))); + EXPECT_TRUE(fast_advertisement); + found_latch.CountDown(); + }, + }, + "0000FE2C-0000-1000-8000-00805F9B34FC"); + + api::ble_v2::BleAdvertisementData advertisement_data; + if (!fast_advertisement_bytes.Empty()) { + advertisement_data.service_uuids.insert( + std::string(kFastAdvertisementServiceUuid)); + advertisement_data.service_data.insert( + {std::string(kFastAdvertisementServiceUuid), fast_advertisement_bytes}); + } + + FindFastAdvertisement(advertisement_data, {}, fetch_latch); + + // We should get no new discoveries. + fetch_latch.Await(kWaitDuration); + EXPECT_FALSE(found_latch.Await(kWaitDuration).result()); + EXPECT_EQ(GetFetchAdvertisementCallbackCount(), 0); +} + +TEST_F(DiscoveredPeripheralTrackerTest, + FoundFastAdvertisementAndGattAdvertisementSimultaneously) { + std::vector service_ids = {std::string(kServiceIdB)}; + ByteArray advertisement_header_bytes = CreateBleAdvertisementHeader( + GenerateRandomAdvertisementHash(), service_ids); + ByteArray fast_advertisement_bytes = CreateFastBleAdvertisement( + ByteArray(std::string(kData)), ByteArray(std::string(kDeviceToken))); + ByteArray advertisement_bytes = CreateBleAdvertisement( + std::string(kServiceIdB), ByteArray(std::string(kData)), + ByteArray(std::string(kDeviceToken))); + CountDownLatch found_latch_a(1); + CountDownLatch found_latch_b(1); + CountDownLatch fetch_latch(1); + + discovered_peripheral_tracker_.StartTracking( + std::string(kServiceIdA), + { + .peripheral_discovered_cb = + [&found_latch_a](BlePeripheral& peripheral, + const std::string& service_id, + const ByteArray& advertisement_bytes, + bool fast_advertisement) { + EXPECT_EQ(advertisement_bytes, ByteArray(std::string(kData))); + EXPECT_TRUE(fast_advertisement); + found_latch_a.CountDown(); + }, + }, + std::string(kFastAdvertisementServiceUuid)); + discovered_peripheral_tracker_.StartTracking( + std::string(kServiceIdB), + { + .peripheral_discovered_cb = + [&found_latch_b](BlePeripheral& peripheral, + const std::string& service_id, + const ByteArray& advertisement_bytes, + bool fast_advertisement) { + EXPECT_EQ(advertisement_bytes, ByteArray(std::string(kData))); + EXPECT_FALSE(fast_advertisement); + found_latch_b.CountDown(); + }, + }, + ""); + + api::ble_v2::BleAdvertisementData advertisement_data; + if (!advertisement_header_bytes.Empty()) { + advertisement_data.service_uuids.insert( + std::string(kCopresenceServiceUuid)); + advertisement_data.service_data.insert( + {std::string(kCopresenceServiceUuid), advertisement_header_bytes}); + } + if (!fast_advertisement_bytes.Empty()) { + advertisement_data.service_uuids.insert( + std::string(kFastAdvertisementServiceUuid)); + advertisement_data.service_data.insert( + {std::string(kFastAdvertisementServiceUuid), fast_advertisement_bytes}); + } + + FindFastAdvertisement(advertisement_data, {advertisement_bytes}, fetch_latch); + + // We should receive two separate peripheral discoveries. + fetch_latch.Await(kWaitDuration); + EXPECT_TRUE(found_latch_a.Await(kWaitDuration).result()); + EXPECT_TRUE(found_latch_b.Await(kWaitDuration).result()); + EXPECT_EQ(GetFetchAdvertisementCallbackCount(), 1); +} + +TEST_F(DiscoveredPeripheralTrackerTest, + FoundBleAdvertisementPeripheralDiscovered) { + std::vector service_ids = {std::string(kServiceIdA)}; + ByteArray advertisement_header_bytes = CreateBleAdvertisementHeader( + GenerateRandomAdvertisementHash(), service_ids); + ByteArray advertisement_bytes = CreateBleAdvertisement( + std::string(kServiceIdA), ByteArray(std::string(kData)), + ByteArray(std::string(kDeviceToken))); + CountDownLatch found_latch(1); + CountDownLatch fetch_latch(1); + + discovered_peripheral_tracker_.StartTracking( + std::string(kServiceIdA), + { + .peripheral_discovered_cb = + [&found_latch](BlePeripheral& peripheral, + const std::string& service_id, + const ByteArray& advertisement_bytes, + bool fast_advertisement) { + EXPECT_EQ(advertisement_bytes, ByteArray(std::string(kData))); + EXPECT_FALSE(fast_advertisement); + found_latch.CountDown(); + }, + }, + ""); + + api::ble_v2::BleAdvertisementData advertisement_data; + if (!advertisement_header_bytes.Empty()) { + advertisement_data.service_uuids.insert( + std::string(kCopresenceServiceUuid)); + advertisement_data.service_data.insert( + {std::string(kCopresenceServiceUuid), advertisement_header_bytes}); + } + + FindAdvertisement(advertisement_data, {advertisement_bytes}, fetch_latch); + + // We should receive a client callback of a peripheral discovery. + fetch_latch.Await(kWaitDuration); + EXPECT_TRUE(found_latch.Await(kWaitDuration).result()); + EXPECT_EQ(GetFetchAdvertisementCallbackCount(), 1); +} + +TEST_F(DiscoveredPeripheralTrackerTest, + FoundBleAdvertisementLegacyPeripheralDiscovered) { + std::vector service_ids = {std::string(kServiceIdA)}; + ByteArray advertisement_header_bytes = CreateBleAdvertisementHeader( + GenerateRandomAdvertisementHash(), service_ids); + ByteArray legacy_advertisement_bytes = CreateLegacyBleAdvertisement( + std::string(kServiceIdA), ByteArray(std::string(kData)), + ByteArray(std::string(kDeviceToken))); + CountDownLatch found_latch(1); + CountDownLatch fetch_latch(1); + + discovered_peripheral_tracker_.StartTracking( + std::string(kServiceIdA), + { + .peripheral_discovered_cb = + [&found_latch]( + BlePeripheral& peripheral, const std::string& service_id, + const ByteArray& advertisement_bytes, + bool fast_advertisement) { found_latch.CountDown(); }, + }, + ""); + + api::ble_v2::BleAdvertisementData advertisement_data; + if (!advertisement_header_bytes.Empty()) { + advertisement_data.service_uuids.insert( + std::string(kCopresenceServiceUuid)); + advertisement_data.service_data.insert( + {std::string(kCopresenceServiceUuid), advertisement_header_bytes}); + } + + FindAdvertisement(advertisement_data, {legacy_advertisement_bytes}, + fetch_latch); + + // We should not receive a client callback with the Version/SocketVersion kV1. + fetch_latch.Await(kWaitDuration); + EXPECT_FALSE(found_latch.Await(kWaitDuration).result()); + EXPECT_EQ(GetFetchAdvertisementCallbackCount(), 1); +} + +TEST_F(DiscoveredPeripheralTrackerTest, + FoundBleAdvertisementFavorLatestPeripheral) { + std::vector service_ids = {std::string(kServiceIdA)}; + ByteArray advertisement_header_bytes = CreateBleAdvertisementHeader( + GenerateRandomAdvertisementHash(), service_ids); + ByteArray advertisement_bytes = CreateBleAdvertisement( + std::string(kServiceIdA), ByteArray(std::string(kData2)), + ByteArray(std::string(kDeviceToken))); + ByteArray legacy_advertisement_bytes = CreateLegacyBleAdvertisement( + std::string(kServiceIdA), ByteArray(std::string(kData)), + ByteArray(std::string(kDeviceToken))); + CountDownLatch found_latch(1); + CountDownLatch fetch_latch(1); + int callback_times = 0; + + discovered_peripheral_tracker_.StartTracking( + std::string(kServiceIdA), + { + .peripheral_discovered_cb = + [&callback_times, &found_latch]( + BlePeripheral& peripheral, const std::string& service_id, + const ByteArray& advertisement_bytes, + bool fast_advertisement) { + callback_times++; + EXPECT_EQ(advertisement_bytes, ByteArray(std::string(kData2))); + EXPECT_FALSE(fast_advertisement); + found_latch.CountDown(); + }, + }, + ""); + + api::ble_v2::BleAdvertisementData advertisement_data; + if (!advertisement_header_bytes.Empty()) { + advertisement_data.service_uuids.insert( + std::string(kCopresenceServiceUuid)); + advertisement_data.service_data.insert( + {std::string(kCopresenceServiceUuid), advertisement_header_bytes}); + } + + FindAdvertisement(advertisement_data, + {advertisement_bytes, legacy_advertisement_bytes}, + fetch_latch); + + // We should only receive one callback with data from the V2 GATT + // advertisement. + fetch_latch.Await(kWaitDuration); + EXPECT_EQ(callback_times, 1); + EXPECT_TRUE(found_latch.Await(kWaitDuration).result()); + EXPECT_EQ(GetFetchAdvertisementCallbackCount(), 1); +} + +TEST_F(DiscoveredPeripheralTrackerTest, + FoundBleAdvertisementDuplicateAdvertisements) { + std::vector service_ids = {std::string(kServiceIdA)}; + ByteArray advertisement_header_bytes = CreateBleAdvertisementHeader( + GenerateRandomAdvertisementHash(), service_ids); + ByteArray advertisement_bytes = CreateBleAdvertisement( + std::string(kServiceIdA), ByteArray(std::string(kData)), + ByteArray(std::string(kDeviceToken))); + CountDownLatch found_latch(3); + CountDownLatch fetch_latch(3); + int callback_times = 0; + + discovered_peripheral_tracker_.StartTracking( + std::string(kServiceIdA), + { + .peripheral_discovered_cb = + [&callback_times, &found_latch]( + BlePeripheral& peripheral, const std::string& service_id, + const ByteArray& advertisement_bytes, + bool fast_advertisement) { + callback_times++; + EXPECT_EQ(advertisement_bytes, ByteArray(std::string(kData))); + EXPECT_FALSE(fast_advertisement); + found_latch.CountDown(); + }, + }, + ""); + + api::ble_v2::BleAdvertisementData advertisement_data; + if (!advertisement_header_bytes.Empty()) { + advertisement_data.service_uuids.insert( + std::string(kCopresenceServiceUuid)); + advertisement_data.service_data.insert( + {std::string(kCopresenceServiceUuid), advertisement_header_bytes}); + } + + FindAdvertisement(advertisement_data, {advertisement_bytes}, fetch_latch); + FindAdvertisement(advertisement_data, {advertisement_bytes}, fetch_latch); + FindAdvertisement(advertisement_data, {advertisement_bytes}, fetch_latch); + + // We should only receive ONE client callback of a peripheral discovery. And + // we should also do only ONE GATT read. + fetch_latch.Await(kWaitDuration); + EXPECT_EQ(callback_times, 1); + // The count down won't be finished since the callback only been called once. + // Let it time out. + EXPECT_FALSE(found_latch.Await(3 * kWaitDuration).result()); + EXPECT_EQ(GetFetchAdvertisementCallbackCount(), 1); +} + +TEST_F(DiscoveredPeripheralTrackerTest, + FoundBleAdvertisementUntrackedServiceId) { + std::vector service_ids = {std::string(kServiceIdA), + std::string(kServiceIdB)}; + ByteArray advertisement_header_bytes = CreateBleAdvertisementHeader( + GenerateRandomAdvertisementHash(), service_ids); + ByteArray advertisement_bytes = CreateBleAdvertisement( + std::string(kServiceIdB), ByteArray(std::string(kData)), + ByteArray(std::string(kDeviceToken))); + CountDownLatch found_latch(1); + CountDownLatch fetch_latch(1); + + discovered_peripheral_tracker_.StartTracking( + std::string(kServiceIdA), + { + .peripheral_discovered_cb = + [&found_latch]( + BlePeripheral& peripheral, const std::string& service_id, + const ByteArray& advertisement_bytes, + bool fast_advertisement) { found_latch.CountDown(); }, + }, + ""); + + api::ble_v2::BleAdvertisementData advertisement_data; + if (!advertisement_header_bytes.Empty()) { + advertisement_data.service_uuids.insert( + std::string(kCopresenceServiceUuid)); + advertisement_data.service_data.insert( + {std::string(kCopresenceServiceUuid), advertisement_header_bytes}); + } + + FindAdvertisement(advertisement_data, {advertisement_bytes}, fetch_latch); + + // We should get no new discoveries. + fetch_latch.Await(kWaitDuration); + EXPECT_FALSE(found_latch.Await(kWaitDuration).result()); + EXPECT_EQ(GetFetchAdvertisementCallbackCount(), 1); +} + +TEST_F(DiscoveredPeripheralTrackerTest, + LostPeripheralForFastAdvertisementLost) { + std::vector service_ids = {}; + ByteArray advertisement_header_bytes = CreateBleAdvertisementHeader( + GenerateRandomAdvertisementHash(), service_ids); + ByteArray fast_advertisement_bytes = CreateFastBleAdvertisement( + ByteArray(std::string(kData)), ByteArray(std::string(kDeviceToken))); + CountDownLatch found_latch(1); + CountDownLatch lost_latch(2); + CountDownLatch fetch_latch(1); + int lost_callback_times = 0; + + discovered_peripheral_tracker_.StartTracking( + std::string(kServiceIdA), + { + .peripheral_discovered_cb = + [&found_latch](BlePeripheral& peripheral, + const std::string& service_id, + const ByteArray& advertisement_bytes, + bool fast_advertisement) { + EXPECT_EQ(advertisement_bytes, ByteArray(std::string(kData))); + EXPECT_TRUE(fast_advertisement); + found_latch.CountDown(); + }, + .peripheral_lost_cb = + [&lost_latch, &lost_callback_times]( + BlePeripheral& peripheral, const std::string& service_id) { + lost_callback_times++; + lost_latch.CountDown(); + }, + }, + std::string(kFastAdvertisementServiceUuid)); + + api::ble_v2::BleAdvertisementData advertisement_data; + if (!advertisement_header_bytes.Empty()) { + advertisement_data.service_uuids.insert( + std::string(kCopresenceServiceUuid)); + advertisement_data.service_data.insert( + {std::string(kCopresenceServiceUuid), advertisement_header_bytes}); + } + if (!fast_advertisement_bytes.Empty()) { + advertisement_data.service_uuids.insert( + std::string(kFastAdvertisementServiceUuid)); + advertisement_data.service_data.insert( + {std::string(kFastAdvertisementServiceUuid), fast_advertisement_bytes}); + } + + FindFastAdvertisement(advertisement_data, {fast_advertisement_bytes}, + fetch_latch); + + // We should receive a client callback of a peripheral discovery. + fetch_latch.Await(kWaitDuration); + EXPECT_TRUE(found_latch.Await(kWaitDuration).result()); + EXPECT_EQ(GetFetchAdvertisementCallbackCount(), 0); + + // Then, go through two cycles of onLost. The first cycle should include the + // recently discovered peripheral in its 'found' pool. The second one should + // trigger the onLost callback. + discovered_peripheral_tracker_.ProcessLostGattAdvertisements(); + discovered_peripheral_tracker_.ProcessLostGattAdvertisements(); + + // We should receive a client callback of a lost peripheral. + EXPECT_FALSE(lost_latch.Await(kWaitDuration).result()); + EXPECT_EQ(lost_callback_times, 1); +} + +TEST_F(DiscoveredPeripheralTrackerTest, + FoundFastAdvertisementAlmostLostPeripheral) { + std::vector service_ids = {}; + ByteArray advertisement_header_bytes = CreateBleAdvertisementHeader( + GenerateRandomAdvertisementHash(), service_ids); + ByteArray fast_advertisement_bytes = CreateFastBleAdvertisement( + ByteArray(std::string(kData)), ByteArray(std::string(kDeviceToken))); + CountDownLatch found_latch(1); + CountDownLatch lost_latch(1); + CountDownLatch fetch_latch(1); + + discovered_peripheral_tracker_.StartTracking( + std::string(kServiceIdA), + { + .peripheral_discovered_cb = + [&found_latch](BlePeripheral& peripheral, + const std::string& service_id, + const ByteArray& advertisement_bytes, + bool fast_advertisement) { + EXPECT_EQ(advertisement_bytes, ByteArray(std::string(kData))); + EXPECT_TRUE(fast_advertisement); + found_latch.CountDown(); + }, + .peripheral_lost_cb = + [&lost_latch](BlePeripheral& peripheral, + const std::string& service_id) { + lost_latch.CountDown(); + }, + }, + std::string(kFastAdvertisementServiceUuid)); + + api::ble_v2::BleAdvertisementData advertisement_data; + if (!advertisement_header_bytes.Empty()) { + advertisement_data.service_uuids.insert( + std::string(kCopresenceServiceUuid)); + advertisement_data.service_data.insert( + {std::string(kCopresenceServiceUuid), advertisement_header_bytes}); + } + if (!fast_advertisement_bytes.Empty()) { + advertisement_data.service_uuids.insert( + std::string(kFastAdvertisementServiceUuid)); + advertisement_data.service_data.insert( + {std::string(kFastAdvertisementServiceUuid), fast_advertisement_bytes}); + } + + // Start tracking a service ID and then process a loop of discoveries and + // onLost alarms. + for (int i = 0; i < 20; i++) { + FindFastAdvertisement(advertisement_data, {fast_advertisement_bytes}, + fetch_latch); + discovered_peripheral_tracker_.ProcessLostGattAdvertisements(); + } + + // We should only receive ONE client callback of a peripheral discovery, ZERO + // GATT reads, and no onLost calls. + fetch_latch.Await(kWaitDuration); + EXPECT_TRUE(found_latch.Await(kWaitDuration).result()); + EXPECT_EQ(GetFetchAdvertisementCallbackCount(), 0); + EXPECT_FALSE(lost_latch.Await(kWaitDuration).result()); +} + +TEST_F(DiscoveredPeripheralTrackerTest, LostPeripheralForAdvertisementLost) { + std::vector service_ids = {std::string(kServiceIdA)}; + ByteArray advertisement_header_bytes = CreateBleAdvertisementHeader( + GenerateRandomAdvertisementHash(), service_ids); + ByteArray advertisement_bytes = CreateBleAdvertisement( + std::string(kServiceIdA), ByteArray(std::string(kData)), + ByteArray(std::string(kDeviceToken))); + CountDownLatch found_latch(1); + CountDownLatch lost_latch(1); + CountDownLatch fetch_latch(1); + + discovered_peripheral_tracker_.StartTracking( + std::string(kServiceIdA), + { + .peripheral_discovered_cb = + [&found_latch](BlePeripheral& peripheral, + const std::string& service_id, + const ByteArray& advertisement_bytes, + bool fast_advertisement) { + EXPECT_EQ(advertisement_bytes, ByteArray(std::string(kData))); + EXPECT_FALSE(fast_advertisement); + found_latch.CountDown(); + }, + .peripheral_lost_cb = + [&lost_latch](BlePeripheral& peripheral, + const std::string& service_id) { + lost_latch.CountDown(); + }, + }, + ""); + + api::ble_v2::BleAdvertisementData advertisement_data; + if (!advertisement_header_bytes.Empty()) { + advertisement_data.service_uuids.insert( + std::string(kCopresenceServiceUuid)); + advertisement_data.service_data.insert( + {std::string(kCopresenceServiceUuid), advertisement_header_bytes}); + } + + FindAdvertisement(advertisement_data, {advertisement_bytes}, fetch_latch); + + // We should receive a client callback of a peripheral discovery. + fetch_latch.Await(kWaitDuration); + EXPECT_TRUE(found_latch.Await(kWaitDuration).result()); + EXPECT_EQ(GetFetchAdvertisementCallbackCount(), 1); + + // Then, go through two cycles of onLost. The first cycle should include the + // recently discovered eripheral in its 'found' pool. The second one should + // trigger the onLost callback. + discovered_peripheral_tracker_.ProcessLostGattAdvertisements(); + discovered_peripheral_tracker_.ProcessLostGattAdvertisements(); + + // We should receive a client callback of a lost peripheral + EXPECT_TRUE(lost_latch.Await(kWaitDuration).result()); +} + +TEST_F(DiscoveredPeripheralTrackerTest, + LostPeripheralForFastAndGattAdvertisementLost) { + std::vector service_ids = {std::string(kServiceIdB)}; + ByteArray advertisement_header_bytes = CreateBleAdvertisementHeader( + GenerateRandomAdvertisementHash(), service_ids); + ByteArray fast_advertisement_bytes = CreateFastBleAdvertisement( + ByteArray(std::string(kData)), ByteArray(std::string(kDeviceToken))); + ByteArray advertisement_bytes = CreateBleAdvertisement( + std::string(kServiceIdB), ByteArray(std::string(kData)), + ByteArray(std::string(kDeviceToken))); + CountDownLatch found_latch_a(1); + CountDownLatch found_latch_b(1); + CountDownLatch lost_latch_a(1); + CountDownLatch lost_latch_b(1); + CountDownLatch fetch_latch(1); + + discovered_peripheral_tracker_.StartTracking( + std::string(kServiceIdA), + { + .peripheral_discovered_cb = + [&found_latch_a](BlePeripheral& peripheral, + const std::string& service_id, + const ByteArray& advertisement_bytes, + bool fast_advertisement) { + EXPECT_EQ(advertisement_bytes, ByteArray(std::string(kData))); + EXPECT_TRUE(fast_advertisement); + found_latch_a.CountDown(); + }, + .peripheral_lost_cb = + [&lost_latch_a](BlePeripheral& peripheral, + const std::string& service_id) { + lost_latch_a.CountDown(); + }, + }, + std::string(kFastAdvertisementServiceUuid)); + discovered_peripheral_tracker_.StartTracking( + std::string(kServiceIdB), + { + .peripheral_discovered_cb = + [&found_latch_b](BlePeripheral& peripheral, + const std::string& service_id, + const ByteArray& advertisement_bytes, + bool fast_advertisement) { + EXPECT_EQ(advertisement_bytes, ByteArray(std::string(kData))); + EXPECT_FALSE(fast_advertisement); + found_latch_b.CountDown(); + }, + .peripheral_lost_cb = + [&lost_latch_b](BlePeripheral& peripheral, + const std::string& service_id) { + lost_latch_b.CountDown(); + }, + }, + ""); + + api::ble_v2::BleAdvertisementData advertisement_data; + if (!advertisement_header_bytes.Empty()) { + advertisement_data.service_uuids.insert( + std::string(kCopresenceServiceUuid)); + advertisement_data.service_data.insert( + {std::string(kCopresenceServiceUuid), advertisement_header_bytes}); + } + if (!fast_advertisement_bytes.Empty()) { + advertisement_data.service_uuids.insert( + std::string(kFastAdvertisementServiceUuid)); + advertisement_data.service_data.insert( + {std::string(kFastAdvertisementServiceUuid), fast_advertisement_bytes}); + } + + FindFastAdvertisement(advertisement_data, {advertisement_bytes}, fetch_latch); + + // We should receive two separate peripheral discoveries. + fetch_latch.Await(kWaitDuration); + EXPECT_TRUE(found_latch_a.Await(kWaitDuration).result()); + EXPECT_TRUE(found_latch_b.Await(kWaitDuration).result()); + EXPECT_EQ(GetFetchAdvertisementCallbackCount(), 1); + + // Then, go through two cycles of onLost. The first cycle should include the + // recently discovered peripheral in its 'found' pool. The second one should + // trigger the onLost callback. + discovered_peripheral_tracker_.ProcessLostGattAdvertisements(); + discovered_peripheral_tracker_.ProcessLostGattAdvertisements(); + + // We should receive two client callbacks of a lost peripheral from each + // service ID. + EXPECT_TRUE(lost_latch_a.Await(kWaitDuration).result()); + EXPECT_TRUE(lost_latch_b.Await(kWaitDuration).result()); +} + +TEST_F(DiscoveredPeripheralTrackerTest, + LostPeripheralNotCallbackForUntrackedServiceId) { + std::vector service_ids = {std::string(kServiceIdA)}; + ByteArray advertisement_header_bytes = CreateBleAdvertisementHeader( + GenerateRandomAdvertisementHash(), service_ids); + ByteArray advertisement_bytes = CreateBleAdvertisement( + std::string(kServiceIdA), ByteArray(std::string(kData)), + ByteArray(std::string(kDeviceToken))); + CountDownLatch found_latch(1); + CountDownLatch lost_latch(1); + CountDownLatch fetch_latch(1); + + discovered_peripheral_tracker_.StartTracking( + std::string(kServiceIdA), + { + .peripheral_discovered_cb = + [&found_latch](BlePeripheral& peripheral, + const std::string& service_id, + const ByteArray& advertisement_bytes, + bool fast_advertisement) { + EXPECT_EQ(advertisement_bytes, ByteArray(std::string(kData))); + EXPECT_FALSE(fast_advertisement); + found_latch.CountDown(); + }, + .peripheral_lost_cb = + [&lost_latch](BlePeripheral& peripheral, + const std::string& service_id) { + lost_latch.CountDown(); + }, + }, + ""); + + api::ble_v2::BleAdvertisementData advertisement_data; + if (!advertisement_header_bytes.Empty()) { + advertisement_data.service_uuids.insert( + std::string(kCopresenceServiceUuid)); + advertisement_data.service_data.insert( + {std::string(kCopresenceServiceUuid), advertisement_header_bytes}); + } + + FindAdvertisement(advertisement_data, {advertisement_bytes}, fetch_latch); + + // We should receive a client callback of a peripheral discovery. + fetch_latch.Await(kWaitDuration); + EXPECT_TRUE(found_latch.Await(kWaitDuration).result()); + EXPECT_EQ(GetFetchAdvertisementCallbackCount(), 1); + + // Then, stop tracking the service ID and go through two cycles of onLost. + discovered_peripheral_tracker_.StopTracking(std::string(kServiceIdA)); + discovered_peripheral_tracker_.ProcessLostGattAdvertisements(); + discovered_peripheral_tracker_.ProcessLostGattAdvertisements(); + + // We should NOT receive a client callback of a lost peripheral + EXPECT_FALSE(lost_latch.Await(kWaitDuration).result()); +} + +} // namespace + +} // namespace mediums +} // namespace connections +} // namespace nearby +} // namespace location