diff --git a/fastpair/internal/ble/BUILD b/fastpair/internal/ble/BUILD new file mode 100644 index 00000000..2fe38e54 --- /dev/null +++ b/fastpair/internal/ble/BUILD @@ -0,0 +1,59 @@ +# 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. + +licenses(["notice"]) + +cc_library( + name = "ble", + srcs = [ + "ble.cc", + ], + hdrs = [ + "ble.h", + ], + visibility = [ + "//fastpair:__subpackages__", + ], + deps = [ + "//internal/platform:base", + "//internal/platform:comm", + "//internal/platform:logging", + "//internal/platform:types", + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/time", + ], +) + +cc_test( + name = "ble_test", + size = "small", + srcs = [ + "ble_test.cc", + ], + shard_count = 16, + deps = [ + ":ble", + "//internal/platform:base", + "//internal/platform:comm", + "//internal/platform:test_util", + "//internal/platform:types", + "//internal/platform/implementation/g3", # build_cleaner: keep + "@com_github_protobuf_matchers//protobuf-matchers", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/time", + "@com_google_googletest//:gtest_main", + ], +) diff --git a/fastpair/internal/ble/ble.cc b/fastpair/internal/ble/ble.cc new file mode 100644 index 00000000..8186b3c3 --- /dev/null +++ b/fastpair/internal/ble/ble.cc @@ -0,0 +1,208 @@ +// 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 "fastpair/internal/ble/ble.h" + +#include +#include +#include + +#include "internal/platform/exception.h" +#include "internal/platform/logging.h" +#include "internal/platform/system_clock.h" + +namespace location { +namespace nearby { +namespace fastpair { + +constexpr absl::Duration Ble::kPauseBetweenToggle; + +Ble::~Ble() { + // We never enabled Bluetooth, nothing to do. + if (!ever_saved_state_.Get()) { + NEARBY_LOG(INFO, "BT adapter was not used. Not touching HW."); + return; + } + + NEARBY_LOG(INFO, "Bring BT adapter to original state"); + if (!SetBluetoothState(originally_enabled_.Get())) { + NEARBY_LOG(INFO, "Failed to restore BT adapter original state."); + } +} + +bool Ble::Enable() { + if (!SaveOriginalState()) { + return false; + } + + return SetBluetoothState(true); +} + +bool Ble::Disable() { + if (!SaveOriginalState()) { + return false; + } + + return SetBluetoothState(false); +} + +bool Ble::IsEnabled() const { + return IsAdapterValid() && IsInDesiredState(true); +} + +bool Ble::Toggle() { + if (!SaveOriginalState()) { + return false; + } + + if (!SetBluetoothState(false)) { + NEARBY_LOG(INFO, "BT Toggle: Failed to turn BT off."); + return false; + } + + if (SystemClock::Sleep(kPauseBetweenToggle).Raised(Exception::kInterrupted)) { + NEARBY_LOG(INFO, "BT Toggle: interrupted before turing on."); + return false; + } + + if (!SetBluetoothState(true)) { + NEARBY_LOG(INFO, "BT Toggle: Failed to turn BT on."); + return false; + } + + return true; +} + +bool Ble::SetBluetoothState(bool enable) { + return adapter_.SetStatus(enable ? BluetoothAdapter::Status::kEnabled + : BluetoothAdapter::Status::kDisabled); +} + +bool Ble::IsInDesiredState(bool should_be_enabled) const { + return adapter_.IsEnabled() == should_be_enabled; +} + +bool Ble::SaveOriginalState() { + if (!IsAdapterValid()) { + return false; + } + + // If we haven't saved the original state of the radio, save it. + if (!ever_saved_state_.Set(true)) { + originally_enabled_.Set(adapter_.IsEnabled()); + } + + return true; +} + +bool Ble::IsAvailable() const { + MutexLock lock(&mutex_); + return IsAvailableLocked(); +} + +bool Ble::IsAvailableLocked() const { + return medium_.IsValid() && adapter_.IsValid() && adapter_.IsEnabled(); +} + +bool Ble::StartScanning(const std::string& service_id, + const std::string& fast_pair_service_uuid, + DiscoveredPeripheralCallback callback) { + MutexLock lock(&mutex_); + + discovered_peripheral_callback_ = std::move(callback); + + if (service_id.empty()) { + NEARBY_LOGS(INFO) + << "Refusing to start BLE scanning with empty service id."; + return false; + } + + if (IsScanningLocked()) { + NEARBY_LOGS(INFO) << "Refusing to start BLE scanning because " + "another scanning is already in-progress."; + return false; + } + + if (!IsEnabled()) { + NEARBY_LOGS(INFO) + << "Can't start BLE scanning because Bluetooth was NOT enabled"; + return false; + } + + if (!IsAvailableLocked()) { + NEARBY_LOGS(INFO) << "Can't scan BLE scanning because BLE isn't available."; + return false; + } + + if (!medium_.StartScanning( + service_id, fast_pair_service_uuid, + { + .peripheral_discovered_cb = + [this](BlePeripheral& peripheral, + const std::string& service_id, + const ByteArray& medium_advertisement_bytes, + bool fast_advertisement) { + // Don't bother trying to parse zero byte advertisements. + if (medium_advertisement_bytes.size() == 0) { + NEARBY_LOGS(INFO) << "Skipping zero byte advertisement " + << "with service_id: " << service_id; + return; + } + discovered_peripheral_callback_.peripheral_discovered_cb( + peripheral, service_id, medium_advertisement_bytes, + fast_advertisement); + }, + .peripheral_lost_cb = + [this](BlePeripheral& peripheral, + const std::string& service_id) { + discovered_peripheral_callback_.peripheral_lost_cb( + peripheral, service_id); + }, + })) { + NEARBY_LOGS(INFO) << "Failed to start BLE scanning"; + return false; + } + + NEARBY_LOGS(INFO) << "Start BLE scanning with service id=" << service_id; + // Mark the fact that we're currently performing a BLE scanning. + is_scanning_ = true; + return true; +} + +bool Ble::StopScanning(const std::string& service_id) { + MutexLock lock(&mutex_); + + if (!IsScanningLocked()) { + NEARBY_LOGS(INFO) << "Can't stop BLE scanning because we never " + "started scanning."; + return false; + } + + NEARBY_LOG(INFO, "Stop BLE scanning with service id=%s", service_id.c_str()); + bool ret = medium_.StopScanning(service_id); + is_scanning_ = false; + return ret; +} + +bool Ble::IsScanning() { + MutexLock lock(&mutex_); + + return IsScanningLocked(); +} + +bool Ble::IsScanningLocked() { return is_scanning_; } + +} // namespace fastpair +} // namespace nearby +} // namespace location diff --git a/fastpair/internal/ble/ble.h b/fastpair/internal/ble/ble.h new file mode 100644 index 00000000..46452ddb --- /dev/null +++ b/fastpair/internal/ble/ble.h @@ -0,0 +1,130 @@ +// 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 THIRD_PARTY_NEARBY_FASTPAIR_INTERNAL_BLE_BLE_H_ +#define THIRD_PARTY_NEARBY_FASTPAIR_INTERNAL_BLE_BLE_H_ + +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" +#include "absl/time/clock.h" +#include "internal/platform/atomic_boolean.h" +#include "internal/platform/ble.h" +#include "internal/platform/bluetooth_adapter.h" +#include "internal/platform/multi_thread_executor.h" +#include "internal/platform/mutex.h" + +namespace location { +namespace nearby { +namespace fastpair { + +class Ble { + public: + using DiscoveredPeripheralCallback = BleMedium::DiscoveredPeripheralCallback; + + Ble() = default; + Ble(Ble&&) = default; + Ble& operator=(Ble&&) = default; + + // Reverts the Ble to its original state. + ~Ble(); + + // Enables Bluetooth. Returns true if enabled successfully. + // This must be called before attempting to invoke any other methods of + // this class. + bool Enable(); + + // Disables Bluetooth. Returns true if disabled successfully. + bool Disable(); + + // Returns true if the Bluetooth radio is currently enabled. + bool IsEnabled() const; + + // Turn BT radio Off, delay for kPauseBetweenToggle and then turn it On. + // This will block calling thread for at least kPauseBetweenToggle duration. + bool Toggle(); + + // Returns true if Ble communications are supported by a platform. + bool IsAvailable() const ABSL_LOCKS_EXCLUDED(mutex_); + + // Returns true if this object owns a valid platform implementation. + bool IsMediumValid() const ABSL_LOCKS_EXCLUDED(mutex_) { + MutexLock lock(&mutex_); + return medium_.IsValid(); + } + + // Returns true if this object has a valid BluetoothAdapter reference. + bool IsAdapterValid() const { return adapter_.IsValid(); } + + // Return true if Ble is currenlty scanning. + bool IsScanning() ABSL_LOCKS_EXCLUDED(mutex_); + + // Enables Ble scanning mode. Will report any discoverable peripherals in + // range through a callback. Returns true, if scanning mode was enabled, + // false otherwise. + bool StartScanning(const std::string& service_id, + const std::string& fast_pair_service_uuid, + DiscoveredPeripheralCallback callback) + ABSL_LOCKS_EXCLUDED(mutex_); + + // Disables Ble discovery mode. + bool StopScanning(const std::string& service_id) ABSL_LOCKS_EXCLUDED(mutex_); + + // Return BleMedium + BleMedium& getMedium() { return medium_; } + + // Return BluetoothAdapter + BluetoothAdapter& GetBluetoothAdapter() { return adapter_; } + + private: + mutable Mutex mutex_; + // BluetoothAdapter::IsValid() will return false if BT is not supported. + BluetoothAdapter adapter_; + DiscoveredPeripheralCallback discovered_peripheral_callback_; + BleMedium medium_ ABSL_GUARDED_BY(mutex_){adapter_}; + bool is_scanning_ = false; + static constexpr absl::Duration kPauseBetweenToggle = absl::Seconds(3); + + // Same as IsAvailable(), but must be called with mutex_ held. + bool IsAvailableLocked() const ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_); + + // Same as IsDiscovering(), but must be called with mutex_ held. + bool IsScanningLocked() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_); + + bool SetBluetoothState(bool enable); + + bool IsInDesiredState(bool should_be_enabled) const; + + // To be called in enable(), disable(), and toggle(). This will remember the + // original state of the ble before any ble state has been modified. + // Returns false if Bluetooth doesn't exist on the device and the state cannot + // be obtained. + bool SaveOriginalState(); + + // The Ble's original state, before we modified it. True if + // originally enabled, false if originally disabled. + // We restore the radio to its original state in the destructor. + AtomicBoolean originally_enabled_{false}; + + // false if we never modified the radio state, true otherwise. + AtomicBoolean ever_saved_state_{false}; +}; + +} // namespace fastpair +} // namespace nearby +} // namespace location + +#endif // THIRD_PARTY_NEARBY_FASTPAIR_INTERNAL_BLE_BLE_H_ diff --git a/fastpair/internal/ble/ble_test.cc b/fastpair/internal/ble/ble_test.cc new file mode 100644 index 00000000..34983a0b --- /dev/null +++ b/fastpair/internal/ble/ble_test.cc @@ -0,0 +1,129 @@ +// 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 "fastpair/internal/ble/ble.h" + +#include + +#include "gtest/gtest.h" +#include "internal/platform/ble.h" +#include "internal/platform/count_down_latch.h" +#include "internal/platform/medium_environment.h" + +namespace location { +namespace nearby { +namespace fastpair { +namespace { + +using FeatureFlags = FeatureFlags::Flags; + +constexpr absl::Duration kWaitDuration = absl::Milliseconds(1000); +constexpr absl::string_view kServiceID{"com.google.location.nearby.apps.test"}; +constexpr absl::string_view kAdvertisementString{"96C12E"}; +constexpr absl::string_view kFastPairServiceUuid{"\x2c\xfe"}; + +class BleTest : public ::testing::TestWithParam { + protected: + using DiscoveredPeripheralCallback = BleMedium::DiscoveredPeripheralCallback; + + MediumEnvironment& env_{MediumEnvironment::Instance()}; +}; + +TEST_F(BleTest, ConstructorDestructorWorks) { + Ble ble; + EXPECT_TRUE(ble.IsAdapterValid()); +} + +TEST_F(BleTest, CanEnable) { + Ble ble; + EXPECT_TRUE(ble.IsAdapterValid()); + EXPECT_FALSE(ble.IsEnabled()); + EXPECT_TRUE(ble.Enable()); + EXPECT_TRUE(ble.IsEnabled()); +} + +TEST_F(BleTest, CanDisable) { + Ble ble; + EXPECT_TRUE(ble.IsAdapterValid()); + EXPECT_FALSE(ble.IsEnabled()); + EXPECT_TRUE(ble.Enable()); + EXPECT_TRUE(ble.IsEnabled()); + EXPECT_TRUE(ble.Disable()); + EXPECT_FALSE(ble.IsEnabled()); +} + +TEST_F(BleTest, CanToggle) { + Ble ble; + EXPECT_TRUE(ble.IsAdapterValid()); + EXPECT_FALSE(ble.IsEnabled()); + EXPECT_TRUE(ble.Toggle()); + EXPECT_TRUE(ble.IsEnabled()); +} + +TEST_F(BleTest, CanConstructValidObject) { + env_.Start(); + Ble ble_a; + Ble ble_b; + + EXPECT_TRUE(ble_a.IsMediumValid()); + EXPECT_TRUE(ble_a.IsAdapterValid()); + EXPECT_FALSE(ble_a.IsAvailable()); + EXPECT_TRUE(ble_b.IsMediumValid()); + EXPECT_TRUE(ble_b.IsAdapterValid()); + EXPECT_FALSE(ble_b.IsAvailable()); + EXPECT_NE(&ble_a.GetBluetoothAdapter(), &ble_b.GetBluetoothAdapter()); + env_.Stop(); +} + +TEST_F(BleTest, CanStartDiscovery) { + env_.Start(); + Ble ble_a; + Ble ble_b; + ble_a.Enable(); + ble_b.Enable(); + std::string service_id(kServiceID); + ByteArray advertisement_bytes{std::string(kAdvertisementString)}; + std::string fast_pair_service_uuid(kFastPairServiceUuid); + CountDownLatch accept_latch(1); + CountDownLatch lost_latch(1); + ble_b.getMedium().StartAdvertising(service_id, advertisement_bytes, + fast_pair_service_uuid); + + EXPECT_TRUE(ble_a.StartScanning( + service_id, fast_pair_service_uuid, + DiscoveredPeripheralCallback{ + .peripheral_discovered_cb = + [&accept_latch]( + BlePeripheral& peripheral, const std::string& service_id, + const ByteArray& advertisement_bytes, + bool fast_advertisement) { accept_latch.CountDown(); }, + .peripheral_lost_cb = + [&lost_latch](BlePeripheral& peripheral, + const std::string& service_id) { + lost_latch.CountDown(); + }, + })); + EXPECT_TRUE(ble_a.IsScanning()); + EXPECT_TRUE(accept_latch.Await(kWaitDuration).result()); + ble_b.getMedium().StopAdvertising(service_id); + EXPECT_TRUE(lost_latch.Await(kWaitDuration).result()); + EXPECT_TRUE(ble_a.StopScanning(service_id)); + EXPECT_FALSE(ble_a.IsScanning()); + env_.Stop(); +} + +} // namespace +} // namespace fastpair +} // namespace nearby +} // namespace location diff --git a/internal/platform/BUILD b/internal/platform/BUILD index b7a67d87..35d4e12e 100644 --- a/internal/platform/BUILD +++ b/internal/platform/BUILD @@ -46,6 +46,7 @@ cc_library( defines = ["NO_WEBRTC"], visibility = [ "//connections:__subpackages__", + "//fastpair:__subpackages__", "//internal:__pkg__", "//internal/analytics:__subpackages__", "//internal/platform:__subpackages__", @@ -102,6 +103,7 @@ cc_library( defines = ["NO_WEBRTC"], visibility = [ "//connections:__subpackages__", + "//fastpair:__subpackages__", "//googlemac/iPhone/Shared/Nearby/Connections:__subpackages__", "//internal/analytics:__subpackages__", "//internal/crypto:__subpackages__", @@ -194,6 +196,7 @@ cc_library( defines = ["NO_WEBRTC"], visibility = [ "//connections/implementation:__subpackages__", + "//fastpair:__subpackages__", "//internal/platform:__pkg__", "//internal/platform/implementation:__subpackages__", "//presence:__subpackages__", @@ -216,6 +219,7 @@ cc_library( defines = ["NO_WEBRTC"], visibility = [ "//connections:__subpackages__", + "//fastpair:__subpackages__", "//internal/platform:__pkg__", "//internal/platform/implementation:__subpackages__", "//presence:__subpackages__", @@ -335,6 +339,7 @@ cc_library( defines = ["NO_WEBRTC"], visibility = [ "//connections:__subpackages__", + "//fastpair:__subpackages__", "//internal/analytics:__subpackages__", "//internal/platform:__pkg__", "//internal/platform/implementation/ios:__subpackages__", @@ -384,6 +389,7 @@ cc_library( defines = ["NO_WEBRTC"], visibility = [ "//connections:__subpackages__", + "//fastpair:__subpackages__", "//googlemac/iPhone/Shared/Nearby/Connections:__subpackages__", "//internal/analytics:__subpackages__", "//internal/platform:__pkg__", diff --git a/internal/platform/implementation/g3/BUILD b/internal/platform/implementation/g3/BUILD index 9ce7e30d..5c875c02 100644 --- a/internal/platform/implementation/g3/BUILD +++ b/internal/platform/implementation/g3/BUILD @@ -119,6 +119,7 @@ cc_library( defines = ["NO_WEBRTC"], visibility = [ "//connections:__subpackages__", + "//fastpair:__subpackages__", "//internal/analytics:__subpackages__", "//internal/crypto:__subpackages__", "//internal/platform:__subpackages__",