diff --git a/presence/data_types.h b/presence/data_types.h index 7deaa8a8..76c790ac 100644 --- a/presence/data_types.h +++ b/presence/data_types.h @@ -24,13 +24,24 @@ namespace nearby { namespace presence { // Holds the callback of stop scan for client to invoke later. -struct ScanSession { +// TODO(b/254895067) Rework status for absl::Status +class ScanSession { + public: + ScanSession() + : stop_scan_callback_( + []() { return Status{Status::Value::kNotImplemented}; }) {} + explicit ScanSession(std::function stop_scan_callback) + : stop_scan_callback_(stop_scan_callback) {} + + Status StopScan() { + return stop_scan_callback_(); + } + + private: // Nearby library would provide the implementation of this callback in // runtime. Assigning with a default value NotImplemented to surface potential // issue where library failed to provide the implementation. - std::function stop_scan_callback = []() { - return Status{Status::Value::kNotImplemented}; - }; + std::function stop_scan_callback_; }; // Callers would provide the implementation of these callbacks. If callers diff --git a/presence/implementation/BUILD b/presence/implementation/BUILD index 25fbb722..aa5f11e9 100644 --- a/presence/implementation/BUILD +++ b/presence/implementation/BUILD @@ -24,6 +24,7 @@ cc_library( "encryption.cc", "ldt.cc", "np_ldt.c", + "scan_manager.cc", "service_controller_impl.cc", ], hdrs = [ @@ -58,10 +59,14 @@ cc_library( "//presence/implementation/mediums", "//third_party/tink/cc/subtle", "@boringssl//:crypto", + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/random", + "@com_google_absl//absl/random:distributions", "@com_google_absl//absl/status", "@com_google_absl//absl/status:statusor", "@com_google_absl//absl/strings", "@com_google_absl//absl/strings:str_format", + "@com_google_absl//absl/synchronization", "@com_google_absl//absl/time", "@com_google_absl//absl/types:optional", "@com_google_absl//absl/types:variant", @@ -184,3 +189,20 @@ cc_test( "@com_google_googletest//:gtest_main", ], ) + +cc_test( + name = "scan_manager_test", + size = "small", + srcs = ["scan_manager_test.cc"], + deps = [ + ":internal", + "//internal/platform:comm", + "//internal/platform:test_util", + "//internal/platform:types", + "//internal/platform/implementation/g3", # build_cleaner: keep + "//presence/implementation/mediums", + "@com_github_protobuf_matchers//protobuf-matchers", + "@com_google_absl//absl/random", + "@com_google_googletest//:gtest_main", + ], +) diff --git a/presence/implementation/advertisement_factory.cc b/presence/implementation/advertisement_factory.cc index 58d1e58c..4613901a 100644 --- a/presence/implementation/advertisement_factory.cc +++ b/presence/implementation/advertisement_factory.cc @@ -24,6 +24,7 @@ #include "internal/proto/credential.pb.h" #include "presence/data_element.h" #include "presence/implementation/base_broadcast_request.h" +#include "presence/implementation/mediums/ble.h" namespace nearby { namespace presence { @@ -31,9 +32,7 @@ namespace presence { using ::nearby::internal::IdentityType; namespace { - constexpr uint8_t kBaseVersion = 0; -constexpr location::nearby::Uuid kServiceData(0xFCF1ULL << 32, 0); constexpr size_t kMaxBaseNpAdvSize = 26; absl::StatusOr CreateDataElementHeader(size_t length, @@ -175,7 +174,7 @@ AdvertisementFactory::CreateBaseNpAdvertisement( } } advert.service_data.insert( - {kServiceData, location::nearby::ByteArray(payload)}); + {kPresenceServiceUuid, location::nearby::ByteArray(payload)}); return advert; } diff --git a/presence/implementation/mediums/mediums.h b/presence/implementation/mediums/mediums.h index a8aa78e4..8d557225 100644 --- a/presence/implementation/mediums/mediums.h +++ b/presence/implementation/mediums/mediums.h @@ -28,7 +28,7 @@ namespace presence { class Mediums { public: // Returns a handle to the Ble medium. - Ble& GetBle(); + Ble& GetBle() { return ble_; } private: location::nearby::BluetoothAdapter adapter_; diff --git a/presence/implementation/scan_manager.cc b/presence/implementation/scan_manager.cc new file mode 100644 index 00000000..5c26eba3 --- /dev/null +++ b/presence/implementation/scan_manager.cc @@ -0,0 +1,117 @@ +// 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 "presence/implementation/scan_manager.h" + +#include +#include +#include +#include +#include + +#include "absl/random/random.h" +#include "absl/random/uniform_int_distribution.h" +#include "absl/types/variant.h" +#include "internal/platform/implementation/ble_v2.h" +#include "internal/platform/uuid.h" +#include "presence/data_types.h" +#include "presence/implementation/advertisement_decoder.h" +#include "presence/implementation/mediums/ble.h" +#include "presence/presence_device.h" +#include "presence/scan_request.h" +#include "presence/status.h" + +namespace { +using BleAdvertisementData = + ::location::nearby::api::ble_v2::BleAdvertisementData; +using BlePeripheral = ::location::nearby::api::ble_v2::BlePeripheral; +using BleOperationStatus = ::location::nearby::api::ble_v2::BleOperationStatus; +using ScanningSession = + ::location::nearby::api::ble_v2::BleMedium::ScanningSession; +using ScanningCallback = + ::location::nearby::api::ble_v2::BleMedium::ScanningCallback; +} // namespace + +namespace nearby { +namespace presence { + +ScanSession ScanManager::StartScan(ScanRequest scan_request, ScanCallback cb) { + absl::BitGen gen; + uint64_t id = absl::uniform_int_distribution(0, UINT64_MAX)(gen); + ScanningCallback callback = ScanningCallback{ + .start_scanning_result = + [start_scan_client = + std::move(cb.start_scan_cb)](BleOperationStatus ble_status) { + Status status; + if (ble_status == BleOperationStatus::kSucceeded) { + status = Status{.value = Status::Value::kSuccess}; + } else { + status = Status{.value = Status::Value::kError}; + } + start_scan_client(status); + }, + // TODO(b/256686710): Track known devices + .advertisement_found_cb = + [this](BlePeripheral& peripheral, BleAdvertisementData data) { + NotifyFoundBle(data, peripheral); + }}; + std::unique_ptr scanning_session = + mediums_->GetBle().StartScanning(scan_request, std::move(callback)); + auto modified_scanning_session = ScanSession( + [scanning_session_cb = std::move(scanning_session->stop_scanning), this, + id]() { + absl::MutexLock lock(&mutex_); + int erased = absl::erase_if( + scanning_callbacks_, + [id](const auto& entry) { return id == entry.first; }); + if (erased == 0) return Status{.value = Status::Value::kError}; + BleOperationStatus st = scanning_session_cb(); + if (st != BleOperationStatus::kSucceeded) { + return Status{.value = Status::Value::kError}; + } + return Status{.value = Status::Value::kSuccess}; + }); + absl::MutexLock lock(&mutex_); + // We will not be needing the start_scan_cb anymore, so cb is ok to use here. + scanning_callbacks_.emplace(id, MapElement{ + .request = scan_request, + .callback = cb, + .decoder = AdvertisementDecoder( + credential_manager_, scan_request), + }); + return modified_scanning_session; +} + +void ScanManager::NotifyFoundBle(BleAdvertisementData data, + const BlePeripheral& peripheral) { + absl::MutexLock lock(&mutex_); + auto advertisement_data = + data.service_data[kPresenceServiceUuid].AsStringView(); + for (const auto& entry : scanning_callbacks_) { + auto candidate = entry.second; + auto advert = candidate.decoder.DecodeAdvertisement(advertisement_data); + if (!advert.ok()) { + // This advertisement is not relevant to the current element, skip. + continue; + } + if (candidate.decoder.MatchesScanFilter(advert.value())) { + // TODO(b/256913915): Provide more information in PresenceDevice once + // fully implemented + candidate.callback.on_discovered_cb(PresenceDevice()); + } + } +} + +} // namespace presence +} // namespace nearby diff --git a/presence/implementation/scan_manager.h b/presence/implementation/scan_manager.h index affb91b1..e1d731c0 100644 --- a/presence/implementation/scan_manager.h +++ b/presence/implementation/scan_manager.h @@ -15,8 +15,18 @@ #ifndef THIRD_PARTY_NEARBY_PRESENCE_IMPLEMENTATION_SCAN_MANAGER_H_ #define THIRD_PARTY_NEARBY_PRESENCE_IMPLEMENTATION_SCAN_MANAGER_H_ +#include +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/synchronization/mutex.h" +#include "presence/data_types.h" +#include "presence/implementation/advertisement_decoder.h" #include "presence/implementation/credential_manager.h" #include "presence/implementation/mediums/mediums.h" +#include "presence/scan_request.h" namespace nearby { namespace presence { @@ -32,9 +42,29 @@ class ScanManager { } ~ScanManager() = default; + ScanSession StartScan(ScanRequest scan_request, ScanCallback cb) + ABSL_LOCKS_EXCLUDED(mutex_); + // Below functions are test only. + // Reference: go/totw/135#augmenting-the-public-api-for-tests + int ScanningCallbacksLengthForTest() ABSL_LOCKS_EXCLUDED(mutex_) { + absl::MutexLock lock(&mutex_); + return scanning_callbacks_.size(); + } + private: + struct MapElement { + ScanRequest request; + ScanCallback callback; + AdvertisementDecoder decoder; + }; + mutable absl::Mutex mutex_; Mediums* mediums_; CredentialManager* credential_manager_; + absl::flat_hash_map scanning_callbacks_ + ABSL_GUARDED_BY(mutex_); + void NotifyFoundBle( + location::nearby::api::ble_v2::BleAdvertisementData data, + const location::nearby::api::ble_v2::BlePeripheral& peripheral); }; } // namespace presence diff --git a/presence/implementation/scan_manager_test.cc b/presence/implementation/scan_manager_test.cc new file mode 100644 index 00000000..e943804e --- /dev/null +++ b/presence/implementation/scan_manager_test.cc @@ -0,0 +1,196 @@ +// 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 "presence/implementation/scan_manager.h" + +#include + +#include +#include +#include +#include + +#include "gtest/gtest.h" +#include "absl/random/random.h" +#include "internal/platform/bluetooth_adapter.h" +#include "internal/platform/count_down_latch.h" +#include "internal/platform/medium_environment.h" +#include "presence/implementation/advertisement_factory.h" +#include "presence/implementation/base_broadcast_request.h" +#include "presence/implementation/credential_manager_impl.h" +#include "presence/implementation/mediums/ble.h" +#include "presence/implementation/mediums/mediums.h" + +namespace nearby { +namespace presence { +namespace { + +using BleOperationStatus = location::nearby::api::ble_v2::BleOperationStatus; + +class ScanManagerTest : public testing::Test { + protected: + void SetUp() override { env_.Start(); } + void TearDown() override { env_.Stop(); } + std::vector identity_types_ = { + nearby::internal::IdentityType::IDENTITY_TYPE_PUBLIC, + }; + std::vector extended_properties_ = { + DataElement(ActionBit::kPresenceManagerAction)}; + std::vector> + filters_ = {PresenceScanFilter{ + .scan_type = ScanType::kPresenceScan, + .extended_properties = extended_properties_, + }}; + ScanRequest scan_request_ = { + .account_name = "Test account", + .identity_types = identity_types_, + .scan_filters = filters_, + .use_ble = true, + .scan_type = ScanType::kPresenceScan, + .power_mode = PowerMode::kBalanced, + .scan_only_when_screen_on = true, + }; + CredentialManagerImpl credential_manager_; + location::nearby::MediumEnvironment& env_ = { + location::nearby::MediumEnvironment::Instance()}; + location::nearby::api::ble_v2::AdvertiseParameters params_ = { + .tx_power_level = Ble::TxPowerLevel::kHigh, + .is_connectable = true, + }; + location::nearby::CountDownLatch start_latch_{1}; + location::nearby::CountDownLatch found_latch_{1}; +}; + +TEST_F(ScanManagerTest, CanStartThenStopScanning) { + Mediums mediums; + ScanManager manager(mediums, credential_manager_); + ScanCallback scanning_callback = { + .start_scan_cb = + [this](Status status) { + if (status.Ok()) { + start_latch_.CountDown(); + } + }, + .on_discovered_cb = + [this](PresenceDevice pd) { found_latch_.CountDown(); }}; + // Set up advertiser + // Create BroadcastRequest + PresenceBroadcast::BroadcastSection section = { + .identity = internal::IDENTITY_TYPE_PUBLIC, + .extended_properties = extended_properties_, + .account_name = "Test account"}; + PresenceBroadcast presence_request = {.sections = {section}}; + BroadcastRequest input = {.tx_power = 30, .variant = presence_request}; + + absl::StatusOr request = + BaseBroadcastRequest::Create(input); + AdvertisementFactory factory(&credential_manager_); + absl::StatusOr advertisement = + factory.CreateAdvertisement(request.ValueOrDie()); + location::nearby::BluetoothAdapter adapter2; + location::nearby::BleV2Medium ble2(adapter2); + ASSERT_TRUE(ble2.StartAdvertising(advertisement.ValueOrDie(), params_)); + + // Start scanning + ScanSession scan_session = + manager.StartScan(scan_request_, std::move(scanning_callback)); + // Ensure that we are in a good state. + env_.Sync(); + EXPECT_EQ(manager.ScanningCallbacksLengthForTest(), 1); + ASSERT_TRUE(ble2.IsValid()); + ASSERT_TRUE(mediums.GetBle().IsAvailable()); + EXPECT_TRUE(start_latch_.Await(absl::Milliseconds(500)).result()); + EXPECT_TRUE(found_latch_.Await(absl::Milliseconds(1500)).result()); + EXPECT_TRUE(scan_session.StopScan().Ok()); + EXPECT_EQ(manager.ScanningCallbacksLengthForTest(), 0); +} + +TEST_F(ScanManagerTest, CannotStopScanTwice) { + Mediums mediums; + ScanManager manager(mediums, credential_manager_); + ScanCallback scanning_callback = ScanCallback{ + .start_scan_cb = + [this](Status status) { + if (status.Ok()) { + start_latch_.CountDown(); + } + }, + }; + + auto scan_session = + manager.StartScan(scan_request_, std::move(scanning_callback)); + NEARBY_LOGS(INFO) << "Start scan"; + EXPECT_TRUE(start_latch_.Await(absl::Milliseconds(1000)).result()); + // Ensure that we have started scanning before we try to stop. + env_.Sync(); + NEARBY_LOGS(INFO) << "Stop scan"; + EXPECT_TRUE(scan_session.StopScan().Ok()); + NEARBY_LOGS(INFO) << "Stop scan again"; + EXPECT_FALSE(scan_session.StopScan().Ok()); +} + +TEST_F(ScanManagerTest, TestNoFilter) { + Mediums mediums; + ScanManager manager(mediums, credential_manager_); + ScanCallback scanning_callback = { + .start_scan_cb = + [this](Status status) { + if (status.Ok()) { + start_latch_.CountDown(); + } + }, + .on_discovered_cb = + [this](PresenceDevice pd) { found_latch_.CountDown(); }}; + // Set up advertiser + // Create BroadcastRequest + PresenceBroadcast::BroadcastSection section = { + .identity = internal::IDENTITY_TYPE_PUBLIC, + .extended_properties = {}, + .account_name = "Test account"}; + PresenceBroadcast presence_request = {.sections = {section}}; + BroadcastRequest input = {.tx_power = 30, .variant = presence_request}; + + absl::StatusOr request = + BaseBroadcastRequest::Create(input); + AdvertisementFactory factory(&credential_manager_); + absl::StatusOr advertisement = + factory.CreateAdvertisement(request.ValueOrDie()); + location::nearby::BluetoothAdapter adapter2; + location::nearby::BleV2Medium ble2(adapter2); + ASSERT_TRUE(ble2.StartAdvertising(advertisement.ValueOrDie(), params_)); + + // Start scanning + ScanRequest scan_request = { + .account_name = "Test account", + .identity_types = identity_types_, + .scan_filters = {}, + .use_ble = true, + .scan_type = ScanType::kPresenceScan, + .power_mode = PowerMode::kBalanced, + .scan_only_when_screen_on = true, + }; + ScanSession scan_session = + manager.StartScan(scan_request, std::move(scanning_callback)); + + EXPECT_EQ(manager.ScanningCallbacksLengthForTest(), 1); + ASSERT_TRUE(ble2.IsValid()); + ASSERT_TRUE(mediums.GetBle().IsAvailable()); + EXPECT_TRUE(start_latch_.Await(absl::Milliseconds(500)).result()); + EXPECT_TRUE(found_latch_.Await(absl::Milliseconds(1500)).result()); + EXPECT_TRUE(scan_session.StopScan().Ok()); + EXPECT_EQ(manager.ScanningCallbacksLengthForTest(), 0); +} +} // namespace +} // namespace presence +} // namespace nearby diff --git a/presence/implementation/service_controller_impl.h b/presence/implementation/service_controller_impl.h index 0fe978e7..6e67abb5 100644 --- a/presence/implementation/service_controller_impl.h +++ b/presence/implementation/service_controller_impl.h @@ -21,6 +21,7 @@ #include "presence/data_types.h" #include "presence/implementation/credential_manager_impl.h" #include "presence/implementation/mediums/mediums.h" +#include "presence/implementation/scan_manager.h" #include "presence/implementation/service_controller.h" #include "presence/scan_request.h" /* @@ -42,6 +43,8 @@ class ServiceControllerImpl : public ServiceController { Mediums mediums_; // NOLINT: further impl will use it. CredentialManagerImpl credential_manager_; // NOLINT: further impl will use it. + ScanManager scan_manager_{ + mediums_, credential_manager_}; // NOLINT: further impl will use it. }; } // namespace presence