From 8a65546060175e8dad0cbe33e8150af36f59f2c4 Mon Sep 17 00:00:00 2001 From: Jason Graffius Date: Mon, 9 Sep 2024 22:35:59 +0000 Subject: [PATCH] pw_bluetooth_sapphire: Copybara import of FIDL code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitOrigin-RevId: 7be7343b906dad28fd475dbe26d3d02571545b68 Change-Id: Ic25c0f2799e943889812f7b8ab6eae98be2a148f Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/230711 Lint: Lint 🤖 Reviewed-by: Ben Lawson Reviewed-by: Ted Pudlik Presubmit-Verified: CQ Bot Account Commit-Queue: Auto-Submit Pigweed-Auto-Submit: Ben Lawson --- .bazelrc | 16 +- pw_async_fuchsia/BUILD.bazel | 1 + pw_bluetooth_sapphire/fuchsia/BUILD.bazel | 3 + .../fuchsia/host/controllers/BUILD.bazel | 99 + .../host/controllers/fidl_controller.cc | 525 +++ .../host/controllers/fidl_controller_test.cc | 707 ++++ .../fuchsia/host/controllers/helpers.cc | 52 + .../host/controllers/fidl_controller.h | 179 + .../fuchsia/host/controllers/helpers.h | 24 + .../fuchsia/host/fidl/BUILD.bazel | 227 + .../fuchsia/host/fidl/adapter_test_fixture.cc | 67 + .../host/fidl/bredr_connection_server.cc | 244 ++ .../host/fidl/bredr_connection_server_test.cc | 241 ++ .../fuchsia/host/fidl/fake_gatt_fixture.cc | 26 + .../host/fidl/fake_hci_transport_server.cc | 185 + .../fuchsia/host/fidl/gatt2_client_server.cc | 347 ++ .../host/fidl/gatt2_client_server_test.cc | 953 +++++ .../host/fidl/gatt2_remote_service_server.cc | 583 +++ .../fidl/gatt2_remote_service_server_test.cc | 1568 +++++++ .../fuchsia/host/fidl/gatt2_server_server.cc | 378 ++ .../host/fidl/gatt2_server_server_test.cc | 1398 ++++++ .../fuchsia/host/fidl/gatt_client_server.cc | 165 + .../host/fidl/gatt_client_server_test.cc | 72 + .../host/fidl/gatt_remote_service_server.cc | 417 ++ .../fidl/gatt_remote_service_server_test.cc | 213 + .../fuchsia/host/fidl/gatt_server_server.cc | 442 ++ .../fuchsia/host/fidl/helpers.cc | 2915 +++++++++++++ .../fuchsia/host/fidl/helpers_test.cc | 2734 ++++++++++++ .../fuchsia/host/fidl/host_server.cc | 1176 ++++++ .../fuchsia/host/fidl/host_server_test.cc | 1682 ++++++++ .../fidl/host_server_watch_peers_fuzztest.cc | 168 + .../fuchsia/host/fidl/iso_stream_server.cc | 170 + .../host/fidl/iso_stream_server_test.cc | 287 ++ .../host/fidl/low_energy_central_server.cc | 703 +++ .../fidl/low_energy_central_server_test.cc | 1400 ++++++ .../host/fidl/low_energy_connection_server.cc | 234 + .../fidl/low_energy_connection_server_test.cc | 385 ++ .../host/fidl/low_energy_peripheral_server.cc | 565 +++ .../fidl/low_energy_peripheral_server_test.cc | 1093 +++++ .../host/fidl/measure_tape/BUILD.bazel | 42 + .../hlcpp_measure_tape_for_peer.cc | 232 + .../hlcpp_measure_tape_for_peer.h | 51 + ...pp_measure_tape_for_read_by_type_result.cc | 114 + ...cpp_measure_tape_for_read_by_type_result.h | 51 + .../meta/host_server_watch_peers_fuzzer.cml | 19 + .../fuchsia/host/fidl/profile_server.cc | 1335 ++++++ .../fuchsia/host/fidl/profile_server_test.cc | 3751 +++++++++++++++++ .../fuchsia/host/fidl/adapter_test_fixture.h | 68 + .../host/fidl/bredr_connection_server.h | 102 + .../host/fidl/fake_adapter_test_fixture.h | 48 + .../fuchsia/host/fidl/fake_gatt_fixture.h | 62 + .../host/fidl/fake_hci_transport_server.h | 146 + .../fuchsia/host/fidl/fake_vendor_server.h | 111 + .../fuchsia/host/fidl/gatt2_client_server.h | 98 + .../host/fidl/gatt2_remote_service_server.h | 112 + .../fuchsia/host/fidl/gatt2_server_ids.h | 51 + .../fuchsia/host/fidl/gatt2_server_server.h | 128 + .../fuchsia/host/fidl/gatt_client_server.h | 58 + .../host/fidl/gatt_remote_service_server.h | 92 + .../fuchsia/host/fidl/gatt_server_server.h | 87 + .../fuchsia/host/fidl/helpers.h | 339 ++ .../fuchsia/host/fidl/host_server.h | 306 ++ .../fuchsia/host/fidl/iso_stream_server.h | 62 + .../host/fidl/low_energy_central_server.h | 201 + .../host/fidl/low_energy_connection_server.h | 78 + .../host/fidl/low_energy_peripheral_server.h | 222 + .../fuchsia/host/fidl/profile_server.h | 383 ++ .../fuchsia/host/fidl/server_base.h | 134 + .../fuchsia/host/socket/BUILD.bazel | 80 + .../fuchsia/host/socket/README.md | 2 + .../host/socket/socket_channel_relay.h | 698 +++ .../fuchsia/host/socket/socket_factory.h | 151 + .../host/socket/socket_channel_relay_test.cc | 815 ++++ .../socket_factory_l2cap_integration_test.cc | 327 ++ .../host/socket/socket_factory_test.cc | 165 + .../fuchsia/lib/fidl/BUILD.bazel | 2 +- 76 files changed, 33358 insertions(+), 9 deletions(-) create mode 100644 pw_bluetooth_sapphire/fuchsia/host/controllers/BUILD.bazel create mode 100644 pw_bluetooth_sapphire/fuchsia/host/controllers/fidl_controller.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/controllers/fidl_controller_test.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/controllers/helpers.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/controllers/public/pw_bluetooth_sapphire/fuchsia/host/controllers/fidl_controller.h create mode 100644 pw_bluetooth_sapphire/fuchsia/host/controllers/public/pw_bluetooth_sapphire/fuchsia/host/controllers/helpers.h create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/BUILD.bazel create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/adapter_test_fixture.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/bredr_connection_server.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/bredr_connection_server_test.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/fake_gatt_fixture.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/fake_hci_transport_server.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_client_server.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_client_server_test.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_remote_service_server.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_remote_service_server_test.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_server_server.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_server_server_test.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_client_server.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_client_server_test.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_remote_service_server.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_remote_service_server_test.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_server_server.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/helpers.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/helpers_test.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/host_server.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/host_server_test.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/host_server_watch_peers_fuzztest.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/iso_stream_server.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/iso_stream_server_test.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_central_server.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_central_server_test.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_connection_server.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_connection_server_test.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_peripheral_server.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_peripheral_server_test.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/measure_tape/BUILD.bazel create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/measure_tape/hlcpp_measure_tape_for_peer.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/measure_tape/hlcpp_measure_tape_for_peer.h create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/measure_tape/hlcpp_measure_tape_for_read_by_type_result.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/measure_tape/hlcpp_measure_tape_for_read_by_type_result.h create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/meta/host_server_watch_peers_fuzzer.cml create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/profile_server.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/profile_server_test.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/adapter_test_fixture.h create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/bredr_connection_server.h create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/fake_adapter_test_fixture.h create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/fake_gatt_fixture.h create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/fake_hci_transport_server.h create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/fake_vendor_server.h create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_client_server.h create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_remote_service_server.h create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_server_ids.h create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_server_server.h create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_client_server.h create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_remote_service_server.h create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_server_server.h create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/helpers.h create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/host_server.h create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/iso_stream_server.h create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_central_server.h create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_connection_server.h create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_peripheral_server.h create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/profile_server.h create mode 100644 pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/server_base.h create mode 100644 pw_bluetooth_sapphire/fuchsia/host/socket/BUILD.bazel create mode 100644 pw_bluetooth_sapphire/fuchsia/host/socket/README.md create mode 100644 pw_bluetooth_sapphire/fuchsia/host/socket/public/pw_bluetooth_sapphire/fuchsia/host/socket/socket_channel_relay.h create mode 100644 pw_bluetooth_sapphire/fuchsia/host/socket/public/pw_bluetooth_sapphire/fuchsia/host/socket/socket_factory.h create mode 100644 pw_bluetooth_sapphire/fuchsia/host/socket/socket_channel_relay_test.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/socket/socket_factory_l2cap_integration_test.cc create mode 100644 pw_bluetooth_sapphire/fuchsia/host/socket/socket_factory_test.cc diff --git a/.bazelrc b/.bazelrc index d0f0f8f322..09c35fd74a 100644 --- a/.bazelrc +++ b/.bazelrc @@ -200,16 +200,16 @@ test --test_output=errors # linux-amd64. This is the only platform that the Fuchsia SDK supports. common --enable_platform_specific_config # Incompatible host platforms. -common:macos --deleted_packages=//pw_bluetooth_sapphire/fuchsia/...,//pw_async_fuchsia/... -common:windows --deleted_packages=//pw_bluetooth_sapphire/fuchsia/...,//pw_async_fuchsia/... +common:macos --deleted_packages=//pw_bluetooth_sapphire/fuchsia/... +common:windows --deleted_packages=//pw_bluetooth_sapphire/fuchsia/... # Target build configurations for which @fuchsia_sdk is irrelevant and thus # targets should be pruned to avoid fetching @fuchsia_sdk. -common:rp2350 --deleted_packages=//pw_bluetooth_sapphire/fuchsia/...,//pw_async_fuchsia/... -common:rp2040 --deleted_packages=//pw_bluetooth_sapphire/fuchsia/...,//pw_async_fuchsia/... -common:stm32f429i_baremetal --deleted_packages=//pw_bluetooth_sapphire/fuchsia/...,//pw_async_fuchsia/... -common:stm32f429i_freertos --deleted_packages=//pw_bluetooth_sapphire/fuchsia/...,//pw_async_fuchsia/... -common:lm3s6965evb --deleted_packages=//pw_bluetooth_sapphire/fuchsia/...,//pw_async_fuchsia/... -common:microbit --deleted_packages=//pw_bluetooth_sapphire/fuchsia/...,//pw_async_fuchsia/... +common:rp2350 --deleted_packages=//pw_bluetooth_sapphire/fuchsia/... +common:rp2040 --deleted_packages=//pw_bluetooth_sapphire/fuchsia/... +common:stm32f429i_baremetal --deleted_packages=//pw_bluetooth_sapphire/fuchsia/... +common:stm32f429i_freertos --deleted_packages=//pw_bluetooth_sapphire/fuchsia/... +common:lm3s6965evb --deleted_packages=//pw_bluetooth_sapphire/fuchsia/... +common:microbit --deleted_packages=//pw_bluetooth_sapphire/fuchsia/... # Import `--config=fuchsia` scoped pigweed backends. import %workspace%/pw_bluetooth_sapphire/fuchsia/backends.bazelrc diff --git a/pw_async_fuchsia/BUILD.bazel b/pw_async_fuchsia/BUILD.bazel index bb30f73235..2377c70bef 100644 --- a/pw_async_fuchsia/BUILD.bazel +++ b/pw_async_fuchsia/BUILD.bazel @@ -36,6 +36,7 @@ cc_library( "public", "public_overrides", ], + target_compatible_with = ["@platforms//os:fuchsia"], deps = [ ":util", "//pw_async:task_facade", diff --git a/pw_bluetooth_sapphire/fuchsia/BUILD.bazel b/pw_bluetooth_sapphire/fuchsia/BUILD.bazel index 8ff92e04aa..846e1eecdd 100644 --- a/pw_bluetooth_sapphire/fuchsia/BUILD.bazel +++ b/pw_bluetooth_sapphire/fuchsia/BUILD.bazel @@ -30,6 +30,8 @@ qemu_tests = [ "//pw_bluetooth_sapphire/fuchsia/bt_host:unittest_pkg", "//pw_bluetooth_sapphire/fuchsia/host/att:test_pkg", "//pw_bluetooth_sapphire/fuchsia/host/common:test_pkg", + "//pw_bluetooth_sapphire/fuchsia/host/controllers:test_pkg", + "//pw_bluetooth_sapphire/fuchsia/host/fidl:test_pkg", "//pw_bluetooth_sapphire/fuchsia/host/gap:test_pkg", "//pw_bluetooth_sapphire/fuchsia/host/gatt:test_pkg", "//pw_bluetooth_sapphire/fuchsia/host/hci:test_pkg", @@ -39,6 +41,7 @@ qemu_tests = [ "//pw_bluetooth_sapphire/fuchsia/host/sco:test_pkg", "//pw_bluetooth_sapphire/fuchsia/host/sdp:test_pkg", "//pw_bluetooth_sapphire/fuchsia/host/sm:test_pkg", + "//pw_bluetooth_sapphire/fuchsia/host/socket:test_pkg", "//pw_bluetooth_sapphire/fuchsia/host/testing:test_pkg", "//pw_bluetooth_sapphire/fuchsia/host/transport:test_pkg", "//pw_bluetooth_sapphire/fuchsia/lib/fidl:test_pkg", diff --git a/pw_bluetooth_sapphire/fuchsia/host/controllers/BUILD.bazel b/pw_bluetooth_sapphire/fuchsia/host/controllers/BUILD.bazel new file mode 100644 index 0000000000..1005805639 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/controllers/BUILD.bazel @@ -0,0 +1,99 @@ +# Copyright 2024 The Pigweed Authors +# +# 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. + +load( + "@fuchsia_sdk//fuchsia:defs.bzl", + "fuchsia_cc_test", + "fuchsia_unittest_package", +) + +package(default_visibility = ["//visibility:public"]) + +cc_library( + name = "public", + hdrs = [ + "public/pw_bluetooth_sapphire/fuchsia/host/controllers/fidl_controller.h", + "public/pw_bluetooth_sapphire/fuchsia/host/controllers/helpers.h", + ], + includes = [ + "public", + ], + tags = ["manual"], +) + +cc_library( + name = "controllers", + deps = [ + ":fidl_controller", + ], +) + +cc_library( + name = "helpers", + srcs = [ + "helpers.cc", + ], + deps = [ + ":public", + "@fuchsia_sdk//pkg/zx", + "@pigweed//pw_bluetooth", + ], +) + +cc_library( + name = "fidl_controller", + srcs = [ + "fidl_controller.cc", + ], + deps = [ + ":helpers", + ":public", + "//pw_bluetooth_sapphire/host/common", + "//pw_bluetooth_sapphire/host/iso", + "//pw_bluetooth_sapphire/host/transport", + "@fuchsia_sdk//fidl/fuchsia.hardware.bluetooth:fuchsia.hardware.bluetooth_cpp", + "@fuchsia_sdk//pkg/async-cpp", + "@fuchsia_sdk//pkg/zx", + "@pigweed//pw_bluetooth", + ], +) + +fuchsia_cc_test( + name = "controllers_test", + testonly = True, + srcs = [ + "fidl_controller_test.cc", + ], + death_unittest = True, + visibility = ["//visibility:public"], + deps = [ + ":controllers", + "//pw_bluetooth_sapphire/fuchsia/host/fidl:fake_vendor_server", + "//pw_bluetooth_sapphire/host/testing", + "//pw_bluetooth_sapphire/host/testing:gtest_main", + "//pw_bluetooth_sapphire/host/testing:loop_fixture", + "//pw_bluetooth_sapphire/host/testing:test_helpers", + ], +) + +fuchsia_unittest_package( + name = "test_pkg", + package_name = "controllers_tests", + testonly = True, + fuchsia_api_level = "23", + unit_tests = [ + ":controllers_test", + ], + visibility = ["//visibility:public"], +) diff --git a/pw_bluetooth_sapphire/fuchsia/host/controllers/fidl_controller.cc b/pw_bluetooth_sapphire/fuchsia/host/controllers/fidl_controller.cc new file mode 100644 index 0000000000..12c926da0e --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/controllers/fidl_controller.cc @@ -0,0 +1,525 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/controllers/fidl_controller.h" + +#include "pw_bluetooth_sapphire/fuchsia/host/controllers/helpers.h" +#include "pw_bluetooth_sapphire/internal/host/common/assert.h" +#include "pw_bluetooth_sapphire/internal/host/common/byte_buffer.h" +#include "pw_bluetooth_sapphire/internal/host/common/log.h" +#include "zircon/status.h" + +namespace bt::controllers { + +namespace fhbt = fuchsia_hardware_bluetooth; +using ReceivedPacket = fhbt::ReceivedPacket; + +namespace { +pw::bluetooth::Controller::FeaturesBits VendorFeaturesToFeaturesBits( + fhbt::VendorFeatures features) { + pw::bluetooth::Controller::FeaturesBits out{0}; + if (features.acl_priority_command().has_value() && + features.acl_priority_command()) { + out |= pw::bluetooth::Controller::FeaturesBits::kSetAclPriorityCommand; + } + if (features.android_vendor_extensions().has_value()) { + // Ignore the content of android_vendor_extension field now. + out |= pw::bluetooth::Controller::FeaturesBits::kAndroidVendorExtensions; + } + return out; +} + +fhbt::VendorAclPriority AclPriorityToFidl(pw::bluetooth::AclPriority priority) { + switch (priority) { + case pw::bluetooth::AclPriority::kNormal: + return fhbt::VendorAclPriority::kNormal; + case pw::bluetooth::AclPriority::kSource: + case pw::bluetooth::AclPriority::kSink: + return fhbt::VendorAclPriority::kHigh; + } +} + +fhbt::VendorAclDirection AclPriorityToFidlAclDirection( + pw::bluetooth::AclPriority priority) { + switch (priority) { + // The direction for kNormal is arbitrary. + case pw::bluetooth::AclPriority::kNormal: + case pw::bluetooth::AclPriority::kSource: + return fhbt::VendorAclDirection::kSource; + case pw::bluetooth::AclPriority::kSink: + return fhbt::VendorAclDirection::kSink; + } +} + +fhbt::ScoCodingFormat ScoCodingFormatToFidl( + pw::bluetooth::Controller::ScoCodingFormat coding_format) { + switch (coding_format) { + case pw::bluetooth::Controller::ScoCodingFormat::kCvsd: + return fhbt::ScoCodingFormat::kCvsd; + case pw::bluetooth::Controller::ScoCodingFormat::kMsbc: + return fhbt::ScoCodingFormat::kMsbc; + default: + BT_PANIC("invalid SCO coding format"); + } +} + +fhbt::ScoEncoding ScoEncodingToFidl( + pw::bluetooth::Controller::ScoEncoding encoding) { + switch (encoding) { + case pw::bluetooth::Controller::ScoEncoding::k8Bits: + return fhbt::ScoEncoding::kBits8; + case pw::bluetooth::Controller::ScoEncoding::k16Bits: + return fhbt::ScoEncoding::kBits16; + default: + BT_PANIC("invalid SCO encoding"); + } +} + +fhbt::ScoSampleRate ScoSampleRateToFidl( + pw::bluetooth::Controller::ScoSampleRate sample_rate) { + switch (sample_rate) { + case pw::bluetooth::Controller::ScoSampleRate::k8Khz: + return fhbt::ScoSampleRate::kKhz8; + case pw::bluetooth::Controller::ScoSampleRate::k16Khz: + return fhbt::ScoSampleRate::kKhz16; + default: + BT_PANIC("invalid SCO sample rate"); + } +} + +} // namespace + +VendorEventHandler::VendorEventHandler( + std::function unbind_callback) + : unbind_callback_(std::move(unbind_callback)) {} + +void VendorEventHandler::handle_unknown_event( + fidl::UnknownEventMetadata metadata) { + bt_log(WARN, + "controllers", + "Unknown event from Vendor server: %lu", + metadata.event_ordinal); +} + +void VendorEventHandler::on_fidl_error(fidl::UnbindInfo error) { + bt_log(ERROR, + "controllers", + "Vendor protocol closed: %s", + error.FormatDescription().c_str()); + unbind_callback_(ZX_ERR_PEER_CLOSED); +} + +HciEventHandler::HciEventHandler( + std::function unbind_callback, + std::function + on_receive_callback) + : on_receive_callback_(std::move(on_receive_callback)), + unbind_callback_(std::move(unbind_callback)) {} + +void HciEventHandler::OnReceive( + fuchsia_hardware_bluetooth::ReceivedPacket& packet) { + on_receive_callback_(std::move(packet)); +} + +void HciEventHandler::handle_unknown_event( + fidl::UnknownEventMetadata metadata) { + bt_log(WARN, + "controllers", + "Unknown event from Hci server: %lu", + metadata.event_ordinal); +} + +void HciEventHandler::on_fidl_error(fidl::UnbindInfo error) { + bt_log(ERROR, + "controllers", + "Hci protocol closed: %s", + error.FormatDescription().c_str()); + unbind_callback_(ZX_ERR_PEER_CLOSED); +} + +FidlController::FidlController(fidl::ClientEnd vendor_client_end, + async_dispatcher_t* dispatcher) + : vendor_event_handler_([this](zx_status_t status) { OnError(status); }), + hci_event_handler_([this](zx_status_t status) { OnError(status); }, + [this](fhbt::ReceivedPacket packet) { + OnReceive(std::move(packet)); + }), + sco_event_handler_( + [this](zx_status_t status) { OnScoUnbind(status); }, + [this](fhbt::ScoPacket packet) { OnReceiveSco(std::move(packet)); }), + dispatcher_(dispatcher) { + BT_ASSERT(vendor_client_end.is_valid()); + vendor_client_end_ = std::move(vendor_client_end); +} + +FidlController::~FidlController() { CleanUp(); } + +void FidlController::Initialize(PwStatusCallback complete_callback, + PwStatusCallback error_callback) { + initialize_complete_cb_ = std::move(complete_callback); + error_cb_ = std::move(error_callback); + + vendor_ = fidl::Client( + std::move(vendor_client_end_), dispatcher_, &vendor_event_handler_); + + // Connect to Hci protocol + vendor_->OpenHciTransport().Then( + [this](fidl::Result& result) { + if (!result.is_ok()) { + bt_log(ERROR, + "bt-host", + "Failed to open HciTransport: %s", + result.error_value().FormatDescription().c_str()); + if (result.error_value().is_framework_error()) { + OnError(result.error_value().framework_error().status()); + } else { + OnError(result.error_value().domain_error()); + } + return; + } + + InitializeHci(std::move(result->channel())); + }); +} + +void FidlController::InitializeHci( + fidl::ClientEnd hci_client_end) { + hci_ = fidl::Client( + std::move(hci_client_end), dispatcher_, &hci_event_handler_); + + initialize_complete_cb_(PW_STATUS_OK); +} + +void FidlController::Close(PwStatusCallback callback) { + CleanUp(); + callback(PW_STATUS_OK); +} + +void FidlController::SendCommand(pw::span command) { + BufferView view = BufferView(command); + fidl::VectorView vec_view = fidl::VectorView::FromExternal( + const_cast(view.data()), view.size()); + fidl::ObjectView obj_view = + fidl::ObjectView>::FromExternal(&vec_view); + // Use Wire bindings to avoid copying `command`. + hci_.wire() + ->Send(fhbt::wire::SentPacket::WithCommand(obj_view)) + .Then([this](fidl::WireUnownedResult& result) { + if (!result.ok()) { + bt_log(ERROR, + "controllers", + "failed to send command: %s", + result.status_string()); + OnError(result.status()); + return; + } + }); +} + +void FidlController::SendAclData(pw::span data) { + BufferView view = BufferView(data); + fidl::VectorView vec_view = fidl::VectorView::FromExternal( + const_cast(view.data()), view.size()); + fidl::ObjectView obj_view = + fidl::ObjectView>::FromExternal(&vec_view); + // Use Wire bindings to avoid copying `data`. + hci_.wire() + ->Send(fhbt::wire::SentPacket::WithAcl(obj_view)) + .Then([this](fidl::WireUnownedResult& result) { + if (!result.ok()) { + bt_log(ERROR, + "controllers", + "failed to send ACL: %s", + result.status_string()); + OnError(result.status()); + return; + } + }); +} + +void FidlController::SendScoData(pw::span data) { + if (!sco_connection_) { + bt_log(ERROR, + "controllers", + "SendScoData() called when SCO is not configured"); + OnError(ZX_ERR_BAD_STATE); + return; + } + BufferView view = BufferView(data); + fidl::VectorView vec_view = fidl::VectorView::FromExternal( + const_cast(view.data()), view.size()); + // Use Wire bindings to avoid copying `data`. + sco_connection_.value().wire()->Send(vec_view).Then( + [this](fidl::WireUnownedResult& result) { + if (!result.ok()) { + bt_log(ERROR, + "controllers", + "failed to send SCO: %s", + result.status_string()); + OnError(result.status()); + return; + } + }); +} + +void FidlController::SendIsoData(pw::span data) { + BufferView view = BufferView(data); + fidl::VectorView vec_view = fidl::VectorView::FromExternal( + const_cast(view.data()), view.size()); + fidl::ObjectView obj_view = + fidl::ObjectView>::FromExternal(&vec_view); + // Use Wire bindings to avoid copying `data`. + hci_.wire() + ->Send(fhbt::wire::SentPacket::WithIso(obj_view)) + .Then([this](fidl::WireUnownedResult& result) { + if (!result.ok()) { + bt_log(ERROR, + "controllers", + "failed to send ISO: %s", + result.status_string()); + OnError(result.status()); + return; + } + }); +} + +void FidlController::ConfigureSco(ScoCodingFormat coding_format, + ScoEncoding encoding, + ScoSampleRate sample_rate, + PwStatusCallback callback) { + if (sco_connection_) { + callback(pw::Status::AlreadyExists()); + return; + } + + fidl::Request request; + request.coding_format() = ScoCodingFormatToFidl(coding_format); + request.encoding() = ScoEncodingToFidl(encoding); + request.sample_rate() = ScoSampleRateToFidl(sample_rate); + + auto [client_end, server_end] = + fidl::CreateEndpoints().value(); + request.connection() = std::move(server_end); + sco_connection_ = + fidl::Client(std::move(client_end), dispatcher_, &sco_event_handler_); + + fit::result<::fidl::OneWayError> result = + hci_->ConfigureSco(std::move(request)); + + if (!result.is_ok()) { + bt_log(WARN, + "controllers", + "Failed to configure SCO: %s", + result.error_value().FormatDescription().c_str()); + sco_connection_.reset(); + callback(ZxStatusToPwStatus(result.error_value().status())); + return; + } + + callback(ZxStatusToPwStatus(ZX_OK)); +} + +void FidlController::ResetSco(pw::Callback callback) { + if (!sco_connection_) { + bt_log(WARN, "controllers", "ResetSco(): no SCO connection configured"); + callback(pw::Status::FailedPrecondition()); + return; + } + if (reset_sco_cb_) { + bt_log(WARN, "controllers", "ResetSco(): already pending", ); + callback(pw::Status::AlreadyExists()); + return; + } + reset_sco_cb_ = std::move(callback); + + fit::result result = sco_connection_.value()->Stop(); + if (result.is_error()) { + OnError(ZX_ERR_BAD_STATE); + return; + } +} + +void FidlController::GetFeatures( + pw::Callback callback) { + vendor_->GetFeatures().Then( + [this, cb = std::move(callback)]( + fidl::Result& result) mutable { + if (result.is_error()) { + bt_log(WARN, + "controllers", + "GetFeatures(): %s", + result.error_value().status_string()); + OnError(ZX_ERR_BAD_STATE); + return; + } + + FidlController::FeaturesBits features_bits = + VendorFeaturesToFeaturesBits(result.value()); + cb(features_bits); + }); +} + +void FidlController::EncodeVendorCommand( + pw::bluetooth::VendorCommandParameters parameters, + pw::Callback>)> callback) { + BT_ASSERT(vendor_); + + if (!std::holds_alternative( + parameters)) { + callback(pw::Status::Unimplemented()); + return; + } + + pw::bluetooth::SetAclPriorityCommandParameters params = + std::get(parameters); + + fhbt::VendorSetAclPriorityParams priority_params; + priority_params.connection_handle(params.connection_handle); + priority_params.priority(AclPriorityToFidl(params.priority)); + priority_params.direction(AclPriorityToFidlAclDirection(params.priority)); + + auto command = fhbt::VendorCommand::WithSetAclPriority(priority_params); + + vendor_->EncodeCommand(command).Then( + [cb = std::move(callback)]( + fidl::Result& result) mutable { + if (!result.is_ok()) { + bt_log(ERROR, + "controllers", + "Failed to encode vendor command: %s", + result.error_value().FormatDescription().c_str()); + if (result.error_value().is_framework_error()) { + cb(ZxStatusToPwStatus( + result.error_value().framework_error().status())); + } else { + cb(ZxStatusToPwStatus(result.error_value().domain_error())); + } + return; + } + auto span = pw::as_bytes(pw::span(result->encoded())); + cb(span); + }); +} + +void FidlController::ScoEventHandler::OnReceive(fhbt::ScoPacket& packet) { + on_receive_callback_(std::move(packet)); +} + +void FidlController::ScoEventHandler::on_fidl_error(fidl::UnbindInfo error) { + bt_log(DEBUG, + "controllers", + "SCO protocol closed: %s", + error.FormatDescription().c_str()); + unbind_callback_(ZX_ERR_PEER_CLOSED); +} + +void FidlController::ScoEventHandler::handle_unknown_event( + fidl::UnknownEventMetadata metadata) { + bt_log(WARN, + "controllers", + "Unknown event from ScoConnection server: %lu", + metadata.event_ordinal); +} + +FidlController::ScoEventHandler::ScoEventHandler( + pw::Function unbind_callback, + pw::Function + on_receive_callback) + : on_receive_callback_(std::move(on_receive_callback)), + unbind_callback_(std::move(unbind_callback)) {} + +void FidlController::OnReceive(ReceivedPacket packet) { + switch (packet.Which()) { + case ReceivedPacket::Tag::kEvent: + event_cb_(BufferView(packet.event().value()).subspan()); + break; + case ReceivedPacket::Tag::kAcl: + acl_cb_(BufferView(packet.acl().value()).subspan()); + break; + case ReceivedPacket::Tag::kIso: + iso_cb_(BufferView(packet.iso().value()).subspan()); + break; + default: + bt_log(WARN, + "controllers", + "OnReceive: unknown packet type %lu", + static_cast(packet.Which())); + } + fit::result result = hci_->AckReceive(); + if (result.is_error()) { + OnError(ZX_ERR_IO); + } +} + +void FidlController::OnReceiveSco( + fuchsia_hardware_bluetooth::ScoPacket packet) { + fit::result result = sco_connection_.value()->AckReceive(); + if (result.is_error()) { + OnError(ZX_ERR_IO); + return; + } + sco_cb_(BufferView(packet.packet()).subspan()); +} + +void FidlController::OnScoUnbind(zx_status_t status) { + // The server shouldn't close a ScoConnection on its own. It should only close + // after the host calls Stop(). + if (!reset_sco_cb_) { + bt_log(ERROR, + "controllers", + "ScoConnection closed unexpectedly (Stop() not called): %s", + zx_status_get_string(status)); + OnError(ZX_ERR_BAD_STATE); + return; + } + bt_log(DEBUG, "controllers", "ScoConnection closed"); + sco_connection_.reset(); + reset_sco_cb_(pw::OkStatus()); + reset_sco_cb_ = nullptr; +} + +void FidlController::OnError(zx_status_t status) { + CleanUp(); + + // If |initialize_complete_cb_| has already been called, then initialization + // is complete and we use |error_cb_| + if (initialize_complete_cb_) { + initialize_complete_cb_(ZxStatusToPwStatus(status)); + // Clean up |error_cb_| since we only need one callback to be called during + // initialization. + error_cb_ = nullptr; + } else if (error_cb_) { + error_cb_(ZxStatusToPwStatus(status)); + } +} + +void FidlController::CleanUp() { + if (shutting_down_) { + return; + } + shutting_down_ = true; + + if (hci_) { + auto hci_endpoint = hci_.UnbindMaybeGetEndpoint(); + } + if (vendor_) { + auto vendor_endpoint = vendor_.UnbindMaybeGetEndpoint(); + } + if (sco_connection_) { + auto sco_endpoint = sco_connection_->UnbindMaybeGetEndpoint(); + sco_connection_.reset(); + } +} + +} // namespace bt::controllers diff --git a/pw_bluetooth_sapphire/fuchsia/host/controllers/fidl_controller_test.cc b/pw_bluetooth_sapphire/fuchsia/host/controllers/fidl_controller_test.cc new file mode 100644 index 0000000000..065a52fa12 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/controllers/fidl_controller_test.cc @@ -0,0 +1,707 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/controllers/fidl_controller.h" + +#include + +#include "gmock/gmock.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/fake_vendor_server.h" +#include "pw_bluetooth_sapphire/internal/host/common/byte_buffer.h" +#include "pw_bluetooth_sapphire/internal/host/hci-spec/protocol.h" +#include "pw_bluetooth_sapphire/internal/host/testing/loop_fixture.h" +#include "pw_bluetooth_sapphire/internal/host/testing/test_helpers.h" + +namespace fhbt = fuchsia_hardware_bluetooth; + +namespace bt::controllers { + +namespace { + +constexpr hci_spec::ConnectionHandle kConnectionHandle = 0x0001; +const StaticByteBuffer kSetAclPriorityNormalCommand(0x00); +const StaticByteBuffer kSetAclPrioritySourceCommand(0x01); +const StaticByteBuffer kSetAclPrioritySinkCommand(0x02); + +} // namespace + +class FidlControllerTest : public bt::testing::TestLoopFixture { + public: + void SetUp() override { + auto [vendor_client_end, vendor_server_end] = + ::fidl::Endpoints::Create(); + + fake_vendor_server_.emplace(std::move(vendor_server_end), dispatcher()); + fidl_controller_.emplace(std::move(vendor_client_end), dispatcher()); + } + + void InitializeController() { + controller()->Initialize( + [&](pw::Status cb_complete_status) { + complete_status_ = cb_complete_status; + }, + [&](pw::Status cb_error) { controller_error_ = cb_error; }); + ASSERT_FALSE(complete_status_.has_value()); + ASSERT_FALSE(controller_error_.has_value()); + } + + FidlController* controller() { return &fidl_controller_.value(); } + + fidl::testing::FakeHciTransportServer* hci_server() { + return fake_vendor_server_->hci_server(); + } + + fidl::testing::FakeVendorServer* vendor_server() { + return &fake_vendor_server_.value(); + } + + std::optional complete_status() const { return complete_status_; } + + std::optional controller_error() const { + return controller_error_; + } + + private: + std::optional complete_status_; + std::optional controller_error_; + std::optional fake_vendor_server_; + std::optional fidl_controller_; +}; + +TEST_F(FidlControllerTest, SendAndReceiveAclPackets) { + RETURN_IF_FATAL(InitializeController()); + RunLoopUntilIdle(); + ASSERT_THAT(complete_status(), ::testing::Optional(PW_STATUS_OK)); + + const StaticByteBuffer acl_packet_0(0x00, 0x01, 0x02, 0x03); + controller()->SendAclData(acl_packet_0.subspan()); + RunLoopUntilIdle(); + ASSERT_EQ(hci_server()->acl_packets_received().size(), 1u); + EXPECT_THAT(hci_server()->acl_packets_received()[0], BufferEq(acl_packet_0)); + + const StaticByteBuffer acl_packet_1(0x04, 0x05, 0x06, 0x07); + controller()->SendAclData(acl_packet_1.subspan()); + RunLoopUntilIdle(); + ASSERT_EQ(hci_server()->acl_packets_received().size(), 2u); + EXPECT_THAT(hci_server()->acl_packets_received()[1], BufferEq(acl_packet_1)); + + std::vector received_acl; + controller()->SetReceiveAclFunction([&](pw::span buffer) { + received_acl.emplace_back(BufferView(buffer.data(), buffer.size())); + }); + + hci_server()->SendAcl(acl_packet_0.view()); + RunLoopUntilIdle(); + ASSERT_EQ(received_acl.size(), 1u); + EXPECT_THAT(received_acl[0], BufferEq(acl_packet_0)); + EXPECT_EQ(hci_server()->acks_received(), 1u); + + hci_server()->SendAcl(acl_packet_1.view()); + RunLoopUntilIdle(); + ASSERT_EQ(received_acl.size(), 2u); + EXPECT_THAT(received_acl[1], BufferEq(acl_packet_1)); + EXPECT_EQ(hci_server()->acks_received(), 2u); + + std::optional close_status; + controller()->Close([&](pw::Status status) { close_status = status; }); + ASSERT_TRUE(close_status.has_value()); + EXPECT_EQ(close_status.value(), PW_STATUS_OK); +} + +TEST_F(FidlControllerTest, SendCommandsAndReceiveEvents) { + RETURN_IF_FATAL(InitializeController()); + RunLoopUntilIdle(); + ASSERT_THAT(complete_status(), ::testing::Optional(PW_STATUS_OK)); + + const StaticByteBuffer packet_0(0x00, 0x01, 0x02, 0x03); + controller()->SendCommand(packet_0.subspan()); + RunLoopUntilIdle(); + ASSERT_EQ(hci_server()->commands_received().size(), 1u); + EXPECT_THAT(hci_server()->commands_received()[0], BufferEq(packet_0)); + + const StaticByteBuffer packet_1(0x04, 0x05, 0x06, 0x07); + controller()->SendCommand(packet_1.subspan()); + RunLoopUntilIdle(); + ASSERT_EQ(hci_server()->commands_received().size(), 2u); + EXPECT_THAT(hci_server()->commands_received()[1], BufferEq(packet_1)); + + std::vector events; + controller()->SetEventFunction([&](pw::span buffer) { + events.emplace_back(BufferView(buffer.data(), buffer.size())); + }); + + hci_server()->SendEvent(packet_1.view()); + RunLoopUntilIdle(); + ASSERT_EQ(events.size(), 1u); + EXPECT_THAT(events[0], BufferEq(packet_1)); + EXPECT_EQ(hci_server()->acks_received(), 1u); + + hci_server()->SendEvent(packet_1.view()); + RunLoopUntilIdle(); + ASSERT_EQ(events.size(), 2u); + EXPECT_THAT(events[1], BufferEq(packet_1)); + EXPECT_EQ(hci_server()->acks_received(), 2u); + + std::optional close_status; + controller()->Close([&](pw::Status status) { close_status = status; }); + ASSERT_TRUE(close_status.has_value()); + EXPECT_EQ(close_status.value(), PW_STATUS_OK); +} + +TEST_F(FidlControllerTest, SendScoWhenNotConfigured) { + RETURN_IF_FATAL(InitializeController()); + RunLoopUntilIdle(); + ASSERT_THAT(complete_status(), ::testing::Optional(PW_STATUS_OK)); + + const StaticByteBuffer packet_0(0x00, 0x01, 0x02, 0x03); + controller()->SendScoData(packet_0.subspan()); + RunLoopUntilIdle(); + ASSERT_EQ(hci_server()->sco_packets_received().size(), 0u); + ASSERT_THAT(controller_error(), ::testing::Optional(PW_STATUS_UNKNOWN)); +} + +TEST_F(FidlControllerTest, SendAndReceiveSco) { + RETURN_IF_FATAL(InitializeController()); + RunLoopUntilIdle(); + ASSERT_THAT(complete_status(), ::testing::Optional(PW_STATUS_OK)); + + int controller_cb_count = 0; + controller()->ConfigureSco(pw::bluetooth::Controller::ScoCodingFormat::kCvsd, + pw::bluetooth::Controller::ScoEncoding::k8Bits, + pw::bluetooth::Controller::ScoSampleRate::k8Khz, + [&controller_cb_count](pw::Status status) { + controller_cb_count++; + EXPECT_EQ(status, PW_STATUS_OK); + }); + EXPECT_EQ(controller_cb_count, 1); + + const StaticByteBuffer packet_0(0x00, 0x01, 0x02, 0x03); + controller()->SendScoData(packet_0.subspan()); + RunLoopUntilIdle(); + ASSERT_EQ(hci_server()->sco_packets_received().size(), 1u); + EXPECT_THAT(hci_server()->sco_packets_received()[0], BufferEq(packet_0)); + + const StaticByteBuffer packet_1(0x04, 0x05, 0x06, 0x07); + controller()->SendScoData(packet_1.subspan()); + RunLoopUntilIdle(); + ASSERT_EQ(hci_server()->sco_packets_received().size(), 2u); + EXPECT_THAT(hci_server()->sco_packets_received()[1], BufferEq(packet_1)); + + std::vector received_sco; + controller()->SetReceiveScoFunction([&](pw::span buffer) { + received_sco.emplace_back(BufferView(buffer.data(), buffer.size())); + }); + + hci_server()->SendSco(packet_1.view()); + RunLoopUntilIdle(); + ASSERT_EQ(received_sco.size(), 1u); + EXPECT_THAT(received_sco[0], BufferEq(packet_1)); + EXPECT_EQ(hci_server()->sco_acks_received(), 1u); + EXPECT_EQ(hci_server()->acks_received(), 0u); + + hci_server()->SendSco(packet_1.view()); + RunLoopUntilIdle(); + ASSERT_EQ(received_sco.size(), 2u); + EXPECT_THAT(received_sco[1], BufferEq(packet_1)); + EXPECT_EQ(hci_server()->sco_acks_received(), 2u); + + std::optional close_status; + controller()->Close([&](pw::Status status) { close_status = status; }); + ASSERT_TRUE(close_status.has_value()); + EXPECT_EQ(close_status.value(), PW_STATUS_OK); +} + +TEST_F(FidlControllerTest, SendAndReceiveIso) { + RETURN_IF_FATAL(InitializeController()); + RunLoopUntilIdle(); + ASSERT_THAT(complete_status(), ::testing::Optional(PW_STATUS_OK)); + + const StaticByteBuffer iso_packet_0(0x00, 0x01, 0x02, 0x03); + controller()->SendIsoData(iso_packet_0.subspan()); + RunLoopUntilIdle(); + ASSERT_EQ(hci_server()->iso_packets_received().size(), 1u); + EXPECT_THAT(hci_server()->iso_packets_received()[0], BufferEq(iso_packet_0)); + + const StaticByteBuffer iso_packet_1(0x04, 0x05, 0x06, 0x07); + controller()->SendIsoData(iso_packet_1.subspan()); + RunLoopUntilIdle(); + ASSERT_EQ(hci_server()->iso_packets_received().size(), 2u); + EXPECT_THAT(hci_server()->iso_packets_received()[1], BufferEq(iso_packet_1)); + + std::vector received_iso; + controller()->SetReceiveIsoFunction([&](pw::span buffer) { + received_iso.emplace_back(BufferView(buffer.data(), buffer.size())); + }); + + hci_server()->SendIso(iso_packet_0.view()); + RunLoopUntilIdle(); + ASSERT_EQ(received_iso.size(), 1u); + EXPECT_THAT(received_iso[0], BufferEq(iso_packet_0)); + EXPECT_EQ(hci_server()->acks_received(), 1u); + + hci_server()->SendIso(iso_packet_1.view()); + RunLoopUntilIdle(); + ASSERT_EQ(received_iso.size(), 2u); + EXPECT_THAT(received_iso[1], BufferEq(iso_packet_1)); + EXPECT_EQ(hci_server()->acks_received(), 2u); + + std::optional close_status; + controller()->Close([&](pw::Status status) { close_status = status; }); + ASSERT_TRUE(close_status.has_value()); + EXPECT_EQ(close_status.value(), PW_STATUS_OK); +} + +TEST_F(FidlControllerTest, + ConfigureScoWithFormatCvsdEncoding8BitsSampleRate8Khz) { + RETURN_IF_FATAL(InitializeController()); + RunLoopUntilIdle(); + ASSERT_THAT(complete_status(), ::testing::Optional(PW_STATUS_OK)); + + int device_cb_count = 0; + hci_server()->set_check_configure_sco( + [&device_cb_count](fhbt::ScoCodingFormat format, + fhbt::ScoEncoding encoding, + fhbt::ScoSampleRate rate) mutable { + device_cb_count++; + EXPECT_EQ(format, fhbt::ScoCodingFormat::kCvsd); + EXPECT_EQ(encoding, fhbt::ScoEncoding::kBits8); + EXPECT_EQ(rate, fhbt::ScoSampleRate::kKhz8); + }); + + int controller_cb_count = 0; + controller()->ConfigureSco(pw::bluetooth::Controller::ScoCodingFormat::kCvsd, + pw::bluetooth::Controller::ScoEncoding::k8Bits, + pw::bluetooth::Controller::ScoSampleRate::k8Khz, + [&controller_cb_count](pw::Status status) { + controller_cb_count++; + EXPECT_EQ(status, PW_STATUS_OK); + }); + + EXPECT_EQ(device_cb_count, 0); + EXPECT_EQ(controller_cb_count, 1); + RunLoopUntilIdle(); + EXPECT_EQ(controller_cb_count, 1); + EXPECT_EQ(device_cb_count, 1); +} + +TEST_F(FidlControllerTest, + ConfigureScoWithFormatCvsdEncoding16BitsSampleRate8Khz) { + RETURN_IF_FATAL(InitializeController()); + RunLoopUntilIdle(); + ASSERT_THAT(complete_status(), ::testing::Optional(PW_STATUS_OK)); + + hci_server()->set_check_configure_sco([](fhbt::ScoCodingFormat format, + fhbt::ScoEncoding encoding, + fhbt::ScoSampleRate rate) { + EXPECT_EQ(format, fhbt::ScoCodingFormat::kCvsd); + EXPECT_EQ(encoding, fhbt::ScoEncoding::kBits16); + EXPECT_EQ(rate, fhbt::ScoSampleRate::kKhz8); + }); + + int config_cb_count = 0; + controller()->ConfigureSco(pw::bluetooth::Controller::ScoCodingFormat::kCvsd, + pw::bluetooth::Controller::ScoEncoding::k16Bits, + pw::bluetooth::Controller::ScoSampleRate::k8Khz, + [&](pw::Status status) { + config_cb_count++; + EXPECT_EQ(status, PW_STATUS_OK); + }); + RunLoopUntilIdle(); + EXPECT_EQ(config_cb_count, 1); +} + +TEST_F(FidlControllerTest, + ConfigureScoWithFormatCvsdEncoding16BitsSampleRate16Khz) { + RETURN_IF_FATAL(InitializeController()); + RunLoopUntilIdle(); + ASSERT_THAT(complete_status(), ::testing::Optional(PW_STATUS_OK)); + + hci_server()->set_check_configure_sco([](fhbt::ScoCodingFormat format, + fhbt::ScoEncoding encoding, + fhbt::ScoSampleRate rate) { + EXPECT_EQ(format, fhbt::ScoCodingFormat::kCvsd); + EXPECT_EQ(encoding, fhbt::ScoEncoding::kBits16); + EXPECT_EQ(rate, fhbt::ScoSampleRate::kKhz16); + }); + + int config_cb_count = 0; + controller()->ConfigureSco(pw::bluetooth::Controller::ScoCodingFormat::kCvsd, + pw::bluetooth::Controller::ScoEncoding::k16Bits, + pw::bluetooth::Controller::ScoSampleRate::k16Khz, + [&](pw::Status status) { + config_cb_count++; + EXPECT_EQ(status, PW_STATUS_OK); + }); + RunLoopUntilIdle(); + EXPECT_EQ(config_cb_count, 1); +} + +TEST_F(FidlControllerTest, + ConfigureScoWithFormatMsbcEncoding16BitsSampleRate16Khz) { + RETURN_IF_FATAL(InitializeController()); + RunLoopUntilIdle(); + ASSERT_THAT(complete_status(), ::testing::Optional(PW_STATUS_OK)); + + hci_server()->set_check_configure_sco([](fhbt::ScoCodingFormat format, + fhbt::ScoEncoding encoding, + fhbt::ScoSampleRate rate) { + EXPECT_EQ(format, fhbt::ScoCodingFormat::kMsbc); + EXPECT_EQ(encoding, fhbt::ScoEncoding::kBits16); + EXPECT_EQ(rate, fhbt::ScoSampleRate::kKhz16); + }); + + int config_cb_count = 0; + controller()->ConfigureSco(pw::bluetooth::Controller::ScoCodingFormat::kMsbc, + pw::bluetooth::Controller::ScoEncoding::k16Bits, + pw::bluetooth::Controller::ScoSampleRate::k16Khz, + [&](pw::Status status) { + config_cb_count++; + EXPECT_EQ(status, PW_STATUS_OK); + }); + RunLoopUntilIdle(); + EXPECT_EQ(config_cb_count, 1); +} + +TEST_F(FidlControllerTest, ConfigureAndResetScoTwice) { + RETURN_IF_FATAL(InitializeController()); + RunLoopUntilIdle(); + ASSERT_THAT(complete_status(), ::testing::Optional(PW_STATUS_OK)); + + int device_cb_count = 0; + hci_server()->set_reset_sco_callback( + [&device_cb_count]() { device_cb_count++; }); + + int config_cb_count_0 = 0; + controller()->ConfigureSco(pw::bluetooth::Controller::ScoCodingFormat::kMsbc, + pw::bluetooth::Controller::ScoEncoding::k16Bits, + pw::bluetooth::Controller::ScoSampleRate::k16Khz, + [&](pw::Status status) { + config_cb_count_0++; + EXPECT_EQ(status, PW_STATUS_OK); + }); + EXPECT_EQ(config_cb_count_0, 1); + + int reset_cb_count_0 = 0; + controller()->ResetSco([&](pw::Status status) { + reset_cb_count_0++; + EXPECT_EQ(status, PW_STATUS_OK); + }); + + EXPECT_EQ(device_cb_count, 0); + EXPECT_EQ(reset_cb_count_0, 0); + RunLoopUntilIdle(); + EXPECT_EQ(device_cb_count, 1); + EXPECT_EQ(reset_cb_count_0, 1); + + int config_cb_count_1 = 0; + controller()->ConfigureSco(pw::bluetooth::Controller::ScoCodingFormat::kMsbc, + pw::bluetooth::Controller::ScoEncoding::k16Bits, + pw::bluetooth::Controller::ScoSampleRate::k16Khz, + [&](pw::Status status) { + config_cb_count_1++; + EXPECT_EQ(status, PW_STATUS_OK) << status.str(); + }); + EXPECT_EQ(config_cb_count_1, 1); + + int reset_cb_count_1 = 0; + controller()->ResetSco([&](pw::Status status) { + reset_cb_count_1++; + EXPECT_EQ(status, PW_STATUS_OK); + }); + + EXPECT_EQ(reset_cb_count_1, 0); + RunLoopUntilIdle(); + EXPECT_EQ(device_cb_count, 2); + EXPECT_EQ(reset_cb_count_0, 1); + EXPECT_EQ(reset_cb_count_1, 1); +} + +TEST_F(FidlControllerTest, CloseUnbindsHciProtocol) { + RETURN_IF_FATAL(InitializeController()); + RunLoopUntilIdle(); + ASSERT_THAT(complete_status(), ::testing::Optional(PW_STATUS_OK)); + + std::optional close_status; + controller()->Close([&](pw::Status status) { close_status = status; }); + RunLoopUntilIdle(); + ASSERT_TRUE(close_status.has_value()); + EXPECT_EQ(close_status.value(), PW_STATUS_OK); + EXPECT_FALSE(hci_server()->bound()); +} + +TEST_F(FidlControllerTest, HciServerClosesProtocol) { + RETURN_IF_FATAL(InitializeController()); + RunLoopUntilIdle(); + ASSERT_THAT(complete_status(), ::testing::Optional(PW_STATUS_OK)); + + hci_server()->Unbind(); + RunLoopUntilIdle(); + ASSERT_THAT(controller_error(), + ::testing::Optional(pw::Status::Unavailable())); +} + +TEST_F(FidlControllerTest, VendorGetFeatures) { + RETURN_IF_FATAL(InitializeController()); + RunLoopUntilIdle(); + ASSERT_THAT(complete_status(), ::testing::Optional(PW_STATUS_OK)); + + std::optional features; + controller()->GetFeatures( + [&](FidlController::FeaturesBits bits) { features = bits; }); + RunLoopUntilIdle(); + ASSERT_TRUE(features.has_value()); + EXPECT_TRUE(*features & FidlController::FeaturesBits::kSetAclPriorityCommand); + + std::optional close_status; + controller()->Close([&](pw::Status status) { close_status = status; }); + ASSERT_TRUE(close_status.has_value()); + EXPECT_EQ(close_status.value(), PW_STATUS_OK); +} + +TEST_F(FidlControllerTest, VendorEncodeSetAclPriorityCommandNormal) { + RETURN_IF_FATAL(InitializeController()); + RunLoopUntilIdle(); + ASSERT_THAT(complete_status(), ::testing::Optional(PW_STATUS_OK)); + + pw::bluetooth::SetAclPriorityCommandParameters params; + params.connection_handle = kConnectionHandle; + params.priority = pw::bluetooth::AclPriority::kNormal; + + std::optional buffer; + controller()->EncodeVendorCommand( + params, [&](pw::Result> result) { + ASSERT_TRUE(result.ok()); + buffer.emplace( + BufferView(result.value().data(), result.value().size())); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(buffer); + EXPECT_THAT(*buffer, BufferEq(kSetAclPriorityNormalCommand)); +} + +TEST_F(FidlControllerTest, VendorEncodeSetAclPriorityCommandSink) { + RETURN_IF_FATAL(InitializeController()); + RunLoopUntilIdle(); + ASSERT_THAT(complete_status(), ::testing::Optional(PW_STATUS_OK)); + + pw::bluetooth::SetAclPriorityCommandParameters params; + params.connection_handle = kConnectionHandle; + params.priority = pw::bluetooth::AclPriority::kSink; + + std::optional buffer; + controller()->EncodeVendorCommand( + params, [&](pw::Result> result) { + ASSERT_TRUE(result.ok()); + buffer.emplace( + BufferView(result.value().data(), result.value().size())); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(buffer); + EXPECT_THAT(*buffer, BufferEq(kSetAclPrioritySinkCommand)); +} + +TEST_F(FidlControllerTest, VendorEncodeSetAclPriorityCommandSource) { + RETURN_IF_FATAL(InitializeController()); + RunLoopUntilIdle(); + ASSERT_THAT(complete_status(), ::testing::Optional(PW_STATUS_OK)); + + pw::bluetooth::SetAclPriorityCommandParameters params; + params.connection_handle = kConnectionHandle; + params.priority = pw::bluetooth::AclPriority::kSource; + + std::optional buffer; + controller()->EncodeVendorCommand( + params, [&](pw::Result> result) { + ASSERT_TRUE(result.ok()); + buffer.emplace( + BufferView(result.value().data(), result.value().size())); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(buffer); + EXPECT_THAT(*buffer, BufferEq(kSetAclPrioritySourceCommand)); +} + +TEST_F(FidlControllerTest, VendorServerClosesChannelBeforeOpenHci) { + RETURN_IF_FATAL(InitializeController()); + ASSERT_THAT(complete_status(), std::nullopt); + ASSERT_THAT(controller_error(), std::nullopt); + + vendor_server()->Unbind(); + RunLoopUntilIdle(); + ASSERT_THAT(complete_status(), + ::testing::Optional(pw::Status::Unavailable())); + ASSERT_THAT(controller_error(), std::nullopt); +} + +TEST_F(FidlControllerTest, VendorServerClosesProtocolBeforeInitialize) { + vendor_server()->Unbind(); + RunLoopUntilIdle(); + + RETURN_IF_FATAL(InitializeController()); + RunLoopUntilIdle(); + ASSERT_THAT(complete_status(), + ::testing::Optional(pw::Status::Unavailable())); + ASSERT_THAT(controller_error(), std::nullopt); +} + +TEST_F(FidlControllerTest, VendorOpenHciError) { + // Make OpenHci() return error during controller initialization + vendor_server()->set_open_hci_error(true); + + RETURN_IF_FATAL(InitializeController()); + RunLoopUntilIdle(); + ASSERT_THAT(complete_status(), ::testing::Optional(pw::Status::Internal())); + ASSERT_THAT(controller_error(), std::nullopt); +} + +TEST_F(FidlControllerTest, VendorServerClosesProtocol) { + RETURN_IF_FATAL(InitializeController()); + RunLoopUntilIdle(); + ASSERT_THAT(complete_status(), ::testing::Optional(PW_STATUS_OK)); + + vendor_server()->Unbind(); + RunLoopUntilIdle(); + ASSERT_THAT(controller_error(), + ::testing::Optional(pw::Status::Unavailable())); +} + +// Attempting to send a command once the HciTransport protocol has been closed +// from the server end may trigger the driver to terminate. Verify that a clean +// shutdown will still occur. +TEST_F(FidlControllerTest, EventClosesDriver) { + RETURN_IF_FATAL(InitializeController()); + RunLoopUntilIdle(); + ASSERT_THAT(complete_status(), ::testing::Optional(PW_STATUS_OK)); + + const StaticByteBuffer kCommandPacket(0x00, 0x01, 0x02, 0x03); + const StaticByteBuffer kEventPacket(0x04, 0x05, 0x06, 0x07); + controller()->SetEventFunction([&](pw::span /* buffer */) { + hci_server()->Unbind(); + controller()->SendCommand(kCommandPacket.subspan()); + }); + hci_server()->SendEvent(kEventPacket.view()); + RunLoopUntilIdle(); +} + +TEST_F(FidlControllerTest, ScoServerClosesProtocolUnexpectedly) { + RETURN_IF_FATAL(InitializeController()); + RunLoopUntilIdle(); + ASSERT_THAT(complete_status(), ::testing::Optional(PW_STATUS_OK)); + + int config_cb_count = 0; + controller()->ConfigureSco(pw::bluetooth::Controller::ScoCodingFormat::kMsbc, + pw::bluetooth::Controller::ScoEncoding::k16Bits, + pw::bluetooth::Controller::ScoSampleRate::k16Khz, + [&](pw::Status status) { + config_cb_count++; + EXPECT_EQ(status, PW_STATUS_OK); + }); + RunLoopUntilIdle(); + EXPECT_EQ(config_cb_count, 1); + + EXPECT_TRUE(hci_server()->UnbindSco()); + RunLoopUntilIdle(); + EXPECT_THAT(controller_error(), ::testing::Optional(pw::Status::Unknown())); +} + +TEST_F(FidlControllerTest, ConfigureScoAlreadyConfigured) { + RETURN_IF_FATAL(InitializeController()); + RunLoopUntilIdle(); + ASSERT_THAT(complete_status(), ::testing::Optional(PW_STATUS_OK)); + + int config_cb_count_0 = 0; + controller()->ConfigureSco(pw::bluetooth::Controller::ScoCodingFormat::kMsbc, + pw::bluetooth::Controller::ScoEncoding::k16Bits, + pw::bluetooth::Controller::ScoSampleRate::k16Khz, + [&](pw::Status status) { + config_cb_count_0++; + EXPECT_EQ(status, PW_STATUS_OK); + }); + int config_cb_count_1 = 0; + controller()->ConfigureSco(pw::bluetooth::Controller::ScoCodingFormat::kMsbc, + pw::bluetooth::Controller::ScoEncoding::k16Bits, + pw::bluetooth::Controller::ScoSampleRate::k16Khz, + [&](pw::Status status) { + config_cb_count_1++; + EXPECT_EQ(status, PW_STATUS_ALREADY_EXISTS); + }); + EXPECT_EQ(config_cb_count_0, 1); + EXPECT_EQ(config_cb_count_1, 1); + RunLoopUntilIdle(); + EXPECT_EQ(config_cb_count_0, 1); + EXPECT_EQ(config_cb_count_1, 1); +} + +TEST_F(FidlControllerTest, ResetScoWhenNotConfiguredFails) { + RETURN_IF_FATAL(InitializeController()); + RunLoopUntilIdle(); + ASSERT_THAT(complete_status(), ::testing::Optional(PW_STATUS_OK)); + + int device_cb_count = 0; + hci_server()->set_reset_sco_callback( + [&device_cb_count]() { device_cb_count++; }); + + int reset_cb_count = 0; + controller()->ResetSco([&](pw::Status status) { + reset_cb_count++; + EXPECT_EQ(status, PW_STATUS_FAILED_PRECONDITION); + }); + + EXPECT_EQ(device_cb_count, 0); + EXPECT_EQ(reset_cb_count, 1); + RunLoopUntilIdle(); + EXPECT_EQ(device_cb_count, 0); + EXPECT_EQ(reset_cb_count, 1); +} + +TEST_F(FidlControllerTest, ResetScoAlreadyPending) { + RETURN_IF_FATAL(InitializeController()); + RunLoopUntilIdle(); + ASSERT_THAT(complete_status(), ::testing::Optional(PW_STATUS_OK)); + + int device_cb_count = 0; + hci_server()->set_reset_sco_callback( + [&device_cb_count]() { device_cb_count++; }); + + int config_cb_count_0 = 0; + controller()->ConfigureSco(pw::bluetooth::Controller::ScoCodingFormat::kMsbc, + pw::bluetooth::Controller::ScoEncoding::k16Bits, + pw::bluetooth::Controller::ScoSampleRate::k16Khz, + [&](pw::Status status) { + config_cb_count_0++; + EXPECT_EQ(status, PW_STATUS_OK); + }); + EXPECT_EQ(config_cb_count_0, 1); + + int reset_cb_count_0 = 0; + controller()->ResetSco([&](pw::Status status) { + reset_cb_count_0++; + EXPECT_EQ(status, PW_STATUS_OK); + }); + int reset_cb_count_1 = 0; + controller()->ResetSco([&](pw::Status status) { + reset_cb_count_1++; + EXPECT_EQ(status, PW_STATUS_ALREADY_EXISTS); + }); + + EXPECT_EQ(device_cb_count, 0); + EXPECT_EQ(reset_cb_count_0, 0); + EXPECT_EQ(reset_cb_count_1, 1); + RunLoopUntilIdle(); + EXPECT_EQ(device_cb_count, 1); + EXPECT_EQ(reset_cb_count_0, 1); + EXPECT_EQ(reset_cb_count_1, 1); +} + +} // namespace bt::controllers diff --git a/pw_bluetooth_sapphire/fuchsia/host/controllers/helpers.cc b/pw_bluetooth_sapphire/fuchsia/host/controllers/helpers.cc new file mode 100644 index 0000000000..414609cd03 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/controllers/helpers.cc @@ -0,0 +1,52 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/controllers/helpers.h" + +namespace bt::controllers { + +pw::Status ZxStatusToPwStatus(zx_status_t status) { + switch (status) { + case ZX_OK: + return PW_STATUS_OK; + case ZX_ERR_INTERNAL: + return pw::Status::Internal(); + case ZX_ERR_NOT_SUPPORTED: + return pw::Status::Unimplemented(); + case ZX_ERR_NO_RESOURCES: + return pw::Status::ResourceExhausted(); + case ZX_ERR_INVALID_ARGS: + return pw::Status::InvalidArgument(); + case ZX_ERR_OUT_OF_RANGE: + return pw::Status::OutOfRange(); + case ZX_ERR_CANCELED: + return pw::Status::Cancelled(); + case ZX_ERR_PEER_CLOSED: + return pw::Status::Unavailable(); + case ZX_ERR_NOT_FOUND: + return pw::Status::NotFound(); + case ZX_ERR_ALREADY_EXISTS: + return pw::Status::AlreadyExists(); + case ZX_ERR_UNAVAILABLE: + return pw::Status::Unavailable(); + case ZX_ERR_ACCESS_DENIED: + return pw::Status::PermissionDenied(); + case ZX_ERR_IO_DATA_LOSS: + return pw::Status::DataLoss(); + default: + return pw::Status::Unknown(); + } +} + +} // namespace bt::controllers diff --git a/pw_bluetooth_sapphire/fuchsia/host/controllers/public/pw_bluetooth_sapphire/fuchsia/host/controllers/fidl_controller.h b/pw_bluetooth_sapphire/fuchsia/host/controllers/public/pw_bluetooth_sapphire/fuchsia/host/controllers/fidl_controller.h new file mode 100644 index 0000000000..a32d92c8e5 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/controllers/public/pw_bluetooth_sapphire/fuchsia/host/controllers/fidl_controller.h @@ -0,0 +1,179 @@ +// Copyright 2024 The Pigweed Authors +// +// 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. + +#pragma once + +#include +#include +#include +#include + +#include "pw_bluetooth/controller.h" + +namespace bt::controllers { + +class VendorEventHandler + : public fidl::AsyncEventHandler { + public: + explicit VendorEventHandler(std::function unbind_callback); + void on_fidl_error(fidl::UnbindInfo error) override; + void handle_unknown_event( + fidl::UnknownEventMetadata metadata) + override; + + private: + std::function unbind_callback_; +}; + +class HciEventHandler + : public fidl::AsyncEventHandler { + public: + HciEventHandler( + std::function unbind_callback, + std::function + on_receive_callback); + void OnReceive(fuchsia_hardware_bluetooth::ReceivedPacket&) override; + void on_fidl_error(fidl::UnbindInfo error) override; + void handle_unknown_event( + fidl::UnknownEventMetadata + metadata) override; + + private: + std::function + on_receive_callback_; + std::function unbind_callback_; +}; + +class FidlController final : public pw::bluetooth::Controller { + public: + using PwStatusCallback = pw::Callback; + + // |dispatcher| must outlive this object. + FidlController( + fidl::ClientEnd vendor_client_end, + async_dispatcher_t* dispatcher); + + ~FidlController() override; + + // Controller overrides: + void SetEventFunction(DataFunction func) override { + event_cb_ = std::move(func); + } + + void SetReceiveAclFunction(DataFunction func) override { + acl_cb_ = std::move(func); + } + + void SetReceiveScoFunction(DataFunction func) override { + sco_cb_ = std::move(func); + } + + void SetReceiveIsoFunction(DataFunction func) override { + iso_cb_ = std::move(func); + } + + void Initialize(PwStatusCallback complete_callback, + PwStatusCallback error_callback) override; + + void Close(PwStatusCallback callback) override; + + void SendCommand(pw::span command) override; + + void SendAclData(pw::span data) override; + + void SendScoData(pw::span data) override; + + void SendIsoData(pw::span data) override; + + void ConfigureSco(ScoCodingFormat coding_format, + ScoEncoding encoding, + ScoSampleRate sample_rate, + pw::Callback callback) override; + + void ResetSco(pw::Callback callback) override; + + void GetFeatures(pw::Callback callback) override; + void EncodeVendorCommand( + pw::bluetooth::VendorCommandParameters parameters, + pw::Callback>)> callback) + override; + + private: + class ScoEventHandler : public fidl::AsyncEventHandler< + fuchsia_hardware_bluetooth::ScoConnection> { + public: + ScoEventHandler(pw::Function unbind_callback, + pw::Function + on_receive_callback); + + private: + // AsyncEventHandler overrides: + void OnReceive(fuchsia_hardware_bluetooth::ScoPacket& packet) override; + void on_fidl_error(fidl::UnbindInfo error) override; + void handle_unknown_event( + fidl::UnknownEventMetadata + metadata) override; + + pw::Function + on_receive_callback_; + pw::Function unbind_callback_; + }; + + void OnReceive(fuchsia_hardware_bluetooth::ReceivedPacket packet); + void OnReceiveSco(fuchsia_hardware_bluetooth::ScoPacket packet); + + void OnScoUnbind(zx_status_t status); + + // Cleanup and call |error_cb_| with |status| + void OnError(zx_status_t status); + + void CleanUp(); + + // Initializes HCI layer by binding |hci_handle| to |hci_| and opening two-way + // command channel and ACL data channel + void InitializeHci( + fidl::ClientEnd hci_client_end); + + // |vendor_handle_| holds the Vendor channel until Initialize() is called, at + // which point |vendor_| is bound to the channel. This prevents errors from + // being lost before initialization. + fidl::ClientEnd vendor_client_end_; + fidl::Client vendor_; + + fidl::Client hci_; + + VendorEventHandler vendor_event_handler_; + HciEventHandler hci_event_handler_; + + // Only set after ConfigureSco() is called. Unbound on ResetSco(). + std::optional> + sco_connection_; + // Shared across all ScoConnections. + ScoEventHandler sco_event_handler_; + // Valid only when a ResetSco() call is pending. + PwStatusCallback reset_sco_cb_; + + async_dispatcher_t* dispatcher_; + + DataFunction event_cb_; + DataFunction acl_cb_; + DataFunction sco_cb_; + DataFunction iso_cb_; + PwStatusCallback initialize_complete_cb_; + PwStatusCallback error_cb_; + + bool shutting_down_ = false; +}; + +} // namespace bt::controllers diff --git a/pw_bluetooth_sapphire/fuchsia/host/controllers/public/pw_bluetooth_sapphire/fuchsia/host/controllers/helpers.h b/pw_bluetooth_sapphire/fuchsia/host/controllers/public/pw_bluetooth_sapphire/fuchsia/host/controllers/helpers.h new file mode 100644 index 0000000000..1ffbc85759 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/controllers/public/pw_bluetooth_sapphire/fuchsia/host/controllers/helpers.h @@ -0,0 +1,24 @@ +// Copyright 2024 The Pigweed Authors +// +// 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. + +#pragma once + +#include "pw_status/status.h" +#include "zircon/status.h" + +namespace bt::controllers { + +pw::Status ZxStatusToPwStatus(zx_status_t status); + +} diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/BUILD.bazel b/pw_bluetooth_sapphire/fuchsia/host/fidl/BUILD.bazel new file mode 100644 index 0000000000..f01b128290 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/BUILD.bazel @@ -0,0 +1,227 @@ +# Copyright 2024 The Pigweed Authors +# +# 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. + +load( + "@fuchsia_sdk//fuchsia:defs.bzl", + "fuchsia_cc_test", + "fuchsia_unittest_package", +) + +package(default_visibility = ["//visibility:public"]) + +cc_library( + name = "public", + hdrs = [ + "public/pw_bluetooth_sapphire/fuchsia/host/fidl/adapter_test_fixture.h", + "public/pw_bluetooth_sapphire/fuchsia/host/fidl/bredr_connection_server.h", + "public/pw_bluetooth_sapphire/fuchsia/host/fidl/fake_adapter_test_fixture.h", + "public/pw_bluetooth_sapphire/fuchsia/host/fidl/fake_gatt_fixture.h", + "public/pw_bluetooth_sapphire/fuchsia/host/fidl/fake_hci_transport_server.h", + "public/pw_bluetooth_sapphire/fuchsia/host/fidl/fake_vendor_server.h", + "public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_client_server.h", + "public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_remote_service_server.h", + "public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_server_ids.h", + "public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_server_server.h", + "public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_client_server.h", + "public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_remote_service_server.h", + "public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_server_server.h", + "public/pw_bluetooth_sapphire/fuchsia/host/fidl/helpers.h", + "public/pw_bluetooth_sapphire/fuchsia/host/fidl/host_server.h", + "public/pw_bluetooth_sapphire/fuchsia/host/fidl/iso_stream_server.h", + "public/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_central_server.h", + "public/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_connection_server.h", + "public/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_peripheral_server.h", + "public/pw_bluetooth_sapphire/fuchsia/host/fidl/profile_server.h", + "public/pw_bluetooth_sapphire/fuchsia/host/fidl/server_base.h", + ], + includes = [ + "public", + ], + tags = ["manual"], + deps = [ + "//pw_bluetooth_sapphire/fuchsia/host/socket:public", + ], +) + +cc_library( + name = "fidl", + srcs = [ + "bredr_connection_server.cc", + "gatt2_client_server.cc", + "gatt2_remote_service_server.cc", + "gatt2_server_server.cc", + "gatt_client_server.cc", + "gatt_remote_service_server.cc", + "gatt_server_server.cc", + "host_server.cc", + "iso_stream_server.cc", + "low_energy_central_server.cc", + "low_energy_connection_server.cc", + "low_energy_peripheral_server.cc", + "profile_server.cc", + ], + copts = ["-Wno-format"], + deps = [ + ":helpers", + ":public", + "//pw_bluetooth_sapphire/fuchsia/host/fidl/measure_tape:peer_hlcpp", + "//pw_bluetooth_sapphire/fuchsia/host/fidl/measure_tape:read_by_type_result_hlcpp", + "//pw_bluetooth_sapphire/fuchsia/lib/fidl", + "//pw_bluetooth_sapphire/host:stack", + "@fuchsia_sdk//fidl/fuchsia.bluetooth:fuchsia.bluetooth_hlcpp", + "@fuchsia_sdk//fidl/fuchsia.bluetooth.gatt:fuchsia.bluetooth.gatt_hlcpp", + "@fuchsia_sdk//fidl/fuchsia.bluetooth.gatt2:fuchsia.bluetooth.gatt2_hlcpp", + "@fuchsia_sdk//fidl/fuchsia.bluetooth.host:fuchsia.bluetooth.host_hlcpp", + "@fuchsia_sdk//fidl/fuchsia.bluetooth.le:fuchsia.bluetooth.le_hlcpp", + "@fuchsia_sdk//pkg/fit-promise", + "@fuchsia_sdk//pkg/zx", + "@pigweed//pw_intrusive_ptr", + ], +) + +cc_library( + name = "helpers", + srcs = [ + "helpers.cc", + ], + copts = ["-Wno-format"], + deps = [ + ":public", + "//pw_bluetooth_sapphire/host/common", + "//pw_bluetooth_sapphire/host/common:uuid_string_util", + "//pw_bluetooth_sapphire/host/gap", + "//pw_bluetooth_sapphire/host/gatt", + "//pw_bluetooth_sapphire/host/sdp", + "@fuchsia_sdk//fidl/fuchsia.bluetooth.bredr:fuchsia.bluetooth.bredr_cpp", + "@fuchsia_sdk//fidl/fuchsia.bluetooth:fuchsia.bluetooth_hlcpp", + "@fuchsia_sdk//fidl/fuchsia.bluetooth.gatt:fuchsia.bluetooth.gatt_hlcpp", + "@fuchsia_sdk//fidl/fuchsia.bluetooth.gatt2:fuchsia.bluetooth.gatt2_hlcpp", + "@fuchsia_sdk//fidl/fuchsia.bluetooth.host:fuchsia.bluetooth.host_hlcpp", + "@fuchsia_sdk//fidl/fuchsia.hardware.bluetooth:fuchsia.hardware.bluetooth_cpp", + ], +) + +cc_library( + name = "adapter_test_fixture", + testonly = True, + srcs = [ + "adapter_test_fixture.cc", + ], + deps = [ + ":public", + "//pw_async_fuchsia:dispatcher", + "//pw_bluetooth_sapphire/host/gap", + "//pw_bluetooth_sapphire/host/gatt:testing", + "//pw_bluetooth_sapphire/host/l2cap:testing", + "//pw_bluetooth_sapphire/host/testing", + "//pw_bluetooth_sapphire/host/testing:fake_controller", + "//pw_bluetooth_sapphire/host/testing:loop_fixture", + "@fuchsia_sdk//fidl/fuchsia.io:fuchsia.io_hlcpp", + ], +) + +cc_library( + name = "fake_adapter_test_fixture", + testonly = True, + deps = [ + ":public", + "//pw_async_fuchsia:dispatcher", + "//pw_bluetooth_sapphire/host/common", + "//pw_bluetooth_sapphire/host/gatt", + "//pw_bluetooth_sapphire/host/gatt:testing", + "//pw_bluetooth_sapphire/host/testing:loop_fixture", + ], +) + +cc_library( + name = "fake_gatt_test_fixture", + testonly = True, + srcs = [ + "fake_gatt_fixture.cc", + ], + deps = [ + ":public", + "//pw_async_fuchsia:dispatcher", + "//pw_bluetooth_sapphire/host/common", + "//pw_bluetooth_sapphire/host/gatt", + "//pw_bluetooth_sapphire/host/gatt:testing", + "//pw_bluetooth_sapphire/host/testing:loop_fixture", + ], +) + +cc_library( + name = "fake_vendor_server", + testonly = True, + srcs = [ + "fake_hci_transport_server.cc", + ], + deps = [ + ":public", + "//pw_bluetooth_sapphire/host/common", + "//pw_bluetooth_sapphire/host/iso", + "//pw_bluetooth_sapphire/host/transport", + "//pw_unit_test", + "@com_google_googletest//:gtest", + "@fuchsia_sdk//fidl/fuchsia.hardware.bluetooth:fuchsia.hardware.bluetooth_cpp", + "@fuchsia_sdk//pkg/async-cpp", + "@fuchsia_sdk//pkg/fidl_cpp", + "@fuchsia_sdk//pkg/zx", + ], +) + +fuchsia_cc_test( + name = "fidl_test", + testonly = True, + srcs = [ + "bredr_connection_server_test.cc", + "gatt2_client_server_test.cc", + "gatt2_remote_service_server_test.cc", + "gatt2_server_server_test.cc", + "gatt_client_server_test.cc", + "gatt_remote_service_server_test.cc", + "helpers_test.cc", + "host_server_test.cc", + "iso_stream_server_test.cc", + "low_energy_central_server_test.cc", + "low_energy_connection_server_test.cc", + "low_energy_peripheral_server_test.cc", + "profile_server_test.cc", + ], + death_unittest = True, + visibility = ["//visibility:public"], + deps = [ + ":adapter_test_fixture", + ":fake_adapter_test_fixture", + ":fake_gatt_test_fixture", + ":fidl", + "//pw_bluetooth_sapphire/host/gap:testing", + "//pw_bluetooth_sapphire/host/gatt:testing", + "//pw_bluetooth_sapphire/host/testing", + "//pw_bluetooth_sapphire/host/testing:fake_controller", + "//pw_bluetooth_sapphire/host/testing:gtest_main", + "//pw_bluetooth_sapphire/host/testing:loop_fixture", + ], +) + +fuchsia_unittest_package( + name = "test_pkg", + package_name = "bt_host_fidl_tests_bazel", + testonly = True, + fuchsia_api_level = "HEAD", + tags = ["manual"], + unit_tests = [ + ":fidl_test", + ], + visibility = ["//visibility:public"], +) diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/adapter_test_fixture.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/adapter_test_fixture.cc new file mode 100644 index 0000000000..85f013575e --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/adapter_test_fixture.cc @@ -0,0 +1,67 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/fidl/adapter_test_fixture.h" + +namespace bthost::testing { + +using bt::testing::FakeController; +using TestingBase = bt::testing::ControllerTest; + +void AdapterTestFixture::SetUp() { + FakeController::Settings settings; + settings.ApplyDualModeDefaults(); + SetUp(settings); +} + +void AdapterTestFixture::SetUp( + FakeController::Settings settings, + pw::bluetooth::Controller::FeaturesBits features) { + TestingBase::Initialize(features, /*initialize_transport=*/false); + + auto l2cap = std::make_unique(pw_dispatcher()); + l2cap_ = l2cap.get(); + gatt_ = std::make_unique(pw_dispatcher()); + bt::gap::Adapter::Config config = { + .legacy_pairing_enabled = false, + }; + adapter_ = bt::gap::Adapter::Create(pw_dispatcher(), + transport()->GetWeakPtr(), + gatt_->GetWeakPtr(), + config, + std::move(l2cap)); + + test_device()->set_settings(settings); + + bool success = false; + adapter_->Initialize([&](bool result) { success = result; }, [] {}); + RunLoopUntilIdle(); + ASSERT_TRUE(success); + ASSERT_TRUE(adapter_->le()); + ASSERT_TRUE(adapter_->bredr()); +} + +void AdapterTestFixture::TearDown() { + // Drain all scheduled tasks. + RunLoopUntilIdle(); + + // Cleanly shut down the stack. + l2cap_ = nullptr; + adapter_ = nullptr; + RunLoopUntilIdle(); + + gatt_ = nullptr; +} + +} // namespace bthost::testing diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/bredr_connection_server.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/bredr_connection_server.cc new file mode 100644 index 0000000000..6831b37866 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/bredr_connection_server.cc @@ -0,0 +1,244 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/fidl/bredr_connection_server.h" + +#include +namespace fidlbredr = fuchsia::bluetooth::bredr; +namespace fidlbt = fuchsia::bluetooth; + +namespace bthost { + +BrEdrConnectionServer::BrEdrConnectionServer( + fidl::InterfaceRequest request, + bt::l2cap::Channel::WeakPtr channel, + fit::callback closed_callback) + : ServerBase(this, std::move(request)), + channel_(std::move(channel)), + closed_cb_(std::move(closed_callback)), + weak_self_(this) { + binding()->set_error_handler( + [this](zx_status_t /*status*/) { OnProtocolClosed(); }); +} + +BrEdrConnectionServer::~BrEdrConnectionServer() { + if (state_ != State::kDeactivated) { + bt_log(TRACE, "fidl", "Deactivating channel %u in dtor", channel_->id()); + Deactivate(); + } +} + +void BrEdrConnectionServer::Send( + std::vector<::fuchsia::bluetooth::Packet> packets, SendCallback callback) { + for (auto& fidl_packet : packets) { + std::vector& packet = fidl_packet.packet; + if (packet.size() > channel_->max_tx_sdu_size()) { + bt_log(TRACE, + "fidl", + "Dropping %zu bytes for channel %u as max TX SDU is %u ", + packet.size(), + channel_->id(), + channel_->max_tx_sdu_size()); + continue; + } + + // TODO(fxbug.dev/349653544): Avoid making a copy of `packet`, possibly by + // making DynamicByteBuffer wrap a std::vector. + auto buffer = + std::make_unique(bt::BufferView(packet)); + bool write_success = channel_->Send(std::move(buffer)); + if (!write_success) { + bt_log(TRACE, + "fidl", + "Failed to write %zu bytes to channel %u", + buffer->size(), + channel_->id()); + } + } + + fidlbt::Channel_Send_Response response; + // NOLINTNEXTLINE(performance-move-const-arg) + callback(fidlbt::Channel_Send_Result::WithResponse(std::move(response))); +} + +void BrEdrConnectionServer::Receive(ReceiveCallback callback) { + if (receive_cb_) { + binding()->Close(ZX_ERR_BAD_STATE); + OnProtocolClosed(); + return; + } + receive_cb_ = std::move(callback); + ServiceReceiveQueue(); +} + +void BrEdrConnectionServer::WatchChannelParameters( + WatchChannelParametersCallback callback) { + BT_ASSERT_MSG( + !pending_watch_channel_parameters_.has_value(), + "WatchChannelParameters called while there was already a pending call."); + pending_watch_channel_parameters_ = std::move(callback); +} + +void BrEdrConnectionServer::handle_unknown_method(uint64_t ordinal, + bool method_has_response) { + bt_log(WARN, + "fidl", + "BrEdrConnectionServer: received unknown method (ordinal: %lu)", + ordinal); +} + +bool BrEdrConnectionServer::Activate() { + BT_ASSERT(state_ == State::kActivating); + + WeakPtr self = weak_self_.GetWeakPtr(); + bt::l2cap::ChannelId channel_id = channel_->id(); + bool activate_success = channel_->Activate( + [self, channel_id](bt::ByteBufferPtr rx_data) { + // Note: this lambda _may_ be invoked immediately for buffered packets. + if (self.is_alive()) { + self->OnChannelDataReceived(std::move(rx_data)); + } else { + bt_log( + TRACE, + "fidl", + "Ignoring data received on destroyed server (channel_id=%#.4x)", + channel_id); + } + }, + [self, channel_id] { + if (self.is_alive()) { + self->OnChannelClosed(); + } else { + bt_log( + TRACE, + "fidl", + "Ignoring channel closure on destroyed server (channel_id=%#.4x)", + channel_id); + } + }); + if (!activate_success) { + return false; + } + + state_ = State::kActivated; + return true; +} + +void BrEdrConnectionServer::Deactivate() { + BT_ASSERT(state_ != State::kDeactivated); + state_ = State::kDeactivating; + + if (!receive_queue_.empty()) { + bt_log(DEBUG, + "fidl", + "Dropping %zu packets from channel %u due to channel closure", + receive_queue_.size(), + channel_->id()); + receive_queue_.clear(); + } + channel_->Deactivate(); + binding()->Close(ZX_ERR_CONNECTION_RESET); + + state_ = State::kDeactivated; +} + +void BrEdrConnectionServer::OnChannelDataReceived(bt::ByteBufferPtr rx_data) { + // Note: kActivating is deliberately permitted, as ChannelImpl::Activate() + // will synchronously deliver any queued frames. + BT_ASSERT(state_ != State::kDeactivated); + if (state_ == State::kDeactivating) { + bt_log(DEBUG, + "fidl", + "Ignoring %s for channel %u while deactivating", + __func__, + channel_->id()); + return; + } + + BT_ASSERT(rx_data); + if (rx_data->size() == 0) { + bt_log( + DEBUG, "fidl", "Ignoring empty rx_data for channel %u", channel_->id()); + return; + } + + BT_ASSERT(receive_queue_.size() <= receive_queue_max_frames_); + // On a full queue, we drop the oldest element, on the theory that newer data + // is more useful. This should be true, e.g., for real-time applications such + // as voice calls. In the future, we may want to make the drop-head vs. + // drop-tail choice configurable. + if (receive_queue_.size() == receive_queue_max_frames_) { + // TODO(fxbug.dev/42082614): Add a metric for number of dropped frames. + receive_queue_.pop_front(); + } + + receive_queue_.push_back(std::move(rx_data)); + ServiceReceiveQueue(); +} + +void BrEdrConnectionServer::OnChannelClosed() { + if (state_ == State::kDeactivating) { + bt_log(DEBUG, + "fidl", + "Ignoring %s for channel %u while deactivating", + __func__, + channel_->id()); + return; + } + BT_ASSERT(state_ == State::kActivated); + DeactivateAndRequestDestruction(); +} + +void BrEdrConnectionServer::OnProtocolClosed() { + DeactivateAndRequestDestruction(); +} + +void BrEdrConnectionServer::DeactivateAndRequestDestruction() { + Deactivate(); + // closed_cb_ is expected to destroy `this`, so move the callback first. + auto closed_cb = std::move(closed_cb_); + closed_cb(); +} + +void BrEdrConnectionServer::ServiceReceiveQueue() { + if (!receive_cb_ || receive_queue_.empty()) { + return; + } + std::vector buffer = receive_queue_.front()->ToVector(); + receive_queue_.pop_front(); + + ::fuchsia::bluetooth::Channel_Receive_Response response( + {fidlbt::Packet{std::move(buffer)}}); + receive_cb_( + fidlbt::Channel_Receive_Result::WithResponse(std::move(response))); + receive_cb_ = nullptr; +} + +std::unique_ptr BrEdrConnectionServer::Create( + fidl::InterfaceRequest request, + bt::l2cap::Channel::WeakPtr channel, + fit::callback closed_callback) { + if (!channel.is_alive()) { + return nullptr; + } + + std::unique_ptr server(new BrEdrConnectionServer( + std::move(request), std::move(channel), std::move(closed_callback))); + + if (!server->Activate()) { + return nullptr; + } + return server; +} +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/bredr_connection_server_test.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/bredr_connection_server_test.cc new file mode 100644 index 0000000000..442c615f04 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/bredr_connection_server_test.cc @@ -0,0 +1,241 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/fidl/bredr_connection_server.h" + +#include + +#include + +#include "pw_bluetooth_sapphire/internal/host/l2cap/fake_channel.h" +#include "pw_bluetooth_sapphire/internal/host/testing/loop_fixture.h" +#include "pw_bluetooth_sapphire/internal/host/testing/test_helpers.h" + +using FakeChannel = bt::l2cap::testing::FakeChannel; +using Channel = fuchsia::bluetooth::Channel; +namespace fbt = fuchsia::bluetooth; + +namespace bthost { +namespace { + +class BrEdrConnectionServerTest : public bt::testing::TestLoopFixture { + public: + BrEdrConnectionServerTest() + : fake_chan_( + /*id=*/1, /*remote_id=*/2, /*handle=*/3, bt::LinkType::kACL) {} + + FakeChannel& fake_chan() { return fake_chan_; } + + private: + FakeChannel fake_chan_; +}; + +class BrEdrConnectionServerChannelActivatedTest + : public BrEdrConnectionServerTest { + public: + void SetUp() override { + BrEdrConnectionServerTest::SetUp(); + fidl::InterfaceHandle handle; + auto closed_cb = [this]() { server_closed_ = true; }; + server_ = BrEdrConnectionServer::Create( + handle.NewRequest(), fake_chan().AsWeakPtr(), std::move(closed_cb)); + ASSERT_TRUE(server_); + ASSERT_TRUE(fake_chan().activated()); + client_ = handle.Bind(); + } + + void TearDown() override { + BrEdrConnectionServerTest::TearDown(); + if (client_.is_bound()) { + client_.Unbind(); + } + server_.reset(); + RunLoopUntilIdle(); + } + + fidl::InterfacePtr& client() { return client_; } + + bool server_closed() const { return server_closed_; } + + void DestroyServer() { server_.reset(); } + + private: + bool server_closed_ = false; + std::unique_ptr server_; + fidl::InterfacePtr client_; +}; + +TEST_F(BrEdrConnectionServerChannelActivatedTest, SendTwoPackets) { + std::vector sent_packets; + auto chan_send_cb = [&](bt::ByteBufferPtr buffer) { + sent_packets.push_back(std::move(buffer)); + }; + fake_chan().SetSendCallback(std::move(chan_send_cb)); + + const std::vector packet_0 = {0x00, 0x01, 0x03}; + const std::vector packet_1 = {0x04, 0x05, 0x06}; + int send_cb_count = 0; + std::vector packets{fbt::Packet{packet_0}, + fbt::Packet{packet_1}}; + client()->Send(std::move(packets), + [&](fuchsia::bluetooth::Channel_Send_Result result) { + EXPECT_TRUE(result.is_response()); + send_cb_count++; + }); + RunLoopUntilIdle(); + EXPECT_EQ(send_cb_count, 1); + ASSERT_EQ(sent_packets.size(), 2u); + EXPECT_THAT(*sent_packets[0], bt::BufferEq(packet_0)); + EXPECT_THAT(*sent_packets[1], bt::BufferEq(packet_1)); +} + +TEST_F(BrEdrConnectionServerChannelActivatedTest, SendTwoPacketsSeparately) { + std::vector sent_packets; + auto chan_send_cb = [&](bt::ByteBufferPtr buffer) { + sent_packets.push_back(std::move(buffer)); + }; + fake_chan().SetSendCallback(std::move(chan_send_cb)); + + const std::vector packet_0 = {0x00, 0x01, 0x03}; + int send_cb_count = 0; + std::vector packets_0{fbt::Packet{packet_0}}; + client()->Send(std::move(packets_0), + [&](fuchsia::bluetooth::Channel_Send_Result result) { + EXPECT_TRUE(result.is_response()); + send_cb_count++; + }); + RunLoopUntilIdle(); + EXPECT_EQ(send_cb_count, 1); + ASSERT_EQ(sent_packets.size(), 1u); + EXPECT_THAT(*sent_packets[0], bt::BufferEq(packet_0)); + + const std::vector packet_1 = {0x04, 0x05, 0x06}; + std::vector packets_1{fbt::Packet{packet_1}}; + client()->Send(std::move(packets_1), + [&](fuchsia::bluetooth::Channel_Send_Result result) { + EXPECT_TRUE(result.is_response()); + send_cb_count++; + }); + RunLoopUntilIdle(); + EXPECT_EQ(send_cb_count, 2); + ASSERT_EQ(sent_packets.size(), 2u); + EXPECT_THAT(*sent_packets[1], bt::BufferEq(packet_1)); +} + +TEST_F(BrEdrConnectionServerChannelActivatedTest, + SendTooLargePacketDropsPacket) { + std::vector sent_packets; + auto chan_send_cb = [&](bt::ByteBufferPtr buffer) { + sent_packets.push_back(std::move(buffer)); + }; + fake_chan().SetSendCallback(std::move(chan_send_cb)); + + const std::vector packet_0(/*count=*/bt::l2cap::kDefaultMTU + 1, + /*value=*/0x03); + std::vector packets_0{fbt::Packet{packet_0}}; + int send_cb_count = 0; + client()->Send(packets_0, + [&](fuchsia::bluetooth::Channel_Send_Result result) { + EXPECT_TRUE(result.is_response()); + send_cb_count++; + }); + RunLoopUntilIdle(); + EXPECT_EQ(send_cb_count, 1); + ASSERT_EQ(sent_packets.size(), 0u); +} + +TEST_F(BrEdrConnectionServerChannelActivatedTest, + ReceiveManyPacketsAndDropSome) { + for (uint8_t i = 0; i < 2 * BrEdrConnectionServer::kDefaultReceiveQueueLimit; + i++) { + bt::StaticByteBuffer packet(i, 0x01, 0x02); + fake_chan().Receive(packet); + } + RunLoopUntilIdle(); + + std::vector> packets; + for (uint8_t i = 0; i < BrEdrConnectionServer::kDefaultReceiveQueueLimit; + i++) { + client()->Receive( + [&packets](::fuchsia::bluetooth::Channel_Receive_Result result) { + ASSERT_TRUE(result.is_response()); + packets.push_back(std::move(result.response().packets)); + }); + RunLoopUntilIdle(); + ASSERT_EQ(packets.size(), static_cast(i + 1)); + } + + // Some packets were dropped, so only the packets under the queue limit should + // be received. + size_t first_packet_in_q = BrEdrConnectionServer::kDefaultReceiveQueueLimit; + for (size_t i = 0; i < BrEdrConnectionServer::kDefaultReceiveQueueLimit; + i++) { + std::vector packet{ + static_cast(first_packet_in_q + i), 0x01, 0x02}; + ASSERT_EQ(packets[i].size(), 1u); + EXPECT_EQ(packets[i][0].packet, packet); + } +} + +TEST_F(BrEdrConnectionServerChannelActivatedTest, + ReceiveTwiceWithoutResponseClosesConnection) { + std::optional error; + client().set_error_handler([&](zx_status_t status) { error = status; }); + client()->Receive( + [](::fuchsia::bluetooth::Channel_Receive_Result result) { FAIL(); }); + client()->Receive( + [](::fuchsia::bluetooth::Channel_Receive_Result result) { FAIL(); }); + RunLoopUntilIdle(); + ASSERT_TRUE(error.has_value()); + EXPECT_EQ(error.value(), ZX_ERR_BAD_STATE); + client().set_error_handler([](zx_status_t status) {}); +} + +TEST_F(BrEdrConnectionServerChannelActivatedTest, ChannelCloses) { + std::optional error; + client().set_error_handler([&](zx_status_t status) { error = status; }); + fake_chan().Close(); + EXPECT_TRUE(server_closed()); + RunLoopUntilIdle(); + ASSERT_TRUE(error.has_value()); + EXPECT_EQ(error.value(), ZX_ERR_CONNECTION_RESET); +} + +TEST_F(BrEdrConnectionServerChannelActivatedTest, ClientCloses) { + client().Unbind(); + RunLoopUntilIdle(); + EXPECT_TRUE(server_closed()); + EXPECT_FALSE(fake_chan().activated()); +} + +TEST_F(BrEdrConnectionServerTest, ActivateFails) { + fake_chan().set_activate_fails(true); + fidl::InterfaceHandle handle; + bool server_closed = false; + auto closed_cb = [&]() { server_closed = true; }; + std::unique_ptr server = BrEdrConnectionServer::Create( + handle.NewRequest(), fake_chan().AsWeakPtr(), std::move(closed_cb)); + EXPECT_FALSE(server); + EXPECT_FALSE(server_closed); +} + +TEST_F(BrEdrConnectionServerChannelActivatedTest, + DeactivateOnServerDestruction) { + EXPECT_TRUE(fake_chan().activated()); + DestroyServer(); + EXPECT_FALSE(fake_chan().activated()); +} + +} // namespace +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/fake_gatt_fixture.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/fake_gatt_fixture.cc new file mode 100644 index 0000000000..067b3270ab --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/fake_gatt_fixture.cc @@ -0,0 +1,26 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/fidl/fake_gatt_fixture.h" + +namespace bt::fidl::testing { + +FakeGattFixture::FakeGattFixture() + : gatt_(std::make_unique(pw_dispatcher_)), + weak_gatt_(gatt_->GetWeakPtr()), + weak_fake_layer_(gatt_->GetFakePtr()) {} + +void FakeGattFixture::TearDown() { RunLoopUntilIdle(); } + +} // namespace bt::fidl::testing diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/fake_hci_transport_server.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/fake_hci_transport_server.cc new file mode 100644 index 0000000000..ee026472a7 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/fake_hci_transport_server.cc @@ -0,0 +1,185 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/fidl/fake_hci_transport_server.h" + +#include "gtest/gtest.h" +#include "pw_bluetooth_sapphire/internal/host/common/byte_buffer.h" + +namespace fhbt = ::fuchsia_hardware_bluetooth; + +namespace bt::fidl::testing { + +FakeHciTransportServer::FakeHciTransportServer( + ::fidl::ServerEnd server_end, + async_dispatcher_t* dispatcher) + : dispatcher_(dispatcher), + binding_(::fidl::BindServer( + dispatcher_, + std::move(server_end), + this, + std::mem_fn(&FakeHciTransportServer::OnUnbound))) {} + +zx_status_t FakeHciTransportServer::SendEvent(const BufferView& event) { + ::fit::result<::fidl::OneWayError> result = + ::fidl::SendEvent(binding_)->OnReceive( + ::fuchsia_hardware_bluetooth::ReceivedPacket::WithEvent( + event.ToVector())); + return result.is_ok() ? ZX_OK : result.error_value().status(); +} + +zx_status_t FakeHciTransportServer::SendAcl(const BufferView& buffer) { + ::fit::result<::fidl::OneWayError> result = + ::fidl::SendEvent(binding_)->OnReceive( + ::fuchsia_hardware_bluetooth::ReceivedPacket::WithAcl( + buffer.ToVector())); + return result.is_ok() ? ZX_OK : result.error_value().status(); +} + +zx_status_t FakeHciTransportServer::SendSco(const BufferView& buffer) { + if (!sco_server_) { + return ZX_ERR_UNAVAILABLE; + } + return sco_server_->Send(buffer); +} + +zx_status_t FakeHciTransportServer::SendIso(const BufferView& buffer) { + ::fit::result<::fidl::OneWayError> result = + ::fidl::SendEvent(binding_)->OnReceive( + ::fuchsia_hardware_bluetooth::ReceivedPacket::WithIso( + buffer.ToVector())); + return result.is_ok() ? ZX_OK : result.error_value().status(); +} + +bool FakeHciTransportServer::UnbindSco() { + if (!sco_server_) { + return false; + } + sco_server_->Unbind(); + sco_server_.reset(); + return true; +} + +FakeHciTransportServer::ScoConnectionServer::ScoConnectionServer( + ::fidl::ServerEnd server_end, + async_dispatcher_t* dispatcher, + FakeHciTransportServer* hci_server) + : hci_server_(hci_server), + binding_(::fidl::BindServer(dispatcher, std::move(server_end), this)) {} + +zx_status_t FakeHciTransportServer::ScoConnectionServer::Send( + const BufferView& buffer) { + ::fuchsia_hardware_bluetooth::ScoPacket packet(buffer.ToVector()); + fit::result<::fidl::OneWayError> result = + ::fidl::SendEvent(binding_)->OnReceive(packet); + return result.is_ok() ? ZX_OK : result.error_value().status(); +} + +void FakeHciTransportServer::ScoConnectionServer::Unbind() { + binding_.Unbind(); +} + +void FakeHciTransportServer::ScoConnectionServer::Send( + SendRequest& request, SendCompleter::Sync& completer) { + hci_server_->sco_packets_received_.emplace_back(BufferView(request.packet())); + completer.Reply(); +} + +void FakeHciTransportServer::ScoConnectionServer::AckReceive( + AckReceiveCompleter::Sync& completer) { + hci_server_->sco_ack_receive_count_++; +} + +void FakeHciTransportServer::ScoConnectionServer::Stop( + StopCompleter::Sync& completer) { + binding_.Close(ZX_ERR_CANCELED); + if (hci_server_->reset_sco_cb_) { + hci_server_->reset_sco_cb_(); + } + hci_server_->sco_server_.reset(); +} + +void FakeHciTransportServer::ScoConnectionServer::handle_unknown_method( + ::fidl::UnknownMethodMetadata + metadata, + ::fidl::UnknownMethodCompleter::Sync& completer) { + FAIL(); +} + +void FakeHciTransportServer::ScoConnectionServer::OnUnbound( + ::fidl::UnbindInfo info, + ::fidl::ServerEnd server_end) { + if (info.is_user_initiated()) { + return; + } + if (info.is_peer_closed()) { + FAIL() << "OnUnbound() called before Stop()"; + } + + hci_server_->sco_server_.reset(); +} + +void FakeHciTransportServer::Send(SendRequest& request, + SendCompleter::Sync& completer) { + switch (request.Which()) { + case SendRequest::Tag::kIso: + iso_packets_received_.emplace_back(BufferView(request.iso().value())); + break; + case SendRequest::Tag::kAcl: + acl_packets_received_.emplace_back(BufferView(request.acl().value())); + break; + case SendRequest::Tag::kCommand: + commands_received_.emplace_back(BufferView(request.command().value())); + break; + default: + FAIL() << "Send(): unknown packet type"; + } + completer.Reply(); +} + +void FakeHciTransportServer::AckReceive(AckReceiveCompleter::Sync& completer) { + ack_receive_count_++; +} + +void FakeHciTransportServer::ConfigureSco( + ConfigureScoRequest& request, ConfigureScoCompleter::Sync& completer) { + if (!request.connection() || !request.coding_format() || + !request.sample_rate() || !request.encoding()) { + return; + } + + ASSERT_FALSE(sco_server_); + sco_server_.emplace( + std::move(request.connection().value()), dispatcher_, this); + + if (check_configure_sco_) { + check_configure_sco_( + *request.coding_format(), *request.encoding(), *request.sample_rate()); + } +} + +void FakeHciTransportServer::handle_unknown_method( + ::fidl::UnknownMethodMetadata + metadata, + ::fidl::UnknownMethodCompleter::Sync& completer) { + FAIL(); +} + +void FakeHciTransportServer::OnUnbound( + ::fidl::UnbindInfo info, + ::fidl::ServerEnd server_end) { + bound_ = false; +} + +} // namespace bt::fidl::testing diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_client_server.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_client_server.cc new file mode 100644 index 0000000000..fe80b7146b --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_client_server.cc @@ -0,0 +1,347 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_client_server.h" + +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/helpers.h" + +namespace fb = fuchsia::bluetooth; +namespace fbg = fuchsia::bluetooth::gatt2; + +namespace bthost { + +namespace { + +fbg::ServiceInfo RemoteServiceToFidlServiceInfo( + const bt::gatt::RemoteService::WeakPtr& svc) { + fbg::ServiceInfo out; + out.set_handle(fbg::ServiceHandle{svc->handle()}); + auto kind = svc->info().kind == bt::gatt::ServiceKind::PRIMARY + ? fbg::ServiceKind::PRIMARY + : fbg::ServiceKind::SECONDARY; + out.set_kind(kind); + out.set_type(fb::Uuid{svc->uuid().value()}); + return out; +} + +} // namespace + +Gatt2ClientServer::Gatt2ClientServer( + bt::gatt::PeerId peer_id, + bt::gatt::GATT::WeakPtr weak_gatt, + fidl::InterfaceRequest request, + fit::callback error_cb) + : GattServerBase(std::move(weak_gatt), /*impl=*/this, std::move(request)), + peer_id_(peer_id), + server_error_cb_(std::move(error_cb)), + weak_self_(this) { + set_error_handler([this](zx_status_t) { + if (server_error_cb_) { + server_error_cb_(); + } + }); + + // It is safe to bind |this| to the callback because the service watcher is + // unregistered in the destructor. + service_watcher_id_ = gatt()->RegisterRemoteServiceWatcherForPeer( + peer_id_, [this](auto removed, auto added, auto modified) { + // Ignore results before the initial call to ListServices() completes to + // avoid redundant notifications. + if (!list_services_complete_) { + bt_log(TRACE, + "fidl", + "ignoring service watcher update before ListServices() result " + "received"); + return; + } + OnWatchServicesResult(removed, added, modified); + }); +} + +Gatt2ClientServer::~Gatt2ClientServer() { + BT_ASSERT(gatt()->UnregisterRemoteServiceWatcher(service_watcher_id_)); +} + +void Gatt2ClientServer::OnWatchServicesResult( + const std::vector& removed, + const bt::gatt::ServiceList& added, + const bt::gatt::ServiceList& modified) { + // Accumulate all removed services and send in next result. + if (!next_watch_services_result_.has_value()) { + next_watch_services_result_.emplace(); + } + next_watch_services_result_->removed.insert(removed.begin(), removed.end()); + + // Remove any stale updated services (to avoid sending an invalid one to the + // client). + for (bt::att::Handle handle : removed) { + next_watch_services_result_->updated.erase(handle); + } + + // Replace any existing updated services with same handle and add new updates. + for (const bt::gatt::RemoteService::WeakPtr& svc : added) { + next_watch_services_result_->updated[svc->handle()] = svc; + } + for (const bt::gatt::RemoteService::WeakPtr& svc : modified) { + next_watch_services_result_->updated[svc->handle()] = svc; + } + + bt_log(TRACE, + "fidl", + "next watch services result: (removed: %zu, updated: %zu) (peer: %s)", + next_watch_services_result_->removed.size(), + next_watch_services_result_->updated.size(), + bt_str(peer_id_)); + + TrySendNextWatchServicesResult(); +} + +void Gatt2ClientServer::TrySendNextWatchServicesResult() { + if (!watch_services_request_ || !next_watch_services_result_) { + return; + } + + std::vector fidl_removed; + std::transform( + next_watch_services_result_->removed.begin(), + next_watch_services_result_->removed.end(), + std::back_inserter(fidl_removed), + [](const bt::att::Handle& handle) { return fbg::Handle{handle}; }); + + // Don't filter removed services by UUID because we don't know the UUIDs of + // these services currently. + // TODO(fxbug.dev/42111895): Filter removed services by UUID. + + std::vector fidl_updated; + for (const ServiceMap::value_type& svc_pair : + next_watch_services_result_->updated) { + // Filter updated services by UUID. + // NOTE: If clients change UUIDs they are requesting across requests, they + // won't receive existing service with the new UUIDs, only new ones + if (prev_watch_services_uuids_.empty() || + prev_watch_services_uuids_.count(svc_pair.second->uuid()) == 1) { + fidl_updated.push_back(RemoteServiceToFidlServiceInfo(svc_pair.second)); + } + } + + next_watch_services_result_.reset(); + + // Skip sending results that are empty after filtering services by UUID. + if (fidl_removed.empty() && fidl_updated.empty()) { + bt_log(TRACE, + "fidl", + "skipping service watcher update without matching UUIDs (peer: %s)", + bt_str(peer_id_)); + return; + } + + // TODO(fxbug.dev/42165836): Use measure-tape to verify response fits in FIDL + // channel before sending. This is only an issue for peers with very large + // databases. + bt_log(TRACE, + "fidl", + "notifying WatchServices() callback (removed: %zu, updated: %zu, " + "peer: %s)", + fidl_removed.size(), + fidl_updated.size(), + bt_str(peer_id_)); + watch_services_request_.value()(std::move(fidl_updated), + std::move(fidl_removed)); + watch_services_request_.reset(); +} + +// TODO(fxbug.dev/42165818): Do not send privileged services (e.g. Generic +// Attribute Profile Service) to clients. +void Gatt2ClientServer::WatchServices(std::vector fidl_uuids, + WatchServicesCallback callback) { + std::unordered_set uuids; + std::transform(fidl_uuids.begin(), + fidl_uuids.end(), + std::inserter(uuids, uuids.begin()), + [](const fb::Uuid& uuid) { return bt::UUID(uuid.value); }); + + // If the UUID filter list is changed between requests, perform a fresh + // ListServices() call to ensure existing services that match the new UUIDs + // are reported to the client. Dropping the old watch_services_request_ with + // no new results. + if (uuids != prev_watch_services_uuids_) { + bt_log(DEBUG, + "fidl", + "WatchServices: UUIDs changed from previous call (peer: %s)", + bt_str(peer_id_)); + list_services_complete_ = false; + // Clear old watch service results as we're about to get a fresh list of + // services. + next_watch_services_result_.reset(); + prev_watch_services_uuids_ = uuids; + if (watch_services_request_) { + watch_services_request_.value()({}, {}); + watch_services_request_.reset(); + } + } + + // Only allow 1 callback at a time. Close the server if this is violated. + if (watch_services_request_) { + bt_log(WARN, + "fidl", + "%s: call received while previous call is still pending", + __FUNCTION__); + binding()->Close(ZX_ERR_ALREADY_BOUND); + server_error_cb_(); + return; + } + + watch_services_request_.emplace(std::move(callback)); + + auto self = weak_self_.GetWeakPtr(); + + // Return a complete service snapshot on the first call, or on calls that use + // a new UUID filter list. + if (!list_services_complete_) { + std::vector uuids_vector(uuids.begin(), uuids.end()); + gatt()->ListServices( + peer_id_, + std::move(uuids_vector), + [self](bt::att::Result<> status, + const bt::gatt::ServiceList& services) { + if (!self.is_alive()) { + return; + } + if (bt_is_error(status, + INFO, + "fidl", + "WatchServices: ListServices failed (peer: %s)", + bt_str(self->peer_id_))) { + self->binding()->Close(ZX_ERR_CONNECTION_RESET); + self->server_error_cb_(); + return; + } + + bt_log(DEBUG, + "fidl", + "WatchServices: ListServices complete (peer: %s)", + bt_str(self->peer_id_)); + + BT_ASSERT(self->watch_services_request_); + self->list_services_complete_ = true; + self->OnWatchServicesResult( + /*removed=*/{}, /*added=*/services, /*modified=*/{}); + }); + return; + } + + TrySendNextWatchServicesResult(); +} + +void Gatt2ClientServer::ConnectToService( + fbg::ServiceHandle handle, + fidl::InterfaceRequest request) { + bt_log(DEBUG, "fidl", "%s: (handle: 0x%lX)", __FUNCTION__, handle.value); + + if (!fidl_helpers::IsFidlGattServiceHandleValid(handle)) { + request.Close(ZX_ERR_INVALID_ARGS); + return; + } + bt::att::Handle service_handle = static_cast(handle.value); + + // Only allow clients to have 1 RemoteService per service at a time to prevent + // race conditions between multiple RemoteService clients modifying a service, + // and to simplify implementation. A client shouldn't need more than 1 + // RemoteService per service at a time, but if they really need to, they can + // create multiple Client instances. + if (services_.count(service_handle) == 1) { + request.Close(ZX_ERR_ALREADY_EXISTS); + return; + } + + // Mark this connection as in progress. + services_.try_emplace(service_handle, nullptr); + + bt::gatt::RemoteService::WeakPtr service = + gatt()->FindService(peer_id_, service_handle); + if (!service.is_alive()) { + bt_log(INFO, + "fidl", + "service not found (peer: %s, handle: %#.4x)", + bt_str(peer_id_), + service_handle); + services_.erase(service_handle); + request.Close(ZX_ERR_NOT_FOUND); + return; + } + BT_ASSERT(service_handle == service->handle()); + + // This removed handler may be called long after the service is removed from + // the service map or this server is destroyed, since removed handlers are not + // unregistered. If the FIDL client connects->disconnects->connects, it is + // possible for this handler to be called twice (the second call should then + // do nothing). + auto self = weak_self_.GetWeakPtr(); + fit::closure removed_handler = [self, service_handle] { + if (!self.is_alive()) { + return; + } + bt_log(DEBUG, + "fidl", + "service removed (peer: %s, handle: %#.4x)", + bt_str(self->peer_id_), + service_handle); + auto svc_iter = self->services_.find(service_handle); + if (svc_iter == self->services_.end()) { + bt_log(TRACE, + "fidl", + "ignoring service removed callback for already removed service " + "(peer: %s, handle: " + "%#.4x)", + bt_str(self->peer_id_), + service_handle); + return; + } + svc_iter->second->Close(ZX_ERR_CONNECTION_RESET); + self->services_.erase(svc_iter); + }; + + // The only reason RemoteService::AddRemovedHandler() can fail is if the + // service is already shut down, but that should not be possible in this + // synchronous callback (the service would not have been returned in the first + // place). + BT_ASSERT_MSG(service->AddRemovedHandler(std::move(removed_handler)), + "adding service removed handler failed (service may be shut " + "down) (peer: %s, " + "handle: %#.4x)", + bt_str(peer_id_), + service_handle); + + std::unique_ptr remote_service_server = + std::make_unique( + std::move(service), gatt(), peer_id_, std::move(request)); + + // Even if there is already an error, this handler won't be called until the + // next yield to the event loop. + remote_service_server->set_error_handler( + [self, service_handle](zx_status_t status) { + bt_log(TRACE, + "fidl", + "FIDL channel error (peer: %s, handle: %#.4x)", + bt_str(self->peer_id_), + service_handle); + self->services_.erase(service_handle); + }); + + // Error handler should not have been called yet. + BT_ASSERT(services_.count(service_handle) == 1); + services_[service_handle] = std::move(remote_service_server); +} + +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_client_server_test.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_client_server_test.cc new file mode 100644 index 0000000000..8d4cfedfb2 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_client_server_test.cc @@ -0,0 +1,953 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_client_server.h" + +#include "fuchsia/bluetooth/gatt2/cpp/fidl.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/fake_gatt_fixture.h" + +namespace bthost { + +namespace { + +namespace fb = fuchsia::bluetooth; +namespace fbg = fuchsia::bluetooth::gatt2; + +constexpr bt::PeerId kPeerId(1); +constexpr bt::UUID kTestServiceUuid0(uint16_t{0xdead}); +constexpr bt::UUID kTestServiceUuid1(uint16_t{0xbeef}); +constexpr bt::UUID kTestServiceUuid3(uint16_t{0xbaad}); + +class Gatt2ClientServerTest : public bt::fidl::testing::FakeGattFixture { + public: + Gatt2ClientServerTest() = default; + ~Gatt2ClientServerTest() override = default; + + void SetUp() override { + server_ = std::make_unique( + kPeerId, gatt()->GetWeakPtr(), proxy_.NewRequest(), [this]() { + error_cb_called_ = true; + server_.reset(); + }); + proxy_.set_error_handler( + [this](zx_status_t epitaph) { proxy_epitaph_ = epitaph; }); + } + + fbg::Client* proxy() const { return proxy_.get(); } + + void UnbindProxy() { proxy_.Unbind(); } + + std::optional proxy_epitaph() const { return proxy_epitaph_; } + + bool server_error_cb_called() const { return error_cb_called_; } + + private: + std::unique_ptr server_; + fbg::ClientPtr proxy_; + bool error_cb_called_ = false; + std::optional proxy_epitaph_; + + BT_DISALLOW_COPY_ASSIGN_AND_MOVE(Gatt2ClientServerTest); +}; + +TEST_F(Gatt2ClientServerTest, FidlClientClosingProxyCallsServerErrorCallback) { + UnbindProxy(); + RunLoopUntilIdle(); + EXPECT_TRUE(server_error_cb_called()); +} + +TEST_F(Gatt2ClientServerTest, + WatchServicesListsServicesOnFirstRequestAndUpdatesOnSecondRequest) { + const bt::att::Handle kSvcStartHandle0(1); + const bt::att::Handle kSvcEndHandle0(kSvcStartHandle0); + bt::gatt::ServiceData svc_data_0(bt::gatt::ServiceKind::PRIMARY, + kSvcStartHandle0, + kSvcEndHandle0, + kTestServiceUuid0); + fake_gatt()->AddPeerService(kPeerId, svc_data_0); + + std::vector updated; + std::vector removed; + int watch_cb_count = 0; + proxy()->WatchServices(/*uuids=*/{}, + [&](std::vector cb_updated, + std::vector cb_removed) { + watch_cb_count++; + updated = std::move(cb_updated); + removed = std::move(cb_removed); + }); + + RunLoopUntilIdle(); + EXPECT_EQ(watch_cb_count, 1); + EXPECT_EQ(removed.size(), 0u); + ASSERT_EQ(updated.size(), 1u); + ASSERT_TRUE(updated[0].has_handle()); + EXPECT_EQ(updated[0].handle().value, kSvcStartHandle0); + ASSERT_TRUE(updated[0].has_kind()); + EXPECT_EQ(updated[0].kind(), fbg::ServiceKind::PRIMARY); + ASSERT_TRUE(updated[0].has_type()); + EXPECT_EQ(bt::UUID(updated[0].type().value), kTestServiceUuid0); + EXPECT_FALSE(updated[0].has_characteristics()); + EXPECT_FALSE(updated[0].has_includes()); + + updated.clear(); + removed.clear(); + watch_cb_count = 0; + // WatchServices before service update. Request should be queued. + proxy()->WatchServices(/*uuids=*/{}, + [&](std::vector cb_updated, + std::vector cb_removed) { + watch_cb_count++; + updated = std::move(cb_updated); + removed = std::move(cb_removed); + }); + + RunLoopUntilIdle(); + EXPECT_EQ(watch_cb_count, 0); + EXPECT_EQ(updated.size(), 0u); + EXPECT_EQ(removed.size(), 0u); + + const bt::att::Handle kSvcStartHandle1(2); + const bt::att::Handle kSvcEndHandle1(kSvcStartHandle1); + bt::gatt::ServiceData svc_data_1(bt::gatt::ServiceKind::PRIMARY, + kSvcStartHandle1, + kSvcEndHandle1, + kTestServiceUuid0); + fake_gatt()->AddPeerService(kPeerId, svc_data_1, /*notify=*/true); + + RunLoopUntilIdle(); + EXPECT_EQ(watch_cb_count, 1); + EXPECT_EQ(removed.size(), 0u); + EXPECT_EQ(updated.size(), 1u); + ASSERT_TRUE(updated[0].has_handle()); + EXPECT_EQ(updated[0].handle().value, kSvcStartHandle1); + ASSERT_TRUE(updated[0].has_kind()); + EXPECT_EQ(updated[0].kind(), fbg::ServiceKind::PRIMARY); + ASSERT_TRUE(updated[0].has_type()); + EXPECT_EQ(bt::UUID(updated[0].type().value), kTestServiceUuid0); + EXPECT_FALSE(updated[0].has_characteristics()); + EXPECT_FALSE(updated[0].has_includes()); +} + +TEST_F( + Gatt2ClientServerTest, + WatchServicesWithUuidsListsServicesOnFirstRequestAndUpdatesOnSecondRequest) { + const bt::att::Handle kSvcStartHandle0(1); + const bt::att::Handle kSvcEndHandle0(kSvcStartHandle0); + bt::gatt::ServiceData svc_data_0(bt::gatt::ServiceKind::PRIMARY, + kSvcStartHandle0, + kSvcEndHandle0, + kTestServiceUuid0); + fake_gatt()->AddPeerService(kPeerId, svc_data_0); + + const bt::att::Handle kSvcStartHandle1(2); + const bt::att::Handle kSvcEndHandle1(kSvcStartHandle1); + bt::gatt::ServiceData svc_data_1(bt::gatt::ServiceKind::PRIMARY, + kSvcStartHandle1, + kSvcEndHandle1, + kTestServiceUuid1); + fake_gatt()->AddPeerService(kPeerId, svc_data_1); + + std::vector updated; + std::vector removed; + int watch_cb_count = 0; + proxy()->WatchServices( + /*uuids=*/{fb::Uuid{kTestServiceUuid0.value()}}, + [&](std::vector cb_updated, + std::vector cb_removed) { + watch_cb_count++; + updated = std::move(cb_updated); + removed = std::move(cb_removed); + }); + + RunLoopUntilIdle(); + EXPECT_EQ(watch_cb_count, 1); + EXPECT_EQ(removed.size(), 0u); + ASSERT_EQ(updated.size(), 1u); + ASSERT_TRUE(updated[0].has_handle()); + EXPECT_EQ(updated[0].handle().value, kSvcStartHandle0); + + updated.clear(); + removed.clear(); + watch_cb_count = 0; + + // WatchServices before service update. Request should be queued. + proxy()->WatchServices( + /*uuids=*/{fb::Uuid{kTestServiceUuid0.value()}}, + [&](std::vector cb_updated, + std::vector cb_removed) { + watch_cb_count++; + updated = std::move(cb_updated); + removed = std::move(cb_removed); + }); + + // Service should be ignored because UUID does not match. + const bt::att::Handle kSvcStartHandle2(3); + const bt::att::Handle kSvcEndHandle2(kSvcStartHandle2); + bt::gatt::ServiceData svc_data_2(bt::gatt::ServiceKind::PRIMARY, + kSvcStartHandle2, + kSvcEndHandle2, + kTestServiceUuid1); + fake_gatt()->AddPeerService(kPeerId, svc_data_2, /*notify=*/true); + RunLoopUntilIdle(); + EXPECT_EQ(watch_cb_count, 0); + EXPECT_EQ(updated.size(), 0u); + EXPECT_EQ(removed.size(), 0u); + + // Service UUID matches, so response should be received. + const bt::att::Handle kSvcStartHandle3(4); + const bt::att::Handle kSvcEndHandle3(kSvcStartHandle3); + bt::gatt::ServiceData svc_data_3(bt::gatt::ServiceKind::PRIMARY, + kSvcStartHandle3, + kSvcEndHandle3, + kTestServiceUuid0); + fake_gatt()->AddPeerService(kPeerId, svc_data_3, /*notify=*/true); + RunLoopUntilIdle(); + EXPECT_EQ(watch_cb_count, 1); + EXPECT_EQ(removed.size(), 0u); + EXPECT_EQ(updated.size(), 1u); + ASSERT_TRUE(updated[0].has_handle()); + EXPECT_EQ(updated[0].handle().value, kSvcStartHandle3); +} + +TEST_F( + Gatt2ClientServerTest, + WatchServicesWithUuidsListsServicesOnFirstRequestAndListsAgainWhenUuidsChanged) { + const bt::att::Handle kSvcStartHandle0(1); + const bt::att::Handle kSvcEndHandle0(kSvcStartHandle0); + bt::gatt::ServiceData svc_data_0(bt::gatt::ServiceKind::PRIMARY, + kSvcStartHandle0, + kSvcEndHandle0, + kTestServiceUuid0); + fake_gatt()->AddPeerService(kPeerId, svc_data_0); + + const bt::att::Handle kSvcStartHandle1(2); + const bt::att::Handle kSvcEndHandle1(kSvcStartHandle1); + bt::gatt::ServiceData svc_data_1(bt::gatt::ServiceKind::PRIMARY, + kSvcStartHandle1, + kSvcEndHandle1, + kTestServiceUuid1); + fake_gatt()->AddPeerService(kPeerId, svc_data_1); + + std::vector updated; + std::vector removed; + int watch_cb_count = 0; + proxy()->WatchServices( + /*uuids=*/{fb::Uuid{kTestServiceUuid0.value()}}, + [&](std::vector cb_updated, + std::vector cb_removed) { + watch_cb_count++; + updated = std::move(cb_updated); + removed = std::move(cb_removed); + }); + + RunLoopUntilIdle(); + EXPECT_EQ(watch_cb_count, 1); + EXPECT_EQ(removed.size(), 0u); + ASSERT_EQ(updated.size(), 1u); + ASSERT_TRUE(updated[0].has_handle()); + EXPECT_EQ(updated[0].handle().value, kSvcStartHandle0); + + updated.clear(); + removed.clear(); + watch_cb_count = 0; + + // WatchServices before service update. Request should be queued. + proxy()->WatchServices( + /*uuids=*/{fb::Uuid{kTestServiceUuid0.value()}}, + [&](std::vector cb_updated, + std::vector cb_removed) { + watch_cb_count++; + updated = std::move(cb_updated); + removed = std::move(cb_removed); + }); + + // Service should be ignored because UUID does not match. + const bt::att::Handle kSvcStartHandle2(3); + const bt::att::Handle kSvcEndHandle2(kSvcStartHandle2); + bt::gatt::ServiceData svc_data_2(bt::gatt::ServiceKind::PRIMARY, + kSvcStartHandle2, + kSvcEndHandle2, + kTestServiceUuid1); + fake_gatt()->AddPeerService(kPeerId, svc_data_2, /*notify=*/true); + RunLoopUntilIdle(); + EXPECT_EQ(watch_cb_count, 0); + EXPECT_EQ(updated.size(), 0u); + EXPECT_EQ(removed.size(), 0u); + + // Changing the UUID matched means that the previous call completes, but with + // no new updates. + std::vector updated_srv2; + std::vector removed_srv2; + int watch_srv2_cb_count = 0; + proxy()->WatchServices( + /*uuids=*/{fb::Uuid{kTestServiceUuid1.value()}}, + [&](std::vector cb_updated, + std::vector cb_removed) { + watch_srv2_cb_count++; + updated_srv2 = std::move(cb_updated); + removed_srv2 = std::move(cb_removed); + }); + + RunLoopUntilIdle(); + // Initial one should have been completed, with no new info. + EXPECT_EQ(watch_cb_count, 1); + EXPECT_EQ(updated.size(), 0u); + EXPECT_EQ(removed.size(), 0u); + watch_cb_count = 0; + + // Second service UUID completes with the services for the second UUID + EXPECT_EQ(watch_srv2_cb_count, 1); + EXPECT_EQ(updated_srv2.size(), 2u); + EXPECT_EQ(removed_srv2.size(), 0u); + watch_srv2_cb_count = 0; + updated_srv2.clear(); + + // Service UUID matches initial search, which is finished, so no response + // should be received. + const bt::att::Handle kSvcStartHandle3(4); + const bt::att::Handle kSvcEndHandle3(kSvcStartHandle3); + bt::gatt::ServiceData svc_data_3(bt::gatt::ServiceKind::PRIMARY, + kSvcStartHandle3, + kSvcEndHandle3, + kTestServiceUuid0); + fake_gatt()->AddPeerService(kPeerId, svc_data_3, /*notify=*/true); + RunLoopUntilIdle(); + EXPECT_EQ(watch_cb_count, 0); + EXPECT_EQ(watch_srv2_cb_count, 0); + + // Queueing up a new srv2 call should not reply immediately. + proxy()->WatchServices( + /*uuids=*/{fb::Uuid{kTestServiceUuid1.value()}}, + [&](std::vector cb_updated, + std::vector cb_removed) { + watch_srv2_cb_count++; + updated_srv2 = std::move(cb_updated); + removed_srv2 = std::move(cb_removed); + }); + + RunLoopUntilIdle(); + EXPECT_EQ(watch_srv2_cb_count, 0); + + // Switching back to the original UUID at this point re-lists all the services + // again, and the second search finishes with an empty response as the end. + proxy()->WatchServices( + /*uuids=*/{fb::Uuid{kTestServiceUuid0.value()}}, + [&](std::vector cb_updated, + std::vector cb_removed) { + watch_cb_count++; + updated = std::move(cb_updated); + removed = std::move(cb_removed); + }); + + RunLoopUntilIdle(); + EXPECT_EQ(watch_cb_count, 1); + EXPECT_EQ(removed.size(), 0u); + EXPECT_EQ(updated.size(), 2u); + + EXPECT_EQ(watch_srv2_cb_count, 1); + EXPECT_EQ(removed_srv2.size(), 0u); + EXPECT_EQ(updated_srv2.size(), 0u); +} + +TEST_F(Gatt2ClientServerTest, ServiceWatcherResultsIgnoredBeforeWatchServices) { + const bt::att::Handle kSvcStartHandle0(1); + const bt::att::Handle kSvcEndHandle0(kSvcStartHandle0); + bt::gatt::ServiceData svc_data_0(bt::gatt::ServiceKind::PRIMARY, + kSvcStartHandle0, + kSvcEndHandle0, + kTestServiceUuid0); + + const bt::att::Handle kSvcStartHandle1(2); + const bt::att::Handle kSvcEndHandle1(kSvcStartHandle1); + bt::gatt::ServiceData svc_data_1(bt::gatt::ServiceKind::PRIMARY, + kSvcStartHandle1, + kSvcEndHandle1, + kTestServiceUuid0); + + // Notifications should be ignored. + fake_gatt()->AddPeerService(kPeerId, svc_data_0, /*notify=*/true); + fake_gatt()->RemovePeerService(kPeerId, kSvcStartHandle0); + fake_gatt()->AddPeerService(kPeerId, svc_data_1, /*notify=*/true); + + std::vector updated; + std::vector removed; + int watch_cb_count = 0; + proxy()->WatchServices(/*uuids=*/{}, + [&](std::vector cb_updated, + std::vector cb_removed) { + watch_cb_count++; + updated = std::move(cb_updated); + removed = std::move(cb_removed); + }); + + RunLoopUntilIdle(); + EXPECT_EQ(watch_cb_count, 1); + // Since removal notification was ignored, the removed list should be empty. + EXPECT_EQ(removed.size(), 0u); + ASSERT_EQ(updated.size(), 1u); + ASSERT_TRUE(updated[0].has_handle()); + EXPECT_EQ(updated[0].handle().value, kSvcStartHandle1); +} + +TEST_F(Gatt2ClientServerTest, + RemoveConnectedServiceClosesRemoteServiceAndNotifiesServiceWatcher) { + const bt::att::Handle kSvcStartHandle0(1); + const bt::att::Handle kSvcEndHandle0(kSvcStartHandle0); + bt::gatt::ServiceData svc_data_0(bt::gatt::ServiceKind::PRIMARY, + kSvcStartHandle0, + kSvcEndHandle0, + kTestServiceUuid0); + fake_gatt()->AddPeerService(kPeerId, svc_data_0); + + std::vector updated; + std::vector removed; + int watch_cb_count = 0; + proxy()->WatchServices(/*uuids=*/{}, + [&](std::vector cb_updated, + std::vector cb_removed) { + watch_cb_count++; + updated = std::move(cb_updated); + removed = std::move(cb_removed); + }); + + RunLoopUntilIdle(); + EXPECT_EQ(watch_cb_count, 1); + EXPECT_EQ(removed.size(), 0u); + ASSERT_EQ(updated.size(), 1u); + ASSERT_TRUE(updated[0].has_handle()); + EXPECT_EQ(updated[0].handle().value, kSvcStartHandle0); + + fbg::RemoteServicePtr service_ptr; + proxy()->ConnectToService(updated[0].handle(), service_ptr.NewRequest()); + std::optional service_error; + service_ptr.set_error_handler( + [&](zx_status_t status) { service_error = status; }); + + RunLoopUntilIdle(); + EXPECT_FALSE(service_error); + + fake_gatt()->RemovePeerService(kPeerId, kSvcStartHandle0); + + updated.clear(); + removed.clear(); + watch_cb_count = 0; + proxy()->WatchServices(/*uuids=*/{}, + [&](std::vector cb_updated, + std::vector cb_removed) { + watch_cb_count++; + updated = std::move(cb_updated); + removed = std::move(cb_removed); + }); + RunLoopUntilIdle(); + EXPECT_EQ(watch_cb_count, 1); + EXPECT_EQ(removed.size(), 1u); + ASSERT_EQ(updated.size(), 0u); + EXPECT_EQ(removed[0].value, kSvcStartHandle0); + ASSERT_TRUE(service_error.has_value()); + EXPECT_EQ(service_error.value(), ZX_ERR_CONNECTION_RESET); +} + +TEST_F(Gatt2ClientServerTest, ModifiedService) { + const bt::att::Handle kSvcStartHandle0(1); + const bt::att::Handle kSvcEndHandle0(kSvcStartHandle0); + bt::gatt::ServiceData svc_data_0(bt::gatt::ServiceKind::PRIMARY, + kSvcStartHandle0, + kSvcEndHandle0, + kTestServiceUuid0); + fake_gatt()->AddPeerService(kPeerId, svc_data_0); + + std::vector updated; + std::vector removed; + int watch_cb_count = 0; + proxy()->WatchServices(/*uuids=*/{}, + [&](std::vector cb_updated, + std::vector cb_removed) { + watch_cb_count++; + updated = std::move(cb_updated); + removed = std::move(cb_removed); + }); + RunLoopUntilIdle(); + EXPECT_EQ(watch_cb_count, 1); + EXPECT_EQ(removed.size(), 0u); + ASSERT_EQ(updated.size(), 1u); + ASSERT_TRUE(updated[0].has_handle()); + EXPECT_EQ(updated[0].handle().value, kSvcStartHandle0); + + updated.clear(); + removed.clear(); + watch_cb_count = 0; + + proxy()->WatchServices(/*uuids=*/{}, + [&](std::vector cb_updated, + std::vector cb_removed) { + watch_cb_count++; + updated = std::move(cb_updated); + removed = std::move(cb_removed); + }); + RunLoopUntilIdle(); + EXPECT_EQ(watch_cb_count, 0); + + // Adding same service will send "modified" service to service watcher. + fake_gatt()->AddPeerService(kPeerId, svc_data_0, /*notify=*/true); + + RunLoopUntilIdle(); + EXPECT_EQ(watch_cb_count, 1); + EXPECT_EQ(removed.size(), 0u); + ASSERT_EQ(updated.size(), 1u); + ASSERT_TRUE(updated[0].has_handle()); + EXPECT_EQ(updated[0].handle().value, kSvcStartHandle0); +} + +TEST_F(Gatt2ClientServerTest, ReplacedService) { + const bt::att::Handle kSvcStartHandle0(1); + const bt::att::Handle kSvcEndHandle0(kSvcStartHandle0); + bt::gatt::ServiceData svc_data_0(bt::gatt::ServiceKind::PRIMARY, + kSvcStartHandle0, + kSvcEndHandle0, + kTestServiceUuid0); + fake_gatt()->AddPeerService(kPeerId, svc_data_0); + + std::vector updated; + std::vector removed; + int watch_cb_count = 0; + proxy()->WatchServices(/*uuids=*/{}, + [&](std::vector cb_updated, + std::vector cb_removed) { + watch_cb_count++; + updated = std::move(cb_updated); + removed = std::move(cb_removed); + }); + RunLoopUntilIdle(); + EXPECT_EQ(watch_cb_count, 1); + EXPECT_EQ(removed.size(), 0u); + ASSERT_EQ(updated.size(), 1u); + ASSERT_TRUE(updated[0].has_handle()); + EXPECT_EQ(updated[0].handle().value, kSvcStartHandle0); + + updated.clear(); + removed.clear(); + watch_cb_count = 0; + + proxy()->WatchServices(/*uuids=*/{}, + [&](std::vector cb_updated, + std::vector cb_removed) { + watch_cb_count++; + updated = std::move(cb_updated); + removed = std::move(cb_removed); + }); + RunLoopUntilIdle(); + EXPECT_EQ(watch_cb_count, 0); + + // Adding a service with the same handle but different type will send + // "removed" + "added" services to service watcher. + bt::gatt::ServiceData svc_data_1(bt::gatt::ServiceKind::PRIMARY, + kSvcStartHandle0, + kSvcEndHandle0, + kTestServiceUuid1); + fake_gatt()->AddPeerService(kPeerId, svc_data_1, /*notify=*/true); + + RunLoopUntilIdle(); + EXPECT_EQ(watch_cb_count, 1); + ASSERT_EQ(removed.size(), 1u); + ASSERT_EQ(updated.size(), 1u); + EXPECT_EQ(removed[0].value, kSvcStartHandle0); + ASSERT_TRUE(updated[0].has_handle()); + EXPECT_EQ(updated[0].handle().value, kSvcStartHandle0); + ASSERT_TRUE(updated[0].has_type()); + EXPECT_EQ(bt::UUID(updated[0].type().value), kTestServiceUuid1); +} + +// When a service is added and removed between calls to WatchServices, only the +// removed handle is sent in the response. +TEST_F(Gatt2ClientServerTest, + ServiceAddedFollowedByServiceRemovedBetweenWatchServices) { + const bt::att::Handle kSvcStartHandle0(1); + const bt::att::Handle kSvcEndHandle0(kSvcStartHandle0); + bt::gatt::ServiceData svc_data_0(bt::gatt::ServiceKind::PRIMARY, + kSvcStartHandle0, + kSvcEndHandle0, + kTestServiceUuid0); + fake_gatt()->AddPeerService(kPeerId, svc_data_0); + + int watch_cb_count = 0; + proxy()->WatchServices(/*uuids=*/{}, [&](auto, auto) { watch_cb_count++; }); + RunLoopUntilIdle(); + EXPECT_EQ(watch_cb_count, 1); + + const bt::att::Handle kSvcStartHandle1(2); + const bt::att::Handle kSvcEndHandle1(kSvcStartHandle1); + bt::gatt::ServiceData svc_data_1(bt::gatt::ServiceKind::PRIMARY, + kSvcStartHandle1, + kSvcEndHandle1, + kTestServiceUuid1); + fake_gatt()->AddPeerService(kPeerId, svc_data_1); + fake_gatt()->RemovePeerService(kPeerId, kSvcStartHandle1); + RunLoopUntilIdle(); + + std::vector updated; + std::vector removed; + watch_cb_count = 0; + proxy()->WatchServices(/*uuids=*/{}, + [&](std::vector cb_updated, + std::vector cb_removed) { + watch_cb_count++; + updated = std::move(cb_updated); + removed = std::move(cb_removed); + }); + + RunLoopUntilIdle(); + EXPECT_EQ(watch_cb_count, 1); + EXPECT_EQ(removed.size(), 1u); + ASSERT_EQ(updated.size(), 0u); + EXPECT_EQ(removed[0].value, kSvcStartHandle1); +} + +TEST_F(Gatt2ClientServerTest, WatchServicesCalledTwiceClosesServer) { + // Prevent GATT::ListServices() from completing so that we can queue a second + // WatchServices request. + fake_gatt()->stop_list_services(); + + int watch_cb_count_0 = 0; + int watch_cb_count_1 = 0; + proxy()->WatchServices(/*uuids=*/{}, [&](auto, auto) { watch_cb_count_0++; }); + proxy()->WatchServices(/*uuids=*/{}, [&](auto, auto) { watch_cb_count_1++; }); + RunLoopUntilIdle(); + EXPECT_EQ(watch_cb_count_0, 0); + EXPECT_EQ(watch_cb_count_1, 0); + EXPECT_TRUE(server_error_cb_called()); + ASSERT_TRUE(proxy_epitaph()); + EXPECT_EQ(proxy_epitaph().value(), ZX_ERR_ALREADY_BOUND); +} + +TEST_F(Gatt2ClientServerTest, ListServicesFails) { + const bt::att::Handle kSvcStartHandle0(1); + const bt::att::Handle kSvcEndHandle0(kSvcStartHandle0); + bt::gatt::ServiceData svc_data(bt::gatt::ServiceKind::PRIMARY, + kSvcStartHandle0, + kSvcEndHandle0, + kTestServiceUuid0); + fake_gatt()->AddPeerService(kPeerId, svc_data); + + fake_gatt()->set_list_services_status( + bt::ToResult(bt::HostError::kPacketMalformed)); + + int watch_cb_count = 0; + proxy()->WatchServices(/*uuids=*/{}, [&](auto, auto) { watch_cb_count++; }); + RunLoopUntilIdle(); + EXPECT_EQ(watch_cb_count, 0); + EXPECT_TRUE(server_error_cb_called()); + ASSERT_TRUE(proxy_epitaph()); + EXPECT_EQ(proxy_epitaph().value(), ZX_ERR_CONNECTION_RESET); +} + +TEST_F(Gatt2ClientServerTest, ConnectToServiceInvalidHandle) { + fbg::ServiceHandle invalid_handle{static_cast(bt::att::kHandleMax) + + 1}; + fbg::RemoteServicePtr service_ptr; + proxy()->ConnectToService(invalid_handle, service_ptr.NewRequest()); + + std::optional service_epitaph; + service_ptr.set_error_handler( + [&](zx_status_t epitaph) { service_epitaph = epitaph; }); + + RunLoopUntilIdle(); + ASSERT_TRUE(service_epitaph); + EXPECT_EQ(service_epitaph.value(), ZX_ERR_INVALID_ARGS); +} + +TEST_F(Gatt2ClientServerTest, ConnectToServiceServiceAlreadyConnected) { + const bt::att::Handle kSvcStartHandle0(1); + const bt::att::Handle kSvcEndHandle0(kSvcStartHandle0); + bt::gatt::ServiceData svc_data(bt::gatt::ServiceKind::PRIMARY, + kSvcStartHandle0, + kSvcEndHandle0, + kTestServiceUuid0); + fake_gatt()->AddPeerService(kPeerId, svc_data); + + fbg::RemoteServicePtr service_ptr_0; + proxy()->ConnectToService(fbg::ServiceHandle{kSvcStartHandle0}, + service_ptr_0.NewRequest()); + + std::optional service_epitaph_0; + service_ptr_0.set_error_handler( + [&](zx_status_t epitaph) { service_epitaph_0 = epitaph; }); + + RunLoopUntilIdle(); + EXPECT_FALSE(service_epitaph_0); + + fbg::RemoteServicePtr service_ptr_1; + proxy()->ConnectToService(fbg::ServiceHandle{kSvcStartHandle0}, + service_ptr_1.NewRequest()); + + std::optional service_epitaph_1; + service_ptr_1.set_error_handler( + [&](zx_status_t epitaph) { service_epitaph_1 = epitaph; }); + + RunLoopUntilIdle(); + EXPECT_FALSE(service_epitaph_0); + ASSERT_TRUE(service_epitaph_1); + EXPECT_EQ(service_epitaph_1.value(), ZX_ERR_ALREADY_EXISTS); +} + +TEST_F(Gatt2ClientServerTest, + ConnectToServiceNotFoundThenConnectToServiceWithSameHandleSucceeds) { + const bt::att::Handle kSvcStartHandle0(1); + const bt::att::Handle kSvcEndHandle0(kSvcStartHandle0); + + fbg::RemoteServicePtr service_ptr_0; + proxy()->ConnectToService(fbg::ServiceHandle{kSvcStartHandle0}, + service_ptr_0.NewRequest()); + + std::optional service_epitaph_0; + service_ptr_0.set_error_handler( + [&](zx_status_t epitaph) { service_epitaph_0 = epitaph; }); + + RunLoopUntilIdle(); + ASSERT_TRUE(service_epitaph_0); + EXPECT_EQ(service_epitaph_0.value(), ZX_ERR_NOT_FOUND); + + // Add a service with the same handle as the service that was previously not + // found. + bt::gatt::ServiceData svc_data(bt::gatt::ServiceKind::PRIMARY, + kSvcStartHandle0, + kSvcEndHandle0, + kTestServiceUuid0); + fake_gatt()->AddPeerService(kPeerId, svc_data); + + // Connecting to the service after it is added should succeed. + fbg::RemoteServicePtr service_ptr_1; + proxy()->ConnectToService(fbg::ServiceHandle{kSvcStartHandle0}, + service_ptr_1.NewRequest()); + + std::optional service_epitaph_1; + service_ptr_1.set_error_handler( + [&](zx_status_t epitaph) { service_epitaph_1 = epitaph; }); + + RunLoopUntilIdle(); + EXPECT_FALSE(service_epitaph_1); +} + +TEST_F(Gatt2ClientServerTest, + ClientClosesRemoteServiceAndReconnectsFollowedByServiceRemoved) { + const bt::att::Handle kSvcStartHandle0(1); + const bt::att::Handle kSvcEndHandle0(kSvcStartHandle0); + bt::gatt::ServiceData svc_data(bt::gatt::ServiceKind::PRIMARY, + kSvcStartHandle0, + kSvcEndHandle0, + kTestServiceUuid0); + fake_gatt()->AddPeerService(kPeerId, svc_data); + + fbg::RemoteServicePtr service_ptr_0; + proxy()->ConnectToService(fbg::ServiceHandle{kSvcStartHandle0}, + service_ptr_0.NewRequest()); + + std::optional service_epitaph_0; + service_ptr_0.set_error_handler( + [&](zx_status_t epitaph) { service_epitaph_0 = epitaph; }); + + RunLoopUntilIdle(); + EXPECT_FALSE(service_epitaph_0); + + service_ptr_0.Unbind(); + + fbg::RemoteServicePtr service_ptr_1; + proxy()->ConnectToService(fbg::ServiceHandle{kSvcStartHandle0}, + service_ptr_1.NewRequest()); + + std::optional service_epitaph_1; + service_ptr_1.set_error_handler( + [&](zx_status_t epitaph) { service_epitaph_1 = epitaph; }); + + RunLoopUntilIdle(); + EXPECT_FALSE(service_epitaph_1); + + // Server should not crash when both service removed handlers are called (one + // was registered for each call to ConnectToService). The second handler + // should do nothing. + fake_gatt()->RemovePeerService(kPeerId, kSvcStartHandle0); + RunLoopUntilIdle(); + ASSERT_TRUE(service_epitaph_1); + EXPECT_EQ(service_epitaph_1.value(), ZX_ERR_CONNECTION_RESET); +} + +// The service removed handler should gracefully handle being called after the +// service has already been removed from the FIDL server. +TEST_F(Gatt2ClientServerTest, + ClientClosesRemoteServiceFollowedByServiceRemoved) { + const bt::att::Handle kSvcStartHandle0(1); + const bt::att::Handle kSvcEndHandle0(kSvcStartHandle0); + bt::gatt::ServiceData svc_data(bt::gatt::ServiceKind::PRIMARY, + kSvcStartHandle0, + kSvcEndHandle0, + kTestServiceUuid0); + fake_gatt()->AddPeerService(kPeerId, svc_data); + + fbg::RemoteServicePtr service_ptr_0; + proxy()->ConnectToService(fbg::ServiceHandle{kSvcStartHandle0}, + service_ptr_0.NewRequest()); + + std::optional service_epitaph_0; + service_ptr_0.set_error_handler( + [&](zx_status_t epitaph) { service_epitaph_0 = epitaph; }); + + RunLoopUntilIdle(); + EXPECT_FALSE(service_epitaph_0); + + // Unbinding the client end should remove the service from the server's map. + service_ptr_0.Unbind(); + RunLoopUntilIdle(); + + // Server should not crash when service removed handler is called and finds + // that the service isn't in the server's map. + fake_gatt()->RemovePeerService(kPeerId, kSvcStartHandle0); + RunLoopUntilIdle(); +} + +TEST_F( + Gatt2ClientServerTest, + WatchServicesWithDifferentUuidsBetweenFirstAndSecondRequestListsServicesOnSecondRequest) { + const bt::att::Handle kSvcStartHandle0(1); + const bt::att::Handle kSvcEndHandle0(kSvcStartHandle0); + bt::gatt::ServiceData svc_data_0(bt::gatt::ServiceKind::PRIMARY, + kSvcStartHandle0, + kSvcEndHandle0, + kTestServiceUuid0); + fake_gatt()->AddPeerService(kPeerId, svc_data_0); + + const bt::att::Handle kSvcStartHandle1(2); + const bt::att::Handle kSvcEndHandle1(kSvcStartHandle1); + bt::gatt::ServiceData svc_data_1(bt::gatt::ServiceKind::PRIMARY, + kSvcStartHandle1, + kSvcEndHandle1, + kTestServiceUuid1); + fake_gatt()->AddPeerService(kPeerId, svc_data_1); + + std::vector updated; + std::vector removed; + int watch_cb_count = 0; + proxy()->WatchServices( + /*uuids=*/{fb::Uuid{kTestServiceUuid0.value()}}, + [&](std::vector cb_updated, + std::vector cb_removed) { + watch_cb_count++; + updated = std::move(cb_updated); + removed = std::move(cb_removed); + }); + + RunLoopUntilIdle(); + EXPECT_EQ(watch_cb_count, 1); + EXPECT_EQ(removed.size(), 0u); + ASSERT_EQ(updated.size(), 1u); + ASSERT_TRUE(updated[0].has_handle()); + EXPECT_EQ(updated[0].handle().value, kSvcStartHandle0); + + updated.clear(); + removed.clear(); + watch_cb_count = 0; + + // Service with UUID not in next WatchServices() UUID list should be ignored. + const bt::att::Handle kSvcStartHandle2(3); + const bt::att::Handle kSvcEndHandle2(kSvcStartHandle2); + bt::gatt::ServiceData svc_data_2(bt::gatt::ServiceKind::PRIMARY, + kSvcStartHandle2, + kSvcEndHandle2, + kTestServiceUuid3); + fake_gatt()->AddPeerService(kPeerId, svc_data_2); + + // UUIDs changed, so WatchServices() should immediately receive a response + // with all existing services that match UUIDs. + proxy()->WatchServices( + /*uuids=*/{fb::Uuid{kTestServiceUuid0.value()}, + fb::Uuid{kTestServiceUuid1.value()}}, + [&](std::vector cb_updated, + std::vector cb_removed) { + watch_cb_count++; + updated = std::move(cb_updated); + removed = std::move(cb_removed); + }); + RunLoopUntilIdle(); + EXPECT_EQ(watch_cb_count, 1); + EXPECT_EQ(removed.size(), 0u); + EXPECT_EQ(updated.size(), 2u); + ASSERT_TRUE(updated[0].has_handle()); + ASSERT_TRUE(updated[1].has_handle()); + std::sort(updated.begin(), + updated.end(), + [](fbg::ServiceInfo& a, fbg::ServiceInfo& b) { + return a.handle().value < b.handle().value; + }); + EXPECT_EQ(updated[0].handle().value, kSvcStartHandle0); + EXPECT_EQ(updated[1].handle().value, kSvcStartHandle1); +} + +TEST_F( + Gatt2ClientServerTest, + WatchServicesWithReorderedUuidsBetweenFirstAndSecondRequestDoesNotListsServicesOnSecondRequest) { + const bt::att::Handle kSvcStartHandle0(1); + const bt::att::Handle kSvcEndHandle0(kSvcStartHandle0); + bt::gatt::ServiceData svc_data_0(bt::gatt::ServiceKind::PRIMARY, + kSvcStartHandle0, + kSvcEndHandle0, + kTestServiceUuid0); + fake_gatt()->AddPeerService(kPeerId, svc_data_0); + + const bt::att::Handle kSvcStartHandle1(2); + const bt::att::Handle kSvcEndHandle1(kSvcStartHandle1); + bt::gatt::ServiceData svc_data_1(bt::gatt::ServiceKind::PRIMARY, + kSvcStartHandle1, + kSvcEndHandle1, + kTestServiceUuid1); + fake_gatt()->AddPeerService(kPeerId, svc_data_1); + + std::vector updated; + std::vector removed; + int watch_cb_count = 0; + proxy()->WatchServices( + /*uuids=*/{fb::Uuid{kTestServiceUuid0.value()}, + fb::Uuid{kTestServiceUuid1.value()}}, + [&](std::vector cb_updated, + std::vector cb_removed) { + watch_cb_count++; + updated = std::move(cb_updated); + removed = std::move(cb_removed); + }); + + RunLoopUntilIdle(); + EXPECT_EQ(watch_cb_count, 1); + EXPECT_EQ(removed.size(), 0u); + EXPECT_EQ(updated.size(), 2u); + + updated.clear(); + removed.clear(); + watch_cb_count = 0; + + // UUIDs order changed but set of UUIDs is the same, so WatchServices() should + // not receive a response. + proxy()->WatchServices( + /*uuids=*/{fb::Uuid{kTestServiceUuid1.value()}, + fb::Uuid{kTestServiceUuid0.value()}}, + [&](std::vector cb_updated, + std::vector cb_removed) { + watch_cb_count++; + updated = std::move(cb_updated); + removed = std::move(cb_removed); + }); + RunLoopUntilIdle(); + EXPECT_EQ(watch_cb_count, 0); +} + +} // namespace + +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_remote_service_server.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_remote_service_server.cc new file mode 100644 index 0000000000..afaa0b9f76 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_remote_service_server.cc @@ -0,0 +1,583 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_remote_service_server.h" + +#include + +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/helpers.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/measure_tape/hlcpp_measure_tape_for_read_by_type_result.h" +#include "pw_bluetooth_sapphire/internal/host/att/att.h" +#include "pw_bluetooth_sapphire/internal/host/common/identifier.h" + +namespace fbg = fuchsia::bluetooth::gatt2; +namespace measure_fbg = measure_tape::fuchsia::bluetooth::gatt2; + +namespace bthost { +namespace { + +bt::att::ResultFunction<> MakeStatusCallback( + bt::PeerId peer_id, + const char* request_name, + fbg::Handle fidl_handle, + fit::function)> callback) { + return [peer_id, fidl_handle, callback = std::move(callback), request_name]( + bt::att::Result<> status) { + if (bt_is_error(status, + INFO, + "fidl", + "%s: error (peer: %s, handle: 0x%lX)", + request_name, + bt_str(peer_id), + fidl_handle.value)) { + callback(fpromise::error( + fidl_helpers::AttErrorToGattFidlError(status.error_value()))); + return; + } + + callback(fpromise::ok()); + }; +} + +fbg::Characteristic CharacteristicToFidl( + const bt::gatt::CharacteristicData& characteristic, + const std::map& + descriptors) { + fbg::Characteristic fidl_char; + fidl_char.set_handle(fbg::Handle{characteristic.value_handle}); + fidl_char.set_type(fuchsia::bluetooth::Uuid{characteristic.type.value()}); + + // The FIDL property bitfield combines the properties and extended properties + // bits. We mask away the kExtendedProperties property. + constexpr uint8_t kRemoveExtendedPropertiesMask = 0x7F; + fbg::CharacteristicPropertyBits fidl_properties = + static_cast( + characteristic.properties & kRemoveExtendedPropertiesMask); + if (characteristic.extended_properties) { + if (*characteristic.extended_properties & + bt::gatt::ExtendedProperty::kReliableWrite) { + fidl_properties |= fbg::CharacteristicPropertyBits::RELIABLE_WRITE; + } + if (*characteristic.extended_properties & + bt::gatt::ExtendedProperty::kWritableAuxiliaries) { + fidl_properties |= fbg::CharacteristicPropertyBits::WRITABLE_AUXILIARIES; + } + } + fidl_char.set_properties(fidl_properties); + + if (!descriptors.empty()) { + std::vector fidl_descriptors; + for (const auto& [handle, data] : descriptors) { + fbg::Descriptor fidl_descriptor; + fidl_descriptor.set_handle(fbg::Handle{handle.value}); + fidl_descriptor.set_type(fuchsia::bluetooth::Uuid{data.type.value()}); + fidl_descriptors.push_back(std::move(fidl_descriptor)); + } + fidl_char.set_descriptors(std::move(fidl_descriptors)); + } + + return fidl_char; +} + +// Returned result is supposed to match Read{Characteristic, Descriptor}Callback +// (result type is converted by FIDL move constructor). +[[nodiscard]] fpromise::result<::fuchsia::bluetooth::gatt2::ReadValue, + ::fuchsia::bluetooth::gatt2::Error> +ReadResultToFidl(bt::PeerId peer_id, + fbg::Handle handle, + bt::att::Result<> status, + const bt::ByteBuffer& value, + bool maybe_truncated, + const char* request) { + if (bt_is_error(status, + INFO, + "fidl", + "%s: error (peer: %s, handle: 0x%lX)", + request, + bt_str(peer_id), + handle.value)) { + return fpromise::error( + fidl_helpers::AttErrorToGattFidlError(status.error_value())); + } + + fbg::ReadValue fidl_value; + fidl_value.set_handle(handle); + fidl_value.set_value(value.ToVector()); + fidl_value.set_maybe_truncated(maybe_truncated); + return fpromise::ok(std::move(fidl_value)); +} + +void FillInReadOptionsDefaults(fbg::ReadOptions& options) { + if (options.is_short_read()) { + return; + } + if (!options.long_read().has_offset()) { + options.long_read().set_offset(0); + } + if (!options.long_read().has_max_bytes()) { + options.long_read().set_max_bytes(fbg::MAX_VALUE_LENGTH); + } +} + +void FillInDefaultWriteOptions(fbg::WriteOptions& options) { + if (!options.has_write_mode()) { + *options.mutable_write_mode() = fbg::WriteMode::DEFAULT; + } + if (!options.has_offset()) { + *options.mutable_offset() = 0; + } +} + +bt::gatt::ReliableMode ReliableModeFromFidl(const fbg::WriteMode& mode) { + return mode == fbg::WriteMode::RELIABLE ? bt::gatt::ReliableMode::kEnabled + : bt::gatt::ReliableMode::kDisabled; +} + +} // namespace + +Gatt2RemoteServiceServer::Gatt2RemoteServiceServer( + bt::gatt::RemoteService::WeakPtr service, + bt::gatt::GATT::WeakPtr gatt, + bt::PeerId peer_id, + fidl::InterfaceRequest request) + : GattServerBase(std::move(gatt), this, std::move(request)), + service_(std::move(service)), + peer_id_(peer_id), + weak_self_(this) {} + +Gatt2RemoteServiceServer::~Gatt2RemoteServiceServer() { + // Disable all notifications to prevent leaks. + for (auto& [_, notifier] : characteristic_notifiers_) { + service_->DisableNotifications(notifier.characteristic_handle, + notifier.handler_id, + /*status_callback=*/[](auto /*status*/) {}); + } + characteristic_notifiers_.clear(); +} + +void Gatt2RemoteServiceServer::Close(zx_status_t status) { + binding()->Close(status); +} + +void Gatt2RemoteServiceServer::DiscoverCharacteristics( + DiscoverCharacteristicsCallback callback) { + auto res_cb = [callback = std::move(callback)]( + bt::att::Result<> status, + const bt::gatt::CharacteristicMap& characteristics) { + if (status.is_error()) { + callback({}); + return; + } + + std::vector fidl_characteristics; + for (const auto& [_, characteristic] : characteristics) { + const auto& [data, descriptors] = characteristic; + fidl_characteristics.push_back(CharacteristicToFidl(data, descriptors)); + } + callback(std::move(fidl_characteristics)); + }; + + service_->DiscoverCharacteristics(std::move(res_cb)); +} + +void Gatt2RemoteServiceServer::ReadByType(::fuchsia::bluetooth::Uuid uuid, + ReadByTypeCallback callback) { + service_->ReadByType( + fidl_helpers::UuidFromFidl(uuid), + [self = weak_self_.GetWeakPtr(), + cb = std::move(callback), + func = __FUNCTION__]( + bt::att::Result<> status, + std::vector results) { + if (!self.is_alive()) { + return; + } + + if (status == ToResult(bt::HostError::kInvalidParameters)) { + bt_log(WARN, + "fidl", + "%s: called with invalid parameters (peer: %s)", + func, + bt_str(self->peer_id_)); + cb(fpromise::error(fbg::Error::INVALID_PARAMETERS)); + return; + } else if (status.is_error()) { + cb(fpromise::error(fbg::Error::UNLIKELY_ERROR)); + return; + } + + const size_t kVectorOverhead = + sizeof(fidl_message_header_t) + sizeof(fidl_vector_t); + const size_t kMaxBytes = ZX_CHANNEL_MAX_MSG_BYTES - kVectorOverhead; + size_t bytes_used = 0; + + std::vector fidl_results; + fidl_results.reserve(results.size()); + + for (const bt::gatt::RemoteService::ReadByTypeResult& result : + results) { + fuchsia::bluetooth::gatt2::ReadByTypeResult fidl_result; + fidl_result.set_handle(fbg::Handle{result.handle.value}); + if (result.result.is_ok()) { + fbg::ReadValue read_value; + read_value.set_handle(fbg::Handle{result.handle.value}); + read_value.set_value(result.result.value()->ToVector()); + read_value.set_maybe_truncated(result.maybe_truncated); + fidl_result.set_value(std::move(read_value)); + } else { + fidl_result.set_error(fidl_helpers::AttErrorToGattFidlError( + bt::att::Error(result.result.error_value()))); + } + + measure_fbg::Size result_size = measure_fbg::Measure(fidl_result); + BT_ASSERT(result_size.num_handles == 0); + bytes_used += result_size.num_bytes; + + if (bytes_used > kMaxBytes) { + cb(fpromise::error( + fuchsia::bluetooth::gatt2::Error::TOO_MANY_RESULTS)); + return; + } + + fidl_results.push_back(std::move(fidl_result)); + } + + cb(fpromise::ok(std::move(fidl_results))); + }); +} + +void Gatt2RemoteServiceServer::ReadCharacteristic( + fbg::Handle fidl_handle, + fbg::ReadOptions options, + ReadCharacteristicCallback callback) { + if (!fidl_helpers::IsFidlGattHandleValid(fidl_handle)) { + callback(fpromise::error(fbg::Error::INVALID_HANDLE)); + return; + } + bt::gatt::CharacteristicHandle handle( + static_cast(fidl_handle.value)); + + FillInReadOptionsDefaults(options); + + const char* kRequestName = __FUNCTION__; + bt::gatt::RemoteService::ReadValueCallback read_cb = + [peer_id = peer_id_, + fidl_handle, + kRequestName, + callback = std::move(callback)](bt::att::Result<> status, + const bt::ByteBuffer& value, + bool maybe_truncated) { + callback(ReadResultToFidl(peer_id, + fidl_handle, + status, + value, + maybe_truncated, + kRequestName)); + }; + + if (options.is_short_read()) { + service_->ReadCharacteristic(handle, std::move(read_cb)); + return; + } + + service_->ReadLongCharacteristic(handle, + options.long_read().offset(), + options.long_read().max_bytes(), + std::move(read_cb)); +} + +void Gatt2RemoteServiceServer::WriteCharacteristic( + fbg::Handle fidl_handle, + std::vector value, + fbg::WriteOptions options, + WriteCharacteristicCallback callback) { + if (!fidl_helpers::IsFidlGattHandleValid(fidl_handle)) { + callback(fpromise::error(fbg::Error::INVALID_HANDLE)); + return; + } + bt::gatt::CharacteristicHandle handle( + static_cast(fidl_handle.value)); + + FillInDefaultWriteOptions(options); + + bt::att::ResultFunction<> write_cb = MakeStatusCallback( + peer_id_, __FUNCTION__, fidl_handle, std::move(callback)); + + if (options.write_mode() == fbg::WriteMode::WITHOUT_RESPONSE) { + if (options.offset() != 0) { + write_cb(bt::ToResult(bt::HostError::kInvalidParameters)); + return; + } + service_->WriteCharacteristicWithoutResponse( + handle, std::move(value), std::move(write_cb)); + return; + } + + const uint16_t kMaxShortWriteValueLength = + service_->att_mtu() - sizeof(bt::att::OpCode) - + sizeof(bt::att::WriteRequestParams); + if (options.offset() == 0 && + options.write_mode() == fbg::WriteMode::DEFAULT && + value.size() <= kMaxShortWriteValueLength) { + service_->WriteCharacteristic( + handle, std::move(value), std::move(write_cb)); + return; + } + + service_->WriteLongCharacteristic(handle, + options.offset(), + std::move(value), + ReliableModeFromFidl(options.write_mode()), + std::move(write_cb)); +} + +void Gatt2RemoteServiceServer::ReadDescriptor( + ::fuchsia::bluetooth::gatt2::Handle fidl_handle, + ::fuchsia::bluetooth::gatt2::ReadOptions options, + ReadDescriptorCallback callback) { + if (!fidl_helpers::IsFidlGattHandleValid(fidl_handle)) { + callback(fpromise::error(fbg::Error::INVALID_HANDLE)); + return; + } + bt::gatt::DescriptorHandle handle( + static_cast(fidl_handle.value)); + + FillInReadOptionsDefaults(options); + + const char* kRequestName = __FUNCTION__; + bt::gatt::RemoteService::ReadValueCallback read_cb = + [peer_id = peer_id_, + fidl_handle, + kRequestName, + callback = std::move(callback)](bt::att::Result<> status, + const bt::ByteBuffer& value, + bool maybe_truncated) { + callback(ReadResultToFidl(peer_id, + fidl_handle, + status, + value, + maybe_truncated, + kRequestName)); + }; + + if (options.is_short_read()) { + service_->ReadDescriptor(handle, std::move(read_cb)); + return; + } + + service_->ReadLongDescriptor(handle, + options.long_read().offset(), + options.long_read().max_bytes(), + std::move(read_cb)); +} + +void Gatt2RemoteServiceServer::WriteDescriptor( + fbg::Handle fidl_handle, + std::vector value, + fbg::WriteOptions options, + WriteDescriptorCallback callback) { + if (!fidl_helpers::IsFidlGattHandleValid(fidl_handle)) { + callback(fpromise::error(fbg::Error::INVALID_HANDLE)); + return; + } + bt::gatt::DescriptorHandle handle( + static_cast(fidl_handle.value)); + + FillInDefaultWriteOptions(options); + + bt::att::ResultFunction<> write_cb = MakeStatusCallback( + peer_id_, __FUNCTION__, fidl_handle, std::move(callback)); + + // WITHOUT_RESPONSE and RELIABLE write modes are not supported for + // descriptors. + if (options.write_mode() == fbg::WriteMode::WITHOUT_RESPONSE || + options.write_mode() == fbg::WriteMode::RELIABLE) { + write_cb(bt::ToResult(bt::HostError::kInvalidParameters)); + return; + } + + const uint16_t kMaxShortWriteValueLength = + service_->att_mtu() - sizeof(bt::att::OpCode) - + sizeof(bt::att::WriteRequestParams); + if (options.offset() == 0 && value.size() <= kMaxShortWriteValueLength) { + service_->WriteDescriptor(handle, std::move(value), std::move(write_cb)); + return; + } + + service_->WriteLongDescriptor( + handle, options.offset(), std::move(value), std::move(write_cb)); +} + +void Gatt2RemoteServiceServer::RegisterCharacteristicNotifier( + fbg::Handle fidl_handle, + fidl::InterfaceHandle notifier_handle, + RegisterCharacteristicNotifierCallback callback) { + bt::gatt::CharacteristicHandle char_handle( + static_cast(fidl_handle.value)); + NotifierId notifier_id = next_notifier_id_++; + auto self = weak_self_.GetWeakPtr(); + + auto value_cb = [self, notifier_id, fidl_handle](const bt::ByteBuffer& value, + bool maybe_truncated) { + if (!self.is_alive()) { + return; + } + + auto notifier_iter = self->characteristic_notifiers_.find(notifier_id); + // The lower layers guarantee that the status callback is always invoked + // before sending notifications. Notifiers are only removed during + // destruction (addressed by previous `self` check) and in the + // `DisableNotifications` completion callback in + // `OnCharacteristicNotifierError`, so no notifications should be received + // after removing a notifier. + BT_ASSERT_MSG( + notifier_iter != self->characteristic_notifiers_.end(), + "characteristic notification value received after notifier unregistered" + "(peer: %s, characteristic: 0x%lX) ", + bt_str(self->peer_id_), + fidl_handle.value); + CharacteristicNotifier& notifier = notifier_iter->second; + + // The `- 1` is needed because there is one unacked notification that we've + // already sent to the client aside from the values in the queue. + if (notifier.queued_values.size() == kMaxPendingNotifierValues - 1) { + bt_log(WARN, + "fidl", + "GATT CharacteristicNotifier pending values limit reached, " + "closing protocol (peer: " + "%s, characteristic: %#.2x)", + bt_str(self->peer_id_), + notifier.characteristic_handle.value); + self->OnCharacteristicNotifierError( + notifier_id, notifier.characteristic_handle, notifier.handler_id); + return; + } + + fbg::ReadValue fidl_value; + fidl_value.set_handle(fidl_handle); + fidl_value.set_value(value.ToVector()); + fidl_value.set_maybe_truncated(maybe_truncated); + + bt_log(TRACE, + "fidl", + "Queueing GATT notification value (characteristic: %#.2x)", + notifier.characteristic_handle.value); + notifier.queued_values.push(std::move(fidl_value)); + + self->MaybeNotifyNextValue(notifier_id); + }; + + auto status_cb = [self, + service = service_, + char_handle, + notifier_id, + notifier_handle = std::move(notifier_handle), + callback = std::move(callback)]( + bt::att::Result<> status, + bt::gatt::IdType handler_id) mutable { + if (!self.is_alive()) { + if (status.is_ok()) { + // Disable this handler so it doesn't leak. + service->DisableNotifications( + char_handle, handler_id, [](auto /*status*/) { + // There is no notifier to clean up because the server has been + // destroyed. + }); + } + return; + } + + if (status.is_error()) { + callback(fpromise::error( + fidl_helpers::AttErrorToGattFidlError(status.error_value()))); + return; + } + + CharacteristicNotifier notifier{.handler_id = handler_id, + .characteristic_handle = char_handle, + .notifier = notifier_handle.Bind()}; + auto [notifier_iter, emplaced] = self->characteristic_notifiers_.emplace( + notifier_id, std::move(notifier)); + BT_ASSERT(emplaced); + + // When the client closes the protocol, unregister the notifier. + notifier_iter->second.notifier.set_error_handler( + [self, char_handle, handler_id, notifier_id](auto /*status*/) { + self->OnCharacteristicNotifierError( + notifier_id, char_handle, handler_id); + }); + + callback(fpromise::ok()); + }; + + service_->EnableNotifications( + char_handle, std::move(value_cb), std::move(status_cb)); +} + +void Gatt2RemoteServiceServer::MaybeNotifyNextValue(NotifierId notifier_id) { + auto notifier_iter = characteristic_notifiers_.find(notifier_id); + if (notifier_iter == characteristic_notifiers_.end()) { + return; + } + CharacteristicNotifier& notifier = notifier_iter->second; + + if (notifier.queued_values.empty()) { + return; + } + + if (!notifier.last_value_ack) { + return; + } + notifier.last_value_ack = false; + + fbg::ReadValue value = std::move(notifier.queued_values.front()); + notifier.queued_values.pop(); + + bt_log(DEBUG, + "fidl", + "Sending GATT notification value (handle: 0x%lX)", + value.handle().value); + auto self = weak_self_.GetWeakPtr(); + notifier.notifier->OnNotification(std::move(value), [self, notifier_id]() { + if (!self.is_alive()) { + return; + } + + auto notifier_iter = self->characteristic_notifiers_.find(notifier_id); + if (notifier_iter == self->characteristic_notifiers_.end()) { + return; + } + notifier_iter->second.last_value_ack = true; + self->MaybeNotifyNextValue(notifier_id); + }); +} + +void Gatt2RemoteServiceServer::OnCharacteristicNotifierError( + NotifierId notifier_id, + bt::gatt::CharacteristicHandle char_handle, + bt::gatt::IdType handler_id) { + auto self = weak_self_.GetWeakPtr(); + service_->DisableNotifications( + char_handle, handler_id, [self, notifier_id](auto /*status*/) { + if (!self.is_alive()) { + return; + } + // Clear the notifier regardless of status. Wait until this callback is + // called in order to prevent the value callback from being called for + // an erased notifier. + self->characteristic_notifiers_.erase(notifier_id); + }); +} + +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_remote_service_server_test.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_remote_service_server_test.cc new file mode 100644 index 0000000000..11894bec09 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_remote_service_server_test.cc @@ -0,0 +1,1568 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_remote_service_server.h" + +#include + +#include +#include + +#include "fuchsia/bluetooth/gatt2/cpp/fidl.h" +#include "gtest/gtest.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/fake_gatt_fixture.h" +#include "pw_bluetooth_sapphire/internal/host/gatt/remote_service.h" +#include "pw_bluetooth_sapphire/internal/host/testing/test_helpers.h" + +namespace bthost { +namespace { + +namespace fbg = fuchsia::bluetooth::gatt2; + +constexpr bt::PeerId kPeerId(1); + +constexpr bt::att::Handle kServiceStartHandle = 0x0001; +constexpr bt::att::Handle kServiceEndHandle = 0xFFFE; +const bt::UUID kServiceUuid(uint16_t{0x180D}); +const bt::UUID kCharacteristicUuid(uint16_t{0x180E}); +const bt::UUID kDescriptorUuid(uint16_t{0x180F}); + +class Gatt2RemoteServiceServerTest : public bt::fidl::testing::FakeGattFixture { + public: + Gatt2RemoteServiceServerTest() = default; + ~Gatt2RemoteServiceServerTest() override = default; + + void SetUp() override { + { + auto [svc, client] = fake_gatt()->AddPeerService( + kPeerId, + bt::gatt::ServiceData(bt::gatt::ServiceKind::PRIMARY, + kServiceStartHandle, + kServiceEndHandle, + kServiceUuid)); + service_ = std::move(svc); + fake_client_ = std::move(client); + } + + fidl::InterfaceHandle handle; + server_ = std::make_unique( + service_, gatt()->GetWeakPtr(), kPeerId, handle.NewRequest()); + proxy_.Bind(std::move(handle)); + } + + void TearDown() override { + // Clear any previous expectations that are based on the ATT Write Request, + // so that write requests sent during RemoteService::ShutDown() are ignored. + fake_client()->set_write_request_callback({}); + + bt::fidl::testing::FakeGattFixture::TearDown(); + } + + pw::async::HeapDispatcher& heap_dispatcher() { return heap_dispatcher_; } + + protected: + const bt::gatt::testing::FakeClient::WeakPtr& fake_client() const { + BT_ASSERT(fake_client_.is_alive()); + return fake_client_; + } + + fbg::RemoteServicePtr& service_proxy() { return proxy_; } + bt::gatt::RemoteService::WeakPtr service() { return service_; } + + void DestroyServer() { server_.reset(); } + + private: + std::unique_ptr server_; + + fbg::RemoteServicePtr proxy_; + bt::gatt::RemoteService::WeakPtr service_; + bt::gatt::testing::FakeClient::WeakPtr fake_client_; + pw::async_fuchsia::FuchsiaDispatcher pw_dispatcher_{dispatcher()}; + pw::async::HeapDispatcher heap_dispatcher_{pw_dispatcher_}; + + BT_DISALLOW_COPY_ASSIGN_AND_MOVE(Gatt2RemoteServiceServerTest); +}; + +TEST_F(Gatt2RemoteServiceServerTest, DiscoverCharacteristics) { + bt::gatt::Properties properties = + static_cast( + bt::gatt::Property::kAuthenticatedSignedWrites) | + static_cast( + bt::gatt::Property::kExtendedProperties); + bt::gatt::ExtendedProperties ext_properties = + static_cast( + bt::gatt::ExtendedProperty::kReliableWrite) | + static_cast( + bt::gatt::ExtendedProperty::kWritableAuxiliaries); + constexpr bt::att::Handle kCharacteristicHandle(kServiceStartHandle + 1); + constexpr bt::att::Handle kCharacteristicValueHandle(kCharacteristicHandle + + 1); + const bt::UUID kCharacteristicUuid(uint16_t{0x0000}); + bt::gatt::CharacteristicData characteristic(properties, + ext_properties, + kCharacteristicHandle, + kCharacteristicValueHandle, + kCharacteristicUuid); + fake_client()->set_characteristics({characteristic}); + + constexpr bt::att::Handle kDescriptorHandle(kCharacteristicValueHandle + 1); + const bt::UUID kDescriptorUuid(uint16_t{0x0001}); + bt::gatt::DescriptorData descriptor(kDescriptorHandle, kDescriptorUuid); + fake_client()->set_descriptors({descriptor}); + + std::optional> fidl_characteristics; + service_proxy()->DiscoverCharacteristics( + [&](std::vector chars) { + fidl_characteristics = std::move(chars); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_characteristics.has_value()); + ASSERT_EQ(fidl_characteristics->size(), 1u); + const fbg::Characteristic& fidl_characteristic = + fidl_characteristics->front(); + + ASSERT_TRUE(fidl_characteristic.has_handle()); + EXPECT_EQ(fidl_characteristic.handle().value, + static_cast(kCharacteristicValueHandle)); + + ASSERT_TRUE(fidl_characteristic.has_type()); + EXPECT_EQ(fidl_characteristic.type().value, kCharacteristicUuid.value()); + + ASSERT_TRUE(fidl_characteristic.has_properties()); + EXPECT_EQ(fidl_characteristic.properties(), + fbg::CharacteristicPropertyBits::AUTHENTICATED_SIGNED_WRITES | + fbg::CharacteristicPropertyBits::RELIABLE_WRITE | + fbg::CharacteristicPropertyBits::WRITABLE_AUXILIARIES); + + EXPECT_FALSE(fidl_characteristic.has_permissions()); + + ASSERT_TRUE(fidl_characteristic.has_descriptors()); + ASSERT_EQ(fidl_characteristic.descriptors().size(), 1u); + const fbg::Descriptor& fidl_descriptor = + fidl_characteristic.descriptors().front(); + + ASSERT_TRUE(fidl_descriptor.has_handle()); + EXPECT_EQ(fidl_descriptor.handle().value, + static_cast(kDescriptorHandle)); + + ASSERT_TRUE(fidl_descriptor.has_type()); + EXPECT_EQ(fidl_descriptor.type().value, kDescriptorUuid.value()); + + EXPECT_FALSE(fidl_descriptor.has_permissions()); +} + +TEST_F(Gatt2RemoteServiceServerTest, DiscoverCharacteristicsWithNoDescriptors) { + bt::gatt::Properties properties = 0; + bt::gatt::ExtendedProperties ext_properties = 0; + constexpr bt::att::Handle kCharacteristicHandle(kServiceStartHandle + 1); + constexpr bt::att::Handle kCharacteristicValueHandle(kCharacteristicHandle + + 1); + const bt::UUID kCharacteristicUuid(uint16_t{0x0000}); + bt::gatt::CharacteristicData characteristic(properties, + ext_properties, + kCharacteristicHandle, + kCharacteristicValueHandle, + kCharacteristicUuid); + fake_client()->set_characteristics({characteristic}); + + std::optional> fidl_characteristics; + service_proxy()->DiscoverCharacteristics( + [&](std::vector chars) { + fidl_characteristics = std::move(chars); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_characteristics.has_value()); + ASSERT_EQ(fidl_characteristics->size(), 1u); + const fbg::Characteristic& fidl_characteristic = + fidl_characteristics->front(); + EXPECT_FALSE(fidl_characteristic.has_descriptors()); +} + +TEST_F(Gatt2RemoteServiceServerTest, ReadByTypeSuccess) { + constexpr bt::UUID kCharUuid(uint16_t{0xfefe}); + + constexpr bt::att::Handle kHandle = kServiceStartHandle; + const auto kValue = bt::StaticByteBuffer(0x00, 0x01, 0x02); + const std::vector kValues = { + {kHandle, kValue.view(), /*maybe_truncated=*/false}}; + + size_t read_count = 0; + fake_client()->set_read_by_type_request_callback([&](const bt::UUID& type, + bt::att::Handle start, + bt::att::Handle end, + auto callback) { + switch (read_count++) { + case 0: + callback(fit::ok(kValues)); + break; + case 1: + callback(fit::error(bt::gatt::Client::ReadByTypeError{ + bt::att::Error(bt::att::ErrorCode::kAttributeNotFound), start})); + break; + default: + FAIL(); + } + }); + + std::optional fidl_result; + service_proxy()->ReadByType( + fuchsia::bluetooth::Uuid{kCharUuid.value()}, + [&](auto cb_result) { fidl_result = std::move(cb_result); }); + + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_result.has_value()); + ASSERT_TRUE(fidl_result->is_response()); + const auto& response = fidl_result->response(); + ASSERT_EQ(1u, response.results.size()); + const fbg::ReadByTypeResult& result0 = response.results[0]; + ASSERT_TRUE(result0.has_handle()); + EXPECT_EQ(result0.handle().value, static_cast(kHandle)); + + EXPECT_FALSE(result0.has_error()); + + ASSERT_TRUE(result0.has_value()); + const fbg::ReadValue& read_value = result0.value(); + ASSERT_TRUE(read_value.has_handle()); + EXPECT_EQ(read_value.handle().value, static_cast(kHandle)); + ASSERT_TRUE(read_value.has_maybe_truncated()); + EXPECT_FALSE(read_value.maybe_truncated()); + + ASSERT_TRUE(read_value.has_value()); + const std::vector& value = read_value.value(); + EXPECT_TRUE( + ContainersEqual(bt::BufferView(value.data(), value.size()), kValue)); +} + +TEST_F(Gatt2RemoteServiceServerTest, ReadByTypeResultPermissionError) { + constexpr bt::UUID kCharUuid(uint16_t{0xfefe}); + + size_t read_count = 0; + fake_client()->set_read_by_type_request_callback([&](const bt::UUID& type, + bt::att::Handle start, + bt::att::Handle end, + auto callback) { + ASSERT_EQ(0u, read_count++); + callback(fit::error(bt::gatt::Client::ReadByTypeError{ + bt::att::Error(bt::att::ErrorCode::kInsufficientAuthorization), + kServiceEndHandle})); + }); + + std::optional fidl_result; + service_proxy()->ReadByType( + fuchsia::bluetooth::Uuid{kCharUuid.value()}, + [&](auto cb_result) { fidl_result = std::move(cb_result); }); + + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_result.has_value()); + ASSERT_TRUE(fidl_result->is_response()); + const auto& response = fidl_result->response(); + ASSERT_EQ(1u, response.results.size()); + const fbg::ReadByTypeResult& result0 = response.results[0]; + ASSERT_TRUE(result0.has_handle()); + EXPECT_EQ(result0.handle().value, static_cast(kServiceEndHandle)); + EXPECT_FALSE(result0.has_value()); + ASSERT_TRUE(result0.has_error()); + EXPECT_EQ(fbg::Error::INSUFFICIENT_AUTHORIZATION, result0.error()); +} + +TEST_F(Gatt2RemoteServiceServerTest, ReadByTypeReturnsError) { + constexpr bt::UUID kCharUuid(uint16_t{0xfefe}); + + size_t read_count = 0; + fake_client()->set_read_by_type_request_callback([&](const bt::UUID& type, + bt::att::Handle start, + bt::att::Handle end, + auto callback) { + switch (read_count++) { + case 0: + callback(fit::error(bt::gatt::Client::ReadByTypeError{ + bt::Error(bt::HostError::kPacketMalformed), std::nullopt})); + break; + default: + FAIL(); + } + }); + + std::optional fidl_result; + service_proxy()->ReadByType( + fuchsia::bluetooth::Uuid{kCharUuid.value()}, + [&](auto cb_result) { fidl_result = std::move(cb_result); }); + + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_result.has_value()); + ASSERT_TRUE(fidl_result->is_err()); + const auto& err = fidl_result->err(); + EXPECT_EQ(fbg::Error::UNLIKELY_ERROR, err); +} + +TEST_F(Gatt2RemoteServiceServerTest, ReadByTypeInvalidUuid) { + constexpr bt::UUID kCharUuid = bt::gatt::types::kCharacteristicDeclaration; + + fake_client()->set_read_by_type_request_callback( + [&](const bt::UUID& type, + bt::att::Handle start, + bt::att::Handle end, + auto callback) { FAIL(); }); + + std::optional fidl_result; + service_proxy()->ReadByType( + fuchsia::bluetooth::Uuid{kCharUuid.value()}, + [&](auto cb_result) { fidl_result = std::move(cb_result); }); + + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_result.has_value()); + ASSERT_TRUE(fidl_result->is_err()); + const auto& err = fidl_result->err(); + EXPECT_EQ(fbg::Error::INVALID_PARAMETERS, err); +} + +TEST_F(Gatt2RemoteServiceServerTest, ReadByTypeTooManyResults) { + constexpr bt::UUID kCharUuid(uint16_t{0xfefe}); + const auto value = + bt::StaticByteBuffer(0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06); + + size_t read_count = 0; + fake_client()->set_read_by_type_request_callback([&](const bt::UUID& type, + bt::att::Handle start, + bt::att::Handle end, + auto callback) { + read_count++; + + // Ensure that more results are received than can fit in a channel. Each + // result is larger than the value payload, so receiving as many values as + // will fit in a channel is guaranteed to fill the channel and then some. + const size_t max_value_count = + static_cast(ZX_CHANNEL_MAX_MSG_BYTES) / value.size(); + if (read_count == max_value_count) { + callback(fit::error(bt::gatt::Client::ReadByTypeError{ + bt::att::Error(bt::att::ErrorCode::kAttributeNotFound), start})); + return; + } + + // Dispatch callback to prevent recursing too deep and breaking the stack. + (void)heap_dispatcher().Post( + [start, cb = std::move(callback), &value = value]( + pw::async::Context /*ctx*/, pw::Status status) { + if (status.ok()) { + std::vector values = { + {start, value.view(), /*maybe_truncated=*/false}}; + cb(fit::ok(values)); + } + }); + }); + + std::optional fidl_result; + service_proxy()->ReadByType( + fuchsia::bluetooth::Uuid{kCharUuid.value()}, + [&](auto cb_result) { fidl_result = std::move(cb_result); }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_result.has_value()); + ASSERT_TRUE(fidl_result->is_err()); + const auto& err = fidl_result->err(); + EXPECT_EQ(fbg::Error::TOO_MANY_RESULTS, err); +} + +TEST_F(Gatt2RemoteServiceServerTest, DiscoverAndReadShortCharacteristic) { + constexpr bt::att::Handle kHandle = 3; + constexpr bt::att::Handle kValueHandle = kHandle + 1; + const auto kValue = bt::StaticByteBuffer(0x00, 0x01, 0x02, 0x03, 0x04); + + bt::gatt::CharacteristicData char_data(bt::gatt::Property::kRead, + std::nullopt, + kHandle, + kValueHandle, + kServiceUuid); + fake_client()->set_characteristics({char_data}); + + std::optional> fidl_characteristics; + service_proxy()->DiscoverCharacteristics( + [&](std::vector chars) { + fidl_characteristics = std::move(chars); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_characteristics.has_value()); + ASSERT_EQ(fidl_characteristics->size(), 1u); + const fbg::Characteristic& fidl_char = fidl_characteristics->front(); + ASSERT_TRUE(fidl_char.has_handle()); + + size_t read_count = 0; + fake_client()->set_read_request_callback( + [&](bt::att::Handle handle, bt::gatt::Client::ReadCallback callback) { + read_count++; + EXPECT_EQ(handle, kValueHandle); + callback(fit::ok(), kValue, /*maybe_truncated=*/false); + }); + fake_client()->set_read_blob_request_callback( + [](auto, auto, auto) { FAIL(); }); + + fbg::ReadOptions options = + fbg::ReadOptions::WithShortRead(fbg::ShortReadOptions()); + std::optional> fidl_result; + service_proxy()->ReadCharacteristic( + fidl_char.handle(), std::move(options), [&](auto result) { + fidl_result = std::move(result); + }); + RunLoopUntilIdle(); + EXPECT_EQ(read_count, 1u); + ASSERT_TRUE(fidl_result.has_value()); + ASSERT_TRUE(fidl_result->is_ok()) + << static_cast(fidl_result->error()); + const fbg::ReadValue& read_value = fidl_result->value(); + EXPECT_TRUE(ContainersEqual(kValue, read_value.value())); + EXPECT_FALSE(read_value.maybe_truncated()); +} + +TEST_F(Gatt2RemoteServiceServerTest, + DiscoverAndReadLongCharacteristicWithOffsetAndMaxBytes) { + constexpr bt::att::Handle kHandle = 3; + constexpr bt::att::Handle kValueHandle = kHandle + 1; + const auto kValue = bt::StaticByteBuffer(0x00, 0x01, 0x02, 0x03, 0x04, 0x05); + constexpr uint16_t kOffset = 1; + constexpr uint16_t kMaxBytes = 3; + + bt::gatt::CharacteristicData char_data(bt::gatt::Property::kRead, + std::nullopt, + kHandle, + kValueHandle, + kServiceUuid); + fake_client()->set_characteristics({char_data}); + + std::optional> fidl_characteristics; + service_proxy()->DiscoverCharacteristics( + [&](std::vector chars) { + fidl_characteristics = std::move(chars); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_characteristics.has_value()); + ASSERT_EQ(fidl_characteristics->size(), 1u); + const fbg::Characteristic& fidl_char = fidl_characteristics->front(); + ASSERT_TRUE(fidl_char.has_handle()); + + fbg::LongReadOptions long_options; + long_options.set_offset(kOffset); + long_options.set_max_bytes(kMaxBytes); + fbg::ReadOptions read_options; + read_options.set_long_read(std::move(long_options)); + + size_t read_count = 0; + fake_client()->set_read_request_callback([](auto, auto) { FAIL(); }); + fake_client()->set_read_blob_request_callback( + [&](bt::att::Handle handle, + uint16_t offset, + bt::gatt::Client::ReadCallback cb) { + read_count++; + EXPECT_EQ(handle, kValueHandle); + EXPECT_EQ(offset, kOffset); + cb(fit::ok(), kValue.view(offset), /*maybe_truncated=*/false); + }); + + std::optional> fidl_result; + service_proxy()->ReadCharacteristic( + fidl_char.handle(), std::move(read_options), [&](auto result) { + fidl_result = std::move(result); + }); + RunLoopUntilIdle(); + EXPECT_EQ(read_count, 1u); + ASSERT_TRUE(fidl_result.has_value()); + ASSERT_TRUE(fidl_result->is_ok()) + << static_cast(fidl_result->error()); + const fbg::ReadValue& read_value = fidl_result->value(); + EXPECT_TRUE( + ContainersEqual(kValue.view(kOffset, kMaxBytes), read_value.value())); + EXPECT_TRUE(read_value.maybe_truncated()); +} + +TEST_F(Gatt2RemoteServiceServerTest, ReadCharacteristicHandleTooLarge) { + fbg::Handle handle; + handle.value = std::numeric_limits::max() + 1ULL; + + fbg::ReadOptions options = + fbg::ReadOptions::WithShortRead(fbg::ShortReadOptions()); + std::optional> fidl_result; + service_proxy()->ReadCharacteristic( + handle, std::move(options), [&](auto result) { + fidl_result = std::move(result); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_result.has_value()); + ASSERT_TRUE(fidl_result->is_error()); + EXPECT_EQ(fidl_result->error(), fbg::Error::INVALID_HANDLE); +} + +// Trying to read a characteristic that doesn't exist should return a FAILURE +// error. +TEST_F(Gatt2RemoteServiceServerTest, ReadCharacteristicFailure) { + constexpr bt::att::Handle kHandle = 3; + fbg::ReadOptions options = + fbg::ReadOptions::WithShortRead(fbg::ShortReadOptions()); + std::optional> fidl_result; + service_proxy()->ReadCharacteristic( + fbg::Handle{kHandle}, std::move(options), [&](auto result) { + fidl_result = std::move(result); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_result.has_value()); + ASSERT_TRUE(fidl_result->is_error()); + EXPECT_EQ(fidl_result->error(), fbg::Error::UNLIKELY_ERROR); +} + +TEST_F(Gatt2RemoteServiceServerTest, DiscoverAndReadShortDescriptor) { + constexpr bt::att::Handle kCharacteristicHandle = 2; + constexpr bt::att::Handle kCharacteristicValueHandle = 3; + constexpr bt::att::Handle kDescriptorHandle = 4; + const auto kDescriptorValue = + bt::StaticByteBuffer(0x00, 0x01, 0x02, 0x03, 0x04); + + bt::gatt::CharacteristicData char_data(bt::gatt::Property::kRead, + std::nullopt, + kCharacteristicHandle, + kCharacteristicValueHandle, + kServiceUuid); + fake_client()->set_characteristics({char_data}); + bt::gatt::DescriptorData desc_data(kDescriptorHandle, kDescriptorUuid); + fake_client()->set_descriptors({desc_data}); + + std::optional> fidl_characteristics; + service_proxy()->DiscoverCharacteristics( + [&](std::vector chars) { + fidl_characteristics = std::move(chars); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_characteristics.has_value()); + ASSERT_EQ(fidl_characteristics->size(), 1u); + const fbg::Characteristic& fidl_char = fidl_characteristics->front(); + ASSERT_TRUE(fidl_char.has_descriptors()); + ASSERT_EQ(fidl_char.descriptors().size(), 1u); + const fbg::Descriptor& fidl_desc = fidl_char.descriptors().front(); + ASSERT_TRUE(fidl_desc.has_handle()); + + size_t read_count = 0; + fake_client()->set_read_request_callback( + [&](bt::att::Handle handle, bt::gatt::Client::ReadCallback callback) { + read_count++; + EXPECT_EQ(handle, kDescriptorHandle); + callback(fit::ok(), kDescriptorValue, /*maybe_truncated=*/false); + }); + fake_client()->set_read_blob_request_callback( + [](auto, auto, auto) { FAIL(); }); + + fbg::ReadOptions options = + fbg::ReadOptions::WithShortRead(fbg::ShortReadOptions()); + std::optional> fidl_result; + service_proxy()->ReadDescriptor( + fidl_desc.handle(), std::move(options), [&](auto result) { + fidl_result = std::move(result); + }); + RunLoopUntilIdle(); + EXPECT_EQ(read_count, 1u); + ASSERT_TRUE(fidl_result.has_value()); + ASSERT_TRUE(fidl_result->is_ok()) + << static_cast(fidl_result->error()); + const fbg::ReadValue& read_value = fidl_result->value(); + EXPECT_TRUE(ContainersEqual(kDescriptorValue, read_value.value())); + EXPECT_FALSE(read_value.maybe_truncated()); +} + +TEST_F(Gatt2RemoteServiceServerTest, + DiscoverAndReadLongDescriptorWithOffsetAndMaxBytes) { + constexpr bt::att::Handle kCharacteristicHandle = 2; + constexpr bt::att::Handle kCharacteristicValueHandle = 3; + constexpr bt::att::Handle kDescriptorHandle = 4; + constexpr uint16_t kOffset = 1; + constexpr uint16_t kMaxBytes = 3; + const auto kDescriptorValue = + bt::StaticByteBuffer(0x00, 0x01, 0x02, 0x03, 0x04); + + bt::gatt::CharacteristicData char_data(bt::gatt::Property::kRead, + std::nullopt, + kCharacteristicHandle, + kCharacteristicValueHandle, + kServiceUuid); + fake_client()->set_characteristics({char_data}); + bt::gatt::DescriptorData desc_data(kDescriptorHandle, kDescriptorUuid); + fake_client()->set_descriptors({desc_data}); + + std::optional> fidl_characteristics; + service_proxy()->DiscoverCharacteristics( + [&](std::vector chars) { + fidl_characteristics = std::move(chars); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_characteristics.has_value()); + ASSERT_EQ(fidl_characteristics->size(), 1u); + const fbg::Characteristic& fidl_char = fidl_characteristics->front(); + ASSERT_TRUE(fidl_char.has_descriptors()); + ASSERT_EQ(fidl_char.descriptors().size(), 1u); + const fbg::Descriptor& fidl_desc = fidl_char.descriptors().front(); + ASSERT_TRUE(fidl_desc.has_handle()); + + fbg::LongReadOptions long_options; + long_options.set_offset(kOffset); + long_options.set_max_bytes(kMaxBytes); + fbg::ReadOptions read_options; + read_options.set_long_read(std::move(long_options)); + + size_t read_count = 0; + fake_client()->set_read_request_callback([](auto, auto) { FAIL(); }); + fake_client()->set_read_blob_request_callback( + [&](bt::att::Handle handle, + uint16_t offset, + bt::gatt::Client::ReadCallback cb) { + read_count++; + EXPECT_EQ(handle, kDescriptorHandle); + EXPECT_EQ(offset, kOffset); + cb(fit::ok(), kDescriptorValue.view(offset), /*maybe_truncated=*/false); + }); + + std::optional> fidl_result; + service_proxy()->ReadDescriptor( + fidl_desc.handle(), std::move(read_options), [&](auto result) { + fidl_result = std::move(result); + }); + RunLoopUntilIdle(); + RunLoopUntilIdle(); + EXPECT_EQ(read_count, 1u); + ASSERT_TRUE(fidl_result.has_value()); + ASSERT_TRUE(fidl_result->is_ok()) + << static_cast(fidl_result->error()); + const fbg::ReadValue& read_value = fidl_result->value(); + EXPECT_TRUE(ContainersEqual(kDescriptorValue.view(kOffset, kMaxBytes), + read_value.value())); + EXPECT_TRUE(read_value.maybe_truncated()); +} + +TEST_F(Gatt2RemoteServiceServerTest, ReadDescriptorHandleTooLarge) { + fbg::Handle handle; + handle.value = + static_cast(std::numeric_limits::max()) + 1; + + fbg::ReadOptions options = + fbg::ReadOptions::WithShortRead(fbg::ShortReadOptions()); + std::optional> fidl_result; + service_proxy()->ReadDescriptor(handle, std::move(options), [&](auto result) { + fidl_result = std::move(result); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_result.has_value()); + ASSERT_TRUE(fidl_result->is_error()); + EXPECT_EQ(fidl_result->error(), fbg::Error::INVALID_HANDLE); +} + +// Trying to read a descriptor that doesn't exist should return a FAILURE error. +TEST_F(Gatt2RemoteServiceServerTest, ReadDescriptorFailure) { + constexpr bt::att::Handle kHandle = 3; + fbg::ReadOptions options = + fbg::ReadOptions::WithShortRead(fbg::ShortReadOptions()); + std::optional> fidl_result; + service_proxy()->ReadDescriptor( + fbg::Handle{kHandle}, std::move(options), [&](auto result) { + fidl_result = std::move(result); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_result.has_value()); + ASSERT_TRUE(fidl_result->is_error()); + EXPECT_EQ(fidl_result->error(), fbg::Error::UNLIKELY_ERROR); +} + +TEST_F(Gatt2RemoteServiceServerTest, WriteCharacteristicHandleTooLarge) { + fbg::Handle handle; + handle.value = std::numeric_limits::max() + 1ULL; + + std::optional> fidl_result; + service_proxy()->WriteCharacteristic( + handle, /*value=*/{}, fbg::WriteOptions(), [&](auto result) { + fidl_result = std::move(result); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_result.has_value()); + ASSERT_TRUE(fidl_result->is_error()); + EXPECT_EQ(fidl_result->error(), fbg::Error::INVALID_HANDLE); +} + +TEST_F(Gatt2RemoteServiceServerTest, + WriteCharacteristicWithoutResponseAndNonZeroOffsetReturnsError) { + fbg::Handle handle; + handle.value = 3; + fbg::WriteOptions options; + options.set_write_mode(fbg::WriteMode::WITHOUT_RESPONSE); + options.set_offset(1); + std::optional> fidl_result; + service_proxy()->WriteCharacteristic( + handle, /*value=*/{}, std::move(options), [&](auto result) { + fidl_result = std::move(result); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_result.has_value()); + ASSERT_TRUE(fidl_result->is_error()); + EXPECT_EQ(fidl_result->error(), fbg::Error::INVALID_PARAMETERS); +} + +TEST_F(Gatt2RemoteServiceServerTest, WriteCharacteristicWithoutResponse) { + constexpr bt::att::Handle kHandle = 3; + constexpr bt::att::Handle kValueHandle = kHandle + 1; + const auto kValue = bt::StaticByteBuffer(0x00, 0x01, 0x02, 0x03, 0x04); + + bt::gatt::CharacteristicData char_data( + bt::gatt::Property::kWriteWithoutResponse, + std::nullopt, + kHandle, + kValueHandle, + kServiceUuid); + fake_client()->set_characteristics({char_data}); + + int write_count = 0; + fake_client()->set_write_without_rsp_callback( + [&](bt::att::Handle handle, + const bt::ByteBuffer& value, + bt::att::ResultFunction<> cb) { + write_count++; + EXPECT_EQ(handle, kValueHandle); + EXPECT_TRUE(ContainersEqual(value, kValue)); + cb(fit::ok()); + }); + + std::optional> fidl_characteristics; + service_proxy()->DiscoverCharacteristics( + [&](std::vector chars) { + fidl_characteristics = std::move(chars); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_characteristics.has_value()); + ASSERT_EQ(fidl_characteristics->size(), 1u); + const fbg::Characteristic& fidl_char = fidl_characteristics->front(); + ASSERT_TRUE(fidl_char.has_handle()); + + fbg::WriteOptions options; + options.set_write_mode(fbg::WriteMode::WITHOUT_RESPONSE); + + std::optional> fidl_result; + service_proxy()->WriteCharacteristic( + fidl_char.handle(), + kValue.ToVector(), + std::move(options), + [&](auto result) { fidl_result = std::move(result); }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_result.has_value()); + EXPECT_TRUE(fidl_result->is_ok()); + EXPECT_EQ(write_count, 1); +} + +TEST_F(Gatt2RemoteServiceServerTest, + WriteCharacteristicWithoutResponseValueTooLong) { + constexpr bt::att::Handle kHandle = 3; + constexpr bt::att::Handle kValueHandle = kHandle + 1; + ASSERT_EQ(fake_client()->mtu(), bt::att::kLEMinMTU); + bt::StaticByteBuffer kValue; + kValue.Fill(0x03); + + bt::gatt::CharacteristicData char_data( + bt::gatt::Property::kWriteWithoutResponse, + std::nullopt, + kHandle, + kValueHandle, + kServiceUuid); + fake_client()->set_characteristics({char_data}); + + int write_count = 0; + fake_client()->set_write_without_rsp_callback( + [&](bt::att::Handle handle, + const bt::ByteBuffer& value, + bt::att::ResultFunction<> callback) { + write_count++; + EXPECT_EQ(handle, kValueHandle); + EXPECT_TRUE(ContainersEqual(value, kValue)); + callback(bt::ToResult(bt::HostError::kFailed)); + }); + + std::optional> fidl_characteristics; + service_proxy()->DiscoverCharacteristics( + [&](std::vector chars) { + fidl_characteristics = std::move(chars); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_characteristics.has_value()); + ASSERT_EQ(fidl_characteristics->size(), 1u); + const fbg::Characteristic& fidl_char = fidl_characteristics->front(); + ASSERT_TRUE(fidl_char.has_handle()); + + fbg::WriteOptions options; + options.set_write_mode(fbg::WriteMode::WITHOUT_RESPONSE); + + std::optional> fidl_result; + service_proxy()->WriteCharacteristic( + fidl_char.handle(), + kValue.ToVector(), + std::move(options), + [&](auto result) { fidl_result = std::move(result); }); + RunLoopUntilIdle(); + EXPECT_EQ(write_count, 1); + ASSERT_TRUE(fidl_result.has_value()); + ASSERT_TRUE(fidl_result->is_error()); + EXPECT_EQ(fidl_result->error(), fbg::Error::UNLIKELY_ERROR); +} + +TEST_F(Gatt2RemoteServiceServerTest, WriteShortCharacteristic) { + constexpr bt::att::Handle kHandle = 3; + constexpr bt::att::Handle kValueHandle = kHandle + 1; + const auto kValue = bt::StaticByteBuffer(0x00, 0x01, 0x02, 0x03, 0x04); + + bt::gatt::CharacteristicData char_data(bt::gatt::Property::kWrite, + std::nullopt, + kHandle, + kValueHandle, + kServiceUuid); + fake_client()->set_characteristics({char_data}); + + int write_count = 0; + fake_client()->set_write_request_callback( + [&](bt::att::Handle handle, + const bt::ByteBuffer& value, + bt::att::ResultFunction<> callback) { + write_count++; + EXPECT_EQ(handle, kValueHandle); + EXPECT_TRUE(ContainersEqual(value, kValue)); + callback(fit::ok()); + }); + + std::optional> fidl_characteristics; + service_proxy()->DiscoverCharacteristics( + [&](std::vector chars) { + fidl_characteristics = std::move(chars); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_characteristics.has_value()); + ASSERT_EQ(fidl_characteristics->size(), 1u); + const fbg::Characteristic& fidl_char = fidl_characteristics->front(); + ASSERT_TRUE(fidl_char.has_handle()); + + fbg::WriteOptions options; + std::optional> fidl_result; + service_proxy()->WriteCharacteristic( + fidl_char.handle(), + kValue.ToVector(), + std::move(options), + [&](auto result) { fidl_result = std::move(result); }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_result.has_value()); + EXPECT_TRUE(fidl_result->is_ok()); + EXPECT_EQ(write_count, 1); +} + +TEST_F(Gatt2RemoteServiceServerTest, + WriteShortCharacteristicWithNonZeroOffset) { + constexpr bt::att::Handle kHandle = 3; + constexpr bt::att::Handle kValueHandle = kHandle + 1; + const auto kValue = bt::StaticByteBuffer(0x00, 0x01, 0x02, 0x03, 0x04); + const uint16_t kOffset = 1; + + bt::gatt::CharacteristicData char_data(bt::gatt::Property::kWrite, + std::nullopt, + kHandle, + kValueHandle, + kServiceUuid); + fake_client()->set_characteristics({char_data}); + + int write_count = 0; + fake_client()->set_execute_prepare_writes_callback( + [&](bt::att::PrepareWriteQueue prep_write_queue, + bt::gatt::ReliableMode reliable, + bt::att::ResultFunction<> callback) { + write_count++; + ASSERT_EQ(prep_write_queue.size(), 1u); + EXPECT_EQ(prep_write_queue.front().handle(), kValueHandle); + EXPECT_EQ(prep_write_queue.front().offset(), kOffset); + EXPECT_EQ(reliable, bt::gatt::ReliableMode::kDisabled); + EXPECT_TRUE(ContainersEqual(prep_write_queue.front().value(), kValue)); + callback(fit::ok()); + }); + + std::optional> fidl_characteristics; + service_proxy()->DiscoverCharacteristics( + [&](std::vector chars) { + fidl_characteristics = std::move(chars); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_characteristics.has_value()); + ASSERT_EQ(fidl_characteristics->size(), 1u); + const fbg::Characteristic& fidl_char = fidl_characteristics->front(); + ASSERT_TRUE(fidl_char.has_handle()); + + fbg::WriteOptions options; + options.set_offset(kOffset); + std::optional> fidl_result; + service_proxy()->WriteCharacteristic( + fidl_char.handle(), + kValue.ToVector(), + std::move(options), + [&](auto result) { fidl_result = std::move(result); }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_result.has_value()); + EXPECT_TRUE(fidl_result->is_ok()); + EXPECT_EQ(write_count, 1); +} + +TEST_F(Gatt2RemoteServiceServerTest, WriteShortCharacteristicWithReliableMode) { + constexpr bt::att::Handle kHandle = 3; + constexpr bt::att::Handle kValueHandle = kHandle + 1; + const auto kValue = bt::StaticByteBuffer(0x00, 0x01, 0x02, 0x03, 0x04); + + bt::gatt::CharacteristicData char_data(bt::gatt::Property::kWrite, + std::nullopt, + kHandle, + kValueHandle, + kServiceUuid); + fake_client()->set_characteristics({char_data}); + + int write_count = 0; + fake_client()->set_execute_prepare_writes_callback( + [&](bt::att::PrepareWriteQueue prep_write_queue, + bt::gatt::ReliableMode reliable, + bt::att::ResultFunction<> callback) { + write_count++; + ASSERT_EQ(prep_write_queue.size(), 1u); + EXPECT_EQ(reliable, bt::gatt::ReliableMode::kEnabled); + EXPECT_EQ(prep_write_queue.front().handle(), kValueHandle); + EXPECT_EQ(prep_write_queue.front().offset(), 0u); + EXPECT_TRUE(ContainersEqual(prep_write_queue.front().value(), kValue)); + callback(fit::ok()); + }); + + std::optional> fidl_characteristics; + service_proxy()->DiscoverCharacteristics( + [&](std::vector chars) { + fidl_characteristics = std::move(chars); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_characteristics.has_value()); + ASSERT_EQ(fidl_characteristics->size(), 1u); + const fbg::Characteristic& fidl_char = fidl_characteristics->front(); + ASSERT_TRUE(fidl_char.has_handle()); + + fbg::WriteOptions options; + options.set_write_mode(fbg::WriteMode::RELIABLE); + std::optional> fidl_result; + service_proxy()->WriteCharacteristic( + fidl_char.handle(), + kValue.ToVector(), + std::move(options), + [&](auto result) { fidl_result = std::move(result); }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_result.has_value()); + EXPECT_TRUE(fidl_result->is_ok()); + EXPECT_EQ(write_count, 1); +} + +TEST_F(Gatt2RemoteServiceServerTest, WriteLongCharacteristicDefaultOptions) { + constexpr bt::att::Handle kHandle = 3; + constexpr bt::att::Handle kValueHandle = kHandle + 1; + constexpr size_t kHeaderSize = + sizeof(bt::att::OpCode) + sizeof(bt::att::PrepareWriteRequestParams); + const uint16_t kMtu = fake_client()->mtu(); + const size_t kFirstPacketValueSize = kMtu - kHeaderSize; + bt::DynamicByteBuffer kValue(kMtu); + kValue.Fill(0x03); + + bt::gatt::CharacteristicData char_data(bt::gatt::Property::kWrite, + std::nullopt, + kHandle, + kValueHandle, + kServiceUuid); + fake_client()->set_characteristics({char_data}); + + int write_count = 0; + fake_client()->set_execute_prepare_writes_callback( + [&](bt::att::PrepareWriteQueue prep_write_queue, + bt::gatt::ReliableMode reliable, + bt::att::ResultFunction<> callback) { + write_count++; + EXPECT_EQ(reliable, bt::gatt::ReliableMode::kDisabled); + ASSERT_EQ(prep_write_queue.size(), 2u); + EXPECT_EQ(prep_write_queue.front().handle(), kValueHandle); + EXPECT_EQ(prep_write_queue.front().offset(), 0u); + EXPECT_TRUE(ContainersEqual(kValue.view(0, kFirstPacketValueSize), + prep_write_queue.front().value())); + prep_write_queue.pop(); + EXPECT_EQ(prep_write_queue.front().handle(), kValueHandle); + EXPECT_EQ(prep_write_queue.front().offset(), kFirstPacketValueSize); + EXPECT_TRUE(ContainersEqual(kValue.view(kFirstPacketValueSize), + prep_write_queue.front().value())); + callback(fit::ok()); + }); + + std::optional> fidl_characteristics; + service_proxy()->DiscoverCharacteristics( + [&](std::vector chars) { + fidl_characteristics = std::move(chars); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_characteristics.has_value()); + ASSERT_EQ(fidl_characteristics->size(), 1u); + const fbg::Characteristic& fidl_char = fidl_characteristics->front(); + ASSERT_TRUE(fidl_char.has_handle()); + + std::optional> fidl_result; + service_proxy()->WriteCharacteristic( + fidl_char.handle(), + kValue.ToVector(), + fbg::WriteOptions(), + [&](auto result) { fidl_result = std::move(result); }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_result.has_value()); + EXPECT_TRUE(fidl_result->is_ok()); + EXPECT_EQ(write_count, 1); +} + +TEST_F(Gatt2RemoteServiceServerTest, WriteDescriptorHandleTooLarge) { + fbg::Handle handle; + handle.value = std::numeric_limits::max() + 1ULL; + + std::optional> fidl_result; + service_proxy()->WriteDescriptor( + handle, /*value=*/{}, fbg::WriteOptions(), [&](auto result) { + fidl_result = std::move(result); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_result.has_value()); + ASSERT_TRUE(fidl_result->is_error()); + EXPECT_EQ(fidl_result->error(), fbg::Error::INVALID_HANDLE); +} + +TEST_F(Gatt2RemoteServiceServerTest, + WriteDescriptorWithoutResponseNotSupported) { + constexpr bt::att::Handle kHandle = 3; + fbg::WriteOptions options; + options.set_write_mode(fbg::WriteMode::WITHOUT_RESPONSE); + + std::optional> fidl_result; + service_proxy()->WriteDescriptor( + fbg::Handle{kHandle}, /*value=*/{}, std::move(options), [&](auto result) { + fidl_result = std::move(result); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_result.has_value()); + ASSERT_TRUE(fidl_result->is_error()); + EXPECT_EQ(fidl_result->error(), fbg::Error::INVALID_PARAMETERS); +} + +TEST_F(Gatt2RemoteServiceServerTest, WriteDescriptorReliableNotSupported) { + constexpr bt::att::Handle kHandle = 3; + fbg::WriteOptions options; + options.set_write_mode(fbg::WriteMode::RELIABLE); + + std::optional> fidl_result; + service_proxy()->WriteDescriptor( + fbg::Handle{kHandle}, /*value=*/{}, std::move(options), [&](auto result) { + fidl_result = std::move(result); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_result.has_value()); + ASSERT_TRUE(fidl_result->is_error()); + EXPECT_EQ(fidl_result->error(), fbg::Error::INVALID_PARAMETERS); +} + +TEST_F(Gatt2RemoteServiceServerTest, WriteShortDescriptor) { + constexpr bt::att::Handle kHandle = 3; + constexpr bt::att::Handle kCharacteristicValueHandle = kHandle + 1; + bt::gatt::CharacteristicData char_data(bt::gatt::Property::kWrite, + std::nullopt, + kHandle, + kCharacteristicValueHandle, + kServiceUuid); + fake_client()->set_characteristics({char_data}); + + constexpr bt::att::Handle kDescriptorHandle(kCharacteristicValueHandle + 1); + const bt::UUID kDescriptorUuid(uint16_t{0x0001}); + bt::gatt::DescriptorData descriptor(kDescriptorHandle, kDescriptorUuid); + const auto kValue = bt::StaticByteBuffer(0x00, 0x01, 0x02, 0x03, 0x04); + fake_client()->set_descriptors({descriptor}); + + int write_count = 0; + fake_client()->set_write_request_callback( + [&](bt::att::Handle handle, + const bt::ByteBuffer& value, + bt::att::ResultFunction<> callback) { + write_count++; + EXPECT_EQ(handle, kDescriptorHandle); + EXPECT_TRUE(ContainersEqual(value, kValue)); + callback(fit::ok()); + }); + + std::optional> fidl_characteristics; + service_proxy()->DiscoverCharacteristics( + [&](std::vector chars) { + fidl_characteristics = std::move(chars); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_characteristics.has_value()); + ASSERT_EQ(fidl_characteristics->size(), 1u); + const fbg::Characteristic& fidl_char = fidl_characteristics->front(); + ASSERT_TRUE(fidl_char.has_descriptors()); + ASSERT_EQ(fidl_char.descriptors().size(), 1u); + + fbg::WriteOptions options; + std::optional> fidl_result; + service_proxy()->WriteDescriptor( + fidl_char.descriptors().front().handle(), + kValue.ToVector(), + std::move(options), + [&](auto result) { fidl_result = std::move(result); }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_result.has_value()); + EXPECT_TRUE(fidl_result->is_ok()); + EXPECT_EQ(write_count, 1); +} + +TEST_F(Gatt2RemoteServiceServerTest, WriteShortDescriptorWithNonZeroOffset) { + constexpr bt::att::Handle kHandle = 3; + constexpr bt::att::Handle kCharacteristicValueHandle = kHandle + 1; + bt::gatt::CharacteristicData char_data(bt::gatt::Property::kWrite, + std::nullopt, + kHandle, + kCharacteristicValueHandle, + kServiceUuid); + fake_client()->set_characteristics({char_data}); + + constexpr bt::att::Handle kDescriptorHandle(kCharacteristicValueHandle + 1); + const bt::UUID kDescriptorUuid(uint16_t{0x0001}); + bt::gatt::DescriptorData descriptor(kDescriptorHandle, kDescriptorUuid); + fake_client()->set_descriptors({descriptor}); + + const auto kValue = bt::StaticByteBuffer(0x00, 0x01, 0x02, 0x03, 0x04); + const uint16_t kOffset = 1; + + int write_count = 0; + fake_client()->set_execute_prepare_writes_callback( + [&](bt::att::PrepareWriteQueue prep_write_queue, + bt::gatt::ReliableMode reliable, + bt::att::ResultFunction<> callback) { + write_count++; + ASSERT_EQ(prep_write_queue.size(), 1u); + EXPECT_EQ(prep_write_queue.front().handle(), kDescriptorHandle); + EXPECT_EQ(prep_write_queue.front().offset(), kOffset); + EXPECT_EQ(reliable, bt::gatt::ReliableMode::kDisabled); + EXPECT_TRUE(ContainersEqual(prep_write_queue.front().value(), kValue)); + callback(fit::ok()); + }); + + std::optional> fidl_characteristics; + service_proxy()->DiscoverCharacteristics( + [&](std::vector chars) { + fidl_characteristics = std::move(chars); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_characteristics.has_value()); + ASSERT_EQ(fidl_characteristics->size(), 1u); + const fbg::Characteristic& fidl_char = fidl_characteristics->front(); + ASSERT_TRUE(fidl_char.has_descriptors()); + ASSERT_EQ(fidl_char.descriptors().size(), 1u); + + fbg::WriteOptions options; + options.set_offset(kOffset); + std::optional> fidl_result; + service_proxy()->WriteDescriptor( + fidl_char.descriptors().front().handle(), + kValue.ToVector(), + std::move(options), + [&](auto result) { fidl_result = std::move(result); }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_result.has_value()); + EXPECT_TRUE(fidl_result->is_ok()); + EXPECT_EQ(write_count, 1); +} + +TEST_F(Gatt2RemoteServiceServerTest, WriteLongDescriptorDefaultOptions) { + constexpr bt::att::Handle kHandle = 3; + constexpr bt::att::Handle kCharacteristicValueHandle = kHandle + 1; + bt::gatt::CharacteristicData char_data(bt::gatt::Property::kWrite, + std::nullopt, + kHandle, + kCharacteristicValueHandle, + kServiceUuid); + fake_client()->set_characteristics({char_data}); + + constexpr bt::att::Handle kDescriptorHandle(kCharacteristicValueHandle + 1); + const bt::UUID kDescriptorUuid(uint16_t{0x0001}); + bt::gatt::DescriptorData descriptor(kDescriptorHandle, kDescriptorUuid); + fake_client()->set_descriptors({descriptor}); + + constexpr size_t kHeaderSize = + sizeof(bt::att::OpCode) + sizeof(bt::att::PrepareWriteRequestParams); + const uint16_t kMtu = fake_client()->mtu(); + const size_t kFirstPacketValueSize = kMtu - kHeaderSize; + bt::DynamicByteBuffer kValue(kMtu); + kValue.Fill(0x03); + + int write_count = 0; + fake_client()->set_execute_prepare_writes_callback( + [&](bt::att::PrepareWriteQueue prep_write_queue, + bt::gatt::ReliableMode reliable, + bt::att::ResultFunction<> callback) { + write_count++; + EXPECT_EQ(reliable, bt::gatt::ReliableMode::kDisabled); + ASSERT_EQ(prep_write_queue.size(), 2u); + EXPECT_EQ(prep_write_queue.front().handle(), kDescriptorHandle); + EXPECT_EQ(prep_write_queue.front().offset(), 0u); + EXPECT_TRUE(ContainersEqual(kValue.view(0, kFirstPacketValueSize), + prep_write_queue.front().value())); + prep_write_queue.pop(); + EXPECT_EQ(prep_write_queue.front().handle(), kDescriptorHandle); + EXPECT_EQ(prep_write_queue.front().offset(), kFirstPacketValueSize); + EXPECT_TRUE(ContainersEqual(kValue.view(kFirstPacketValueSize), + prep_write_queue.front().value())); + callback(fit::ok()); + }); + + std::optional> fidl_characteristics; + service_proxy()->DiscoverCharacteristics( + [&](std::vector chars) { + fidl_characteristics = std::move(chars); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_characteristics.has_value()); + ASSERT_EQ(fidl_characteristics->size(), 1u); + const fbg::Characteristic& fidl_char = fidl_characteristics->front(); + ASSERT_TRUE(fidl_char.has_descriptors()); + ASSERT_EQ(fidl_char.descriptors().size(), 1u); + + std::optional> fidl_result; + service_proxy()->WriteDescriptor( + fidl_char.descriptors().front().handle(), + kValue.ToVector(), + fbg::WriteOptions(), + [&](auto result) { fidl_result = std::move(result); }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_result.has_value()); + EXPECT_TRUE(fidl_result->is_ok()); + EXPECT_EQ(write_count, 1); +} + +class FakeCharacteristicNotifier + : public fbg::testing::CharacteristicNotifier_TestBase { + public: + struct Notification { + fbg::ReadValue value; + OnNotificationCallback notification_cb; + }; + explicit FakeCharacteristicNotifier( + fidl::InterfaceRequest request) + : binding_(this, std::move(request)) { + binding_.set_error_handler([this](zx_status_t status) { error_ = status; }); + } + + void Unbind() { binding_.Unbind(); } + + void OnNotification(fbg::ReadValue value, + OnNotificationCallback callback) override { + notifications_.push_back( + Notification{std::move(value), std::move(callback)}); + } + + std::vector& notifications() { return notifications_; } + + std::optional error() const { return error_; } + + private: + void NotImplemented_(const std::string& name) override { + FAIL() << name << " is not implemented"; + } + + fidl::Binding binding_; + std::vector notifications_; + std::optional error_; +}; + +class Gatt2RemoteServiceServerCharacteristicNotifierTest + : public Gatt2RemoteServiceServerTest { + public: + void SetUp() override { + Gatt2RemoteServiceServerTest::SetUp(); + bt::gatt::CharacteristicData characteristic(bt::gatt::Property::kNotify, + /*ext_props=*/std::nullopt, + char_handle_, + char_value_handle_, + kCharacteristicUuid); + fake_client()->set_characteristics({characteristic}); + bt::gatt::DescriptorData ccc_descriptor( + ccc_descriptor_handle_, bt::gatt::types::kClientCharacteristicConfig); + fake_client()->set_descriptors({ccc_descriptor}); + + std::optional> fidl_characteristics; + service_proxy()->DiscoverCharacteristics( + [&](std::vector chars) { + fidl_characteristics = std::move(chars); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_characteristics.has_value()); + ASSERT_EQ(fidl_characteristics->size(), 1u); + characteristic_ = std::move(fidl_characteristics->front()); + } + + protected: + const fbg::Characteristic& characteristic() const { return characteristic_; } + bt::att::Handle characteristic_handle() const { return char_handle_; } + bt::att::Handle characteristic_value_handle() const { + return char_value_handle_; + } + bt::att::Handle ccc_descriptor_handle() const { + return ccc_descriptor_handle_; + } + + private: + fbg::Characteristic characteristic_; + const bt::att::Handle char_handle_ = 2; + const bt::att::Handle char_value_handle_ = 3; + const bt::att::Handle ccc_descriptor_handle_ = 4; +}; + +TEST_F(Gatt2RemoteServiceServerCharacteristicNotifierTest, + RegisterCharacteristicNotifierReceiveNotificationsAndUnregister) { + const auto kValue0 = bt::StaticByteBuffer(0x01, 0x02, 0x03); + const auto kValue1 = bt::StaticByteBuffer(0x04, 0x05, 0x06); + + // Respond to CCC write with success status so that notifications are enabled. + fake_client()->set_write_request_callback([&](bt::att::Handle handle, + const bt::ByteBuffer& /*value*/, + auto status_callback) { + EXPECT_EQ(handle, ccc_descriptor_handle()); + status_callback(fit::ok()); + }); + + fidl::InterfaceHandle notifier_handle; + FakeCharacteristicNotifier notifier_server(notifier_handle.NewRequest()); + auto& notifications = notifier_server.notifications(); + + std::optional> register_result; + auto register_cb = [&](fpromise::result result) { + register_result = result; + }; + service_proxy()->RegisterCharacteristicNotifier(characteristic().handle(), + std::move(notifier_handle), + std::move(register_cb)); + RunLoopUntilIdle(); + ASSERT_TRUE(register_result.has_value()); + EXPECT_TRUE(register_result->is_ok()); + + // Send 2 notifications to test flow control. + service()->HandleNotificationForTesting(characteristic_value_handle(), + kValue0, + /*maybe_truncated=*/false); + service()->HandleNotificationForTesting(characteristic_value_handle(), + kValue1, + /*maybe_truncated=*/true); + RunLoopUntilIdle(); + ASSERT_EQ(notifications.size(), 1u); + fbg::ReadValue& notification_0 = notifications[0].value; + ASSERT_TRUE(notification_0.has_value()); + EXPECT_TRUE(bt::ContainersEqual(notification_0.value(), kValue0)); + ASSERT_TRUE(notification_0.has_handle()); + EXPECT_EQ(notification_0.handle().value, + static_cast(characteristic_value_handle())); + ASSERT_TRUE(notification_0.has_maybe_truncated()); + ASSERT_FALSE(notification_0.maybe_truncated()); + + notifications[0].notification_cb(); + RunLoopUntilIdle(); + ASSERT_EQ(notifications.size(), 2u); + fbg::ReadValue& notification_1 = notifications[1].value; + ASSERT_TRUE(notification_1.has_value()); + EXPECT_TRUE(bt::ContainersEqual(notification_1.value(), kValue1)); + ASSERT_TRUE(notification_1.has_handle()); + EXPECT_EQ(notification_1.handle().value, + static_cast(characteristic_value_handle())); + ASSERT_TRUE(notification_1.has_maybe_truncated()); + ASSERT_TRUE(notification_1.maybe_truncated()); + + notifications[1].notification_cb(); + RunLoopUntilIdle(); + EXPECT_EQ(notifications.size(), 2u); + + notifier_server.Unbind(); + RunLoopUntilIdle(); + + // Notifications should be ignored after notifier is unregistered. + service()->HandleNotificationForTesting(characteristic_value_handle(), + kValue0, + /*maybe_truncated=*/false); + RunLoopUntilIdle(); +} + +TEST_F(Gatt2RemoteServiceServerCharacteristicNotifierTest, + QueueTooManyNotificationsAndCloseNotifier) { + const auto kValue = bt::StaticByteBuffer(0x01, 0x02, 0x03); + + // Respond to CCC write with success status so that notifications are enabled. + fake_client()->set_write_request_callback([&](bt::att::Handle handle, + const bt::ByteBuffer& /*value*/, + auto status_callback) { + EXPECT_EQ(handle, ccc_descriptor_handle()); + status_callback(fit::ok()); + }); + + fidl::InterfaceHandle notifier_handle; + FakeCharacteristicNotifier notifier_server(notifier_handle.NewRequest()); + auto& notifications = notifier_server.notifications(); + + std::optional> register_result; + auto register_cb = [&](fpromise::result result) { + register_result = result; + }; + service_proxy()->RegisterCharacteristicNotifier(characteristic().handle(), + std::move(notifier_handle), + std::move(register_cb)); + RunLoopUntilIdle(); + ASSERT_TRUE(register_result.has_value()); + EXPECT_TRUE(register_result->is_ok()); + + // Fill the pending notifier values queue. + for (size_t i = 0; i < Gatt2RemoteServiceServer::kMaxPendingNotifierValues; + i++) { + service()->HandleNotificationForTesting(characteristic_value_handle(), + kValue, + /*maybe_truncated=*/false); + } + RunLoopUntilIdle(); + ASSERT_EQ(notifications.size(), 1u); + EXPECT_FALSE(notifier_server.error().has_value()); + + // This notification should exceed the max queue size. + service()->HandleNotificationForTesting(characteristic_value_handle(), + kValue, + /*maybe_truncated=*/false); + RunLoopUntilIdle(); + EXPECT_TRUE(notifier_server.error().has_value()); + + // Notifications should be ignored after notifier is unregistered due to an + // error. + service()->HandleNotificationForTesting(characteristic_value_handle(), + kValue, + /*maybe_truncated=*/false); + RunLoopUntilIdle(); + notifications[0].notification_cb(); + EXPECT_EQ(notifications.size(), 1u); +} + +TEST_F(Gatt2RemoteServiceServerCharacteristicNotifierTest, + RegisterCharacteristicNotifierWriteError) { + // Respond to CCC write with error status so that registration fails. + fake_client()->set_write_request_callback([&](bt::att::Handle handle, + const bt::ByteBuffer& /*value*/, + auto status_callback) { + EXPECT_EQ(handle, ccc_descriptor_handle()); + status_callback( + bt::ToResult(bt::att::ErrorCode::kInsufficientAuthentication)); + }); + + fidl::InterfaceHandle notifier_handle; + FakeCharacteristicNotifier notifier_server(notifier_handle.NewRequest()); + std::optional> register_result; + auto register_cb = [&](fpromise::result result) { + register_result = result; + }; + service_proxy()->RegisterCharacteristicNotifier(characteristic().handle(), + std::move(notifier_handle), + std::move(register_cb)); + RunLoopUntilIdle(); + ASSERT_TRUE(register_result.has_value()); + ASSERT_TRUE(register_result->is_error()); + EXPECT_EQ(register_result->error(), fbg::Error::INSUFFICIENT_AUTHENTICATION); + EXPECT_TRUE(notifier_server.error().has_value()); +} + +TEST_F( + Gatt2RemoteServiceServerCharacteristicNotifierTest, + RegisterCharacteristicNotifierAndDestroyServerBeforeStatusCallbackCausesNotificationsToBeDisabled) { + // Respond to CCC write with success status so that notifications are enabled. + int ccc_write_count = 0; + bt::att::ResultFunction<> enable_notifications_status_cb = nullptr; + fake_client()->set_write_request_callback( + [&](bt::att::Handle handle, + const bt::ByteBuffer& value, + bt::att::ResultFunction<> status_callback) { + ccc_write_count++; + EXPECT_EQ(handle, ccc_descriptor_handle()); + if (ccc_write_count == 1) { + EXPECT_NE(value[0], 0u); // Enable value + enable_notifications_status_cb = std::move(status_callback); + } else { + EXPECT_EQ(value[0], 0u); // Disable value + status_callback(fit::ok()); + } + }); + + fidl::InterfaceHandle notifier_handle; + FakeCharacteristicNotifier notifier_server(notifier_handle.NewRequest()); + + std::optional> register_result; + auto register_cb = [&](fpromise::result result) { + register_result = result; + }; + service_proxy()->RegisterCharacteristicNotifier(characteristic().handle(), + std::move(notifier_handle), + std::move(register_cb)); + RunLoopUntilIdle(); + EXPECT_FALSE(register_result.has_value()); + EXPECT_EQ(ccc_write_count, 1); + + DestroyServer(); + ASSERT_TRUE(enable_notifications_status_cb); + enable_notifications_status_cb(fit::ok()); + RunLoopUntilIdle(); + // Notifications should have been disabled in enable notifications status + // callback. + EXPECT_EQ(ccc_write_count, 2); +} + +TEST_F( + Gatt2RemoteServiceServerCharacteristicNotifierTest, + RegisterCharacteristicNotifierAndDestroyServerAfterStatusCallbackCausesNotificationsToBeDisabled) { + // Respond to CCC write with success status so that notifications are enabled. + int ccc_write_count = 0; + bt::att::ResultFunction<> enable_notifications_status_cb = nullptr; + fake_client()->set_write_request_callback( + [&](bt::att::Handle handle, + const bt::ByteBuffer& value, + bt::att::ResultFunction<> status_callback) { + ccc_write_count++; + EXPECT_EQ(handle, ccc_descriptor_handle()); + if (ccc_write_count == 1) { + EXPECT_NE(value[0], 0u); // Enable value + } else { + EXPECT_EQ(value[0], 0u); // Disable value + } + status_callback(fit::ok()); + }); + + fidl::InterfaceHandle notifier_handle; + FakeCharacteristicNotifier notifier_server(notifier_handle.NewRequest()); + + std::optional> register_result; + auto register_cb = [&](fpromise::result result) { + register_result = result; + }; + service_proxy()->RegisterCharacteristicNotifier(characteristic().handle(), + std::move(notifier_handle), + std::move(register_cb)); + RunLoopUntilIdle(); + EXPECT_TRUE(register_result.has_value()); + EXPECT_TRUE(register_result->is_ok()); + EXPECT_EQ(ccc_write_count, 1); + + DestroyServer(); + RunLoopUntilIdle(); + // Notifications should have been disabled in the server destructor. + EXPECT_EQ(ccc_write_count, 2); +} + +} // namespace +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_server_server.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_server_server.cc new file mode 100644 index 0000000000..9318a4c04a --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_server_server.cc @@ -0,0 +1,378 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_server_server.h" + +#include +#include + +#include "fuchsia/bluetooth/cpp/fidl.h" +#include "fuchsia/bluetooth/gatt2/cpp/fidl.h" +#include "lib/fidl/cpp/interface_ptr.h" +#include "lib/fit/function.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_server_ids.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/helpers.h" +#include "pw_bluetooth_sapphire/internal/host/att/att.h" +#include "pw_bluetooth_sapphire/internal/host/common/log.h" +#include "pw_bluetooth_sapphire/internal/host/common/uuid.h" +#include "pw_bluetooth_sapphire/internal/host/gatt/gatt_defs.h" +#include "pw_bluetooth_sapphire/internal/host/gatt/server.h" + +namespace fbt = fuchsia::bluetooth; +namespace btg = bt::gatt; + +using fuchsia::bluetooth::gatt2::AttributePermissions; +using fuchsia::bluetooth::gatt2::Characteristic; +using fuchsia::bluetooth::gatt2::Descriptor; +using fgatt2Err = fuchsia::bluetooth::gatt2::Error; +using fuchsia::bluetooth::gatt2::Handle; +using fuchsia::bluetooth::gatt2::INITIAL_VALUE_CHANGED_CREDITS; +using fuchsia::bluetooth::gatt2::LocalService; +using fuchsia::bluetooth::gatt2::LocalService_ReadValue_Result; +using fuchsia::bluetooth::gatt2::LocalService_WriteValue_Result; +using fuchsia::bluetooth::gatt2::LocalServicePeerUpdateRequest; +using fuchsia::bluetooth::gatt2::LocalServiceWriteValueRequest; +using fuchsia::bluetooth::gatt2::PublishServiceError; +using fuchsia::bluetooth::gatt2::SecurityRequirements; +using fuchsia::bluetooth::gatt2::ServiceInfo; +using fuchsia::bluetooth::gatt2::ServiceKind; +using fuchsia::bluetooth::gatt2::ValueChangedParameters; + +namespace bthost { + +Gatt2ServerServer::Gatt2ServerServer( + btg::GATT::WeakPtr gatt, + fidl::InterfaceRequest request) + : GattServerBase(std::move(gatt), this, std::move(request)), + weak_self_(this) {} + +Gatt2ServerServer::~Gatt2ServerServer() { + // Remove all services from the local GATT host. + for (const auto& iter : services_) { + gatt()->UnregisterService(iter.first.value()); + } +} + +void Gatt2ServerServer::RemoveService(InternalServiceId id) { + // Close the connection to the FIDL LocalService. + services_.erase(id); + // Unregister the service from the local GATT host. Don't remove the ID from + // service_id_mapping_ to prevent service ID reuse. + gatt()->UnregisterService(id.value()); +} + +void Gatt2ServerServer::PublishService( + ServiceInfo info, + fidl::InterfaceHandle service, + PublishServiceCallback callback) { + if (!info.has_handle()) { + bt_log(WARN, "fidl", "%s: `info` missing required `handle`", __FUNCTION__); + callback(fpromise::error(PublishServiceError::INVALID_SERVICE_HANDLE)); + return; + } + if (!info.has_type()) { + bt_log( + WARN, "fidl", "%s: `info` missing required `type` UUID", __FUNCTION__); + callback(fpromise::error(PublishServiceError::INVALID_UUID)); + return; + } + if (!info.has_characteristics()) { + bt_log(WARN, + "fidl", + "%s: `info` missing required `characteristics`", + __FUNCTION__); + callback(fpromise::error(PublishServiceError::INVALID_CHARACTERISTICS)); + return; + } + + ClientServiceId client_svc_id(info.handle().value); + if (service_id_mapping_.count(client_svc_id) != 0) { + bt_log(WARN, + "fidl", + "%s: cannot publish service with already-used `handle`", + __FUNCTION__); + callback(fpromise::error(PublishServiceError::INVALID_SERVICE_HANDLE)); + return; + } + + bt::UUID service_type(info.type().value); + // The default value for kind is PRIMARY if not present. + bool primary = info.has_kind() ? info.kind() == ServiceKind::PRIMARY : true; + + // Process the FIDL service tree. + auto gatt_svc = std::make_unique(primary, service_type); + for (const auto& fidl_chrc : info.characteristics()) { + btg::CharacteristicPtr maybe_chrc = + fidl_helpers::Gatt2CharacteristicFromFidl(fidl_chrc); + if (!maybe_chrc) { + callback(fpromise::error(PublishServiceError::INVALID_CHARACTERISTICS)); + return; + } + + gatt_svc->AddCharacteristic(std::move(maybe_chrc)); + } + + auto self = weak_self_.GetWeakPtr(); + auto id_cb = [self, + service_handle = std::move(service), + client_svc_id, + callback = std::move(callback)](btg::IdType raw_id) mutable { + if (!self.is_alive()) { + return; + } + + if (raw_id == bt::gatt::kInvalidId) { + bt_log(INFO, + "bt-host", + "internal error publishing service (handle: %lu)", + client_svc_id.value()); + callback(fpromise::error(PublishServiceError::UNLIKELY_ERROR)); + return; + } + InternalServiceId internal_id(raw_id); + auto error_handler = + [self, client_svc_id, internal_id](zx_status_t status) { + bt_log(INFO, + "bt-host", + "LocalService shut down, removing GATT service (id: %lu)", + client_svc_id.value()); + self->RemoveService(internal_id); + }; + + fidl::InterfacePtr service_ptr = service_handle.Bind(); + service_ptr.set_error_handler(error_handler); + service_ptr.events().OnSuppressDiscovery = [self, internal_id]() { + self->OnSuppressDiscovery(internal_id); + }; + service_ptr.events().OnNotifyValue = + [self, internal_id](ValueChangedParameters update) { + self->OnNotifyValue(internal_id, std::move(update)); + }; + service_ptr.events().OnIndicateValue = [self, internal_id]( + ValueChangedParameters update, + zx::eventpair confirm) { + self->OnIndicateValue(internal_id, std::move(update), std::move(confirm)); + }; + self->services_.emplace(internal_id, + Service{.local_svc_ptr = std::move(service_ptr)}); + self->service_id_mapping_[client_svc_id] = internal_id; + callback(fpromise::ok()); + }; + + gatt()->RegisterService( + std::move(gatt_svc), + std::move(id_cb), + fit::bind_member<&Gatt2ServerServer::OnReadRequest>(this), + fit::bind_member<&Gatt2ServerServer::OnWriteRequest>(this), + fit::bind_member<&Gatt2ServerServer::OnClientCharacteristicConfiguration>( + this)); +} + +void Gatt2ServerServer::OnReadRequest(bt::PeerId peer_id, + bt::gatt::IdType service_id, + btg::IdType id, + uint16_t offset, + btg::ReadResponder responder) { + auto svc_iter = services_.find(InternalServiceId(service_id)); + // GATT must only send read requests for registered services. + BT_ASSERT(svc_iter != services_.end()); + + auto cb = [responder = std::move(responder)]( + LocalService_ReadValue_Result res) mutable { + if (res.is_err()) { + responder(fit::error(fidl_helpers::Gatt2ErrorCodeFromFidl(res.err())), + bt::BufferView()); + return; + } + responder(fit::ok(), bt::BufferView(res.response().value)); + }; + svc_iter->second.local_svc_ptr->ReadValue( + fbt::PeerId{peer_id.value()}, Handle{id}, offset, std::move(cb)); +} + +void Gatt2ServerServer::OnWriteRequest(bt::PeerId peer_id, + bt::gatt::IdType service_id, + btg::IdType id, + uint16_t offset, + const bt::ByteBuffer& value, + btg::WriteResponder responder) { + auto svc_iter = services_.find(InternalServiceId(service_id)); + // GATT must only send write requests for registered services. + BT_ASSERT(svc_iter != services_.end()); + + auto cb = [responder = std::move(responder)]( + LocalService_WriteValue_Result result) mutable { + // If this was a Write Without Response request, the response callback will + // be null. + if (responder) { + fit::result rsp = fit::ok(); + if (!result.is_response()) { + rsp = fit::error(fidl_helpers::Gatt2ErrorCodeFromFidl(result.err())); + } + responder(rsp); + } + }; + + LocalServiceWriteValueRequest params; + params.set_peer_id(fbt::PeerId{peer_id.value()}); + params.set_handle(Handle{id}); + params.set_offset(offset); + params.set_value(value.ToVector()); + svc_iter->second.local_svc_ptr->WriteValue(std::move(params), std::move(cb)); +} + +void Gatt2ServerServer::OnClientCharacteristicConfiguration( + bt::gatt::IdType service_id, + bt::gatt::IdType chrc_id, + bt::PeerId peer_id, + bool notify, + bool indicate) { + auto svc_iter = services_.find(InternalServiceId(service_id)); + // GATT must only send CCC updates for registered services. + BT_ASSERT(svc_iter != services_.end()); + + auto cb = []() { + bt_log(TRACE, "fidl", "characteristic configuration acknowledged"); + }; + svc_iter->second.local_svc_ptr->CharacteristicConfiguration( + fbt::PeerId{peer_id.value()}, Handle{chrc_id}, notify, indicate, cb); +} + +void Gatt2ServerServer::OnSuppressDiscovery(InternalServiceId service_id) { + // TODO(fxbug.dev/42180948): This event is not yet supported + bt_log(ERROR, + "fidl", + "%s not supported - see https://fxbug.dev/42180948", + __FUNCTION__); +} + +bool Gatt2ServerServer::ValidateValueChangedEvent( + InternalServiceId service_id, + const fuchsia::bluetooth::gatt2::ValueChangedParameters& update, + const char* update_type) { + auto iter = services_.find(service_id); + // It is impossible for clients to send events to a closed service. + BT_ASSERT(iter != services_.end()); + // Subtract credit before validating parameters so that credits aren't + // permanently lost from the client's perspective. + SubtractCredit(iter->second); + + if (!update.has_handle()) { + bt_log(WARN, "fidl", "ValueChangedParameters missing required `handle`"); + return false; + } + if (!update.has_value()) { + bt_log(WARN, "fidl", "ValueChangedParameters missing required `value`"); + return false; + } + return true; +} + +void Gatt2ServerServer::OnNotifyValue( + InternalServiceId service_id, + fuchsia::bluetooth::gatt2::ValueChangedParameters update) { + if (!ValidateValueChangedEvent(service_id, update, "notification")) { + RemoveService(service_id); + return; + } + bt::gatt::IndicationCallback indicate_cb = nullptr; + if (!update.has_peer_ids() || update.peer_ids().empty()) { + gatt()->UpdateConnectedPeers(service_id.value(), + update.handle().value, + update.value(), + /*indicate_cb=*/nullptr); + return; + } + for (auto peer_id : update.peer_ids()) { + gatt()->SendUpdate(service_id.value(), + update.handle().value, + bt::PeerId(peer_id.value), + update.value(), + /*indicate_cb=*/nullptr); + } +} + +void Gatt2ServerServer::OnIndicateValue( + InternalServiceId service_id, + fuchsia::bluetooth::gatt2::ValueChangedParameters update, + zx::eventpair confirmation) { + if (!ValidateValueChangedEvent(service_id, update, "indication")) { + RemoveService(service_id); + return; + } + + if (!update.has_peer_ids() || update.peer_ids().empty()) { + auto indicate_cb = + [confirm = std::move(confirmation)](bt::att::Result<> status) mutable { + if (status.is_error()) { + bt_log(WARN, "fidl", "indication failed: %s", bt_str(status)); + return; + } + confirm.signal_peer(/*clear_mask=*/0, ZX_EVENTPAIR_SIGNALED); + }; + gatt()->UpdateConnectedPeers(service_id.value(), + update.handle().value, + update.value(), + std::move(indicate_cb)); + return; + } + + bt::att::ResultFunction<> shared_result_fn = + [pending = update.peer_ids().size(), + confirm = std::move(confirmation)](bt::att::Result<> res) mutable { + if (!confirm.is_valid()) { + // An error was already signaled. + return; + } + if (res.is_error()) { + bt_log(INFO, + "fidl", + "failed to indicate some peers: %s", + bt_str(res.error_value())); + confirm.reset(); // signals ZX_EVENTPAIR_PEER_CLOSED + return; + } + pending--; + if (pending == 0) { + confirm.signal_peer(/*clear_mask=*/0, ZX_EVENTPAIR_SIGNALED); + } + }; + + for (auto peer_id : update.peer_ids()) { + gatt()->SendUpdate(service_id.value(), + update.handle().value, + bt::PeerId(peer_id.value), + update.value(), + shared_result_fn.share()); + } +} + +void Gatt2ServerServer::SubtractCredit(Service& svc) { + // It is impossible for clients to violate the credit system from the server's + // perspective because new credits are granted before the count reaches 0 + // (excessive events will fill the FIDL channel and eventually crash the + // client). + BT_ASSERT(svc.credits > 0); + svc.credits--; + if (svc.credits <= REFRESH_CREDITS_AT) { + // Static cast OK because current_credits > 0 and we never add more than + // INITIAL_VALUE_CHANGED_CREDITS. + uint8_t credits_to_add = + static_cast(INITIAL_VALUE_CHANGED_CREDITS - svc.credits); + svc.local_svc_ptr->ValueChangedCredit(credits_to_add); + svc.credits = INITIAL_VALUE_CHANGED_CREDITS; + } +} + +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_server_server_test.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_server_server_test.cc new file mode 100644 index 0000000000..fc25b73511 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_server_server_test.cc @@ -0,0 +1,1398 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_server_server.h" + +#include +#include +#include + +#include +#include + +#include "fuchsia/bluetooth/cpp/fidl.h" +#include "fuchsia/bluetooth/gatt2/cpp/fidl.h" +#include "lib/fidl/cpp/interface_handle.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/fake_gatt_fixture.h" +#include "pw_unit_test/framework.h" + +namespace bthost { +namespace { + +namespace fbg = fuchsia::bluetooth::gatt2; +namespace fbt = fuchsia::bluetooth; + +const std::vector kBuffer = {0x00, 0x01, 0x02}; + +fbg::Characteristic BuildSimpleCharacteristic(uint64_t handle) { + fbg::Characteristic chrc; + fbg::Handle chrc_handle{handle}; + chrc.set_handle(chrc_handle); + chrc.set_type(fbt::Uuid{{6}}); + chrc.set_properties(fbg::CharacteristicPropertyBits::READ); + fbg::AttributePermissions permissions; + fbg::SecurityRequirements security; + security.set_encryption_required(true); + permissions.set_read(std::move(security)); + chrc.set_permissions(std::move(permissions)); + return chrc; +} + +fbg::ServiceInfo BuildSimpleService(uint64_t svc_handle = 1, + bt::UInt128 svc_type = {5}, + uint64_t chrc_handle = 2) { + fbg::ServiceInfo svc_info; + svc_info.set_handle(fbg::ServiceHandle{svc_handle}); + svc_info.set_type(fbt::Uuid{svc_type}); + + std::vector chrcs; + chrcs.push_back(BuildSimpleCharacteristic(chrc_handle)); + svc_info.set_characteristics(std::move(chrcs)); + + return svc_info; +} + +class MockLocalService final : private ServerBase { + public: + using ReadValueFunction = + fit::function; + using WriteValueFunction = fit::function; + using CccFunction = fit::function; + + MockLocalService(fidl::InterfaceRequest request) + : ServerBase(this, std::move(request)) { + set_error_handler([this](zx_status_t status) { error_ = status; }); + } + + void set_read_value_function(ReadValueFunction func) { + read_value_func_ = std::move(func); + } + void set_write_value_function(WriteValueFunction func) { + write_value_func_ = std::move(func); + } + void set_ccc_function(CccFunction func) { ccc_func_ = std::move(func); } + + int credits() const { return credits_; } + + const std::vector& credit_log() const { return credits_log_; } + + void NotifyValue(fbg::ValueChangedParameters update) { + credits_--; + binding()->events().OnNotifyValue(std::move(update)); + } + + void IndicateValue(fbg::ValueChangedParameters update, + zx::eventpair confirmation) { + credits_--; + binding()->events().OnIndicateValue(std::move(update), + std::move(confirmation)); + } + + std::optional error() const { return error_; } + + private: + // fbg::LocalService overrides: + void CharacteristicConfiguration( + fbt::PeerId peer_id, + fbg::Handle handle, + bool notify, + bool indicate, + CharacteristicConfigurationCallback callback) override { + if (ccc_func_) { + ccc_func_(peer_id, handle, notify, indicate, std::move((callback))); + } + } + void ReadValue(fbt::PeerId peer_id, + fbg::Handle handle, + int32_t offset, + ReadValueCallback callback) override { + if (read_value_func_) { + read_value_func_(peer_id, handle, offset, std::move(callback)); + } + } + void WriteValue(fbg::LocalServiceWriteValueRequest req, + WriteValueCallback callback) override { + if (write_value_func_) { + write_value_func_(std::move(req), std::move(callback)); + } + } + void PeerUpdate(fbg::LocalServicePeerUpdateRequest req, + PeerUpdateCallback callback) override {} + void ValueChangedCredit(uint8_t additional_credit) override { + credits_ += additional_credit; + credits_log_.push_back(additional_credit); + } + + ReadValueFunction read_value_func_; + WriteValueFunction write_value_func_; + CccFunction ccc_func_; + // Use signed integer because in tests it is possible to have negative + // credits. + int credits_ = fbg::INITIAL_VALUE_CHANGED_CREDITS; + std::vector credits_log_; + std::optional error_; +}; + +class Gatt2ServerServerTest : public bt::fidl::testing::FakeGattFixture { + public: + ~Gatt2ServerServerTest() override = default; + + Gatt2ServerServerTest() {} + + void SetUp() override { + // Create and connect to production GATT2 Server implementation + fidl::InterfaceHandle server_handle; + server_ = std::make_unique(gatt()->GetWeakPtr(), + server_handle.NewRequest()); + server_ptr_ = server_handle.Bind(); + } + + void TearDown() override {} + + protected: + fbg::ServerPtr& server_ptr() { return server_ptr_; } + + void DestroyServer() { server_.reset(); } + + private: + // Proxy interface to the GATT2 Server implementation. + fbg::ServerPtr server_ptr_; + // Raw GATT2 Server implementation + std::unique_ptr server_; +}; + +TEST_F(Gatt2ServerServerTest, + PublishAndRemoveServiceWithTwoCharacteristicsSuccess) { + fbg::ServiceInfo svc_info; + svc_info.set_handle(fbg::ServiceHandle{1}); + bt::UInt128 svc_type = {5}; + svc_info.set_type(fbt::Uuid{svc_type}); + + std::vector characteristics; + characteristics.push_back(BuildSimpleCharacteristic(/*handle=*/2)); + characteristics.push_back(BuildSimpleCharacteristic(/*handle=*/3)); + svc_info.set_characteristics(std::move(characteristics)); + + fidl::InterfaceHandle local_service_handle_0; + fidl::InterfaceRequest request = + local_service_handle_0.NewRequest(); + + int cb_count = 0; + auto cb = [&](fbg::Server_PublishService_Result res) { + EXPECT_TRUE(res.is_response()); + cb_count++; + }; + server_ptr()->PublishService( + std::move(svc_info), std::move(local_service_handle_0), std::move(cb)); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count, 1); + ASSERT_EQ(fake_gatt()->local_services().size(), 1u); + bt::gatt::Service* service = + fake_gatt()->local_services().begin()->second.service.get(); + EXPECT_EQ(service->type(), svc_type); + EXPECT_TRUE(service->primary()); + ASSERT_EQ(service->characteristics().size(), 2u); + EXPECT_EQ(service->characteristics()[0]->id(), 2u); + EXPECT_EQ(service->characteristics()[1]->id(), 3u); + + request.Close(ZX_ERR_PEER_CLOSED); + RunLoopUntilIdle(); + EXPECT_EQ(fake_gatt()->local_services().size(), 0u); +} + +TEST_F(Gatt2ServerServerTest, DestroyingServerUnregistersService) { + bt::UInt128 svc_type = {5}; + fbg::ServiceInfo svc_info = BuildSimpleService(/*svc_handle=*/1, svc_type); + + fidl::InterfaceHandle local_service_handle; + fidl::InterfaceRequest request = + local_service_handle.NewRequest(); + + int cb_count = 0; + auto cb = [&](fbg::Server_PublishService_Result res) { + EXPECT_TRUE(res.is_response()); + cb_count++; + }; + server_ptr()->PublishService( + std::move(svc_info), std::move(local_service_handle), std::move(cb)); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count, 1); + ASSERT_EQ(fake_gatt()->local_services().size(), 1u); + EXPECT_EQ( + fake_gatt()->local_services().begin()->second.service->type().value(), + svc_type); + DestroyServer(); + EXPECT_EQ(fake_gatt()->local_services().size(), 0u); +} + +TEST_F(Gatt2ServerServerTest, PublishServiceWithoutHandleFails) { + fbg::ServiceInfo svc_info = BuildSimpleService(); + svc_info.clear_handle(); + + fidl::InterfaceHandle local_service_handle; + auto request = local_service_handle.NewRequest(); + + int cb_count = 0; + auto cb = [&](fbg::Server_PublishService_Result res) { + ASSERT_TRUE(res.is_err()); + EXPECT_EQ(res.err(), fbg::PublishServiceError::INVALID_SERVICE_HANDLE); + cb_count++; + }; + server_ptr()->PublishService( + std::move(svc_info), std::move(local_service_handle), std::move(cb)); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count, 1); + EXPECT_EQ(fake_gatt()->local_services().size(), 0u); +} + +TEST_F(Gatt2ServerServerTest, PublishServiceWithoutTypeFails) { + fbg::ServiceInfo svc_info = BuildSimpleService(); + svc_info.clear_type(); + + fidl::InterfaceHandle local_service_handle; + auto request = local_service_handle.NewRequest(); + + int cb_count = 0; + auto cb = [&](fbg::Server_PublishService_Result res) { + ASSERT_TRUE(res.is_err()); + EXPECT_EQ(res.err(), fbg::PublishServiceError::INVALID_UUID); + cb_count++; + }; + server_ptr()->PublishService( + std::move(svc_info), std::move(local_service_handle), std::move(cb)); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count, 1); + EXPECT_EQ(fake_gatt()->local_services().size(), 0u); +} + +TEST_F(Gatt2ServerServerTest, PublishServiceWithoutCharacteristicsFails) { + fbg::ServiceInfo svc_info = BuildSimpleService(); + svc_info.clear_characteristics(); + + fidl::InterfaceHandle local_service_handle; + auto request = local_service_handle.NewRequest(); + + int cb_count = 0; + auto cb = [&](fbg::Server_PublishService_Result res) { + ASSERT_TRUE(res.is_err()); + EXPECT_EQ(res.err(), fbg::PublishServiceError::INVALID_CHARACTERISTICS); + cb_count++; + }; + server_ptr()->PublishService( + std::move(svc_info), std::move(local_service_handle), std::move(cb)); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count, 1); + EXPECT_EQ(fake_gatt()->local_services().size(), 0u); +} + +TEST_F(Gatt2ServerServerTest, PublishServiceWithReusedHandleFails) { + fbg::ServiceInfo svc_info_0 = BuildSimpleService(); + + fidl::InterfaceHandle local_service_handle_0; + auto request_0 = local_service_handle_0.NewRequest(); + + int cb_count_0 = 0; + auto cb_0 = [&](fbg::Server_PublishService_Result res) { + EXPECT_TRUE(res.is_response()); + cb_count_0++; + }; + server_ptr()->PublishService(std::move(svc_info_0), + std::move(local_service_handle_0), + std::move(cb_0)); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count_0, 1); + EXPECT_EQ(fake_gatt()->local_services().size(), 1u); + + fbg::ServiceInfo svc_info_1 = BuildSimpleService(); + + fidl::InterfaceHandle local_service_handle_1; + auto request_1 = local_service_handle_1.NewRequest(); + + int cb_count_1 = 0; + auto cb_1 = [&](fbg::Server_PublishService_Result res) { + ASSERT_TRUE(res.is_err()); + EXPECT_EQ(res.err(), fbg::PublishServiceError::INVALID_SERVICE_HANDLE); + cb_count_1++; + }; + server_ptr()->PublishService(std::move(svc_info_1), + std::move(local_service_handle_1), + std::move(cb_1)); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count_1, 1); + EXPECT_EQ(fake_gatt()->local_services().size(), 1u); +} + +TEST_F(Gatt2ServerServerTest, + PublishServiceWithReusedHandleAcrossTwoGattServersSuccceeds) { + // Both services are identical. + fbg::ServiceInfo svc_info_0 = BuildSimpleService(); + fbg::ServiceInfo svc_info_1 = BuildSimpleService(); + + fidl::InterfaceHandle local_service_handle_0; + auto request_0 = local_service_handle_0.NewRequest(); + + int cb_count_0 = 0; + auto cb_0 = [&](fbg::Server_PublishService_Result res) { + EXPECT_TRUE(res.is_response()); + cb_count_0++; + }; + server_ptr()->PublishService(std::move(svc_info_0), + std::move(local_service_handle_0), + std::move(cb_0)); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count_0, 1); + EXPECT_EQ(fake_gatt()->local_services().size(), 1u); + + fidl::InterfaceHandle local_service_handle_1; + auto request_1 = local_service_handle_1.NewRequest(); + + int cb_count_1 = 0; + auto cb_1 = [&](fbg::Server_PublishService_Result res) { + EXPECT_TRUE(res.is_response()); + cb_count_1++; + }; + + // Create and connect to a second GATT Server implementation + fidl::InterfaceHandle server_handle_1; + auto server_1 = std::make_unique( + gatt()->GetWeakPtr(), server_handle_1.NewRequest()); + fidl::InterfacePtr server_ptr_1 = + server_handle_1.Bind(); + // Publish an identical service. + server_ptr_1->PublishService(std::move(svc_info_1), + std::move(local_service_handle_1), + std::move(cb_1)); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count_1, 1); + ASSERT_EQ(fake_gatt()->local_services().size(), 2u); +} + +TEST_F(Gatt2ServerServerTest, PublishTwoServicesSuccess) { + bt::UInt128 svc_type_0 = {5}; + fbg::ServiceInfo svc_info_0 = + BuildSimpleService(/*svc_handle=*/1, svc_type_0); + + fidl::InterfaceHandle local_service_handle_0; + fidl::InterfaceRequest request_0 = + local_service_handle_0.NewRequest(); + + int cb_count_0 = 0; + auto cb_0 = [&](fbg::Server_PublishService_Result res) { + EXPECT_TRUE(res.is_response()); + cb_count_0++; + }; + server_ptr()->PublishService(std::move(svc_info_0), + std::move(local_service_handle_0), + std::move(cb_0)); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count_0, 1); + ASSERT_EQ(fake_gatt()->local_services().size(), 1u); + bt::gatt::Service* service = + fake_gatt()->local_services().begin()->second.service.get(); + EXPECT_EQ(service->type(), svc_type_0); + + bt::UInt128 svc_type_1 = {6}; + fbg::ServiceInfo svc_info_1 = + BuildSimpleService(/*svc_handle=*/9, svc_type_1); + + fidl::InterfaceHandle local_service_handle_1; + fidl::InterfaceRequest request_1 = + local_service_handle_1.NewRequest(); + + int cb_count_1 = 0; + auto cb_1 = [&](fbg::Server_PublishService_Result res) { + EXPECT_TRUE(res.is_response()); + cb_count_1++; + }; + server_ptr()->PublishService(std::move(svc_info_1), + std::move(local_service_handle_1), + std::move(cb_1)); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count_1, 1); + ASSERT_EQ(fake_gatt()->local_services().size(), 2u); + auto svc_iter = fake_gatt()->local_services().begin(); + EXPECT_EQ(svc_iter->second.service->type(), svc_type_0); + svc_iter++; + EXPECT_EQ(svc_iter->second.service->type(), svc_type_1); + + request_0.Close(ZX_ERR_PEER_CLOSED); + request_1.Close(ZX_ERR_PEER_CLOSED); + RunLoopUntilIdle(); + EXPECT_EQ(fake_gatt()->local_services().size(), 0u); +} + +TEST_F(Gatt2ServerServerTest, PublishSecondaryService) { + bt::UInt128 svc_type = {5}; + fbg::ServiceInfo svc_info = BuildSimpleService(/*svc_handle=*/1, svc_type); + svc_info.set_kind(fbg::ServiceKind::SECONDARY); + + fidl::InterfaceHandle local_service_handle; + fidl::InterfaceRequest request = + local_service_handle.NewRequest(); + + int cb_count = 0; + auto cb = [&](fbg::Server_PublishService_Result res) { + EXPECT_TRUE(res.is_response()); + cb_count++; + }; + server_ptr()->PublishService( + std::move(svc_info), std::move(local_service_handle), std::move(cb)); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count, 1); + ASSERT_EQ(fake_gatt()->local_services().size(), 1u); + auto svc_iter = fake_gatt()->local_services().begin(); + EXPECT_EQ(svc_iter->second.service->type(), svc_type); + EXPECT_FALSE(svc_iter->second.service->primary()); +} + +TEST_F(Gatt2ServerServerTest, ReadSuccess) { + uint64_t svc_handle = 1; + fbg::ServiceInfo svc_info; + svc_info.set_handle(fbg::ServiceHandle{svc_handle}); + svc_info.set_type(fbt::Uuid{{5}}); + fbg::Characteristic chrc; + fbg::Handle chrc_handle{2}; + chrc.set_handle(chrc_handle); + chrc.set_type(fbt::Uuid{{0}}); + chrc.set_properties(fbg::CharacteristicPropertyBits::READ); + chrc.set_permissions(fbg::AttributePermissions()); + std::vector chrcs; + chrcs.push_back(std::move(chrc)); + svc_info.set_characteristics(std::move(chrcs)); + + fidl::InterfaceHandle local_service_handle; + MockLocalService local_svc(local_service_handle.NewRequest()); + + int cb_count = 0; + auto cb = [&](fbg::Server_PublishService_Result res) { + EXPECT_TRUE(res.is_response()); + cb_count++; + }; + server_ptr()->PublishService( + std::move(svc_info), std::move(local_service_handle), std::move(cb)); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count, 1); + ASSERT_EQ(fake_gatt()->local_services().size(), 1u); + auto svc_iter = fake_gatt()->local_services().begin(); + ASSERT_EQ(svc_iter->second.service->characteristics().size(), 1u); + + bt::gatt::IdType svc_id = svc_iter->first; + bt::gatt::IdType chrc_id = + svc_iter->second.service->characteristics()[0]->id(); + bt::PeerId peer_id(99); + + int read_value_count = 0; + local_svc.set_read_value_function( + [&](fbt::PeerId cb_peer_id, + fbg::Handle handle, + int32_t offset, + fbg::LocalService::ReadValueCallback callback) { + read_value_count++; + EXPECT_EQ(peer_id.value(), cb_peer_id.value); + EXPECT_EQ(handle.value, chrc_handle.value); + EXPECT_EQ(offset, 3); + callback(fpromise::ok(kBuffer)); + }); + + int read_responder_count = 0; + auto read_responder = [&](fit::result status, + const bt::ByteBuffer& value) { + read_responder_count++; + EXPECT_TRUE(status.is_ok()); + EXPECT_THAT(value, ::testing::ElementsAreArray(kBuffer)); + }; + + svc_iter->second.read_handler( + peer_id, svc_id, chrc_id, /*offset=*/3, read_responder); + RunLoopUntilIdle(); + EXPECT_EQ(read_value_count, 1); + EXPECT_EQ(read_responder_count, 1); +} + +TEST_F(Gatt2ServerServerTest, ReadErrorResponse) { + uint64_t svc_handle = 1; + fbg::ServiceInfo svc_info; + svc_info.set_handle(fbg::ServiceHandle{svc_handle}); + svc_info.set_type(fbt::Uuid{{5}}); + fbg::Characteristic chrc; + fbg::Handle chrc_handle{2}; + chrc.set_handle(chrc_handle); + chrc.set_type(fbt::Uuid{{0}}); + chrc.set_properties(fbg::CharacteristicPropertyBits::READ); + chrc.set_permissions(fbg::AttributePermissions()); + std::vector chrcs; + chrcs.push_back(std::move(chrc)); + svc_info.set_characteristics(std::move(chrcs)); + + fidl::InterfaceHandle local_service_handle; + MockLocalService local_svc(local_service_handle.NewRequest()); + + int cb_count = 0; + auto cb = [&](fbg::Server_PublishService_Result res) { + EXPECT_TRUE(res.is_response()); + cb_count++; + }; + server_ptr()->PublishService( + std::move(svc_info), std::move(local_service_handle), std::move(cb)); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count, 1); + ASSERT_EQ(fake_gatt()->local_services().size(), 1u); + auto svc_iter = fake_gatt()->local_services().begin(); + ASSERT_EQ(svc_iter->second.service->characteristics().size(), 1u); + + bt::gatt::IdType svc_id = svc_iter->first; + bt::gatt::IdType chrc_id = + svc_iter->second.service->characteristics()[0]->id(); + + int read_value_count = 0; + local_svc.set_read_value_function( + [&](fbt::PeerId cb_peer_id, + fbg::Handle handle, + int32_t offset, + fbg::LocalService::ReadValueCallback callback) { + read_value_count++; + callback(fpromise::error(fbg::Error::READ_NOT_PERMITTED)); + }); + + int read_responder_count = 0; + auto read_responder = [&](fit::result status, + const bt::ByteBuffer& value) { + read_responder_count++; + ASSERT_TRUE(status.is_error()); + EXPECT_EQ(status.error_value(), bt::att::ErrorCode::kReadNotPermitted); + EXPECT_EQ(value.size(), 0u); + }; + + svc_iter->second.read_handler( + bt::PeerId(42), svc_id, chrc_id, /*offset=*/0, read_responder); + RunLoopUntilIdle(); + EXPECT_EQ(read_value_count, 1); + EXPECT_EQ(read_responder_count, 1); +} + +TEST_F(Gatt2ServerServerTest, WriteSuccess) { + uint64_t svc_handle = 1; + fbg::ServiceInfo svc_info; + svc_info.set_handle(fbg::ServiceHandle{svc_handle}); + svc_info.set_type(fbt::Uuid{{5}}); + fbg::Characteristic chrc; + fbg::Handle chrc_handle{2}; + chrc.set_handle(chrc_handle); + chrc.set_type(fbt::Uuid{{0}}); + chrc.set_properties(fbg::CharacteristicPropertyBits::WRITE); + chrc.set_permissions(fbg::AttributePermissions()); + std::vector chrcs; + chrcs.push_back(std::move(chrc)); + svc_info.set_characteristics(std::move(chrcs)); + + fidl::InterfaceHandle local_service_handle; + MockLocalService local_svc(local_service_handle.NewRequest()); + + int cb_count = 0; + auto cb = [&](fbg::Server_PublishService_Result res) { + EXPECT_TRUE(res.is_response()); + cb_count++; + }; + server_ptr()->PublishService( + std::move(svc_info), std::move(local_service_handle), std::move(cb)); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count, 1); + ASSERT_EQ(fake_gatt()->local_services().size(), 1u); + auto svc_iter = fake_gatt()->local_services().begin(); + ASSERT_EQ(svc_iter->second.service->characteristics().size(), 1u); + + bt::gatt::IdType svc_id = svc_iter->first; + bt::gatt::IdType chrc_id = + svc_iter->second.service->characteristics()[0]->id(); + bt::PeerId peer_id(99); + bt::StaticByteBuffer value_buffer(0x00, 0x01, 0x02); + + int write_value_count = 0; + local_svc.set_write_value_function( + [&](fbg::LocalServiceWriteValueRequest req, + fbg::LocalService::WriteValueCallback cb) { + write_value_count++; + ASSERT_TRUE(req.has_peer_id()); + EXPECT_EQ(req.peer_id().value, peer_id.value()); + ASSERT_TRUE(req.has_handle()); + EXPECT_EQ(req.handle().value, chrc_handle.value); + ASSERT_TRUE(req.has_offset()); + EXPECT_EQ(req.offset(), 3u); + ASSERT_TRUE(req.has_value()); + EXPECT_THAT(req.value(), ::testing::ElementsAreArray(value_buffer)); + cb(fpromise::ok()); + }); + int write_responder_count = 0; + auto write_responder = [&](fit::result status) { + write_responder_count++; + EXPECT_TRUE(status.is_ok()); + }; + svc_iter->second.write_handler(peer_id, + svc_id, + chrc_id, + /*offset=*/3, + value_buffer, + std::move(write_responder)); + RunLoopUntilIdle(); + EXPECT_EQ(write_value_count, 1); + EXPECT_EQ(write_responder_count, 1); +} + +TEST_F(Gatt2ServerServerTest, WriteError) { + uint64_t svc_handle = 1; + fbg::ServiceInfo svc_info; + svc_info.set_handle(fbg::ServiceHandle{svc_handle}); + svc_info.set_type(fbt::Uuid{{5}}); + fbg::Characteristic chrc; + fbg::Handle chrc_handle{2}; + chrc.set_handle(chrc_handle); + chrc.set_type(fbt::Uuid{{0}}); + chrc.set_properties(fbg::CharacteristicPropertyBits::WRITE); + chrc.set_permissions(fbg::AttributePermissions()); + std::vector chrcs; + chrcs.push_back(std::move(chrc)); + svc_info.set_characteristics(std::move(chrcs)); + + fidl::InterfaceHandle local_service_handle; + MockLocalService local_svc(local_service_handle.NewRequest()); + + int cb_count = 0; + auto cb = [&](fbg::Server_PublishService_Result res) { + EXPECT_TRUE(res.is_response()); + cb_count++; + }; + server_ptr()->PublishService( + std::move(svc_info), std::move(local_service_handle), std::move(cb)); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count, 1); + ASSERT_EQ(fake_gatt()->local_services().size(), 1u); + auto svc_iter = fake_gatt()->local_services().begin(); + ASSERT_EQ(svc_iter->second.service->characteristics().size(), 1u); + + bt::gatt::IdType svc_id = svc_iter->first; + bt::gatt::IdType chrc_id = + svc_iter->second.service->characteristics()[0]->id(); + + int write_value_count = 0; + local_svc.set_write_value_function( + [&](fbg::LocalServiceWriteValueRequest req, + fbg::LocalService::WriteValueCallback cb) { + write_value_count++; + cb(fpromise::error(fbg::Error::WRITE_NOT_PERMITTED)); + }); + int write_responder_count = 0; + auto write_responder = [&](fit::result status) { + write_responder_count++; + ASSERT_TRUE(status.is_error()); + EXPECT_EQ(status.error_value(), bt::att::ErrorCode::kWriteNotPermitted); + }; + svc_iter->second.write_handler(bt::PeerId(4), + svc_id, + chrc_id, + /*offset=*/0, + bt::BufferView(), + std::move(write_responder)); + RunLoopUntilIdle(); + EXPECT_EQ(write_value_count, 1); + EXPECT_EQ(write_responder_count, 1); +} + +TEST_F(Gatt2ServerServerTest, ClientCharacteristicConfiguration) { + const uint64_t chrc_handle = 2; + fbg::ServiceInfo svc_info = + BuildSimpleService(/*svc_handle=*/1, /*svc_type=*/{5}, chrc_handle); + + fidl::InterfaceHandle local_service_handle; + MockLocalService local_svc = local_service_handle.NewRequest(); + + int cb_count = 0; + auto cb = [&](fbg::Server_PublishService_Result res) { + EXPECT_TRUE(res.is_response()); + cb_count++; + }; + server_ptr()->PublishService( + std::move(svc_info), std::move(local_service_handle), std::move(cb)); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count, 1); + ASSERT_EQ(fake_gatt()->local_services().size(), 1u); + auto svc_iter = fake_gatt()->local_services().begin(); + bt::gatt::IdType svc_id = svc_iter->first; + + bt::PeerId peer_id(4); + int ccc_count = 0; + local_svc.set_ccc_function( + [&](fbt::PeerId cb_peer_id, + fbg::Handle handle, + bool notify, + bool indicate, + fbg::LocalService::CharacteristicConfigurationCallback cb) { + ccc_count++; + EXPECT_EQ(peer_id.value(), cb_peer_id.value); + EXPECT_EQ(handle.value, chrc_handle); + EXPECT_TRUE(notify); + EXPECT_FALSE(indicate); + cb(); + }); + svc_iter->second.ccc_callback( + svc_id, chrc_handle, peer_id, /*notify=*/true, /*indicate=*/false); + RunLoopUntilIdle(); + EXPECT_EQ(ccc_count, 1); +} + +TEST_F(Gatt2ServerServerTest, IndicateAllPeers) { + const uint64_t chrc_handle = 2; + fbg::ServiceInfo svc_info = + BuildSimpleService(/*svc_handle=*/1, /*svc_type=*/{5}, chrc_handle); + + fidl::InterfaceHandle local_service_handle; + MockLocalService local_svc = local_service_handle.NewRequest(); + + int cb_count = 0; + auto cb = [&](fbg::Server_PublishService_Result res) { + EXPECT_TRUE(res.is_response()); + cb_count++; + }; + server_ptr()->PublishService( + std::move(svc_info), std::move(local_service_handle), std::move(cb)); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count, 1); + ASSERT_EQ(fake_gatt()->local_services().size(), 1u); + auto svc_iter = fake_gatt()->local_services().begin(); + + fbg::ValueChangedParameters params_0; + params_0.set_handle(fbg::Handle{chrc_handle}); + std::vector buffer = {0x00, 0x01, 0x02}; + params_0.set_value(buffer); + zx::eventpair confirm_ours_0; + zx::eventpair confirm_theirs_0; + zx::eventpair::create(/*options=*/0, &confirm_ours_0, &confirm_theirs_0); + local_svc.IndicateValue(std::move(params_0), std::move(confirm_theirs_0)); + RunLoopUntilIdle(); + ASSERT_EQ(svc_iter->second.updates.size(), 1u); + EXPECT_EQ(svc_iter->second.updates[0].chrc_id, chrc_handle); + EXPECT_EQ(svc_iter->second.updates[0].value, buffer); + + // Eventpair should not be signalled until indicate_cb called. + zx_signals_t observed; + zx_status_t status = + confirm_ours_0.wait_one(ZX_EVENTPAIR_PEER_CLOSED | ZX_EVENTPAIR_SIGNALED, + /*deadline=*/zx::time(0), + &observed); + ASSERT_EQ(status, ZX_ERR_TIMED_OUT); + EXPECT_EQ(observed, 0u); + + svc_iter->second.updates[0].indicate_cb(fit::ok()); + + status = + confirm_ours_0.wait_one(ZX_EVENTPAIR_PEER_CLOSED | ZX_EVENTPAIR_SIGNALED, + /*deadline=*/zx::time(0), + &observed); + ASSERT_EQ(status, ZX_OK); + // The eventpair should have been signaled before it was closed. + EXPECT_EQ(observed, ZX_EVENTPAIR_SIGNALED | ZX_EVENTPAIR_PEER_CLOSED); + observed = 0; + + // Also test with an empty peer_ids vector + fbg::ValueChangedParameters params_1; + params_1.set_peer_ids({}); + params_1.set_handle(fbg::Handle{chrc_handle}); + params_1.set_value(buffer); + zx::eventpair confirm_ours_1; + zx::eventpair confirm_theirs_1; + zx::eventpair::create(/*options=*/0, &confirm_ours_1, &confirm_theirs_1); + local_svc.IndicateValue(std::move(params_1), std::move(confirm_theirs_1)); + RunLoopUntilIdle(); + ASSERT_EQ(svc_iter->second.updates.size(), 2u); + status = + confirm_ours_1.wait_one(ZX_EVENTPAIR_PEER_CLOSED | ZX_EVENTPAIR_SIGNALED, + /*deadline=*/zx::time(0), + &observed); + ASSERT_EQ(status, ZX_ERR_TIMED_OUT); + EXPECT_EQ(observed, 0u); + + svc_iter->second.updates[1].indicate_cb(fit::ok()); + + status = + confirm_ours_1.wait_one(ZX_EVENTPAIR_PEER_CLOSED | ZX_EVENTPAIR_SIGNALED, + /*deadline=*/zx::time(0), + &observed); + ASSERT_EQ(status, ZX_OK); + EXPECT_EQ(observed, ZX_EVENTPAIR_SIGNALED | ZX_EVENTPAIR_PEER_CLOSED); +} + +TEST_F(Gatt2ServerServerTest, IndicateAllPeersError) { + const uint64_t chrc_handle = 2; + fbg::ServiceInfo svc_info = + BuildSimpleService(/*svc_handle=*/1, /*svc_type=*/{5}, chrc_handle); + + fidl::InterfaceHandle local_service_handle; + MockLocalService local_svc = local_service_handle.NewRequest(); + + int cb_count = 0; + auto cb = [&](fbg::Server_PublishService_Result res) { + EXPECT_TRUE(res.is_response()); + cb_count++; + }; + server_ptr()->PublishService( + std::move(svc_info), std::move(local_service_handle), std::move(cb)); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count, 1); + ASSERT_EQ(fake_gatt()->local_services().size(), 1u); + auto svc_iter = fake_gatt()->local_services().begin(); + + fbg::ValueChangedParameters params; + params.set_handle(fbg::Handle{chrc_handle}); + std::vector buffer = {0x00, 0x01, 0x02}; + params.set_value(buffer); + zx::eventpair confirm_ours; + zx::eventpair confirm_theirs; + zx::eventpair::create(/*options=*/0, &confirm_ours, &confirm_theirs); + local_svc.IndicateValue(std::move(params), std::move(confirm_theirs)); + RunLoopUntilIdle(); + ASSERT_EQ(svc_iter->second.updates.size(), 1u); + EXPECT_EQ(svc_iter->second.updates[0].chrc_id, chrc_handle); + EXPECT_EQ(svc_iter->second.updates[0].value, buffer); + + // Eventpair should not be signalled until indicate_cb called. + zx_signals_t observed; + zx_status_t status = + confirm_ours.wait_one(ZX_EVENTPAIR_PEER_CLOSED | ZX_EVENTPAIR_SIGNALED, + /*deadline=*/zx::time(0), + &observed); + ASSERT_EQ(status, ZX_ERR_TIMED_OUT); + EXPECT_EQ(observed, 0u); + + svc_iter->second.updates[0].indicate_cb( + fit::error(bt::att::ErrorCode::kUnlikelyError)); + + status = + confirm_ours.wait_one(ZX_EVENTPAIR_PEER_CLOSED | ZX_EVENTPAIR_SIGNALED, + /*deadline=*/zx::time(0), + &observed); + ASSERT_EQ(status, ZX_OK); + EXPECT_EQ(observed, ZX_EVENTPAIR_PEER_CLOSED); +} + +TEST_F(Gatt2ServerServerTest, + IndicateValueChangedParametersMissingHandleClosesService) { + const uint64_t chrc_handle = 2; + fbg::ServiceInfo svc_info = + BuildSimpleService(/*svc_handle=*/1, /*svc_type=*/{5}, chrc_handle); + + fidl::InterfaceHandle local_service_handle; + MockLocalService local_svc = local_service_handle.NewRequest(); + + int cb_count = 0; + auto cb = [&](fbg::Server_PublishService_Result res) { + EXPECT_TRUE(res.is_response()); + cb_count++; + }; + server_ptr()->PublishService( + std::move(svc_info), std::move(local_service_handle), std::move(cb)); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count, 1); + ASSERT_EQ(fake_gatt()->local_services().size(), 1u); + ASSERT_FALSE(local_svc.error()); + + // No handle is set. + fbg::ValueChangedParameters params_0; + std::vector buffer = {0x00, 0x01, 0x02}; + params_0.set_value(buffer); + zx::eventpair confirm_ours_0; + zx::eventpair confirm_theirs_0; + zx::eventpair::create(/*options=*/0, &confirm_ours_0, &confirm_theirs_0); + local_svc.IndicateValue(std::move(params_0), std::move(confirm_theirs_0)); + RunLoopUntilIdle(); + ASSERT_TRUE(local_svc.error()); + EXPECT_EQ(local_svc.error().value(), ZX_ERR_PEER_CLOSED); + EXPECT_TRUE(fake_gatt()->local_services().empty()); + zx_signals_t observed; + zx_status_t status = + confirm_ours_0.wait_one(ZX_EVENTPAIR_PEER_CLOSED | ZX_EVENTPAIR_SIGNALED, + /*deadline=*/zx::time(0), + &observed); + ASSERT_EQ(status, ZX_OK); + EXPECT_EQ(observed, ZX_EVENTPAIR_PEER_CLOSED); +} + +TEST_F(Gatt2ServerServerTest, + IndicateValueChangedParametersMissingValueClosesService) { + const uint64_t chrc_handle = 2; + fbg::ServiceInfo svc_info = + BuildSimpleService(/*svc_handle=*/1, /*svc_type=*/{5}, chrc_handle); + + fidl::InterfaceHandle local_service_handle; + MockLocalService local_svc = local_service_handle.NewRequest(); + + int cb_count = 0; + auto cb = [&](fbg::Server_PublishService_Result res) { + EXPECT_TRUE(res.is_response()); + cb_count++; + }; + server_ptr()->PublishService( + std::move(svc_info), std::move(local_service_handle), std::move(cb)); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count, 1); + ASSERT_FALSE(local_svc.error()); + ASSERT_EQ(fake_gatt()->local_services().size(), 1u); + + // No value is set. + fbg::ValueChangedParameters params_1; + params_1.set_handle(fbg::Handle{chrc_handle}); + zx::eventpair confirm_ours_1; + zx::eventpair confirm_theirs_1; + zx::eventpair::create(/*options=*/0, &confirm_ours_1, &confirm_theirs_1); + local_svc.IndicateValue(std::move(params_1), std::move(confirm_theirs_1)); + RunLoopUntilIdle(); + ASSERT_TRUE(local_svc.error()); + EXPECT_EQ(local_svc.error().value(), ZX_ERR_PEER_CLOSED); + EXPECT_TRUE(fake_gatt()->local_services().empty()); + zx_signals_t observed; + zx_status_t status = + confirm_ours_1.wait_one(ZX_EVENTPAIR_PEER_CLOSED | ZX_EVENTPAIR_SIGNALED, + /*deadline=*/zx::time(0), + &observed); + ASSERT_EQ(status, ZX_OK); + EXPECT_EQ(observed, ZX_EVENTPAIR_PEER_CLOSED); +} + +TEST_F(Gatt2ServerServerTest, Indicate2PeersSuccess) { + const uint64_t chrc_handle = 2; + fbg::ServiceInfo svc_info = + BuildSimpleService(/*svc_handle=*/1, /*svc_type=*/{5}, chrc_handle); + + fidl::InterfaceHandle local_service_handle; + MockLocalService local_svc = local_service_handle.NewRequest(); + + int cb_count = 0; + auto cb = [&](fbg::Server_PublishService_Result res) { + EXPECT_TRUE(res.is_response()); + cb_count++; + }; + server_ptr()->PublishService( + std::move(svc_info), std::move(local_service_handle), std::move(cb)); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count, 1); + ASSERT_EQ(fake_gatt()->local_services().size(), 1u); + auto svc_iter = fake_gatt()->local_services().begin(); + + bt::PeerId peer_0(0); + bt::PeerId peer_1(1); + fbg::ValueChangedParameters params; + params.set_handle(fbg::Handle{chrc_handle}); + std::vector buffer = {0x00, 0x01, 0x02}; + params.set_value(buffer); + params.set_peer_ids( + {fbt::PeerId{peer_0.value()}, fbt::PeerId{peer_1.value()}}); + zx::eventpair confirm_ours; + zx::eventpair confirm_theirs; + zx::eventpair::create(/*options=*/0, &confirm_ours, &confirm_theirs); + local_svc.IndicateValue(std::move(params), std::move(confirm_theirs)); + RunLoopUntilIdle(); + ASSERT_EQ(svc_iter->second.updates.size(), 2u); + EXPECT_EQ(svc_iter->second.updates[0].chrc_id, chrc_handle); + EXPECT_EQ(svc_iter->second.updates[0].value, buffer); + EXPECT_THAT(svc_iter->second.updates[0].peer, ::testing::Optional(peer_0)); + EXPECT_EQ(svc_iter->second.updates[1].chrc_id, chrc_handle); + EXPECT_EQ(svc_iter->second.updates[1].value, buffer); + EXPECT_THAT(svc_iter->second.updates[1].peer, ::testing::Optional(peer_1)); + + // Eventpair should not be signaled until indicate_cb called for both peers. + zx_signals_t observed; + zx_status_t status = + confirm_ours.wait_one(ZX_EVENTPAIR_PEER_CLOSED | ZX_EVENTPAIR_SIGNALED, + /*deadline=*/zx::time(0), + &observed); + ASSERT_EQ(status, ZX_ERR_TIMED_OUT); + + svc_iter->second.updates[1].indicate_cb(fit::ok()); + + status = + confirm_ours.wait_one(ZX_EVENTPAIR_PEER_CLOSED | ZX_EVENTPAIR_SIGNALED, + /*deadline=*/zx::time(0), + &observed); + ASSERT_EQ(status, ZX_ERR_TIMED_OUT); + + svc_iter->second.updates[0].indicate_cb(fit::ok()); + + status = + confirm_ours.wait_one(ZX_EVENTPAIR_PEER_CLOSED | ZX_EVENTPAIR_SIGNALED, + /*deadline=*/zx::time(0), + &observed); + ASSERT_EQ(status, ZX_OK); + EXPECT_EQ(observed, ZX_EVENTPAIR_PEER_CLOSED | ZX_EVENTPAIR_SIGNALED); +} + +TEST_F(Gatt2ServerServerTest, Indicate2PeersFirstOneFails) { + const uint64_t chrc_handle = 2; + fbg::ServiceInfo svc_info = + BuildSimpleService(/*svc_handle=*/1, /*svc_type=*/{5}, chrc_handle); + + fidl::InterfaceHandle local_service_handle; + MockLocalService local_svc = local_service_handle.NewRequest(); + + int cb_count = 0; + auto cb = [&](fbg::Server_PublishService_Result res) { + EXPECT_TRUE(res.is_response()); + cb_count++; + }; + server_ptr()->PublishService( + std::move(svc_info), std::move(local_service_handle), std::move(cb)); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count, 1); + ASSERT_EQ(fake_gatt()->local_services().size(), 1u); + auto svc_iter = fake_gatt()->local_services().begin(); + + fbg::ValueChangedParameters params; + params.set_handle(fbg::Handle{chrc_handle}); + params.set_value(kBuffer); + params.set_peer_ids({fbt::PeerId{0}, fbt::PeerId{1}}); + zx::eventpair confirm_ours; + zx::eventpair confirm_theirs; + zx::eventpair::create(/*options=*/0, &confirm_ours, &confirm_theirs); + local_svc.IndicateValue(std::move(params), std::move(confirm_theirs)); + RunLoopUntilIdle(); + ASSERT_EQ(svc_iter->second.updates.size(), 2u); + + zx_signals_t observed; + zx_status_t status = + confirm_ours.wait_one(ZX_EVENTPAIR_PEER_CLOSED | ZX_EVENTPAIR_SIGNALED, + /*deadline=*/zx::time(0), + &observed); + ASSERT_EQ(status, ZX_ERR_TIMED_OUT); + + svc_iter->second.updates[0].indicate_cb( + fit::error(bt::att::ErrorCode::kUnlikelyError)); + + status = + confirm_ours.wait_one(ZX_EVENTPAIR_PEER_CLOSED | ZX_EVENTPAIR_SIGNALED, + /*deadline=*/zx::time(0), + &observed); + ASSERT_EQ(status, ZX_OK); + EXPECT_EQ(observed, ZX_EVENTPAIR_PEER_CLOSED); + + svc_iter->second.updates[1].indicate_cb(fit::ok()); +} + +TEST_F(Gatt2ServerServerTest, Indicate2PeersBothFail) { + const uint64_t chrc_handle = 2; + fbg::ServiceInfo svc_info = + BuildSimpleService(/*svc_handle=*/1, /*svc_type=*/{5}, chrc_handle); + + fidl::InterfaceHandle local_service_handle; + MockLocalService local_svc = local_service_handle.NewRequest(); + + int cb_count = 0; + auto cb = [&](fbg::Server_PublishService_Result res) { + EXPECT_TRUE(res.is_response()); + cb_count++; + }; + server_ptr()->PublishService( + std::move(svc_info), std::move(local_service_handle), std::move(cb)); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count, 1); + ASSERT_EQ(fake_gatt()->local_services().size(), 1u); + auto svc_iter = fake_gatt()->local_services().begin(); + + fbg::ValueChangedParameters params; + params.set_handle(fbg::Handle{chrc_handle}); + params.set_value(kBuffer); + params.set_peer_ids({fbt::PeerId{0}, fbt::PeerId{1}}); + zx::eventpair confirm_ours; + zx::eventpair confirm_theirs; + zx::eventpair::create(/*options=*/0, &confirm_ours, &confirm_theirs); + local_svc.IndicateValue(std::move(params), std::move(confirm_theirs)); + RunLoopUntilIdle(); + ASSERT_EQ(svc_iter->second.updates.size(), 2u); + + zx_signals_t observed; + zx_status_t status = + confirm_ours.wait_one(ZX_EVENTPAIR_PEER_CLOSED | ZX_EVENTPAIR_SIGNALED, + /*deadline=*/zx::time(0), + &observed); + ASSERT_EQ(status, ZX_ERR_TIMED_OUT); + + svc_iter->second.updates[1].indicate_cb( + fit::error(bt::att::ErrorCode::kUnlikelyError)); + + status = + confirm_ours.wait_one(ZX_EVENTPAIR_PEER_CLOSED | ZX_EVENTPAIR_SIGNALED, + /*deadline=*/zx::time(0), + &observed); + ASSERT_EQ(status, ZX_OK); + EXPECT_EQ(observed, ZX_EVENTPAIR_PEER_CLOSED); + + svc_iter->second.updates[0].indicate_cb( + fit::error(bt::att::ErrorCode::kUnlikelyError)); +} + +TEST_F(Gatt2ServerServerTest, NotifyAllPeers) { + const uint64_t chrc_handle = 2; + fbg::ServiceInfo svc_info = + BuildSimpleService(/*svc_handle=*/1, /*svc_type=*/{5}, chrc_handle); + + fidl::InterfaceHandle local_service_handle; + MockLocalService local_svc = local_service_handle.NewRequest(); + + int cb_count = 0; + auto cb = [&](fbg::Server_PublishService_Result res) { + EXPECT_TRUE(res.is_response()); + cb_count++; + }; + server_ptr()->PublishService( + std::move(svc_info), std::move(local_service_handle), std::move(cb)); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count, 1); + ASSERT_EQ(fake_gatt()->local_services().size(), 1u); + auto svc_iter = fake_gatt()->local_services().begin(); + + fbg::ValueChangedParameters params_0; + params_0.set_handle(fbg::Handle{chrc_handle}); + params_0.set_value(kBuffer); + local_svc.NotifyValue(std::move(params_0)); + RunLoopUntilIdle(); + ASSERT_EQ(svc_iter->second.updates.size(), 1u); + EXPECT_EQ(svc_iter->second.updates[0].chrc_id, chrc_handle); + EXPECT_EQ(svc_iter->second.updates[0].value, kBuffer); + EXPECT_FALSE(svc_iter->second.updates[0].peer); + EXPECT_FALSE(svc_iter->second.updates[0].indicate_cb); + + // Also test with an empty peer_ids vector + fbg::ValueChangedParameters params_1; + params_1.set_peer_ids({}); + params_1.set_handle(fbg::Handle{chrc_handle}); + params_1.set_value(kBuffer); + local_svc.NotifyValue(std::move(params_1)); + RunLoopUntilIdle(); + ASSERT_EQ(svc_iter->second.updates.size(), 2u); + EXPECT_EQ(svc_iter->second.updates[1].chrc_id, chrc_handle); + EXPECT_EQ(svc_iter->second.updates[1].value, kBuffer); + EXPECT_FALSE(svc_iter->second.updates[1].peer); + EXPECT_FALSE(svc_iter->second.updates[1].indicate_cb); +} + +TEST_F(Gatt2ServerServerTest, NotifyTwoPeers) { + const uint64_t chrc_handle = 2; + fbg::ServiceInfo svc_info = + BuildSimpleService(/*svc_handle=*/1, /*svc_type=*/{5}, chrc_handle); + + fidl::InterfaceHandle local_service_handle; + MockLocalService local_svc = local_service_handle.NewRequest(); + + int cb_count = 0; + auto cb = [&](fbg::Server_PublishService_Result res) { + EXPECT_TRUE(res.is_response()); + cb_count++; + }; + server_ptr()->PublishService( + std::move(svc_info), std::move(local_service_handle), std::move(cb)); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count, 1); + ASSERT_EQ(fake_gatt()->local_services().size(), 1u); + auto svc_iter = fake_gatt()->local_services().begin(); + + fbg::ValueChangedParameters params_0; + params_0.set_handle(fbg::Handle{chrc_handle}); + params_0.set_value(kBuffer); + params_0.set_peer_ids({fbt::PeerId{0}, fbt::PeerId{1}}); + local_svc.NotifyValue(std::move(params_0)); + RunLoopUntilIdle(); + ASSERT_EQ(svc_iter->second.updates.size(), 2u); + EXPECT_EQ(svc_iter->second.updates[0].chrc_id, chrc_handle); + EXPECT_EQ(svc_iter->second.updates[0].value, kBuffer); + EXPECT_THAT(svc_iter->second.updates[0].peer, + ::testing::Optional(bt::PeerId(0))); + EXPECT_FALSE(svc_iter->second.updates[0].indicate_cb); + EXPECT_EQ(svc_iter->second.updates[1].chrc_id, chrc_handle); + EXPECT_EQ(svc_iter->second.updates[1].value, kBuffer); + EXPECT_THAT(svc_iter->second.updates[1].peer, + ::testing::Optional(bt::PeerId(1))); + EXPECT_FALSE(svc_iter->second.updates[1].indicate_cb); +} + +TEST_F(Gatt2ServerServerTest, + NotifyInvalidParametersMissingHandleClosesService) { + const uint64_t chrc_handle = 2; + fbg::ServiceInfo svc_info = + BuildSimpleService(/*svc_handle=*/1, /*svc_type=*/{5}, chrc_handle); + + fidl::InterfaceHandle local_service_handle; + MockLocalService local_svc = local_service_handle.NewRequest(); + + int cb_count = 0; + auto cb = [&](fbg::Server_PublishService_Result res) { + EXPECT_TRUE(res.is_response()); + cb_count++; + }; + server_ptr()->PublishService( + std::move(svc_info), std::move(local_service_handle), std::move(cb)); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count, 1); + ASSERT_EQ(fake_gatt()->local_services().size(), 1u); + ASSERT_FALSE(local_svc.error()); + + // Missing handle. + fbg::ValueChangedParameters params_0; + params_0.set_value(kBuffer); + local_svc.NotifyValue(std::move(params_0)); + RunLoopUntilIdle(); + ASSERT_TRUE(local_svc.error()); + EXPECT_EQ(local_svc.error().value(), ZX_ERR_PEER_CLOSED); + EXPECT_TRUE(fake_gatt()->local_services().empty()); +} + +TEST_F(Gatt2ServerServerTest, + NotifyInvalidParametersMissingValueClosesService) { + const uint64_t chrc_handle = 2; + fbg::ServiceInfo svc_info = + BuildSimpleService(/*svc_handle=*/1, /*svc_type=*/{5}, chrc_handle); + + fidl::InterfaceHandle local_service_handle; + MockLocalService local_svc = local_service_handle.NewRequest(); + + int cb_count = 0; + auto cb = [&](fbg::Server_PublishService_Result res) { + EXPECT_TRUE(res.is_response()); + cb_count++; + }; + server_ptr()->PublishService( + std::move(svc_info), std::move(local_service_handle), std::move(cb)); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count, 1); + ASSERT_EQ(fake_gatt()->local_services().size(), 1u); + ASSERT_FALSE(local_svc.error()); + + // Missing value. + fbg::ValueChangedParameters params_1; + params_1.set_handle(fbg::Handle{chrc_handle}); + local_svc.NotifyValue(std::move(params_1)); + RunLoopUntilIdle(); + ASSERT_TRUE(local_svc.error()); + EXPECT_EQ(local_svc.error().value(), ZX_ERR_PEER_CLOSED); + EXPECT_TRUE(fake_gatt()->local_services().empty()); +} + +TEST_F(Gatt2ServerServerTest, ValueChangedFlowControl) { + const uint64_t chrc_handle = 2; + fbg::ServiceInfo svc_info = + BuildSimpleService(/*svc_handle=*/1, /*svc_type=*/{5}, chrc_handle); + + fidl::InterfaceHandle local_service_handle; + MockLocalService local_svc = local_service_handle.NewRequest(); + + int cb_count = 0; + auto cb = [&](fbg::Server_PublishService_Result res) { + EXPECT_TRUE(res.is_response()); + cb_count++; + }; + server_ptr()->PublishService( + std::move(svc_info), std::move(local_service_handle), std::move(cb)); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count, 1); + ASSERT_EQ(fake_gatt()->local_services().size(), 1u); + auto svc_iter = fake_gatt()->local_services().begin(); + + for (size_t i = 0; i < 3 * fbg::INITIAL_VALUE_CHANGED_CREDITS; i++) { + fbg::ValueChangedParameters params; + params.set_handle(fbg::Handle{chrc_handle}); + params.set_value(kBuffer); + local_svc.NotifyValue(std::move(params)); + } + RunLoopUntilIdle(); + ASSERT_EQ(svc_iter->second.updates.size(), + 3 * fbg::INITIAL_VALUE_CHANGED_CREDITS); + EXPECT_GT(local_svc.credits(), 0); + EXPECT_LE(local_svc.credits(), + static_cast(fbg::INITIAL_VALUE_CHANGED_CREDITS)); + EXPECT_GE(local_svc.credit_log().size(), 2u); + + for (size_t i = 0; i < 3 * fbg::INITIAL_VALUE_CHANGED_CREDITS; i++) { + fbg::ValueChangedParameters params; + params.set_handle(fbg::Handle{chrc_handle}); + params.set_value(kBuffer); + zx::eventpair confirm_ours; + zx::eventpair confirm_theirs; + zx::eventpair::create(/*options=*/0, &confirm_ours, &confirm_theirs); + local_svc.IndicateValue(std::move(params), std::move(confirm_theirs)); + } + RunLoopUntilIdle(); + ASSERT_EQ(svc_iter->second.updates.size(), + 6 * fbg::INITIAL_VALUE_CHANGED_CREDITS); + EXPECT_GT(local_svc.credits(), 0); + EXPECT_LE(local_svc.credits(), + static_cast(fbg::INITIAL_VALUE_CHANGED_CREDITS)); + EXPECT_GE(local_svc.credit_log().size(), 5u); +} + +TEST_F(Gatt2ServerServerTest, PublishServiceWithInvalidCharacteristic) { + fbg::ServiceInfo svc_info; + svc_info.set_handle(fbg::ServiceHandle{1}); + bt::UInt128 svc_type = {5}; + svc_info.set_type(fbt::Uuid{svc_type}); + + fbg::Characteristic chrc_0; + fbg::Handle chrc_handle_0{2}; + chrc_0.set_handle(chrc_handle_0); + + std::vector characteristics; + characteristics.push_back(std::move(chrc_0)); + svc_info.set_characteristics(std::move(characteristics)); + + fidl::InterfaceHandle local_service_handle_0; + fidl::InterfaceRequest request = + local_service_handle_0.NewRequest(); + + int cb_count = 0; + auto cb = [&](fbg::Server_PublishService_Result res) { + cb_count++; + ASSERT_TRUE(res.is_err()); + EXPECT_EQ(res.err(), fbg::PublishServiceError::INVALID_CHARACTERISTICS); + }; + server_ptr()->PublishService( + std::move(svc_info), std::move(local_service_handle_0), std::move(cb)); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count, 1); + ASSERT_EQ(fake_gatt()->local_services().size(), 0u); +} + +TEST_F(Gatt2ServerServerTest, PublishServiceReturnsInvalidId) { + bt::UInt128 svc_type = {5}; + fbg::ServiceInfo svc_info = BuildSimpleService(/*svc_handle=*/1, svc_type); + + fidl::InterfaceHandle local_service_handle; + fidl::InterfaceRequest request = + local_service_handle.NewRequest(); + + int cb_count = 0; + auto cb = [&](fbg::Server_PublishService_Result res) { + cb_count++; + ASSERT_TRUE(res.is_err()); + EXPECT_EQ(res.err(), fbg::PublishServiceError::UNLIKELY_ERROR); + }; + + fake_gatt()->set_register_service_fails(true); + server_ptr()->PublishService( + std::move(svc_info), std::move(local_service_handle), std::move(cb)); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count, 1); + ASSERT_EQ(fake_gatt()->local_services().size(), 0u); +} + +} // namespace +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_client_server.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_client_server.cc new file mode 100644 index 0000000000..38e4b784a2 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_client_server.cc @@ -0,0 +1,165 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_client_server.h" + +#include + +#include + +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_remote_service_server.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/helpers.h" +#include "pw_bluetooth_sapphire/internal/host/common/log.h" +#include "pw_bluetooth_sapphire/internal/host/gatt/gatt.h" + +using fuchsia::bluetooth::ErrorCode; +using fuchsia::bluetooth::Status; + +using fuchsia::bluetooth::gatt::Client; +using fuchsia::bluetooth::gatt::RemoteService; +using fuchsia::bluetooth::gatt::ServiceInfo; +using fuchsia::bluetooth::gatt::ServiceInfoPtr; + +namespace bthost { + +GattClientServer::GattClientServer(bt::gatt::PeerId peer_id, + bt::gatt::GATT::WeakPtr gatt, + fidl::InterfaceRequest request) + : GattServerBase(std::move(gatt), this, std::move(request)), + peer_id_(peer_id), + weak_self_(this) {} + +void GattClientServer::ListServices(::fidl::VectorPtr<::std::string> fidl_uuids, + ListServicesCallback callback) { + // Parse the UUID list. + std::vector uuids; + if (fidl_uuids.has_value()) { + // Allocate all at once and convert in-place. + uuids.resize(fidl_uuids->size()); + for (size_t i = 0; i < uuids.size(); ++i) { + if (!StringToUuid(fidl_uuids.value()[i], &uuids[i])) { + bt_log(WARN, + "fidl", + "%s: Invalid UUID: %s (peer: %s)", + __FUNCTION__, + fidl_uuids.value()[i].c_str(), + bt_str(peer_id_)); + callback(fidl_helpers::NewFidlError( + ErrorCode::INVALID_ARGUMENTS, + "Invalid UUID: " + fidl_uuids.value()[i]), + std::vector(static_cast(0u))); + return; + } + } + } + + auto cb = [callback = std::move(callback), + peer_id = peer_id_, + func = __FUNCTION__](bt::att::Result<> status, auto services) { + std::vector out; + if (status.is_error()) { + bt_log(WARN, + "fidl", + "%s: Failed to discover services (peer: %s)", + func, + bt_str(peer_id)); + auto fidl_status = fidl_helpers::ResultToFidlDeprecated( + status, "Failed to discover services"); + callback(std::move(fidl_status), std::move(out)); + return; + } + + out.resize(services.size()); + + size_t i = 0; + for (const auto& svc : services) { + ServiceInfo service_info; + service_info.id = svc->handle(); + service_info.primary = svc->info().kind == bt::gatt::ServiceKind::PRIMARY; + service_info.type = svc->uuid().ToString(); + out[i++] = std::move(service_info); + } + callback(Status(), std::move(out)); + }; + + gatt()->ListServices(peer_id_, std::move(uuids), std::move(cb)); +} + +void GattClientServer::ConnectToService( + uint64_t id, ::fidl::InterfaceRequest request) { + if (connected_services_.count(id)) { + bt_log(WARN, + "fidl", + "%s: service already requested (service: %lu, peer: %s)", + __FUNCTION__, + id, + bt_str(peer_id_)); + return; + } + + // Initialize an entry so that we remember when this request is in progress. + connected_services_[id] = nullptr; + + bt::gatt::RemoteService::WeakPtr service = gatt()->FindService(peer_id_, id); + + // Automatically called on failure. + auto fail_cleanup = fit::defer([this, id] { connected_services_.erase(id); }); + + if (!service.is_alive()) { + bt_log(WARN, + "fidl", + "%s: failed (service: %lu, peer: %s)", + __FUNCTION__, + id, + bt_str(peer_id_)); + return; + } + + // Clean up the server if either the peer device or the FIDL client + // disconnects. + auto self = weak_self_.GetWeakPtr(); + const char* func = __FUNCTION__; + auto error_cb = [self, id, peer_id = peer_id_, func] { + bt_log(DEBUG, + "fidl", + "%s: service disconnected (service: %lu, peer: %s)", + func, + id, + bt_str(peer_id)); + if (self.is_alive()) { + self->connected_services_.erase(id); + } + }; + + if (!service->AddRemovedHandler(error_cb)) { + bt_log(WARN, + "fidl", + "%s: failed to assign closed handler (service: %lu, peer: %s)", + func, + id, + bt_str(self->peer_id_)); + return; + } + + fail_cleanup.cancel(); + + auto server = std::make_unique( + service->GetWeakPtr(), gatt(), peer_id_, std::move(request)); + server->set_error_handler( + [cb = std::move(error_cb)](zx_status_t status) { cb(); }); + + self->connected_services_[id] = std::move(server); +} + +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_client_server_test.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_client_server_test.cc new file mode 100644 index 0000000000..1840ef8b41 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_client_server_test.cc @@ -0,0 +1,72 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_client_server.h" + +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/fake_gatt_fixture.h" + +namespace bthost { +namespace { + +namespace fgatt = fuchsia::bluetooth::gatt; + +constexpr bt::PeerId kPeerId(1); +constexpr bt::UUID kHeartRate(uint16_t{0x180D}); +constexpr bt::UUID kHid(uint16_t{0x1812}); + +class GattClientServerTest : public bt::fidl::testing::FakeGattFixture { + public: + GattClientServerTest() = default; + ~GattClientServerTest() override = default; + + void SetUp() override { + fidl::InterfaceHandle handle; + server_ = std::make_unique( + kPeerId, gatt()->GetWeakPtr(), handle.NewRequest()); + proxy_.Bind(std::move(handle)); + } + + fgatt::Client* proxy() const { return proxy_.get(); } + + private: + std::unique_ptr server_; + fgatt::ClientPtr proxy_; + + BT_DISALLOW_COPY_ASSIGN_AND_MOVE(GattClientServerTest); +}; + +TEST_F(GattClientServerTest, ListServices) { + bt::gatt::ServiceData data1(bt::gatt::ServiceKind::PRIMARY, 1, 1, kHeartRate); + bt::gatt::ServiceData data2(bt::gatt::ServiceKind::SECONDARY, 2, 2, kHid); + fake_gatt()->AddPeerService(kPeerId, data1); + fake_gatt()->AddPeerService(kPeerId, data2); + + std::vector results; + proxy()->ListServices({}, [&](auto status, auto cb_results) { + EXPECT_FALSE(status.error); + results = std::move(cb_results); + }); + RunLoopUntilIdle(); + ASSERT_EQ(2u, results.size()); + std::sort(results.begin(), results.end(), [](auto& a, auto& b) { + return a.id < b.id; + }); + EXPECT_EQ(kHeartRate.ToString(), results[0].type); + EXPECT_TRUE(results[0].primary); + EXPECT_EQ(kHid.ToString(), results[1].type); + EXPECT_FALSE(results[1].primary); +} + +} // namespace +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_remote_service_server.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_remote_service_server.cc new file mode 100644 index 0000000000..70dcae1997 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_remote_service_server.cc @@ -0,0 +1,417 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_remote_service_server.h" + +#include + +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/helpers.h" +#include "pw_bluetooth_sapphire/internal/host/att/att.h" +#include "pw_bluetooth_sapphire/internal/host/common/log.h" + +using fuchsia::bluetooth::ErrorCode; +using fuchsia::bluetooth::Status; +using fuchsia::bluetooth::gatt::Characteristic; +using fuchsia::bluetooth::gatt::CharacteristicPtr; +using fuchsia::bluetooth::gatt::Descriptor; +using fuchsia::bluetooth::gatt::WriteOptions; + +using bt::ByteBuffer; +using bt::MutableBufferView; +using bt::gatt::CharacteristicData; +using bt::gatt::CharacteristicHandle; +using bt::gatt::DescriptorData; +using bt::gatt::DescriptorHandle; +using bthost::fidl_helpers::CharacteristicHandleFromFidl; +using bthost::fidl_helpers::DescriptorHandleFromFidl; + +namespace bthost { +namespace { + +// We mask away the "extended properties" property. We expose extended +// properties in the same bitfield. +constexpr uint8_t kPropertyMask = 0x7F; + +Characteristic CharacteristicToFidl( + const CharacteristicData& characteristic, + const std::map& descriptors) { + Characteristic fidl_char; + fidl_char.id = static_cast(characteristic.value_handle); + fidl_char.type = characteristic.type.ToString(); + fidl_char.properties = + static_cast(characteristic.properties & kPropertyMask); + fidl_char.descriptors.emplace(); // initialize an empty vector + + // TODO(armansito): Add extended properties. + + for (const auto& [id, descr] : descriptors) { + Descriptor fidl_descr; + fidl_descr.id = static_cast(id.value); + fidl_descr.type = descr.type.ToString(); + fidl_char.descriptors->push_back(std::move(fidl_descr)); + } + + return fidl_char; +} + +void NopStatusCallback(bt::att::Result<>) {} + +} // namespace + +GattRemoteServiceServer::GattRemoteServiceServer( + bt::gatt::RemoteService::WeakPtr service, + bt::gatt::GATT::WeakPtr gatt, + bt::PeerId peer_id, + fidl::InterfaceRequest request) + : GattServerBase(gatt, this, std::move(request)), + service_(std::move(service)), + peer_id_(peer_id), + weak_self_(this) { + BT_DEBUG_ASSERT(service_.is_alive()); +} + +GattRemoteServiceServer::~GattRemoteServiceServer() { + for (const auto& iter : notify_handlers_) { + if (iter.second != bt::gatt::kInvalidId) { + service_->DisableNotifications( + iter.first, iter.second, NopStatusCallback); + } + } +} + +void GattRemoteServiceServer::DiscoverCharacteristics( + DiscoverCharacteristicsCallback callback) { + auto res_cb = [callback = std::move(callback)](bt::att::Result<> status, + const auto& chrcs) { + std::vector fidl_chrcs; + if (status.is_ok()) { + for (const auto& [id, chrc] : chrcs) { + auto& [chr, descs] = chrc; + fidl_chrcs.push_back(CharacteristicToFidl(chr, descs)); + } + } + + callback(fidl_helpers::ResultToFidlDeprecated(status, ""), + std::move(fidl_chrcs)); + }; + + service_->DiscoverCharacteristics(std::move(res_cb)); +} + +void GattRemoteServiceServer::ReadCharacteristic( + uint64_t id, ReadCharacteristicCallback callback) { + auto cb = [callback = std::move(callback)]( + bt::att::Result<> status, const bt::ByteBuffer& value, auto) { + // We always reply with a non-null value. + std::vector vec; + + if (status.is_ok() && value.size()) { + vec.resize(value.size()); + + MutableBufferView vec_view(vec.data(), vec.size()); + value.Copy(&vec_view); + } + + callback(fidl_helpers::ResultToFidlDeprecated(status), std::move(vec)); + }; + + // TODO(fxbug.dev/42141942): The 64 bit `id` can overflow the 16 bits of a + // bt::att:Handle. Fix this. + service_->ReadCharacteristic(CharacteristicHandleFromFidl(id), std::move(cb)); +} + +void GattRemoteServiceServer::ReadLongCharacteristic( + uint64_t id, + uint16_t offset, + uint16_t max_bytes, + ReadLongCharacteristicCallback callback) { + auto cb = [callback = std::move(callback)]( + bt::att::Result<> status, const bt::ByteBuffer& value, auto) { + // We always reply with a non-null value. + std::vector vec; + + if (status.is_ok() && value.size()) { + vec.resize(value.size()); + + MutableBufferView vec_view(vec.data(), vec.size()); + value.Copy(&vec_view); + } + + callback(fidl_helpers::ResultToFidlDeprecated(status), std::move(vec)); + }; + + // TODO(fxbug.dev/42141942): The 64 bit `id` can overflow the 16 bits of a + // bt::att:Handle. Fix this. + service_->ReadLongCharacteristic( + CharacteristicHandleFromFidl(id), offset, max_bytes, std::move(cb)); +} + +void GattRemoteServiceServer::WriteCharacteristic( + uint64_t id, + ::std::vector value, + WriteCharacteristicCallback callback) { + auto cb = [callback = std::move(callback)](bt::att::Result<> status) { + callback(fidl_helpers::ResultToFidlDeprecated(status, "")); + }; + + // TODO(fxbug.dev/42141942): The 64 bit `id` can overflow the 16 bits of a + // bt::att:Handle. Fix this. + service_->WriteCharacteristic( + CharacteristicHandleFromFidl(id), std::move(value), std::move(cb)); +} + +void GattRemoteServiceServer::WriteLongCharacteristic( + uint64_t id, + uint16_t offset, + ::std::vector value, + WriteOptions write_options, + WriteLongCharacteristicCallback callback) { + auto cb = [callback = std::move(callback)](bt::att::Result<> status) { + callback(fidl_helpers::ResultToFidlDeprecated(status, "")); + }; + + auto reliable_mode = fidl_helpers::ReliableModeFromFidl(write_options); + // TODO(fxbug.dev/42141942): The 64 bit `id` can overflow the 16 bits of a + // bt::att:Handle. Fix this. + service_->WriteLongCharacteristic(CharacteristicHandleFromFidl(id), + offset, + std::move(value), + std::move(reliable_mode), + std::move(cb)); +} + +void GattRemoteServiceServer::WriteCharacteristicWithoutResponse( + uint64_t id, ::std::vector value) { + // TODO(fxbug.dev/42141942): The 64 bit `id` can overflow the 16 bits of a + // bt::att:Handle. Fix this. + service_->WriteCharacteristicWithoutResponse(CharacteristicHandleFromFidl(id), + std::move(value), + /*cb=*/[](auto) {}); +} + +void GattRemoteServiceServer::ReadDescriptor(uint64_t id, + ReadDescriptorCallback callback) { + auto cb = [callback = std::move(callback)]( + bt::att::Result<> status, const bt::ByteBuffer& value, auto) { + // We always reply with a non-null value. + std::vector vec; + + if (status.is_ok() && value.size()) { + vec.resize(value.size()); + + MutableBufferView vec_view(vec.data(), vec.size()); + value.Copy(&vec_view); + } + + callback(fidl_helpers::ResultToFidlDeprecated(status), std::move(vec)); + }; + + // TODO(fxbug.dev/42141942): The 64 bit `id` can overflow the 16 bits of a + // bt::att:Handle. Fix this. + service_->ReadDescriptor(DescriptorHandleFromFidl(id), std::move(cb)); +} + +void GattRemoteServiceServer::ReadLongDescriptor( + uint64_t id, + uint16_t offset, + uint16_t max_bytes, + ReadLongDescriptorCallback callback) { + auto cb = [callback = std::move(callback)]( + bt::att::Result<> status, const bt::ByteBuffer& value, auto) { + // We always reply with a non-null value. + std::vector vec; + + if (status.is_ok() && value.size()) { + vec.resize(value.size()); + + MutableBufferView vec_view(vec.data(), vec.size()); + value.Copy(&vec_view); + } + + callback(fidl_helpers::ResultToFidlDeprecated(status), std::move(vec)); + }; + + // TODO(fxbug.dev/42141942): The 64 bit `id` can overflow the 16 bits of a + // bt::att:Handle. Fix this. + service_->ReadLongDescriptor( + DescriptorHandleFromFidl(id), offset, max_bytes, std::move(cb)); +} + +void GattRemoteServiceServer::WriteDescriptor( + uint64_t id, + ::std::vector value, + WriteDescriptorCallback callback) { + // TODO(fxbug.dev/42141942): The 64 bit `id` can overflow the 16 bits of a + // bt::att:Handle. Fix this. + service_->WriteDescriptor( + DescriptorHandleFromFidl(id), + std::move(value), + [callback = std::move(callback)](bt::att::Result<> status) { + callback(fidl_helpers::ResultToFidlDeprecated(status, "")); + }); +} + +void GattRemoteServiceServer::WriteLongDescriptor( + uint64_t id, + uint16_t offset, + ::std::vector value, + WriteLongDescriptorCallback callback) { + // TODO(fxbug.dev/42141942): The 64 bit `id` can overflow the 16 bits of a + // bt::att:Handle. Fix this. + service_->WriteLongDescriptor( + DescriptorHandleFromFidl(id), + offset, + std::move(value), + [callback = std::move(callback)](bt::att::Result<> status) { + callback(fidl_helpers::ResultToFidlDeprecated(status, "")); + }); +} + +void GattRemoteServiceServer::ReadByType(fuchsia::bluetooth::Uuid uuid, + ReadByTypeCallback callback) { + service_->ReadByType( + fidl_helpers::UuidFromFidl(uuid), + [self = weak_self_.GetWeakPtr(), + cb = std::move(callback), + func = __FUNCTION__]( + bt::att::Result<> status, + std::vector results) { + if (!self.is_alive()) { + return; + } + + if (status.is_error()) { + if (status.error_value().is(bt::HostError::kInvalidParameters)) { + bt_log(WARN, + "fidl", + "%s: called with invalid parameters, closing FIDL channel " + "(peer: %s)", + func, + bt_str(self->peer_id_)); + self->binding()->Close(ZX_ERR_INVALID_ARGS); + return; + } + cb(fpromise::error( + fidl_helpers::GattErrorToFidl(status.error_value()))); + return; + } + + if (results.size() > + fuchsia::bluetooth::gatt::MAX_READ_BY_TYPE_RESULTS) { + cb(fpromise::error( + fuchsia::bluetooth::gatt::Error::TOO_MANY_RESULTS)); + return; + } + + std::vector fidl_results; + fidl_results.reserve(results.size()); + + for (const auto& result : results) { + fuchsia::bluetooth::gatt::ReadByTypeResult fidl_result; + fidl_result.set_id(static_cast(result.handle.value)); + if (result.result.is_ok()) { + fidl_result.set_value(result.result.value()->ToVector()); + } else { + fidl_result.set_error(fidl_helpers::GattErrorToFidl( + bt::att::Error(result.result.error_value()))); + } + fidl_results.push_back(std::move(fidl_result)); + } + + cb(fpromise::ok(std::move(fidl_results))); + }); +} + +void GattRemoteServiceServer::NotifyCharacteristic( + uint64_t id, bool enable, NotifyCharacteristicCallback callback) { + // TODO(fxbug.dev/42141942): The 64 bit `id` can overflow the 16 bits of a + // bt::att:Handle. Fix this. + auto handle = CharacteristicHandleFromFidl(id); + if (!enable) { + auto iter = notify_handlers_.find(handle); + if (iter == notify_handlers_.end()) { + callback(fidl_helpers::NewFidlError(ErrorCode::NOT_FOUND, + "characteristic not notifying")); + return; + } + + if (iter->second == bt::gatt::kInvalidId) { + callback(fidl_helpers::NewFidlError( + ErrorCode::IN_PROGRESS, + "characteristic notification registration pending")); + return; + } + + service_->DisableNotifications( + handle, + iter->second, + [callback = std::move(callback)](bt::att::Result<> status) { + callback(fidl_helpers::ResultToFidlDeprecated(status, "")); + }); + notify_handlers_.erase(iter); + + return; + } + + if (notify_handlers_.count(handle) != 0) { + callback(fidl_helpers::NewFidlError(ErrorCode::ALREADY, + "characteristic already notifying")); + return; + } + + // Prevent any races and leaks by marking a notification is in progress + notify_handlers_[handle] = bt::gatt::kInvalidId; + + auto self = weak_self_.GetWeakPtr(); + auto value_cb = [self, id](const ByteBuffer& value, + bool /*maybe_truncated*/) { + if (!self.is_alive()) + return; + + self->binding()->events().OnCharacteristicValueUpdated(id, + value.ToVector()); + }; + + auto status_cb = + [self, svc = service_, handle, callback = std::move(callback)]( + bt::att::Result<> status, HandlerId handler_id) { + if (!self.is_alive()) { + if (status.is_ok()) { + // Disable this handler so it doesn't leak. + svc->DisableNotifications(handle, handler_id, NopStatusCallback); + } + + callback(fidl_helpers::NewFidlError(ErrorCode::FAILED, "canceled")); + return; + } + + if (status.is_ok()) { + BT_DEBUG_ASSERT(handler_id != bt::gatt::kInvalidId); + BT_DEBUG_ASSERT(self->notify_handlers_.count(handle) == 1u); + BT_DEBUG_ASSERT(self->notify_handlers_[handle] == + bt::gatt::kInvalidId); + self->notify_handlers_[handle] = handler_id; + } else { + // Remove our handle holder. + self->notify_handlers_.erase(handle); + } + + callback(fidl_helpers::ResultToFidlDeprecated(status, "")); + }; + + service_->EnableNotifications( + handle, std::move(value_cb), std::move(status_cb)); +} + +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_remote_service_server_test.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_remote_service_server_test.cc new file mode 100644 index 0000000000..256bd5b9a0 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_remote_service_server_test.cc @@ -0,0 +1,213 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_remote_service_server.h" + +#include "gtest/gtest.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/fake_gatt_fixture.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/helpers.h" +#include "pw_bluetooth_sapphire/internal/host/common/byte_buffer.h" +#include "pw_bluetooth_sapphire/internal/host/gatt/remote_service.h" +#include "pw_bluetooth_sapphire/internal/host/gatt/remote_service_manager.h" +#include "pw_bluetooth_sapphire/internal/host/testing/test_helpers.h" + +namespace bthost { +namespace { + +namespace fbgatt = fuchsia::bluetooth::gatt; + +constexpr bt::PeerId kPeerId(1); + +constexpr bt::att::Handle kServiceStartHandle = 0x0021; +constexpr bt::att::Handle kServiceEndHandle = 0x002C; +const bt::UUID kServiceUuid(uint16_t{0x180D}); + +class GattRemoteServiceServerTest : public bt::fidl::testing::FakeGattFixture { + public: + GattRemoteServiceServerTest() = default; + ~GattRemoteServiceServerTest() override = default; + + void SetUp() override { + { + auto [svc, client] = fake_gatt()->AddPeerService( + kPeerId, + bt::gatt::ServiceData(bt::gatt::ServiceKind::PRIMARY, + kServiceStartHandle, + kServiceEndHandle, + kServiceUuid)); + service_ = std::move(svc); + fake_client_ = std::move(client); + } + + fidl::InterfaceHandle handle; + server_ = std::make_unique( + service_, gatt(), kPeerId, handle.NewRequest()); + proxy_.Bind(std::move(handle)); + } + + void TearDown() override { + // Clear any previous expectations that are based on the ATT Write Request, + // so that write requests sent during RemoteService::ShutDown() are ignored. + fake_client()->set_write_request_callback({}); + + bt::fidl::testing::FakeGattFixture::TearDown(); + } + + protected: + const bt::gatt::testing::FakeClient::WeakPtr& fake_client() const { + BT_ASSERT(fake_client_.is_alive()); + return fake_client_; + } + + fbgatt::RemoteServicePtr& service_proxy() { return proxy_; } + + private: + std::unique_ptr server_; + + fbgatt::RemoteServicePtr proxy_; + bt::gatt::RemoteService::WeakPtr service_; + bt::gatt::testing::FakeClient::WeakPtr fake_client_; + + BT_DISALLOW_COPY_ASSIGN_AND_MOVE(GattRemoteServiceServerTest); +}; + +TEST_F(GattRemoteServiceServerTest, ReadByTypeSuccess) { + constexpr bt::UUID kCharUuid(uint16_t{0xfefe}); + + constexpr bt::att::Handle kHandle = kServiceStartHandle; + const auto kValue = bt::StaticByteBuffer(0x00, 0x01, 0x02); + const std::vector kValues = { + {kHandle, kValue.view(), /*maybe_truncated=*/false}}; + + size_t read_count = 0; + fake_client()->set_read_by_type_request_callback([&](const bt::UUID& type, + bt::att::Handle start, + bt::att::Handle end, + auto callback) { + switch (read_count++) { + case 0: + callback(fit::ok(kValues)); + break; + case 1: + callback(fit::error(bt::gatt::Client::ReadByTypeError{ + bt::att::Error(bt::att::ErrorCode::kAttributeNotFound), start})); + break; + default: + FAIL(); + } + }); + + std::optional fidl_result; + service_proxy()->ReadByType( + fidl_helpers::UuidToFidl(kCharUuid), + [&](auto cb_result) { fidl_result = std::move(cb_result); }); + + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_result.has_value()); + ASSERT_TRUE(fidl_result->is_response()); + const auto& response = fidl_result->response(); + ASSERT_EQ(1u, response.results.size()); + const fbgatt::ReadByTypeResult& result0 = response.results[0]; + ASSERT_TRUE(result0.has_id()); + EXPECT_EQ(result0.id(), static_cast(kHandle)); + ASSERT_TRUE(result0.has_value()); + EXPECT_TRUE(ContainersEqual( + bt::BufferView(result0.value().data(), result0.value().size()), kValue)); + EXPECT_FALSE(result0.has_error()); +} + +TEST_F(GattRemoteServiceServerTest, ReadByTypeResultWithError) { + constexpr bt::UUID kCharUuid(uint16_t{0xfefe}); + + size_t read_count = 0; + fake_client()->set_read_by_type_request_callback([&](const bt::UUID& type, + bt::att::Handle start, + bt::att::Handle end, + auto callback) { + ASSERT_EQ(0u, read_count++); + callback(fit::error(bt::gatt::Client::ReadByTypeError{ + bt::att::Error(bt::att::ErrorCode::kInsufficientAuthorization), + kServiceEndHandle})); + }); + + std::optional fidl_result; + service_proxy()->ReadByType( + fidl_helpers::UuidToFidl(kCharUuid), + [&](auto cb_result) { fidl_result = std::move(cb_result); }); + + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_result.has_value()); + ASSERT_TRUE(fidl_result->is_response()); + const auto& response = fidl_result->response(); + ASSERT_EQ(1u, response.results.size()); + const fbgatt::ReadByTypeResult& result0 = response.results[0]; + ASSERT_TRUE(result0.has_id()); + EXPECT_EQ(result0.id(), static_cast(kServiceEndHandle)); + EXPECT_FALSE(result0.has_value()); + ASSERT_TRUE(result0.has_error()); + EXPECT_EQ(fbgatt::Error::INSUFFICIENT_AUTHORIZATION, result0.error()); +} + +TEST_F(GattRemoteServiceServerTest, ReadByTypeError) { + constexpr bt::UUID kCharUuid(uint16_t{0xfefe}); + + size_t read_count = 0; + fake_client()->set_read_by_type_request_callback([&](const bt::UUID& type, + bt::att::Handle start, + bt::att::Handle end, + auto callback) { + switch (read_count++) { + case 0: + callback(fit::error(bt::gatt::Client::ReadByTypeError{ + bt::Error(bt::HostError::kPacketMalformed), std::nullopt})); + break; + default: + FAIL(); + } + }); + + std::optional fidl_result; + service_proxy()->ReadByType( + fidl_helpers::UuidToFidl(kCharUuid), + [&](auto cb_result) { fidl_result = std::move(cb_result); }); + + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_result.has_value()); + ASSERT_TRUE(fidl_result->is_err()); + const auto& err = fidl_result->err(); + EXPECT_EQ(fbgatt::Error::INVALID_RESPONSE, err); +} + +TEST_F(GattRemoteServiceServerTest, + ReadByTypeInvalidParametersErrorClosesChannel) { + constexpr bt::UUID kCharUuid = bt::gatt::types::kCharacteristicDeclaration; + + std::optional error_status; + service_proxy().set_error_handler( + [&](zx_status_t status) { error_status = status; }); + + std::optional fidl_result; + service_proxy()->ReadByType( + fidl_helpers::UuidToFidl(kCharUuid), + [&](auto cb_result) { fidl_result = std::move(cb_result); }); + + RunLoopUntilIdle(); + EXPECT_FALSE(fidl_result.has_value()); + EXPECT_FALSE(service_proxy().is_bound()); + EXPECT_TRUE(error_status.has_value()); + EXPECT_EQ(ZX_ERR_INVALID_ARGS, error_status.value()); +} + +} // namespace +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_server_server.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_server_server.cc new file mode 100644 index 0000000000..84a5c2a4e8 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_server_server.cc @@ -0,0 +1,442 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_server_server.h" + +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/helpers.h" +#include "pw_bluetooth_sapphire/internal/host/common/log.h" +#include "pw_bluetooth_sapphire/internal/host/common/uuid.h" +#include "pw_bluetooth_sapphire/internal/host/gap/low_energy_connection_manager.h" +#include "pw_bluetooth_sapphire/internal/host/gatt/connection.h" +#include "pw_bluetooth_sapphire/internal/host/gatt/gatt_defs.h" +#include "pw_bluetooth_sapphire/internal/host/gatt/server.h" + +using fuchsia::bluetooth::ErrorCode; +using fuchsia::bluetooth::Status; +using GattErrorCode = fuchsia::bluetooth::gatt::ErrorCode; + +using fuchsia::bluetooth::gatt::Characteristic; +using fuchsia::bluetooth::gatt::Descriptor; +using fuchsia::bluetooth::gatt::LocalService; +using fuchsia::bluetooth::gatt::LocalServiceDelegate; +using fuchsia::bluetooth::gatt::LocalServiceDelegatePtr; +using fuchsia::bluetooth::gatt::SecurityRequirementsPtr; +using fuchsia::bluetooth::gatt::ServiceInfo; + +namespace bthost { +namespace { + +fit::result GattStatusFromFidl(GattErrorCode error_code, + bool is_read) { + switch (error_code) { + case GattErrorCode::NO_ERROR: + return fit::ok(); + case GattErrorCode::INVALID_OFFSET: + return fit::error(bt::att::ErrorCode::kInvalidOffset); + case GattErrorCode::INVALID_VALUE_LENGTH: + return fit::error(bt::att::ErrorCode::kInvalidAttributeValueLength); + case GattErrorCode::NOT_PERMITTED: + if (is_read) + return fit::error(bt::att::ErrorCode::kReadNotPermitted); + return fit::error(bt::att::ErrorCode::kWriteNotPermitted); + default: + break; + } + return fit::error(bt::att::ErrorCode::kUnlikelyError); +} + +bt::att::AccessRequirements ParseSecurityRequirements( + const SecurityRequirementsPtr& reqs) { + if (!reqs) { + return bt::att::AccessRequirements(); + } + return bt::att::AccessRequirements(reqs->encryption_required, + reqs->authentication_required, + reqs->authorization_required); +} + +// Carries a either a successful Result or an error message that can be sent as +// a FIDL response. +template ::value>> +struct MaybeResult final { + explicit MaybeResult(Result&& result) + : result(std::forward(result)) {} + + explicit MaybeResult(Error&& error) : error(std::forward(error)) {} + + bool is_error() const { return static_cast(!result); } + + Result result; + Error error; +}; + +using DescriptorResult = MaybeResult; +DescriptorResult NewDescriptor(const Descriptor& fidl_desc) { + auto read_reqs = ParseSecurityRequirements(fidl_desc.permissions->read); + auto write_reqs = ParseSecurityRequirements(fidl_desc.permissions->write); + + bt::UUID type; + if (!bt::StringToUuid(fidl_desc.type, &type)) { + return DescriptorResult("Invalid descriptor UUID"); + } + + return DescriptorResult(std::make_unique( + fidl_desc.id, type, read_reqs, write_reqs)); +} + +using CharacteristicResult = MaybeResult; +CharacteristicResult NewCharacteristic(const Characteristic& fidl_chrc) { + uint8_t props = fidl_chrc.properties & 0xFF; + uint16_t ext_props = (fidl_chrc.properties & 0xFF00) >> 8; + + if (!fidl_chrc.permissions) { + return CharacteristicResult("Characteristic permissions missing"); + } + + bool supports_update = (props & bt::gatt::Property::kNotify) || + (props & bt::gatt::Property::kIndicate); + if (supports_update != static_cast(fidl_chrc.permissions->update)) { + return CharacteristicResult( + supports_update ? "Characteristic update permission required" + : "Characteristic update permission must be null"); + } + + auto read_reqs = ParseSecurityRequirements(fidl_chrc.permissions->read); + auto write_reqs = ParseSecurityRequirements(fidl_chrc.permissions->write); + auto update_reqs = ParseSecurityRequirements(fidl_chrc.permissions->update); + + bt::UUID type; + if (!bt::StringToUuid(fidl_chrc.type, &type)) { + return CharacteristicResult("Invalid characteristic UUID"); + } + + auto chrc = std::make_unique( + fidl_chrc.id, type, props, ext_props, read_reqs, write_reqs, update_reqs); + if (fidl_chrc.descriptors && !fidl_chrc.descriptors->empty()) { + for (const auto& fidl_desc : *fidl_chrc.descriptors) { + auto desc_result = NewDescriptor(fidl_desc); + if (desc_result.is_error()) { + return CharacteristicResult(std::move(desc_result.error)); + } + + chrc->AddDescriptor(std::move(desc_result.result)); + } + } + + return CharacteristicResult(std::move(chrc)); +} + +} // namespace + +// Implements the gatt::LocalService FIDL interface. Instances of this class are +// only created by a GattServerServer. +class GattServerServer::LocalServiceImpl + : public GattServerBase { + public: + LocalServiceImpl(GattServerServer* owner, + uint64_t id, + LocalServiceDelegatePtr delegate, + ::fidl::InterfaceRequest request) + : GattServerBase(owner->gatt(), this, std::move(request)), + owner_(owner), + id_(id), + delegate_(std::move(delegate)) { + BT_DEBUG_ASSERT(owner_); + BT_DEBUG_ASSERT(delegate_); + } + + // The destructor removes the GATT service + ~LocalServiceImpl() override { + CleanUp(); + + // Do not notify the owner in this case. If we got here it means that + // |owner_| deleted us. + } + + // Returns the current delegate. Returns nullptr if the delegate was + // disconnected (e.g. due to a call to RemoveService()). + LocalServiceDelegate* delegate() { return delegate_.get(); } + + private: + // fuchsia::bluetooth::gatt::Service overrides: + void RemoveService() override { + CleanUp(); + owner_->RemoveService(id_); + } + + void NotifyValue(uint64_t characteristic_id, + ::std::string peer_id, + ::std::vector value, + bool confirm) override { + auto id = fidl_helpers::PeerIdFromString(std::move(peer_id)); + if (id) { + bt::gatt::IndicationCallback indication_cb = nullptr; + if (confirm) { + indication_cb = [](bt::att::Result<> result) { + bt_log(DEBUG, "fidl", "indication result: %s", bt_str(result)); + }; + } + gatt()->SendUpdate(id_, + characteristic_id, + *id, + std::move(value), + std::move(indication_cb)); + } + } + + // Unregisters the underlying service if it is still active. + void CleanUp() { + delegate_ = nullptr; // Closes the delegate handle. + gatt()->UnregisterService(id_); + } + + // |owner_| owns this instance and is expected to outlive it. + GattServerServer* owner_; // weak + uint64_t id_; + + // The delegate connection for the corresponding service instance. This gets + // cleared when the service is unregistered (via RemoveService() or + // destruction). + LocalServiceDelegatePtr delegate_; + + BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(LocalServiceImpl); +}; + +GattServerServer::GattServerServer( + bt::gatt::GATT::WeakPtr gatt, + fidl::InterfaceRequest request) + : GattServerBase(gatt, this, std::move(request)), weak_self_(this) {} + +GattServerServer::~GattServerServer() { + // This will remove all of our services from their adapter. + services_.clear(); +} + +void GattServerServer::RemoveService(uint64_t id) { + if (services_.erase(id)) { + bt_log(DEBUG, "fidl", "%s: service removed (id: %lu)", __FUNCTION__, id); + } else { + bt_log(WARN, "fidl", "%s: service id not found: %lu", __FUNCTION__, id); + } +} + +void GattServerServer::PublishService( + ServiceInfo service_info, + fidl::InterfaceHandle delegate, + fidl::InterfaceRequest service_iface, + PublishServiceCallback callback) { + if (!delegate) { + bt_log(WARN, "fidl", "%s: missing service delegate", __FUNCTION__); + auto error = fidl_helpers::NewFidlError(ErrorCode::INVALID_ARGUMENTS, + "A delegate is required"); + callback(std::move(error)); + return; + } + + if (!service_iface) { + bt_log(WARN, "fidl", "%s: missing service interface request", __FUNCTION__); + auto error = fidl_helpers::NewFidlError(ErrorCode::INVALID_ARGUMENTS, + "Service interface is required"); + callback(std::move(error)); + return; + } + + bt::UUID service_type; + if (!bt::StringToUuid(service_info.type, &service_type)) { + bt_log(WARN, + "fidl", + "%s: invalid service UUID %s", + __FUNCTION__, + service_info.type.c_str()); + auto error = fidl_helpers::NewFidlError(ErrorCode::INVALID_ARGUMENTS, + "Invalid service UUID"); + callback(std::move(error)); + return; + } + + // Process the FIDL service tree. + auto service = + std::make_unique(service_info.primary, service_type); + if (service_info.characteristics) { + for (const auto& fidl_chrc : *service_info.characteristics) { + auto chrc_result = NewCharacteristic(fidl_chrc); + if (chrc_result.is_error()) { + auto error = fidl_helpers::NewFidlError(ErrorCode::INVALID_ARGUMENTS, + chrc_result.error); + callback(std::move(error)); + return; + } + + service->AddCharacteristic(std::move(chrc_result.result)); + } + } + + auto self = weak_self_.GetWeakPtr(); + + // Set up event handlers. + auto read_handler = [self](bt::PeerId /*ignore*/, + auto svc_id, + auto id, + auto offset, + auto responder) mutable { + if (self.is_alive()) { + self->OnReadRequest(svc_id, id, offset, std::move(responder)); + } else { + responder(fit::error(bt::att::ErrorCode::kUnlikelyError), + bt::BufferView()); + } + }; + auto write_handler = [self](bt::PeerId /*ignore*/, + auto svc_id, + auto id, + auto offset, + const auto& value, + auto responder) mutable { + if (self.is_alive()) { + self->OnWriteRequest(svc_id, id, offset, value, std::move(responder)); + } else { + responder(fit::error(bt::att::ErrorCode::kUnlikelyError)); + } + }; + auto ccc_callback = [self](auto svc_id, + auto id, + bt::gatt::PeerId peer_id, + bool notify, + bool indicate) { + if (self.is_alive()) + self->OnCharacteristicConfig(svc_id, id, peer_id, notify, indicate); + }; + + auto id_cb = [self, + delegate = std::move(delegate), + service_iface = std::move(service_iface), + callback = std::move(callback)](bt::gatt::IdType id) mutable { + if (!self.is_alive()) + return; + + if (!id) { + // TODO(armansito): Report a more detailed string if registration + // fails due to duplicate ids. + auto error = fidl_helpers::NewFidlError(ErrorCode::FAILED, + "Failed to publish service"); + callback(std::move(error)); + return; + } + + BT_DEBUG_ASSERT(self->services_.find(id) == self->services_.end()); + + // This will be called if either the delegate or the service connection + // closes. + auto connection_error_cb = [self, id](zx_status_t status) { + bt_log(DEBUG, "bt-host", "removing GATT service (id: %lu)", id); + if (self.is_alive()) + self->RemoveService(id); + }; + + auto delegate_ptr = delegate.Bind(); + delegate_ptr.set_error_handler(connection_error_cb); + + auto service_server = std::make_unique( + &self.get(), id, std::move(delegate_ptr), std::move(service_iface)); + service_server->set_error_handler(connection_error_cb); + self->services_[id] = std::move(service_server); + + callback(Status()); + }; + + gatt()->RegisterService(std::move(service), + std::move(id_cb), + std::move(read_handler), + std::move(write_handler), + std::move(ccc_callback)); +} + +void GattServerServer::OnReadRequest(bt::gatt::IdType service_id, + bt::gatt::IdType id, + uint16_t offset, + bt::gatt::ReadResponder responder) { + auto iter = services_.find(service_id); + if (iter == services_.end()) { + bt_log( + WARN, "fidl", "%s: unknown service id %lu", __FUNCTION__, service_id); + responder(fit::error(bt::att::ErrorCode::kUnlikelyError), bt::BufferView()); + return; + } + + auto cb = [responder = std::move(responder)]( + fidl::VectorPtr optional_value, + auto error_code) mutable { + std::vector value; + if (optional_value.has_value()) { + value = std::move(optional_value.value()); + } + responder(GattStatusFromFidl(error_code, /*is_read=*/true), + bt::BufferView(value.data(), value.size())); + }; + + auto* delegate = iter->second->delegate(); + BT_DEBUG_ASSERT(delegate); + delegate->OnReadValue(id, offset, std::move(cb)); +} + +void GattServerServer::OnWriteRequest(bt::gatt::IdType service_id, + bt::gatt::IdType id, + uint16_t offset, + const bt::ByteBuffer& value, + bt::gatt::WriteResponder responder) { + auto iter = services_.find(service_id); + if (iter == services_.end()) { + bt_log( + WARN, "fidl", "%s: unknown service id %lu", __FUNCTION__, service_id); + responder(fit::error(bt::att::ErrorCode::kUnlikelyError)); + return; + } + + auto fidl_value = fidl::To>(value); + auto* delegate = iter->second->delegate(); + BT_DEBUG_ASSERT(delegate); + + if (!responder) { + delegate->OnWriteWithoutResponse(id, offset, std::move(fidl_value)); + return; + } + + auto cb = [responder = std::move(responder)](auto error_code) mutable { + responder(GattStatusFromFidl(error_code, /*is_read=*/false)); + }; + + delegate->OnWriteValue(id, offset, std::move(fidl_value), std::move(cb)); +} + +void GattServerServer::OnCharacteristicConfig(bt::gatt::IdType service_id, + bt::gatt::IdType chrc_id, + bt::gatt::PeerId peer_id, + bool notify, + bool indicate) { + auto iter = services_.find(service_id); + if (iter == services_.end()) { + bt_log( + WARN, "fidl", "%s: unknown service id %lu", __FUNCTION__, service_id); + return; + } + + auto* delegate = iter->second->delegate(); + BT_DEBUG_ASSERT(delegate); + delegate->OnCharacteristicConfiguration( + chrc_id, peer_id.ToString(), notify, indicate); +} + +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/helpers.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/helpers.cc new file mode 100644 index 0000000000..138cf9199c --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/helpers.cc @@ -0,0 +1,2915 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/fidl/helpers.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "fuchsia/bluetooth/bredr/cpp/fidl.h" +#include "fuchsia/bluetooth/cpp/fidl.h" +#include "lib/fpromise/result.h" +#include "pw_bluetooth_sapphire/internal/host/att/att.h" +#include "pw_bluetooth_sapphire/internal/host/common/advertising_data.h" +#include "pw_bluetooth_sapphire/internal/host/common/log.h" +#include "pw_bluetooth_sapphire/internal/host/common/uuid.h" +#include "pw_bluetooth_sapphire/internal/host/gap/discovery_filter.h" +#include "pw_bluetooth_sapphire/internal/host/gap/gap.h" +#include "pw_bluetooth_sapphire/internal/host/sco/sco.h" +#include "pw_bluetooth_sapphire/internal/host/sdp/data_element.h" +#include "pw_bluetooth_sapphire/internal/host/sdp/sdp.h" +#include "pw_bluetooth_sapphire/internal/host/sdp/service_record.h" +#include "pw_bluetooth_sapphire/internal/host/sm/types.h" + +using fuchsia::bluetooth::Error; +using fuchsia::bluetooth::ErrorCode; +using fuchsia::bluetooth::Int8; +using fuchsia::bluetooth::Status; + +namespace fble = fuchsia::bluetooth::le; +namespace fbredr = fuchsia::bluetooth::bredr; +namespace fbt = fuchsia::bluetooth; +namespace fgatt = fuchsia::bluetooth::gatt; +namespace fgatt2 = fuchsia::bluetooth::gatt2; +namespace fsys = fuchsia::bluetooth::sys; +namespace faudio = fuchsia::hardware::audio; +namespace fhbt = fuchsia_hardware_bluetooth; +namespace android_emb = pw::bluetooth::vendor::android_hci; + +const uint8_t BIT_SHIFT_8 = 8; +const uint8_t BIT_SHIFT_16 = 16; + +namespace bthost::fidl_helpers { +// TODO(fxbug.dev/42076395): Add remaining codecs +std::optional FidlToCodecType( + const fbredr::AudioOffloadFeatures& codec) { + switch (codec.Which()) { + case fuchsia::bluetooth::bredr::AudioOffloadFeatures::kSbc: + return android_emb::A2dpCodecType::SBC; + case fuchsia::bluetooth::bredr::AudioOffloadFeatures::kAac: + return android_emb::A2dpCodecType::AAC; + default: + bt_log(WARN, + "fidl", + "Codec type not yet handled: %u", + static_cast(codec.Which())); + return std::nullopt; + } +} + +bt::StaticPacket FidlToScmsTEnable( + bool scms_t_enable) { + bt::StaticPacket scms_t_enable_struct; + + if (scms_t_enable) { + scms_t_enable_struct.view().enabled().Write( + pw::bluetooth::emboss::GenericEnableParam::ENABLE); + } else { + scms_t_enable_struct.view().enabled().Write( + pw::bluetooth::emboss::GenericEnableParam::DISABLE); + } + + scms_t_enable_struct.view().header().Write(0x00); + return scms_t_enable_struct; +} + +std::optional FidlToSamplingFrequency( + fbredr::AudioSamplingFrequency sampling_frequency) { + switch (sampling_frequency) { + case fbredr::AudioSamplingFrequency::HZ_44100: + return android_emb::A2dpSamplingFrequency::HZ_44100; + case fbredr::AudioSamplingFrequency::HZ_48000: + return android_emb::A2dpSamplingFrequency::HZ_48000; + case fbredr::AudioSamplingFrequency::HZ_88200: + return android_emb::A2dpSamplingFrequency::HZ_88200; + case fbredr::AudioSamplingFrequency::HZ_96000: + return android_emb::A2dpSamplingFrequency::HZ_96000; + default: + return std::nullopt; + } +} + +std::optional FidlToBitsPerSample( + fbredr::AudioBitsPerSample bits_per_sample) { + switch (bits_per_sample) { + case fbredr::AudioBitsPerSample::BPS_16: + return android_emb::A2dpBitsPerSample::BITS_PER_SAMPLE_16; + case fbredr::AudioBitsPerSample::BPS_24: + return android_emb::A2dpBitsPerSample::BITS_PER_SAMPLE_24; + case fbredr::AudioBitsPerSample::BPS_32: + return android_emb::A2dpBitsPerSample::BITS_PER_SAMPLE_32; + default: + return std::nullopt; + } +} + +std::optional FidlToChannelMode( + fbredr::AudioChannelMode channel_mode) { + switch (channel_mode) { + case fbredr::AudioChannelMode::MONO: + return android_emb::A2dpChannelMode::MONO; + case fbredr::AudioChannelMode::STEREO: + return android_emb::A2dpChannelMode::STEREO; + default: + return std::nullopt; + } +} + +bt::StaticPacket +FidlToEncoderSettingsSbc(const fbredr::AudioEncoderSettings& encoder_settings, + fbredr::AudioSamplingFrequency sampling_frequency, + fbredr::AudioChannelMode channel_mode) { + bt::StaticPacket sbc; + + switch (encoder_settings.sbc().allocation) { + case fuchsia::media::SbcAllocation::ALLOC_LOUDNESS: + sbc.view().allocation_method().Write( + android_emb::SbcAllocationMethod::LOUDNESS); + break; + case fuchsia::media::SbcAllocation::ALLOC_SNR: + sbc.view().allocation_method().Write( + android_emb::SbcAllocationMethod::SNR); + break; + } + + switch (encoder_settings.sbc().sub_bands) { + case fuchsia::media::SbcSubBands::SUB_BANDS_4: + sbc.view().subbands().Write(android_emb::SbcSubBands::SUBBANDS_4); + break; + case fuchsia::media::SbcSubBands::SUB_BANDS_8: + sbc.view().subbands().Write(android_emb::SbcSubBands::SUBBANDS_8); + break; + } + + switch (encoder_settings.sbc().block_count) { + case fuchsia::media::SbcBlockCount::BLOCK_COUNT_4: + sbc.view().block_length().Write(android_emb::SbcBlockLen::BLOCK_LEN_4); + break; + case fuchsia::media::SbcBlockCount::BLOCK_COUNT_8: + sbc.view().block_length().Write(android_emb::SbcBlockLen::BLOCK_LEN_8); + break; + case fuchsia::media::SbcBlockCount::BLOCK_COUNT_12: + sbc.view().block_length().Write(android_emb::SbcBlockLen::BLOCK_LEN_12); + break; + case fuchsia::media::SbcBlockCount::BLOCK_COUNT_16: + sbc.view().block_length().Write(android_emb::SbcBlockLen::BLOCK_LEN_16); + break; + } + + sbc.view().min_bitpool_value().Write(encoder_settings.sbc().bit_pool); + sbc.view().max_bitpool_value().Write(encoder_settings.sbc().bit_pool); + + switch (channel_mode) { + case fbredr::AudioChannelMode::MONO: + sbc.view().channel_mode().Write(android_emb::SbcChannelMode::MONO); + break; + case fbredr::AudioChannelMode::STEREO: + sbc.view().channel_mode().Write(android_emb::SbcChannelMode::STEREO); + break; + } + + switch (sampling_frequency) { + case fbredr::AudioSamplingFrequency::HZ_44100: + sbc.view().sampling_frequency().Write( + android_emb::SbcSamplingFrequency::HZ_44100); + break; + case fbredr::AudioSamplingFrequency::HZ_48000: + sbc.view().sampling_frequency().Write( + android_emb::SbcSamplingFrequency::HZ_48000); + break; + default: + bt_log(WARN, + "fidl", + "%s: sbc encoder cannot use sampling frequency %hhu", + __FUNCTION__, + static_cast(sampling_frequency)); + } + + return sbc; +} + +bt::StaticPacket +FidlToEncoderSettingsAac(const fbredr::AudioEncoderSettings& encoder_settings, + fbredr::AudioSamplingFrequency sampling_frequency, + fbredr::AudioChannelMode channel_mode) { + bt::StaticPacket aac; + aac.view().object_type().Write( + static_cast(encoder_settings.aac().aot)); + + if (encoder_settings.aac().bit_rate.is_variable()) { + aac.view().variable_bit_rate().Write( + android_emb::AacEnableVariableBitRate::ENABLE); + } + + if (encoder_settings.aac().bit_rate.is_constant()) { + aac.view().variable_bit_rate().Write( + android_emb::AacEnableVariableBitRate::DISABLE); + } + + return aac; +} + +std::optional FidlToDataElement( + const fbredr::DataElement& fidl) { + bt::sdp::DataElement out; + switch (fidl.Which()) { + case fbredr::DataElement::Tag::kInt8: + return bt::sdp::DataElement(fidl.int8()); + case fbredr::DataElement::Tag::kInt16: + return bt::sdp::DataElement(fidl.int16()); + case fbredr::DataElement::Tag::kInt32: + return bt::sdp::DataElement(fidl.int32()); + case fbredr::DataElement::Tag::kInt64: + return bt::sdp::DataElement(fidl.int64()); + case fbredr::DataElement::Tag::kUint8: + return bt::sdp::DataElement(fidl.uint8()); + case fbredr::DataElement::Tag::kUint16: + return bt::sdp::DataElement(fidl.uint16()); + case fbredr::DataElement::Tag::kUint32: + return bt::sdp::DataElement(fidl.uint32()); + case fbredr::DataElement::Tag::kUint64: + return bt::sdp::DataElement(fidl.uint64()); + case fbredr::DataElement::Tag::kStr: { + const std::vector& str = fidl.str(); + bt::DynamicByteBuffer bytes((bt::BufferView(str))); + return bt::sdp::DataElement(bytes); + } + case fbredr::DataElement::Tag::kUrl: + out.SetUrl(fidl.url()); + break; + case fbredr::DataElement::Tag::kB: + return bt::sdp::DataElement(fidl.b()); + case fbredr::DataElement::Tag::kUuid: + out.Set(fidl_helpers::UuidFromFidl(fidl.uuid())); + break; + case fbredr::DataElement::Tag::kSequence: { + std::vector seq; + for (const auto& fidl_elem : fidl.sequence()) { + std::optional elem = + FidlToDataElement(*fidl_elem); + if (!elem) { + return std::nullopt; + } + seq.emplace_back(std::move(elem.value())); + } + out.Set(std::move(seq)); + break; + } + case fbredr::DataElement::Tag::kAlternatives: { + std::vector alts; + for (const auto& fidl_elem : fidl.alternatives()) { + auto elem = FidlToDataElement(*fidl_elem); + if (!elem) { + return std::nullopt; + } + alts.emplace_back(std::move(elem.value())); + } + out.SetAlternative(std::move(alts)); + break; + } + default: + // Types not handled: Null datatype (never used) + bt_log(WARN, "fidl", "Encountered FidlToDataElement type not handled."); + return std::nullopt; + } + return out; +} + +std::optional NewFidlToDataElement( + const fuchsia_bluetooth_bredr::DataElement& fidl) { + bt::sdp::DataElement out; + switch (fidl.Which()) { + case fuchsia_bluetooth_bredr::DataElement::Tag::kInt8: + return bt::sdp::DataElement(fidl.int8().value()); + case fuchsia_bluetooth_bredr::DataElement::Tag::kInt16: + return bt::sdp::DataElement(fidl.int16().value()); + case fuchsia_bluetooth_bredr::DataElement::Tag::kInt32: + return bt::sdp::DataElement(fidl.int32().value()); + case fuchsia_bluetooth_bredr::DataElement::Tag::kInt64: + return bt::sdp::DataElement(fidl.int64().value()); + case fuchsia_bluetooth_bredr::DataElement::Tag::kUint8: + return bt::sdp::DataElement(fidl.uint8().value()); + case fuchsia_bluetooth_bredr::DataElement::Tag::kUint16: + return bt::sdp::DataElement(fidl.uint16().value()); + case fuchsia_bluetooth_bredr::DataElement::Tag::kUint32: + return bt::sdp::DataElement(fidl.uint32().value()); + case fuchsia_bluetooth_bredr::DataElement::Tag::kUint64: + return bt::sdp::DataElement(fidl.uint64().value()); + case fuchsia_bluetooth_bredr::DataElement::Tag::kStr: { + bt::DynamicByteBuffer bytes((bt::BufferView(fidl.str().value()))); + return bt::sdp::DataElement(bytes); + } + case fuchsia_bluetooth_bredr::DataElement::Tag::kUrl: { + out.SetUrl(fidl.url().value()); + break; + } + case fuchsia_bluetooth_bredr::DataElement::Tag::kB: + return bt::sdp::DataElement(fidl.b().value()); + case fuchsia_bluetooth_bredr::DataElement::Tag::kUuid: + out.Set(NewUuidFromFidl(fidl.uuid()->value())); + break; + case fuchsia_bluetooth_bredr::DataElement::Tag::kSequence: { + std::vector seq; + for (const auto& fidl_elem : fidl.sequence().value()) { + std::optional elem = + NewFidlToDataElement(*fidl_elem); + if (!elem) { + return std::nullopt; + } + seq.emplace_back(std::move(elem.value())); + } + out.Set(std::move(seq)); + break; + } + case fuchsia_bluetooth_bredr::DataElement::Tag::kAlternatives: { + std::vector alts; + for (const auto& fidl_elem : fidl.alternatives().value()) { + auto elem = NewFidlToDataElement(*fidl_elem); + if (!elem) { + return std::nullopt; + } + alts.emplace_back(std::move(elem.value())); + } + out.SetAlternative(std::move(alts)); + break; + } + default: + // Types not handled: Null datatype (never used) + bt_log( + WARN, "fidl", "Encountered NewFidlToDataElement type not handled."); + return std::nullopt; + } + return out; +} + +std::optional DataElementToFidl( + const bt::sdp::DataElement& data_element) { + fbredr::DataElement out; + switch (data_element.type()) { + case bt::sdp::DataElement::Type::kNull: + return std::nullopt; + case bt::sdp::DataElement::Type::kUnsignedInt: + switch (data_element.size()) { + case bt::sdp::DataElement::Size::kOneByte: + out.set_uint8(*data_element.Get()); + break; + case bt::sdp::DataElement::Size::kTwoBytes: + out.set_uint16(*data_element.Get()); + break; + case bt::sdp::DataElement::Size::kFourBytes: + out.set_uint32(*data_element.Get()); + break; + case bt::sdp::DataElement::Size::kEightBytes: + out.set_uint64(*data_element.Get()); + break; + case bt::sdp::DataElement::Size::kSixteenBytes: + case bt::sdp::DataElement::Size::kNextOne: + case bt::sdp::DataElement::Size::kNextTwo: + case bt::sdp::DataElement::Size::kNextFour: + bt_log( + WARN, "fidl", "Encountered DataElementToFidl type not handled."); + return std::nullopt; + } + break; + case bt::sdp::DataElement::Type::kSignedInt: + switch (data_element.size()) { + case bt::sdp::DataElement::Size::kOneByte: + out.set_int8(*data_element.Get()); + break; + case bt::sdp::DataElement::Size::kTwoBytes: + out.set_int16(*data_element.Get()); + break; + case bt::sdp::DataElement::Size::kFourBytes: + out.set_int32(*data_element.Get()); + break; + case bt::sdp::DataElement::Size::kEightBytes: + out.set_int64(*data_element.Get()); + break; + case bt::sdp::DataElement::Size::kSixteenBytes: + case bt::sdp::DataElement::Size::kNextOne: + case bt::sdp::DataElement::Size::kNextTwo: + case bt::sdp::DataElement::Size::kNextFour: + bt_log( + WARN, "fidl", "Encountered DataElementToFidl type not handled."); + return std::nullopt; + } + break; + case bt::sdp::DataElement::Type::kUuid: + out.set_uuid(UuidToFidl(*data_element.Get())); + break; + case bt::sdp::DataElement::Type::kString: + out.set_str(data_element.Get()->ToVector()); + break; + case bt::sdp::DataElement::Type::kBoolean: + out.set_b(*data_element.Get()); + break; + case bt::sdp::DataElement::Type::kSequence: { + std::vector> seq; + auto data_element_sequence = + data_element.Get>(); + for (const auto& elem : data_element_sequence.value()) { + std::optional fidl_elem = DataElementToFidl(elem); + if (!fidl_elem) { + bt_log(WARN, + "fidl", + "Encountered DataElementToFidl sequence type not handled."); + return std::nullopt; + } + seq.emplace_back(std::make_unique( + std::move(fidl_elem.value()))); + } + out.set_sequence(std::move(seq)); + break; + } + case bt::sdp::DataElement::Type::kAlternative: { + std::vector> alt; + auto data_element_alt = + data_element.Get>(); + for (const auto& elem : data_element_alt.value()) { + std::optional fidl_elem = DataElementToFidl(elem); + if (!fidl_elem) { + bt_log(WARN, + "fidl", + "Encountered DataElementToFidl alternate type not handled."); + return std::nullopt; + } + alt.emplace_back(std::make_unique( + std::move(fidl_elem.value()))); + } + out.set_alternatives(std::move(alt)); + break; + } + case bt::sdp::DataElement::Type::kUrl: + out.set_url(*data_element.GetUrl()); + break; + } + return out; +} + +namespace { + +fbt::AddressType AddressTypeToFidl(bt::DeviceAddress::Type type) { + switch (type) { + case bt::DeviceAddress::Type::kBREDR: + [[fallthrough]]; + case bt::DeviceAddress::Type::kLEPublic: + return fbt::AddressType::PUBLIC; + case bt::DeviceAddress::Type::kLERandom: + [[fallthrough]]; + case bt::DeviceAddress::Type::kLEAnonymous: + return fbt::AddressType::RANDOM; + } + return fbt::AddressType::PUBLIC; +} + +fbt::Address AddressToFidl(fbt::AddressType type, + const bt::DeviceAddressBytes& value) { + fbt::Address output; + output.type = type; + bt::MutableBufferView value_dst(output.bytes.data(), output.bytes.size()); + value_dst.Write(value.bytes()); + return output; +} + +fbt::Address AddressToFidl(const bt::DeviceAddress& input) { + return AddressToFidl(AddressTypeToFidl(input.type()), input.value()); +} + +bt::sm::SecurityProperties SecurityPropsFromFidl( + const fsys::SecurityProperties& sec_prop) { + auto level = bt::sm::SecurityLevel::kEncrypted; + if (sec_prop.authenticated) { + level = bt::sm::SecurityLevel::kAuthenticated; + } + return bt::sm::SecurityProperties( + level, sec_prop.encryption_key_size, sec_prop.secure_connections); +} + +fsys::SecurityProperties SecurityPropsToFidl( + const bt::sm::SecurityProperties& sec_prop) { + return fsys::SecurityProperties{ + .authenticated = sec_prop.authenticated(), + .secure_connections = sec_prop.secure_connections(), + + // TODO(armansito): Declare the key size as uint8_t in bt::sm? + .encryption_key_size = static_cast(sec_prop.enc_key_size()), + }; +} + +bt::sm::LTK LtkFromFidl(const fsys::Ltk& ltk) { + return bt::sm::LTK( + SecurityPropsFromFidl(ltk.key.security), + bt::hci_spec::LinkKey(ltk.key.data.value, ltk.rand, ltk.ediv)); +} + +fsys::PeerKey LtkToFidlPeerKey(const bt::sm::LTK& ltk) { + return fsys::PeerKey{ + .security = SecurityPropsToFidl(ltk.security()), + .data = fsys::Key{ltk.key().value()}, + }; +} + +fsys::Ltk LtkToFidl(const bt::sm::LTK& ltk) { + return fsys::Ltk{ + .key = LtkToFidlPeerKey(ltk), + .ediv = ltk.key().ediv(), + .rand = ltk.key().rand(), + }; +} + +bt::sm::Key PeerKeyFromFidl(const fsys::PeerKey& key) { + return bt::sm::Key(SecurityPropsFromFidl(key.security), key.data.value); +} + +fsys::PeerKey PeerKeyToFidl(const bt::sm::Key& key) { + return fsys::PeerKey{ + .security = SecurityPropsToFidl(key.security()), + .data = {key.value()}, + }; +} + +fbt::DeviceClass DeviceClassToFidl(bt::DeviceClass input) { + auto bytes = input.bytes(); + fbt::DeviceClass output{static_cast( + bytes[0] | (bytes[1] << BIT_SHIFT_8) | (bytes[2] << BIT_SHIFT_16))}; + return output; +} + +std::optional +UuidToServiceClassIdentifier(const bt::UUID uuid) { + std::optional uuid_16 = uuid.As16Bit(); + if (!uuid_16) { + return std::nullopt; + } + return fbredr::ServiceClassProfileIdentifier(*uuid_16); +} + +std::optional UuidToProtocolIdentifier( + const bt::UUID uuid) { + std::optional uuid_16 = uuid.As16Bit(); + if (!uuid_16) { + return std::nullopt; + } + return fbredr::ProtocolIdentifier(*uuid_16); +} + +fbredr::Information InformationToFidl( + const bt::sdp::ServiceRecord::Information& info) { + fbredr::Information out; + out.set_language(info.language_code); + if (info.name) { + out.set_name(info.name.value()); + } + if (info.description) { + out.set_description(info.description.value()); + } + if (info.provider) { + out.set_provider(info.provider.value()); + } + return out; +} + +fpromise::result, + fuchsia::bluetooth::ErrorCode> +DataElementToServiceUuids(const bt::sdp::DataElement& uuids_element) { + std::vector out; + + const auto service_uuids_list = + uuids_element.Get>(); + if (!service_uuids_list) { + return fpromise::error(fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + } + + for (const auto& uuid_element : service_uuids_list.value()) { + if (uuid_element.type() != bt::sdp::DataElement::Type::kUuid) { + return fpromise::error(fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + } + out.push_back(UuidToFidl(*uuid_element.Get())); + } + + return fpromise::ok(std::move(out)); +} + +fpromise::result, + fuchsia::bluetooth::ErrorCode> +DataElementToProtocolDescriptorList( + const bt::sdp::DataElement& protocols_element) { + std::vector out; + + const auto protocol_list = + protocols_element.Get>(); + if (!protocol_list) { + return fpromise::error(fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + } + + for (const auto& protocol_elt : protocol_list.value()) { + if (protocol_elt.type() != bt::sdp::DataElement::Type::kSequence) { + return fpromise::error(fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + } + + const auto protocol = + protocol_elt.Get>().value(); + if (protocol.size() < 1 || + protocol.at(0).type() != bt::sdp::DataElement::Type::kUuid) { + return fpromise::error(fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + } + + fbredr::ProtocolDescriptor desc; + std::vector params; + for (size_t i = 0; i < protocol.size(); i++) { + if (i == 0) { + std::optional protocol_id = + UuidToProtocolIdentifier((protocol.at(i).Get()).value()); + if (!protocol_id) { + return fpromise::error( + fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + } + desc.set_protocol(protocol_id.value()); + } else { + std::optional param = + DataElementToFidl(protocol.at(i)); + if (!param) { + return fpromise::error( + fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + } + params.emplace_back(std::move(*param)); + } + } + desc.set_params(std::move(params)); + out.emplace_back(std::move(desc)); + } + + return fpromise::ok(std::move(out)); +} + +// Returns the major and minor versions from a combined |version|. +std::pair VersionToMajorMinor(uint16_t version) { + const uint16_t kMajorBitmask = 0xFF00; + const uint16_t kMinorBitmask = 0x00FF; + uint8_t major = static_cast((version & kMajorBitmask) >> + std::numeric_limits::digits); + uint8_t minor = static_cast(version & kMinorBitmask); + return std::make_pair(major, minor); +} + +fpromise::result, + fuchsia::bluetooth::ErrorCode> +DataElementToProfileDescriptors(const bt::sdp::DataElement& profile_element) { + std::vector out; + + const auto profile_desc_list = + profile_element.Get>(); + if (!profile_desc_list) { + return fpromise::error(fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + } + + // [[UUID, Version]] + for (const auto& profile_desc_element : (*profile_desc_list)) { + if (profile_desc_element.type() != bt::sdp::DataElement::Type::kSequence) { + return fpromise::error(fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + } + + // Each profile descriptor entry contains a UUID and uint16_t version. + const auto profile_desc = + *profile_desc_element.Get>(); + if (profile_desc.size() != 2) { + return fpromise::error(fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + } + + std::optional profile_id = profile_desc.at(0).Get(); + std::optional version = profile_desc.at(1).Get(); + if (!profile_id || !version) { + return fpromise::error(fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + } + + fbredr::ProfileDescriptor desc; + std::optional service_class_id = + UuidToServiceClassIdentifier(*profile_id); + if (!service_class_id) { + return fpromise::error(fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + } + desc.set_profile_id(service_class_id.value()); + auto [major, minor] = VersionToMajorMinor(version.value()); + desc.set_major_version(major); + desc.set_minor_version(minor); + out.push_back(std::move(desc)); + } + + return fpromise::ok(std::move(out)); +} + +bool NewAddProtocolDescriptorList( + bt::sdp::ServiceRecord* rec, + bt::sdp::ServiceRecord::ProtocolListId id, + const std::vector& + descriptor_list) { + bt_log(TRACE, "fidl", "ProtocolDescriptorList %d", id); + for (auto& descriptor : descriptor_list) { + if (!descriptor.params().has_value() || + !descriptor.protocol().has_value()) { + return false; + } + bt::sdp::DataElement protocol_params; + if (descriptor.params()->size() > 1) { + std::vector params; + for (auto& fidl_param : *descriptor.params()) { + auto bt_param = NewFidlToDataElement(fidl_param); + if (bt_param) { + params.emplace_back(std::move(bt_param.value())); + } else { + return false; + } + } + protocol_params.Set(std::move(params)); + } else if (descriptor.params()->size() == 1) { + auto param = NewFidlToDataElement(descriptor.params()->front()); + if (param) { + protocol_params = std::move(param).value(); + } else { + return false; + } + protocol_params = + NewFidlToDataElement(descriptor.params()->front()).value(); + } + + bt_log(TRACE, + "fidl", + "Adding protocol descriptor: {%d : %s}", + fidl::ToUnderlying(*descriptor.protocol()), + protocol_params.ToString().c_str()); + rec->AddProtocolDescriptor( + id, + bt::UUID(static_cast(*descriptor.protocol())), + std::move(protocol_params)); + } + return true; +} + +bool AddProtocolDescriptorList( + bt::sdp::ServiceRecord* rec, + bt::sdp::ServiceRecord::ProtocolListId id, + const ::std::vector& descriptor_list) { + bt_log(TRACE, "fidl", "ProtocolDescriptorList %d", id); + for (auto& descriptor : descriptor_list) { + bt::sdp::DataElement protocol_params; + if (!descriptor.has_params() || !descriptor.has_protocol()) { + bt_log(WARN, "fidl", "ProtocolDescriptor missing params/protocol field"); + return false; + } + if (descriptor.params().size() > 1) { + std::vector params; + for (auto& fidl_param : descriptor.params()) { + auto bt_param = FidlToDataElement(fidl_param); + if (bt_param) { + params.emplace_back(std::move(bt_param.value())); + } else { + return false; + } + } + protocol_params.Set(std::move(params)); + } else if (descriptor.params().size() == 1) { + auto param = FidlToDataElement(descriptor.params().front()); + if (param) { + protocol_params = std::move(param).value(); + } else { + return false; + } + protocol_params = FidlToDataElement(descriptor.params().front()).value(); + } + + bt_log(TRACE, + "fidl", + "Adding protocol descriptor: {%d : %s}", + uint16_t(descriptor.protocol()), + bt_str(protocol_params)); + rec->AddProtocolDescriptor( + id, + bt::UUID(static_cast(descriptor.protocol())), + std::move(protocol_params)); + } + return true; +} + +// Returns true if the appearance value (in host byte order) is included in +// fuchsia.bluetooth.Appearance, which is a subset of Bluetooth Assigned +// Numbers, "Appearance Values" +// (https://www.bluetooth.com/specifications/assigned-numbers/). +// +// TODO(fxbug.dev/42145156): Remove this compatibility check with the strict +// Appearance enum. +[[nodiscard]] bool IsAppearanceValid(uint16_t appearance_raw) { + switch (appearance_raw) { + case 0u: // UNKNOWN + [[fallthrough]]; + case 64u: // PHONE + [[fallthrough]]; + case 128u: // COMPUTER + [[fallthrough]]; + case 192u: // WATCH + [[fallthrough]]; + case 193u: // WATCH_SPORTS + [[fallthrough]]; + case 256u: // CLOCK + [[fallthrough]]; + case 320u: // DISPLAY + [[fallthrough]]; + case 384u: // REMOTE_CONTROL + [[fallthrough]]; + case 448u: // EYE_GLASSES + [[fallthrough]]; + case 512u: // TAG + [[fallthrough]]; + case 576u: // KEYRING + [[fallthrough]]; + case 640u: // MEDIA_PLAYER + [[fallthrough]]; + case 704u: // BARCODE_SCANNER + [[fallthrough]]; + case 768u: // THERMOMETER + [[fallthrough]]; + case 769u: // THERMOMETER_EAR + [[fallthrough]]; + case 832u: // HEART_RATE_SENSOR + [[fallthrough]]; + case 833u: // HEART_RATE_SENSOR_BELT + [[fallthrough]]; + case 896u: // BLOOD_PRESSURE + [[fallthrough]]; + case 897u: // BLOOD_PRESSURE_ARM + [[fallthrough]]; + case 898u: // BLOOD_PRESSURE_WRIST + [[fallthrough]]; + case 960u: // HID + [[fallthrough]]; + case 961u: // HID_KEYBOARD + [[fallthrough]]; + case 962u: // HID_MOUSE + [[fallthrough]]; + case 963u: // HID_JOYSTICK + [[fallthrough]]; + case 964u: // HID_GAMEPAD + [[fallthrough]]; + case 965u: // HID_DIGITIZER_TABLET + [[fallthrough]]; + case 966u: // HID_CARD_READER + [[fallthrough]]; + case 967u: // HID_DIGITAL_PEN + [[fallthrough]]; + case 968u: // HID_BARCODE_SCANNER + [[fallthrough]]; + case 1024u: // GLUCOSE_METER + [[fallthrough]]; + case 1088u: // RUNNING_WALKING_SENSOR + [[fallthrough]]; + case 1089u: // RUNNING_WALKING_SENSOR_IN_SHOE + [[fallthrough]]; + case 1090u: // RUNNING_WALKING_SENSOR_ON_SHOE + [[fallthrough]]; + case 1091u: // RUNNING_WALKING_SENSOR_ON_HIP + [[fallthrough]]; + case 1152u: // CYCLING + [[fallthrough]]; + case 1153u: // CYCLING_COMPUTER + [[fallthrough]]; + case 1154u: // CYCLING_SPEED_SENSOR + [[fallthrough]]; + case 1155u: // CYCLING_CADENCE_SENSOR + [[fallthrough]]; + case 1156u: // CYCLING_POWER_SENSOR + [[fallthrough]]; + case 1157u: // CYCLING_SPEED_AND_CADENCE_SENSOR + [[fallthrough]]; + case 3136u: // PULSE_OXIMETER + [[fallthrough]]; + case 3137u: // PULSE_OXIMETER_FINGERTIP + [[fallthrough]]; + case 3138u: // PULSE_OXIMETER_WRIST + [[fallthrough]]; + case 3200u: // WEIGHT_SCALE + [[fallthrough]]; + case 3264u: // PERSONAL_MOBILITY + [[fallthrough]]; + case 3265u: // PERSONAL_MOBILITY_WHEELCHAIR + [[fallthrough]]; + case 3266u: // PERSONAL_MOBILITY_SCOOTER + [[fallthrough]]; + case 3328u: // GLUCOSE_MONITOR + [[fallthrough]]; + case 5184u: // SPORTS_ACTIVITY + [[fallthrough]]; + case 5185u: // SPORTS_ACTIVITY_LOCATION_DISPLAY + [[fallthrough]]; + case 5186u: // SPORTS_ACTIVITY_LOCATION_AND_NAV_DISPLAY + [[fallthrough]]; + case 5187u: // SPORTS_ACTIVITY_LOCATION_POD + [[fallthrough]]; + case 5188u: // SPORTS_ACTIVITY_LOCATION_AND_NAV_POD + return true; + default: + return false; + } +} + +[[nodiscard]] std::optional AppearanceToFidl( + uint16_t appearance_raw) { + if (IsAppearanceValid(appearance_raw)) { + return static_cast(appearance_raw); + } + return std::nullopt; +} + +} // namespace + +std::optional PeerIdFromString(const std::string& id) { + if (id.empty()) { + return std::nullopt; + } + + uint64_t value = 0; + auto [_, error] = + std::from_chars(id.data(), id.data() + id.size(), value, /*base=*/16); + if (error != std::errc()) { + return std::nullopt; + } + return bt::PeerId(value); +} + +ErrorCode HostErrorToFidlDeprecated(bt::HostError host_error) { + switch (host_error) { + case bt::HostError::kFailed: + return ErrorCode::FAILED; + case bt::HostError::kTimedOut: + return ErrorCode::TIMED_OUT; + case bt::HostError::kInvalidParameters: + return ErrorCode::INVALID_ARGUMENTS; + case bt::HostError::kCanceled: + return ErrorCode::CANCELED; + case bt::HostError::kInProgress: + return ErrorCode::IN_PROGRESS; + case bt::HostError::kNotSupported: + return ErrorCode::NOT_SUPPORTED; + case bt::HostError::kNotFound: + return ErrorCode::NOT_FOUND; + default: + break; + } + + return ErrorCode::FAILED; +} + +Status NewFidlError(ErrorCode error_code, const std::string& description) { + Status status; + status.error = std::make_unique(); + status.error->error_code = error_code; + status.error->description = description; + return status; +} + +fsys::Error HostErrorToFidl(bt::HostError error) { + switch (error) { + case bt::HostError::kFailed: + return fsys::Error::FAILED; + case bt::HostError::kTimedOut: + return fsys::Error::TIMED_OUT; + case bt::HostError::kInvalidParameters: + return fsys::Error::INVALID_ARGUMENTS; + case bt::HostError::kCanceled: + return fsys::Error::CANCELED; + case bt::HostError::kInProgress: + return fsys::Error::IN_PROGRESS; + case bt::HostError::kNotSupported: + return fsys::Error::NOT_SUPPORTED; + case bt::HostError::kNotFound: + return fsys::Error::PEER_NOT_FOUND; + default: + break; + } + return fsys::Error::FAILED; +} + +fuchsia::bluetooth::gatt::Error GattErrorToFidl(const bt::att::Error& error) { + return error.Visit( + [](bt::HostError host_error) { + return host_error == bt::HostError::kPacketMalformed + ? fuchsia::bluetooth::gatt::Error::INVALID_RESPONSE + : fuchsia::bluetooth::gatt::Error::FAILURE; + }, + [](bt::att::ErrorCode att_error) { + switch (att_error) { + case bt::att::ErrorCode::kInsufficientAuthorization: + return fuchsia::bluetooth::gatt::Error::INSUFFICIENT_AUTHORIZATION; + case bt::att::ErrorCode::kInsufficientAuthentication: + return fuchsia::bluetooth::gatt::Error::INSUFFICIENT_AUTHENTICATION; + case bt::att::ErrorCode::kInsufficientEncryptionKeySize: + return fuchsia::bluetooth::gatt::Error:: + INSUFFICIENT_ENCRYPTION_KEY_SIZE; + case bt::att::ErrorCode::kInsufficientEncryption: + return fuchsia::bluetooth::gatt::Error::INSUFFICIENT_ENCRYPTION; + case bt::att::ErrorCode::kReadNotPermitted: + return fuchsia::bluetooth::gatt::Error::READ_NOT_PERMITTED; + default: + break; + } + return fuchsia::bluetooth::gatt::Error::FAILURE; + }); +} + +fuchsia::bluetooth::gatt2::Error AttErrorToGattFidlError( + const bt::att::Error& error) { + return error.Visit( + [](bt::HostError host_error) { + switch (host_error) { + case bt::HostError::kPacketMalformed: + return fuchsia::bluetooth::gatt2::Error::INVALID_PDU; + case bt::HostError::kInvalidParameters: + return fuchsia::bluetooth::gatt2::Error::INVALID_PARAMETERS; + default: + break; + } + return fuchsia::bluetooth::gatt2::Error::UNLIKELY_ERROR; + }, + [](bt::att::ErrorCode att_error) { + switch (att_error) { + case bt::att::ErrorCode::kInsufficientAuthorization: + return fuchsia::bluetooth::gatt2::Error::INSUFFICIENT_AUTHORIZATION; + case bt::att::ErrorCode::kInsufficientAuthentication: + return fuchsia::bluetooth::gatt2::Error:: + INSUFFICIENT_AUTHENTICATION; + case bt::att::ErrorCode::kInsufficientEncryptionKeySize: + return fuchsia::bluetooth::gatt2::Error:: + INSUFFICIENT_ENCRYPTION_KEY_SIZE; + case bt::att::ErrorCode::kInsufficientEncryption: + return fuchsia::bluetooth::gatt2::Error::INSUFFICIENT_ENCRYPTION; + case bt::att::ErrorCode::kReadNotPermitted: + return fuchsia::bluetooth::gatt2::Error::READ_NOT_PERMITTED; + case bt::att::ErrorCode::kInvalidHandle: + return fuchsia::bluetooth::gatt2::Error::INVALID_HANDLE; + default: + break; + } + return fuchsia::bluetooth::gatt2::Error::UNLIKELY_ERROR; + }); +} + +bt::UUID UuidFromFidl(const fuchsia::bluetooth::Uuid& input) { + // Conversion must always succeed given the defined size of |input|. + static_assert(sizeof(input.value) == 16, "FIDL UUID definition malformed!"); + return bt::UUID(bt::BufferView(input.value.data(), input.value.size())); +} + +fuchsia::bluetooth::Uuid UuidToFidl(const bt::UUID& uuid) { + fuchsia::bluetooth::Uuid output; + // Conversion must always succeed given the defined size of |input|. + static_assert(sizeof(output.value) == 16, "FIDL UUID definition malformed!"); + output.value = uuid.value(); + return output; +} + +bt::UUID NewUuidFromFidl(const fuchsia_bluetooth::Uuid& input) { + // Conversion must always succeed given the defined size of |input|. + static_assert(sizeof(input.value()) == 16, "FIDL UUID definition malformed!"); + return bt::UUID(bt::BufferView(input.value().data(), input.value().size())); +} + +bt::sm::IOCapability IoCapabilityFromFidl(fsys::InputCapability input, + fsys::OutputCapability output) { + if (input == fsys::InputCapability::NONE && + output == fsys::OutputCapability::NONE) { + return bt::sm::IOCapability::kNoInputNoOutput; + } + + if (input == fsys::InputCapability::KEYBOARD && + output == fsys::OutputCapability::DISPLAY) { + return bt::sm::IOCapability::kKeyboardDisplay; + } + + if (input == fsys::InputCapability::KEYBOARD && + output == fsys::OutputCapability::NONE) { + return bt::sm::IOCapability::kKeyboardOnly; + } + + if (input == fsys::InputCapability::NONE && + output == fsys::OutputCapability::DISPLAY) { + return bt::sm::IOCapability::kDisplayOnly; + } + + if (input == fsys::InputCapability::CONFIRMATION && + output == fsys::OutputCapability::DISPLAY) { + return bt::sm::IOCapability::kDisplayYesNo; + } + + return bt::sm::IOCapability::kNoInputNoOutput; +} + +std::optional BrEdrSecurityModeFromFidl( + const fsys::BrEdrSecurityMode mode) { + switch (mode) { + case fsys::BrEdrSecurityMode::MODE_4: + return bt::gap::BrEdrSecurityMode::Mode4; + case fsys::BrEdrSecurityMode::SECURE_CONNECTIONS_ONLY: + return bt::gap::BrEdrSecurityMode::SecureConnectionsOnly; + default: + bt_log(WARN, "fidl", "BR/EDR security mode not recognized"); + return std::nullopt; + } +} + +bt::gap::LESecurityMode LeSecurityModeFromFidl( + const fsys::LeSecurityMode mode) { + switch (mode) { + case fsys::LeSecurityMode::MODE_1: + return bt::gap::LESecurityMode::Mode1; + case fsys::LeSecurityMode::SECURE_CONNECTIONS_ONLY: + return bt::gap::LESecurityMode::SecureConnectionsOnly; + } + bt_log( + WARN, + "fidl", + "FIDL security mode not recognized, defaulting to SecureConnectionsOnly"); + return bt::gap::LESecurityMode::SecureConnectionsOnly; +} + +std::optional SecurityLevelFromFidl( + const fsys::PairingSecurityLevel level) { + switch (level) { + case fsys::PairingSecurityLevel::ENCRYPTED: + return bt::sm::SecurityLevel::kEncrypted; + case fsys::PairingSecurityLevel::AUTHENTICATED: + return bt::sm::SecurityLevel::kAuthenticated; + default: + return std::nullopt; + }; +} + +fsys::TechnologyType TechnologyTypeToFidl(bt::gap::TechnologyType type) { + switch (type) { + case bt::gap::TechnologyType::kLowEnergy: + return fsys::TechnologyType::LOW_ENERGY; + case bt::gap::TechnologyType::kClassic: + return fsys::TechnologyType::CLASSIC; + case bt::gap::TechnologyType::kDualMode: + return fsys::TechnologyType::DUAL_MODE; + default: + BT_PANIC("invalid technology type: %u", static_cast(type)); + break; + } + + // This should never execute. + return fsys::TechnologyType::DUAL_MODE; +} + +fsys::HostInfo HostInfoToFidl(const bt::gap::Adapter& adapter) { + fsys::HostInfo info; + info.set_id(fbt::HostId{adapter.identifier().value()}); + info.set_technology(TechnologyTypeToFidl(adapter.state().type())); + info.set_local_name(adapter.local_name()); + info.set_discoverable(adapter.IsDiscoverable()); + info.set_discovering(adapter.IsDiscovering()); + std::vector addresses; + addresses.emplace_back(AddressToFidl(fbt::AddressType::PUBLIC, + adapter.state().controller_address)); + if (adapter.le() && adapter.le()->PrivacyEnabled() && + (!adapter.le()->CurrentAddress().IsPublic())) { + addresses.emplace_back(AddressToFidl(adapter.le()->CurrentAddress())); + } + info.set_addresses(std::move(addresses)); + return info; +} + +fsys::Peer PeerToFidl(const bt::gap::Peer& peer) { + fsys::Peer output; + output.set_id(fbt::PeerId{peer.identifier().value()}); + output.set_address(AddressToFidl(peer.address())); + output.set_technology(TechnologyTypeToFidl(peer.technology())); + output.set_connected(peer.connected()); + output.set_bonded(peer.bonded()); + + if (peer.name()) { + output.set_name(*peer.name()); + } + + if (peer.le()) { + const std::optional> + adv_data = peer.le()->parsed_advertising_data(); + if (adv_data.has_value()) { + if (adv_data->get().appearance().has_value()) { + if (auto appearance = + AppearanceToFidl(adv_data->get().appearance().value())) { + output.set_appearance(appearance.value()); + } else { + bt_log(DEBUG, + "fidl", + "omitting unencodeable appearance %#.4x of peer %s", + adv_data->get().appearance().value(), + bt_str(peer.identifier())); + } + } + if (adv_data->get().tx_power()) { + output.set_tx_power(adv_data->get().tx_power().value()); + } + } + } + if (peer.bredr() && peer.bredr()->device_class()) { + output.set_device_class(DeviceClassToFidl(*peer.bredr()->device_class())); + } + if (peer.rssi() != bt::hci_spec::kRSSIInvalid) { + output.set_rssi(peer.rssi()); + } + + if (peer.bredr()) { + std::transform(peer.bredr()->services().begin(), + peer.bredr()->services().end(), + std::back_inserter(*output.mutable_bredr_services()), + UuidToFidl); + } + + // TODO(fxbug.dev/42135180): Populate le_service UUIDs based on GATT results + // as well as advertising and inquiry data. + + return output; +} + +std::optional AddressFromFidlBondingData( + const fuchsia::bluetooth::sys::BondingData& bond) { + bt::DeviceAddressBytes bytes(bond.address().bytes); + bt::DeviceAddress::Type type; + if (bond.has_bredr_bond()) { + // A random identity address can only be present in a LE-only bond. + if (bond.address().type == fbt::AddressType::RANDOM) { + bt_log(WARN, + "fidl", + "BR/EDR or Dual-Mode bond cannot have a random identity address!"); + return std::nullopt; + } + // TODO(fxbug.dev/42102158): We currently assign kBREDR as the address type + // for dual-mode bonds. This makes address management for dual-mode devices + // a bit confusing as we have two "public" address types (i.e. kBREDR and + // kLEPublic). We should align the stack address types with the FIDL address + // types, such that both kBREDR and kLEPublic are represented as the same + // kind of "PUBLIC". + type = bt::DeviceAddress::Type::kBREDR; + } else { + type = bond.address().type == fbt::AddressType::RANDOM + ? bt::DeviceAddress::Type::kLERandom + : bt::DeviceAddress::Type::kLEPublic; + } + + bt::DeviceAddress address(type, bytes); + + if (!address.IsPublic() && !address.IsStaticRandom()) { + bt_log( + ERROR, + "fidl", + "%s: BondingData address is not public or static random (address: %s)", + __FUNCTION__, + bt_str(address)); + return std::nullopt; + } + + return address; +} + +bt::sm::PairingData LePairingDataFromFidl( + bt::DeviceAddress peer_address, + const fuchsia::bluetooth::sys::LeBondData& data) { + bt::sm::PairingData result; + + if (data.has_peer_ltk()) { + result.peer_ltk = LtkFromFidl(data.peer_ltk()); + } + if (data.has_local_ltk()) { + result.local_ltk = LtkFromFidl(data.local_ltk()); + } + if (data.has_irk()) { + result.irk = PeerKeyFromFidl(data.irk()); + // If there is an IRK, there must also be an identity address. Assume that + // the identity address is the peer address, since the peer address is set + // to the identity address upon bonding. + result.identity_address = peer_address; + } + if (data.has_csrk()) { + result.csrk = PeerKeyFromFidl(data.csrk()); + } + return result; +} + +std::optional BredrKeyFromFidl(const fsys::BredrBondData& data) { + if (!data.has_link_key()) { + return std::nullopt; + } + auto key = PeerKeyFromFidl(data.link_key()); + return bt::sm::LTK(key.security(), bt::hci_spec::LinkKey(key.value(), 0, 0)); +} + +std::vector BredrServicesFromFidl( + const fuchsia::bluetooth::sys::BredrBondData& data) { + std::vector services_out; + if (data.has_services()) { + std::transform(data.services().begin(), + data.services().end(), + std::back_inserter(services_out), + UuidFromFidl); + } + return services_out; +} + +fuchsia::bluetooth::sys::BondingData PeerToFidlBondingData( + const bt::gap::Adapter& adapter, const bt::gap::Peer& peer) { + fsys::BondingData out; + + out.set_identifier(fbt::PeerId{peer.identifier().value()}); + out.set_local_address(AddressToFidl(fbt::AddressType::PUBLIC, + adapter.state().controller_address)); + out.set_address(AddressToFidl(peer.address())); + + if (peer.name()) { + out.set_name(*peer.name()); + } + + // LE + if (peer.le() && peer.le()->bond_data()) { + fsys::LeBondData out_le; + const bt::sm::PairingData& bond = *peer.le()->bond_data(); + + // TODO(armansito): Store the peer's preferred connection parameters. + // TODO(fxbug.dev/42137736): Store GATT and AD service UUIDs. + + if (bond.local_ltk) { + out_le.set_local_ltk(LtkToFidl(*bond.local_ltk)); + } + if (bond.peer_ltk) { + out_le.set_peer_ltk(LtkToFidl(*bond.peer_ltk)); + } + if (bond.irk) { + out_le.set_irk(PeerKeyToFidl(*bond.irk)); + } + if (bond.csrk) { + out_le.set_csrk(PeerKeyToFidl(*bond.csrk)); + } + + out.set_le_bond(std::move(out_le)); + } + + // BR/EDR + if (peer.bredr() && peer.bredr()->link_key()) { + fsys::BredrBondData out_bredr; + + // TODO(fxbug.dev/42076955): Populate with history of role switches. + + const auto& services = peer.bredr()->services(); + std::transform(services.begin(), + services.end(), + std::back_inserter(*out_bredr.mutable_services()), + UuidToFidl); + out_bredr.set_link_key(LtkToFidlPeerKey(*peer.bredr()->link_key())); + out.set_bredr_bond(std::move(out_bredr)); + } + + return out; +} + +fble::RemoteDevicePtr NewLERemoteDevice(const bt::gap::Peer& peer) { + bt::AdvertisingData ad; + if (!peer.le()) { + return nullptr; + } + + auto fidl_device = std::make_unique(); + fidl_device->identifier = peer.identifier().ToString(); + fidl_device->connectable = peer.connectable(); + + // Initialize advertising data only if its non-empty. + const std::optional> + adv_data = peer.le()->parsed_advertising_data(); + if (adv_data.has_value()) { + auto data = fidl_helpers::AdvertisingDataToFidlDeprecated(adv_data.value()); + fidl_device->advertising_data = + std::make_unique(std::move(data)); + } else if (peer.le()->advertising_data_error().has_value()) { + // If the peer advertising data has failed to parse, then this conversion + // failed. + return nullptr; + } + + if (peer.rssi() != bt::hci_spec::kRSSIInvalid) { + fidl_device->rssi = std::make_unique(); + fidl_device->rssi->value = peer.rssi(); + } + + return fidl_device; +} + +bool IsScanFilterValid(const fble::ScanFilter& fidl_filter) { + // |service_uuids| and |service_data_uuids| are the only fields that can + // potentially contain invalid data, since they are represented as strings. + if (fidl_filter.service_uuids) { + for (const auto& uuid_str : *fidl_filter.service_uuids) { + if (!bt::IsStringValidUuid(uuid_str)) { + return false; + } + } + } + + if (fidl_filter.service_data_uuids) { + for (const auto& uuid_str : *fidl_filter.service_data_uuids) { + if (!bt::IsStringValidUuid(uuid_str)) + return false; + } + } + + return true; +} + +bool PopulateDiscoveryFilter(const fble::ScanFilter& fidl_filter, + bt::gap::DiscoveryFilter* out_filter) { + BT_DEBUG_ASSERT(out_filter); + + if (fidl_filter.service_uuids) { + std::vector uuids; + for (const auto& uuid_str : *fidl_filter.service_uuids) { + bt::UUID uuid; + if (!bt::StringToUuid(uuid_str, &uuid)) { + bt_log(WARN, + "fidl", + "invalid service UUID given to scan filter: %s", + uuid_str.c_str()); + return false; + } + uuids.push_back(uuid); + } + + if (!uuids.empty()) + out_filter->set_service_uuids(uuids); + } + + if (fidl_filter.service_data_uuids) { + std::vector uuids; + for (const auto& uuid_str : *fidl_filter.service_data_uuids) { + bt::UUID uuid; + if (!bt::StringToUuid(uuid_str, &uuid)) { + bt_log(WARN, + "fidl", + "invalid service data UUID given to scan filter: %s", + uuid_str.c_str()); + return false; + } + uuids.push_back(uuid); + } + + if (!uuids.empty()) + out_filter->set_service_data_uuids(uuids); + } + + if (fidl_filter.connectable) { + out_filter->set_connectable(fidl_filter.connectable->value); + } + + if (fidl_filter.manufacturer_identifier) { + out_filter->set_manufacturer_code( + fidl_filter.manufacturer_identifier->value); + } + + if (fidl_filter.name_substring && + !fidl_filter.name_substring.value_or("").empty()) { + out_filter->set_name_substring(fidl_filter.name_substring.value_or("")); + } + + if (fidl_filter.max_path_loss) { + out_filter->set_pathloss(fidl_filter.max_path_loss->value); + } + + return true; +} + +bt::gap::DiscoveryFilter DiscoveryFilterFromFidl( + const fuchsia::bluetooth::le::Filter& fidl_filter) { + bt::gap::DiscoveryFilter out; + + if (fidl_filter.has_service_uuid()) { + out.set_service_uuids({bt::UUID(fidl_filter.service_uuid().value)}); + } + + if (fidl_filter.has_service_data_uuid()) { + out.set_service_data_uuids( + {bt::UUID(fidl_filter.service_data_uuid().value)}); + } + + if (fidl_filter.has_manufacturer_id()) { + out.set_manufacturer_code(fidl_filter.manufacturer_id()); + } + + if (fidl_filter.has_connectable()) { + out.set_connectable(fidl_filter.connectable()); + } + + if (fidl_filter.has_name()) { + out.set_name_substring(fidl_filter.name()); + } + + if (fidl_filter.has_max_path_loss()) { + out.set_pathloss(fidl_filter.max_path_loss()); + } + + return out; +} + +bt::gap::AdvertisingInterval AdvertisingIntervalFromFidl( + fble::AdvertisingModeHint mode_hint) { + switch (mode_hint) { + case fble::AdvertisingModeHint::VERY_FAST: + return bt::gap::AdvertisingInterval::FAST1; + case fble::AdvertisingModeHint::FAST: + return bt::gap::AdvertisingInterval::FAST2; + case fble::AdvertisingModeHint::SLOW: + return bt::gap::AdvertisingInterval::SLOW; + } + return bt::gap::AdvertisingInterval::SLOW; +} + +std::optional AdvertisingDataFromFidl( + const fble::AdvertisingData& input) { + bt::AdvertisingData output; + + if (input.has_name()) { + if (!output.SetLocalName(input.name())) { + return std::nullopt; + } + } + if (input.has_appearance()) { + output.SetAppearance(static_cast(input.appearance())); + } + if (input.has_tx_power_level()) { + output.SetTxPower(input.tx_power_level()); + } + if (input.has_service_uuids()) { + for (const auto& uuid : input.service_uuids()) { + bt::UUID bt_uuid = UuidFromFidl(uuid); + if (!output.AddServiceUuid(bt_uuid)) { + bt_log(WARN, + "fidl", + "Received more Service UUIDs than fit in a single AD - " + "truncating UUID %s", + bt_str(bt_uuid)); + } + } + } + if (input.has_service_data()) { + for (const auto& entry : input.service_data()) { + if (!output.SetServiceData(UuidFromFidl(entry.uuid), + bt::BufferView(entry.data))) { + return std::nullopt; + } + } + } + if (input.has_manufacturer_data()) { + for (const auto& entry : input.manufacturer_data()) { + bt::BufferView data(entry.data); + if (!output.SetManufacturerData(entry.company_id, data)) { + return std::nullopt; + } + } + } + if (input.has_uris()) { + for (const auto& uri : input.uris()) { + if (!output.AddUri(uri)) { + return std::nullopt; + } + } + } + + return output; +} + +fble::AdvertisingData AdvertisingDataToFidl(const bt::AdvertisingData& input) { + fble::AdvertisingData output; + + if (input.local_name()) { + output.set_name(input.local_name()->name); + } + if (input.appearance()) { + // TODO(fxbug.dev/42145156): Remove this to allow for passing arbitrary + // appearance values to clients in a way that's forward-compatible with + // future BLE revisions. + const uint16_t appearance_raw = input.appearance().value(); + if (auto appearance = AppearanceToFidl(appearance_raw)) { + output.set_appearance(appearance.value()); + } else { + bt_log(DEBUG, + "fidl", + "omitting unencodeable appearance %#.4x of peer %s", + appearance_raw, + input.local_name().has_value() ? input.local_name()->name.c_str() + : ""); + } + } + if (input.tx_power()) { + output.set_tx_power_level(*input.tx_power()); + } + std::unordered_set service_uuids = input.service_uuids(); + if (!service_uuids.empty()) { + std::vector uuids; + uuids.reserve(service_uuids.size()); + for (const auto& uuid : service_uuids) { + uuids.push_back(fbt::Uuid{uuid.value()}); + } + output.set_service_uuids(std::move(uuids)); + } + if (!input.service_data_uuids().empty()) { + std::vector entries; + for (const auto& uuid : input.service_data_uuids()) { + auto data = input.service_data(uuid); + fble::ServiceData entry{fbt::Uuid{uuid.value()}, data.ToVector()}; + entries.push_back(std::move(entry)); + } + output.set_service_data(std::move(entries)); + } + if (!input.manufacturer_data_ids().empty()) { + std::vector entries; + for (const auto& id : input.manufacturer_data_ids()) { + auto data = input.manufacturer_data(id); + fble::ManufacturerData entry{id, data.ToVector()}; + entries.push_back(std::move(entry)); + } + output.set_manufacturer_data(std::move(entries)); + } + if (!input.uris().empty()) { + std::vector uris; + for (const auto& uri : input.uris()) { + uris.push_back(uri); + } + output.set_uris(std::move(uris)); + } + + return output; +} + +fble::AdvertisingDataDeprecated AdvertisingDataToFidlDeprecated( + const bt::AdvertisingData& input) { + fble::AdvertisingDataDeprecated output; + + if (input.local_name()) { + output.name = input.local_name()->name; + } + if (input.appearance()) { + output.appearance = std::make_unique(); + output.appearance->value = *input.appearance(); + } + if (input.tx_power()) { + output.tx_power_level = std::make_unique(); + output.tx_power_level->value = *input.tx_power(); + } + if (!input.service_uuids().empty()) { + output.service_uuids.emplace(); + for (const auto& uuid : input.service_uuids()) { + output.service_uuids->push_back(uuid.ToString()); + } + } + if (!input.service_data_uuids().empty()) { + output.service_data.emplace(); + for (const auto& uuid : input.service_data_uuids()) { + auto data = input.service_data(uuid); + fble::ServiceDataEntry entry{uuid.ToString(), data.ToVector()}; + output.service_data->push_back(std::move(entry)); + } + } + if (!input.manufacturer_data_ids().empty()) { + output.manufacturer_specific_data.emplace(); + for (const auto& id : input.manufacturer_data_ids()) { + auto data = input.manufacturer_data(id); + fble::ManufacturerSpecificDataEntry entry{id, data.ToVector()}; + output.manufacturer_specific_data->push_back(std::move(entry)); + } + } + if (!input.uris().empty()) { + output.uris.emplace(); + for (const auto& uri : input.uris()) { + output.uris->push_back(uri); + } + } + + return output; +} + +fuchsia::bluetooth::le::ScanData AdvertisingDataToFidlScanData( + const bt::AdvertisingData& input, + pw::chrono::SystemClock::time_point timestamp) { + // Reuse bt::AdvertisingData -> fble::AdvertisingData utility, since most + // fields are the same as fble::ScanData. + fble::AdvertisingData fidl_adv_data = AdvertisingDataToFidl(input); + fble::ScanData out; + if (fidl_adv_data.has_tx_power_level()) { + out.set_tx_power(fidl_adv_data.tx_power_level()); + } + if (fidl_adv_data.has_appearance()) { + out.set_appearance(fidl_adv_data.appearance()); + } + if (fidl_adv_data.has_service_uuids()) { + out.set_service_uuids(std::move(*fidl_adv_data.mutable_service_uuids())); + } + if (fidl_adv_data.has_service_data()) { + out.set_service_data(std::move(*fidl_adv_data.mutable_service_data())); + } + if (fidl_adv_data.has_manufacturer_data()) { + out.set_manufacturer_data( + std::move(*fidl_adv_data.mutable_manufacturer_data())); + } + if (fidl_adv_data.has_uris()) { + out.set_uris(std::move(*fidl_adv_data.mutable_uris())); + } + zx_time_t timestamp_ns = timestamp.time_since_epoch().count(); + out.set_timestamp(timestamp_ns); + return out; +} + +fble::Peer PeerToFidlLe(const bt::gap::Peer& peer) { + BT_ASSERT(peer.le()); + + fble::Peer output; + output.set_id(fbt::PeerId{peer.identifier().value()}); + output.set_connectable(peer.connectable()); + + if (peer.rssi() != bt::hci_spec::kRSSIInvalid) { + output.set_rssi(peer.rssi()); + } + + const std::optional> + advertising_data = peer.le()->parsed_advertising_data(); + if (advertising_data.has_value()) { + std::optional timestamp = + peer.le()->parsed_advertising_data_timestamp(); + output.set_advertising_data( + AdvertisingDataToFidl(advertising_data.value())); + output.set_data(AdvertisingDataToFidlScanData(advertising_data.value(), + timestamp.value())); + } + + if (peer.name()) { + output.set_name(peer.name().value()); + } + + output.set_bonded(peer.bonded()); + zx_time_t last_updated_ns = peer.last_updated().time_since_epoch().count(); + output.set_last_updated(last_updated_ns); + + return output; +} + +bt::gatt::ReliableMode ReliableModeFromFidl( + const fgatt::WriteOptions& write_options) { + return (write_options.has_reliable_mode() && + write_options.reliable_mode() == fgatt::ReliableMode::ENABLED) + ? bt::gatt::ReliableMode::kEnabled + : bt::gatt::ReliableMode::kDisabled; +} + +// TODO(fxbug.dev/42141942): The 64 bit `fidl_gatt_id` can overflow the 16 bits +// of a bt:att::Handle that underlies CharacteristicHandles when directly +// casted. Fix this. +bt::gatt::CharacteristicHandle CharacteristicHandleFromFidl( + uint64_t fidl_gatt_id) { + if (fidl_gatt_id > std::numeric_limits::max()) { + bt_log(ERROR, + "fidl", + "Casting a 64-bit FIDL GATT ID with `bits[16, 63] != 0` (0x%lX) to " + "16-bit " + "Characteristic Handle", + fidl_gatt_id); + } + return bt::gatt::CharacteristicHandle( + static_cast(fidl_gatt_id)); +} + +// TODO(fxbug.dev/42141942): The 64 bit `fidl_gatt_id` can overflow the 16 bits +// of a bt:att::Handle that underlies DescriptorHandles when directly casted. +// Fix this. +bt::gatt::DescriptorHandle DescriptorHandleFromFidl(uint64_t fidl_gatt_id) { + if (fidl_gatt_id > std::numeric_limits::max()) { + bt_log(ERROR, + "fidl", + "Casting a 64-bit FIDL GATT ID with `bits[16, 63] != 0` (0x%lX) to " + "16-bit Descriptor " + "Handle", + fidl_gatt_id); + } + return bt::gatt::DescriptorHandle(static_cast(fidl_gatt_id)); +} + +fpromise::result +ServiceDefinitionToServiceRecord( + const fuchsia_bluetooth_bredr::ServiceDefinition& definition) { + bt::sdp::ServiceRecord rec; + std::vector classes; + + if (!definition.service_class_uuids().has_value()) { + bt_log(WARN, "fidl", "Advertised service contains no Service UUIDs"); + return fpromise::error(fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + } + + for (auto& uuid : definition.service_class_uuids().value()) { + bt::UUID btuuid = fidl_helpers::NewUuidFromFidl(uuid); + bt_log(TRACE, "fidl", "Setting Service Class UUID %s", bt_str(btuuid)); + classes.emplace_back(btuuid); + } + + rec.SetServiceClassUUIDs(classes); + + if (definition.protocol_descriptor_list().has_value()) { + if (!NewAddProtocolDescriptorList( + &rec, + bt::sdp::ServiceRecord::kPrimaryProtocolList, + definition.protocol_descriptor_list().value())) { + bt_log(ERROR, "fidl", "Failed to add protocol descriptor list"); + return fpromise::error(fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + } + } + + if (definition.additional_protocol_descriptor_lists().has_value()) { + // It's safe to iterate through this list with a ProtocolListId as + // ProtocolListId = uint8_t, and std::numeric_limits::max() == 255 + // == the MAX_SEQUENCE_LENGTH vector limit from + // fuchsia.bluetooth.bredr/ServiceDefinition.additional_protocol_descriptor_lists. + BT_ASSERT( + definition.additional_protocol_descriptor_lists()->size() <= + std::numeric_limits::max()); + bt::sdp::ServiceRecord::ProtocolListId protocol_list_id = 1; + for (const auto& descriptor_list : + definition.additional_protocol_descriptor_lists().value()) { + if (!NewAddProtocolDescriptorList( + &rec, protocol_list_id, descriptor_list)) { + bt_log( + ERROR, "fidl", "Failed to add additional protocol descriptor list"); + return fpromise::error( + fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + } + protocol_list_id++; + } + } + + if (definition.profile_descriptors().has_value()) { + for (const auto& profile : definition.profile_descriptors().value()) { + if (!profile.profile_id().has_value() || + !profile.major_version().has_value() || + !profile.minor_version().has_value()) { + bt_log(WARN, "fidl", "ProfileDescriptor missing required fields"); + return fpromise::error( + fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + } + bt_log(TRACE, + "fidl", + "Adding Profile %#hx v%d.%d", + static_cast(*profile.profile_id()), + *profile.major_version(), + *profile.minor_version()); + rec.AddProfile(bt::UUID(static_cast(*profile.profile_id())), + *profile.major_version(), + *profile.minor_version()); + } + } + + if (definition.information().has_value()) { + for (const auto& info : definition.information().value()) { + if (!info.language().has_value()) { + return fpromise::error( + fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + } + + const std::string& language = info.language().value(); + std::string name, description, provider; + if (info.name().has_value()) { + name = info.name().value(); + } + if (info.description().has_value()) { + description = info.description().value(); + } + if (info.provider().has_value()) { + provider = info.provider().value(); + } + bt_log(TRACE, + "fidl", + "Adding Info (%s): (%s, %s, %s)", + language.c_str(), + name.c_str(), + description.c_str(), + provider.c_str()); + rec.AddInfo(language, name, description, provider); + } + } + + if (definition.additional_attributes().has_value()) { + for (const auto& attribute : definition.additional_attributes().value()) { + if (!attribute.element() || !attribute.id()) { + bt_log(WARN, "fidl", "Attribute missing required fields"); + return fpromise::error( + fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + } + auto elem = NewFidlToDataElement(*attribute.element()); + if (elem) { + bt_log(TRACE, + "fidl", + "Adding attribute %#x : %s", + *attribute.id(), + elem.value().ToString().c_str()); + rec.SetAttribute(*attribute.id(), std::move(elem.value())); + } + } + } + return fpromise::ok(std::move(rec)); +} + +fpromise::result +ServiceDefinitionToServiceRecord( + const fuchsia::bluetooth::bredr::ServiceDefinition& definition) { + bt::sdp::ServiceRecord rec; + std::vector classes; + + if (!definition.has_service_class_uuids()) { + bt_log(WARN, "fidl", "Advertised service contains no Service UUIDs"); + return fpromise::error(fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + } + + for (auto& uuid : definition.service_class_uuids()) { + bt::UUID btuuid = fidl_helpers::UuidFromFidl(uuid); + bt_log(TRACE, "fidl", "Setting Service Class UUID %s", bt_str(btuuid)); + classes.emplace_back(btuuid); + } + + rec.SetServiceClassUUIDs(classes); + + if (definition.has_protocol_descriptor_list()) { + if (!AddProtocolDescriptorList(&rec, + bt::sdp::ServiceRecord::kPrimaryProtocolList, + definition.protocol_descriptor_list())) { + bt_log(ERROR, "fidl", "Failed to add protocol descriptor list"); + return fpromise::error(fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + } + } + + if (definition.has_additional_protocol_descriptor_lists()) { + // It's safe to iterate through this list with a ProtocolListId as + // ProtocolListId = uint8_t, and std::numeric_limits::max() == 255 + // == the MAX_SEQUENCE_LENGTH vector limit from + // fuchsia.bluetooth.bredr/ServiceDefinition.additional_protocol_descriptor_lists. + BT_ASSERT( + definition.additional_protocol_descriptor_lists().size() <= + std::numeric_limits::max()); + bt::sdp::ServiceRecord::ProtocolListId protocol_list_id = 1; + for (const auto& descriptor_list : + definition.additional_protocol_descriptor_lists()) { + if (!AddProtocolDescriptorList(&rec, protocol_list_id, descriptor_list)) { + bt_log( + ERROR, "fidl", "Failed to add additional protocol descriptor list"); + return fpromise::error( + fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + } + protocol_list_id++; + } + } + + if (definition.has_profile_descriptors()) { + for (const auto& profile : definition.profile_descriptors()) { + if (!profile.has_profile_id() || !profile.has_major_version() || + !profile.has_minor_version()) { + bt_log(ERROR, "fidl", "ProfileDescriptor missing required fields"); + return fpromise::error( + fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + } + bt_log(TRACE, + "fidl", + "Adding Profile %#hx v%d.%d", + static_cast(profile.profile_id()), + profile.major_version(), + profile.minor_version()); + rec.AddProfile(bt::UUID(static_cast(profile.profile_id())), + profile.major_version(), + profile.minor_version()); + } + } + + if (definition.has_information()) { + for (const auto& info : definition.information()) { + if (!info.has_language()) { + return fpromise::error( + fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + } + const std::string& language = info.language(); + std::string name, description, provider; + if (info.has_name()) { + name = info.name(); + } + if (info.has_description()) { + description = info.description(); + } + if (info.has_provider()) { + provider = info.provider(); + } + bt_log(TRACE, + "fidl", + "Adding Info (%s): (%s, %s, %s)", + language.c_str(), + name.c_str(), + description.c_str(), + provider.c_str()); + rec.AddInfo(language, name, description, provider); + } + } + + if (definition.has_additional_attributes()) { + for (const auto& attribute : definition.additional_attributes()) { + if (!attribute.has_element() || !attribute.has_id()) { + bt_log(WARN, "fidl", "Attribute missing required fields"); + return fpromise::error( + fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + } + auto elem = FidlToDataElement(attribute.element()); + if (elem) { + bt_log(TRACE, + "fidl", + "Adding attribute %#x : %s", + attribute.id(), + elem.value().ToString().c_str()); + rec.SetAttribute(attribute.id(), std::move(elem.value())); + } + } + } + return fpromise::ok(std::move(rec)); +} + +fpromise::result +ServiceRecordToServiceDefinition(const bt::sdp::ServiceRecord& record) { + fuchsia::bluetooth::bredr::ServiceDefinition out; + + // Service class UUIDs are mandatory + if (!record.HasAttribute(bt::sdp::kServiceClassIdList)) { + return fpromise::error(fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + } + const bt::sdp::DataElement& service_uuids_element = + record.GetAttribute(bt::sdp::kServiceClassIdList); + auto service_uuids = DataElementToServiceUuids(service_uuids_element); + if (service_uuids.is_error()) { + return fpromise::error(service_uuids.error()); + } + out.set_service_class_uuids(std::move(service_uuids.value())); + + // Primary protocol descriptor list (optional) + if (record.HasAttribute(bt::sdp::kProtocolDescriptorList)) { + const bt::sdp::DataElement& primary_protocol_element = + record.GetAttribute(bt::sdp::kProtocolDescriptorList); + auto primary_protocol = + DataElementToProtocolDescriptorList(primary_protocol_element); + if (primary_protocol.is_error()) { + return fpromise::error(primary_protocol.error()); + } + out.set_protocol_descriptor_list(std::move(primary_protocol.value())); + } + + // Additional protocol descriptor lists (optional) + if (record.HasAttribute(bt::sdp::kAdditionalProtocolDescriptorList)) { + const bt::sdp::DataElement& additional_protocols = + record.GetAttribute(bt::sdp::kAdditionalProtocolDescriptorList); + // Sequence of protocol descriptor list sequences. + if (additional_protocols.type() != bt::sdp::DataElement::Type::kSequence) { + bt_log(WARN, "fidl", "Invalid additional protocol descriptor list"); + return fpromise::error(fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + } + + const auto additional_protocol_list = + additional_protocols.Get>(); + for (const auto& addl_element : additional_protocol_list.value()) { + auto additional_protocol = + DataElementToProtocolDescriptorList(addl_element); + if (additional_protocol.is_error()) { + return fpromise::error(additional_protocol.error()); + } + out.mutable_additional_protocol_descriptor_lists()->emplace_back( + std::move(additional_protocol.value())); + } + } + + // Profile descriptors (optional) + if (record.HasAttribute(bt::sdp::kBluetoothProfileDescriptorList)) { + const bt::sdp::DataElement& profile_descriptors_element = + record.GetAttribute(bt::sdp::kBluetoothProfileDescriptorList); + auto profile_descriptors = + DataElementToProfileDescriptors(profile_descriptors_element); + if (profile_descriptors.is_error()) { + return fpromise::error(profile_descriptors.error()); + } + out.set_profile_descriptors(std::move(profile_descriptors.value())); + } + + // Human-readable information (optional) + const std::vector information = + record.GetInfo(); + for (const auto& info : information) { + fbredr::Information fidl_info = InformationToFidl(info); + out.mutable_information()->emplace_back(std::move(fidl_info)); + } + + // Additional attributes (optional) + const bt::sdp::AttributeId kMinAdditionalAttribute = 0x200; + const std::set additional_attribute_ids = + record.GetAttributesInRange(kMinAdditionalAttribute, 0xffff); + for (const auto additional_attr_id : additional_attribute_ids) { + const bt::sdp::DataElement& additional_attr_elt = + record.GetAttribute(additional_attr_id); + std::optional element = + DataElementToFidl(additional_attr_elt); + if (!element) { + bt_log(WARN, "fidl", "Invalid additional attribute data element"); + return fpromise::error(fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + } + fbredr::Attribute attr; + attr.set_id(additional_attr_id); + attr.set_element(std::move(element.value())); + out.mutable_additional_attributes()->emplace_back(std::move(attr)); + } + return fpromise::ok(std::move(out)); +} + +bt::gap::BrEdrSecurityRequirements FidlToBrEdrSecurityRequirements( + const fbt::ChannelParameters& fidl) { + bt::gap::BrEdrSecurityRequirements requirements{.authentication = false, + .secure_connections = false}; + if (fidl.has_security_requirements()) { + if (fidl.security_requirements().has_authentication_required()) { + requirements.authentication = + fidl.security_requirements().authentication_required(); + } + + if (fidl.security_requirements().has_secure_connections_required()) { + requirements.secure_connections = + fidl.security_requirements().secure_connections_required(); + } + } + return requirements; +} + +std::optional FidlToScoParameterSet( + const fbredr::HfpParameterSet param_set) { + switch (param_set) { + case fbredr::HfpParameterSet::T1: + return bt::sco::kParameterSetT1; + case fbredr::HfpParameterSet::T2: + return bt::sco::kParameterSetT2; + case fbredr::HfpParameterSet::S1: + return bt::sco::kParameterSetS1; + case fbredr::HfpParameterSet::S2: + return bt::sco::kParameterSetS2; + case fbredr::HfpParameterSet::S3: + return bt::sco::kParameterSetS3; + case fbredr::HfpParameterSet::S4: + return bt::sco::kParameterSetS4; + case fbredr::HfpParameterSet::D0: + return bt::sco::kParameterSetD0; + case fbredr::HfpParameterSet::D1: + return bt::sco::kParameterSetD1; + default: + return std::nullopt; + } +} + +std::optional< + bt::StaticPacket> +FidlToScoCodingFormat(const fbt::AssignedCodingFormat format) { + bt::StaticPacket + out; + auto view = out.view(); + // Set to 0 since vendor specific coding formats are not supported. + view.company_id().Write(0); + view.vendor_codec_id().Write(0); + switch (format) { + case fbt::AssignedCodingFormat::A_LAW_LOG: + view.coding_format().Write(pw::bluetooth::emboss::CodingFormat::A_LAW); + break; + case fbt::AssignedCodingFormat::U_LAW_LOG: + view.coding_format().Write(pw::bluetooth::emboss::CodingFormat::U_LAW); + break; + case fbt::AssignedCodingFormat::CVSD: + view.coding_format().Write(pw::bluetooth::emboss::CodingFormat::CVSD); + break; + case fbt::AssignedCodingFormat::TRANSPARENT: + view.coding_format().Write( + pw::bluetooth::emboss::CodingFormat::TRANSPARENT); + break; + case fbt::AssignedCodingFormat::LINEAR_PCM: + view.coding_format().Write( + pw::bluetooth::emboss::CodingFormat::LINEAR_PCM); + break; + case fbt::AssignedCodingFormat::MSBC: + view.coding_format().Write(pw::bluetooth::emboss::CodingFormat::MSBC); + break; + case fbt::AssignedCodingFormat::LC3: + view.coding_format().Write(pw::bluetooth::emboss::CodingFormat::LC3); + break; + case fbt::AssignedCodingFormat::G_729A: + view.coding_format().Write(pw::bluetooth::emboss::CodingFormat::G729A); + break; + default: + return std::nullopt; + } + return out; +} + +fpromise::result FidlToPcmDataFormat( + const faudio::SampleFormat& format) { + switch (format) { + case faudio::SampleFormat::PCM_SIGNED: + return fpromise::ok( + pw::bluetooth::emboss::PcmDataFormat::TWOS_COMPLEMENT); + case faudio::SampleFormat::PCM_UNSIGNED: + return fpromise::ok(pw::bluetooth::emboss::PcmDataFormat::UNSIGNED); + default: + // Other sample formats are not supported by SCO. + return fpromise::error(); + } +} + +pw::bluetooth::emboss::ScoDataPath FidlToScoDataPath( + const fbredr::DataPath& path) { + switch (path) { + case fbredr::DataPath::HOST: + return pw::bluetooth::emboss::ScoDataPath::HCI; + case fbredr::DataPath::OFFLOAD: { + // TODO(fxbug.dev/42136417): Use path from stack configuration file + // instead of this hardcoded value. "6" is the data path usually used in + // Broadcom controllers. + return static_cast(6); + } + case fbredr::DataPath::TEST: + return pw::bluetooth::emboss::ScoDataPath::AUDIO_TEST_MODE; + } +} + +fpromise::result> +FidlToScoParameters(const fbredr::ScoConnectionParameters& params) { + bt::StaticPacket + out; + auto view = out.view(); + + if (!params.has_parameter_set()) { + bt_log(WARN, "fidl", "SCO parameters missing parameter_set"); + return fpromise::error(); + } + auto param_set = FidlToScoParameterSet(params.parameter_set()); + if (!param_set.has_value()) { + bt_log(WARN, "fidl", "Unrecognized SCO parameters parameter_set"); + return fpromise::error(); + } + view.transmit_bandwidth().Write(param_set.value().transmit_receive_bandwidth); + view.receive_bandwidth().Write(param_set.value().transmit_receive_bandwidth); + + if (!params.has_air_coding_format()) { + bt_log(WARN, "fidl", "SCO parameters missing air_coding_format"); + return fpromise::error(); + } + std::optional< + bt::StaticPacket> + air_coding_format = FidlToScoCodingFormat(params.air_coding_format()); + if (!air_coding_format) { + bt_log(WARN, "fidl", "SCO parameters contains unknown air_coding_format"); + return fpromise::error(); + } + view.transmit_coding_format().CopyFrom(air_coding_format->view()); + view.receive_coding_format().CopyFrom(air_coding_format->view()); + + if (!params.has_air_frame_size()) { + bt_log(WARN, "fidl", "SCO parameters missing air_frame_size"); + return fpromise::error(); + } + view.transmit_codec_frame_size_bytes().Write(params.air_frame_size()); + view.receive_codec_frame_size_bytes().Write(params.air_frame_size()); + + if (!params.has_io_bandwidth()) { + bt_log(WARN, "fidl", "SCO parameters missing io_bandwidth"); + return fpromise::error(); + } + view.input_bandwidth().Write(params.io_bandwidth()); + view.output_bandwidth().Write(params.io_bandwidth()); + + if (!params.has_io_coding_format()) { + bt_log(WARN, "fidl", "SCO parameters missing io_coding_format"); + return fpromise::error(); + } + std::optional< + bt::StaticPacket> + io_coding_format = FidlToScoCodingFormat(params.io_coding_format()); + if (!io_coding_format) { + bt_log(WARN, "fidl", "SCO parameters contains unknown io_coding_format"); + return fpromise::error(); + } + view.input_coding_format().CopyFrom(io_coding_format->view()); + view.output_coding_format().CopyFrom(io_coding_format->view()); + + if (!params.has_io_frame_size()) { + bt_log(WARN, "fidl", "SCO parameters missing io_frame_size"); + return fpromise::error(); + } + view.input_coded_data_size_bits().Write(params.io_frame_size()); + view.output_coded_data_size_bits().Write(params.io_frame_size()); + + if (params.has_io_pcm_data_format() && + view.input_coding_format().coding_format().Read() == + pw::bluetooth::emboss::CodingFormat::LINEAR_PCM) { + auto io_pcm_format = FidlToPcmDataFormat(params.io_pcm_data_format()); + if (io_pcm_format.is_error()) { + bt_log(WARN, "fidl", "Unsupported IO PCM data format in SCO parameters"); + return fpromise::error(); + } + view.input_pcm_data_format().Write(io_pcm_format.value()); + view.output_pcm_data_format().Write(io_pcm_format.value()); + + } else if (view.input_coding_format().coding_format().Read() == + pw::bluetooth::emboss::CodingFormat::LINEAR_PCM) { + bt_log(WARN, + "fidl", + "SCO parameters missing io_pcm_data_format (required for linear PCM " + "IO coding format)"); + return fpromise::error(); + } else { + view.input_pcm_data_format().Write( + pw::bluetooth::emboss::PcmDataFormat::NOT_APPLICABLE); + view.output_pcm_data_format().Write( + pw::bluetooth::emboss::PcmDataFormat::NOT_APPLICABLE); + } + + if (params.has_io_pcm_sample_payload_msb_position() && + view.input_coding_format().coding_format().Read() == + pw::bluetooth::emboss::CodingFormat::LINEAR_PCM) { + view.input_pcm_sample_payload_msb_position().Write( + params.io_pcm_sample_payload_msb_position()); + view.output_pcm_sample_payload_msb_position().Write( + params.io_pcm_sample_payload_msb_position()); + } else { + view.input_pcm_sample_payload_msb_position().Write(0u); + view.output_pcm_sample_payload_msb_position().Write(0u); + } + + if (!params.has_path()) { + bt_log(WARN, "fidl", "SCO parameters missing data path"); + return fpromise::error(); + } + auto path = FidlToScoDataPath(params.path()); + view.input_data_path().Write(path); + view.output_data_path().Write(path); + + // For HCI Host transport the transport unit size should be "0". For PCM + // transport the unit size is vendor specific. A unit size of "0" indicates + // "not applicable". + // TODO(fxbug.dev/42136417): Use unit size from stack configuration file + // instead of hardcoding "not applicable". + view.input_transport_unit_size_bits().Write(0u); + view.output_transport_unit_size_bits().Write(0u); + + view.max_latency_ms().Write(param_set.value().max_latency_ms); + view.packet_types().BackingStorage().WriteUInt( + param_set.value().packet_types); + view.retransmission_effort().Write( + static_cast( + param_set.value().retransmission_effort)); + + return fpromise::ok(out); +} + +fpromise::result>> +FidlToScoParametersVector( + const std::vector& params) { + std::vector> + out; + out.reserve(params.size()); + for (const fbredr::ScoConnectionParameters& param : params) { + fpromise::result> + result = FidlToScoParameters(param); + if (result.is_error()) { + return fpromise::error(); + } + out.push_back(result.take_value()); + } + return fpromise::ok(std::move(out)); +} + +bool IsFidlGattHandleValid(fuchsia::bluetooth::gatt2::Handle handle) { + if (handle.value > std::numeric_limits::max()) { + bt_log(ERROR, + "fidl", + "Invalid 64-bit FIDL GATT ID with `bits[16, 63] != 0` (0x%lX)", + handle.value); + return false; + } + return true; +} + +bool IsFidlGattServiceHandleValid( + fuchsia::bluetooth::gatt2::ServiceHandle handle) { + if (handle.value > std::numeric_limits::max()) { + bt_log(ERROR, + "fidl", + "Invalid 64-bit FIDL GATT ID with `bits[16, 63] != 0` (0x%lX)", + handle.value); + return false; + } + return true; +} + +fuchsia::bluetooth::bredr::RxPacketStatus ScoPacketStatusToFidl( + bt::hci_spec::SynchronousDataPacketStatusFlag status) { + switch (status) { + case bt::hci_spec::SynchronousDataPacketStatusFlag::kCorrectlyReceived: + return fuchsia::bluetooth::bredr::RxPacketStatus::CORRECTLY_RECEIVED_DATA; + case bt::hci_spec::SynchronousDataPacketStatusFlag::kPossiblyInvalid: + return fuchsia::bluetooth::bredr::RxPacketStatus::POSSIBLY_INVALID_DATA; + case bt::hci_spec::SynchronousDataPacketStatusFlag::kNoDataReceived: + return fuchsia::bluetooth::bredr::RxPacketStatus::NO_DATA_RECEIVED; + case bt::hci_spec::SynchronousDataPacketStatusFlag::kDataPartiallyLost: + return fuchsia::bluetooth::bredr::RxPacketStatus::DATA_PARTIALLY_LOST; + } +} + +bt::att::ErrorCode Gatt2ErrorCodeFromFidl(fgatt2::Error error_code) { + switch (error_code) { + case fgatt2::Error::INVALID_HANDLE: + return bt::att::ErrorCode::kInvalidHandle; + case fgatt2::Error::READ_NOT_PERMITTED: + return bt::att::ErrorCode::kReadNotPermitted; + case fgatt2::Error::WRITE_NOT_PERMITTED: + return bt::att::ErrorCode::kWriteNotPermitted; + case fgatt2::Error::INVALID_OFFSET: + return bt::att::ErrorCode::kInvalidOffset; + case fgatt2::Error::INVALID_ATTRIBUTE_VALUE_LENGTH: + return bt::att::ErrorCode::kInvalidAttributeValueLength; + case fgatt2::Error::INSUFFICIENT_RESOURCES: + return bt::att::ErrorCode::kInsufficientResources; + case fgatt2::Error::VALUE_NOT_ALLOWED: + return bt::att::ErrorCode::kValueNotAllowed; + default: + break; + } + return bt::att::ErrorCode::kUnlikelyError; +} + +bt::att::AccessRequirements Gatt2AccessRequirementsFromFidl( + const fuchsia::bluetooth::gatt2::SecurityRequirements& reqs) { + return bt::att::AccessRequirements( + reqs.has_encryption_required() ? reqs.encryption_required() : false, + reqs.has_authentication_required() ? reqs.authentication_required() + : false, + reqs.has_authorization_required() ? reqs.authorization_required() + : false); +} + +std::unique_ptr Gatt2DescriptorFromFidl( + const fgatt2::Descriptor& fidl_desc) { + if (!fidl_desc.has_permissions()) { + bt_log( + WARN, "fidl", "FIDL descriptor missing required `permissions` field"); + return nullptr; + } + const fgatt2::AttributePermissions& perm = fidl_desc.permissions(); + bt::att::AccessRequirements read_reqs = + perm.has_read() ? Gatt2AccessRequirementsFromFidl(perm.read()) + : bt::att::AccessRequirements(); + bt::att::AccessRequirements write_reqs = + perm.has_write() ? Gatt2AccessRequirementsFromFidl(perm.write()) + : bt::att::AccessRequirements(); + + if (!fidl_desc.has_type()) { + bt_log(WARN, "fidl", "FIDL descriptor missing required `type` field"); + return nullptr; + } + bt::UUID type(fidl_desc.type().value); + + if (!fidl_desc.has_handle()) { + bt_log(WARN, "fidl", "FIDL characteristic missing required `handle` field"); + return nullptr; + } + return std::make_unique( + fidl_desc.handle().value, type, read_reqs, write_reqs); +} + +std::unique_ptr Gatt2CharacteristicFromFidl( + const fgatt2::Characteristic& fidl_chrc) { + if (!fidl_chrc.has_properties()) { + bt_log(WARN, + "fidl", + "FIDL characteristic missing required `properties` field"); + return nullptr; + } + if (!fidl_chrc.has_permissions()) { + bt_log(WARN, + "fidl", + "FIDL characteristic missing required `permissions` field"); + return nullptr; + } + if (!fidl_chrc.has_type()) { + bt_log(WARN, "fidl", "FIDL characteristic missing required `type` field"); + return nullptr; + } + if (!fidl_chrc.has_handle()) { + bt_log(WARN, "fidl", "FIDL characteristic missing required `handle` field"); + return nullptr; + } + + uint8_t props = static_cast(fidl_chrc.properties()); + const uint16_t ext_props = + static_cast(fidl_chrc.properties()) >> CHAR_BIT; + if (ext_props) { + props |= bt::gatt::Property::kExtendedProperties; + } + + const fgatt2::AttributePermissions& permissions = fidl_chrc.permissions(); + bool supports_update = (props & bt::gatt::Property::kNotify) || + (props & bt::gatt::Property::kIndicate); + if (supports_update != permissions.has_update()) { + bt_log(WARN, + "fidl", + "Characteristic update permission %s", + supports_update ? "required" : "must be null"); + return nullptr; + } + + bt::att::AccessRequirements read_reqs = + permissions.has_read() + ? Gatt2AccessRequirementsFromFidl(permissions.read()) + : bt::att::AccessRequirements(); + bt::att::AccessRequirements write_reqs = + permissions.has_write() + ? Gatt2AccessRequirementsFromFidl(permissions.write()) + : bt::att::AccessRequirements(); + bt::att::AccessRequirements update_reqs = + permissions.has_update() + ? Gatt2AccessRequirementsFromFidl(permissions.update()) + : bt::att::AccessRequirements(); + + bt::UUID type(fidl_chrc.type().value); + + auto chrc = + std::make_unique(fidl_chrc.handle().value, + type, + props, + ext_props, + read_reqs, + write_reqs, + update_reqs); + if (fidl_chrc.has_descriptors()) { + for (const auto& fidl_desc : fidl_chrc.descriptors()) { + std::unique_ptr maybe_desc = + fidl_helpers::Gatt2DescriptorFromFidl(fidl_desc); + if (!maybe_desc) { + // Specific failures are logged in Gatt2DescriptorFromFidl + return nullptr; + } + + chrc->AddDescriptor(std::move(maybe_desc)); + } + } + + return chrc; +} + +const char* DataPathDirectionToString( + pw::bluetooth::emboss::DataPathDirection direction) { + switch (direction) { + case pw::bluetooth::emboss::DataPathDirection::INPUT: + return "input"; + case pw::bluetooth::emboss::DataPathDirection::OUTPUT: + return "output"; + default: + return "invalid"; + } +} + +pw::bluetooth::emboss::DataPathDirection DataPathDirectionFromFidl( + const fuchsia::bluetooth::DataDirection& fidl_direction) { + switch (fidl_direction) { + case fuchsia::bluetooth::DataDirection::INPUT: + return pw::bluetooth::emboss::DataPathDirection::INPUT; + case fuchsia::bluetooth::DataDirection::OUTPUT: + return pw::bluetooth::emboss::DataPathDirection::OUTPUT; + } + BT_PANIC("Unrecognized value for data direction: %" PRIu8, fidl_direction); +} + +// Both of these types use the spec representation, so we can just assign the +// underlying value directly. +pw::bluetooth::emboss::CodingFormat CodingFormatFromFidl( + const fuchsia::bluetooth::AssignedCodingFormat& fidl_format) { + switch (fidl_format) { + case fuchsia::bluetooth::AssignedCodingFormat::U_LAW_LOG: + return pw::bluetooth::emboss::CodingFormat::U_LAW; + case fuchsia::bluetooth::AssignedCodingFormat::A_LAW_LOG: + return pw::bluetooth::emboss::CodingFormat::A_LAW; + case fuchsia::bluetooth::AssignedCodingFormat::CVSD: + return pw::bluetooth::emboss::CodingFormat::CVSD; + case fuchsia::bluetooth::AssignedCodingFormat::TRANSPARENT: + return pw::bluetooth::emboss::CodingFormat::TRANSPARENT; + case fuchsia::bluetooth::AssignedCodingFormat::LINEAR_PCM: + return pw::bluetooth::emboss::CodingFormat::LINEAR_PCM; + case fuchsia::bluetooth::AssignedCodingFormat::MSBC: + return pw::bluetooth::emboss::CodingFormat::MSBC; + case fuchsia::bluetooth::AssignedCodingFormat::LC3: + return pw::bluetooth::emboss::CodingFormat::LC3; + case fuchsia::bluetooth::AssignedCodingFormat::G_729A: + return pw::bluetooth::emboss::CodingFormat::G729A; + } + BT_PANIC("Unrecognized value for coding format: %u", + static_cast(fidl_format)); +} + +bt::StaticPacket CodecIdFromFidl( + const fuchsia::bluetooth::CodecId& fidl_codec_id) { + bt::StaticPacket result; + auto result_view = result.view(); + + if (fidl_codec_id.is_assigned_format()) { + pw::bluetooth::emboss::CodingFormat out_coding_format = + CodingFormatFromFidl(fidl_codec_id.assigned_format()); + result_view.coding_format().Write(out_coding_format); + } else { + BT_ASSERT(fidl_codec_id.is_vendor_format()); + result_view.coding_format().Write( + pw::bluetooth::emboss::CodingFormat::VENDOR_SPECIFIC); + result_view.company_id().Write(fidl_codec_id.vendor_format().company_id()); + result_view.vendor_codec_id().Write( + fidl_codec_id.vendor_format().vendor_id()); + } + return result; +} + +// Note that: +// a) The FIDL values used do not necessarily correspond to Core Spec values. +// b) Only a subset of valid values are implemented in the FIDL type at the +// moment. +pw::bluetooth::emboss::LogicalTransportType LogicalTransportTypeFromFidl( + const fuchsia::bluetooth::LogicalTransportType& fidl_transport_type) { + switch (fidl_transport_type) { + case fuchsia::bluetooth::LogicalTransportType::LE_CIS: + return pw::bluetooth::emboss::LogicalTransportType::LE_CIS; + case fuchsia::bluetooth::LogicalTransportType::LE_BIS: + return pw::bluetooth::emboss::LogicalTransportType::LE_BIS; + } + BT_PANIC("Unrecognized value for logical transport type: %u", + static_cast(fidl_transport_type)); +} + +pw::bluetooth::emboss::StatusCode FidlHciErrorToStatusCode( + fhbt::HciError code) { + switch (code) { + case fuchsia_hardware_bluetooth::HciError::kSuccess: + return pw::bluetooth::emboss::StatusCode::SUCCESS; + case fuchsia_hardware_bluetooth::HciError::kUnknownCommand: + return pw::bluetooth::emboss::StatusCode::UNKNOWN_COMMAND; + case fuchsia_hardware_bluetooth::HciError::kUnknownConnectionId: + return pw::bluetooth::emboss::StatusCode::UNKNOWN_CONNECTION_ID; + case fuchsia_hardware_bluetooth::HciError::kHardwareFailure: + return pw::bluetooth::emboss::StatusCode::HARDWARE_FAILURE; + case fuchsia_hardware_bluetooth::HciError::kPageTimeout: + return pw::bluetooth::emboss::StatusCode::PAGE_TIMEOUT; + case fuchsia_hardware_bluetooth::HciError::kAuthenticationFailure: + return pw::bluetooth::emboss::StatusCode::AUTHENTICATION_FAILURE; + case fuchsia_hardware_bluetooth::HciError::kPinOrKeyMissing: + return pw::bluetooth::emboss::StatusCode::PIN_OR_KEY_MISSING; + case fuchsia_hardware_bluetooth::HciError::kMemoryCapacityExceeded: + return pw::bluetooth::emboss::StatusCode::MEMORY_CAPACITY_EXCEEDED; + case fuchsia_hardware_bluetooth::HciError::kConnectionTimeout: + return pw::bluetooth::emboss::StatusCode::CONNECTION_TIMEOUT; + case fuchsia_hardware_bluetooth::HciError::kConnectionLimitExceeded: + return pw::bluetooth::emboss::StatusCode::CONNECTION_LIMIT_EXCEEDED; + case fuchsia_hardware_bluetooth::HciError:: + kSynchronousConnectionLimitExceeded: + return pw::bluetooth::emboss::StatusCode:: + SYNCHRONOUS_CONNECTION_LIMIT_EXCEEDED; + case fuchsia_hardware_bluetooth::HciError::kConnectionAlreadyExists: + return pw::bluetooth::emboss::StatusCode::CONNECTION_ALREADY_EXISTS; + case fuchsia_hardware_bluetooth::HciError::kCommandDisallowed: + return pw::bluetooth::emboss::StatusCode::COMMAND_DISALLOWED; + case fuchsia_hardware_bluetooth::HciError:: + kConnectionRejectedLimitedResources: + return pw::bluetooth::emboss::StatusCode:: + CONNECTION_REJECTED_LIMITED_RESOURCES; + case fuchsia_hardware_bluetooth::HciError::kConnectionRejectedSecurity: + return pw::bluetooth::emboss::StatusCode::CONNECTION_REJECTED_SECURITY; + case fuchsia_hardware_bluetooth::HciError::kConnectionRejectedBadBdAddr: + return pw::bluetooth::emboss::StatusCode::CONNECTION_REJECTED_BAD_BD_ADDR; + case fuchsia_hardware_bluetooth::HciError::kConnectionAcceptTimeoutExceeded: + return pw::bluetooth::emboss::StatusCode:: + CONNECTION_ACCEPT_TIMEOUT_EXCEEDED; + case fuchsia_hardware_bluetooth::HciError::kUnsupportedFeatureOrParameter: + return pw::bluetooth::emboss::StatusCode:: + UNSUPPORTED_FEATURE_OR_PARAMETER; + case fuchsia_hardware_bluetooth::HciError::kInvalidHcicommandParameters: + return pw::bluetooth::emboss::StatusCode::INVALID_HCI_COMMAND_PARAMETERS; + case fuchsia_hardware_bluetooth::HciError::kRemoteUserTerminatedConnection: + return pw::bluetooth::emboss::StatusCode:: + REMOTE_USER_TERMINATED_CONNECTION; + case fuchsia_hardware_bluetooth::HciError:: + kRemoteDeviceTerminatedConnectionLowResources: + return pw::bluetooth::emboss::StatusCode:: + REMOTE_DEVICE_TERMINATED_CONNECTION_LOW_RESOURCES; + case fuchsia_hardware_bluetooth::HciError:: + kRemoteDeviceTerminatedConnectionPowerOff: + return pw::bluetooth::emboss::StatusCode:: + REMOTE_DEVICE_TERMINATED_CONNECTION_POWER_OFF; + case fuchsia_hardware_bluetooth::HciError::kConnectionTerminatedByLocalHost: + return pw::bluetooth::emboss::StatusCode:: + CONNECTION_TERMINATED_BY_LOCAL_HOST; + case fuchsia_hardware_bluetooth::HciError::kRepeatedAttempts: + return pw::bluetooth::emboss::StatusCode::REPEATED_ATTEMPTS; + case fuchsia_hardware_bluetooth::HciError::kPairingNotAllowed: + return pw::bluetooth::emboss::StatusCode::PAIRING_NOT_ALLOWED; + case fuchsia_hardware_bluetooth::HciError::kUnknownLmpPdu: + return pw::bluetooth::emboss::StatusCode::UNKNOWN_LMP_PDU; + case fuchsia_hardware_bluetooth::HciError::kUnsupportedRemoteFeature: + return pw::bluetooth::emboss::StatusCode::UNSUPPORTED_REMOTE_FEATURE; + case fuchsia_hardware_bluetooth::HciError::kScoOffsetRejected: + return pw::bluetooth::emboss::StatusCode::SCO_OFFSET_REJECTED; + case fuchsia_hardware_bluetooth::HciError::kScoIntervalRejected: + return pw::bluetooth::emboss::StatusCode::SCO_INTERVAL_REJECTED; + case fuchsia_hardware_bluetooth::HciError::kScoAirModeRejected: + return pw::bluetooth::emboss::StatusCode::SCO_AIRMODE_REJECTED; + case fuchsia_hardware_bluetooth::HciError::kInvalidLmpOrLlParameters: + return pw::bluetooth::emboss::StatusCode::INVALID_LMP_OR_LL_PARAMETERS; + case fuchsia_hardware_bluetooth::HciError::kUnspecifiedError: + return pw::bluetooth::emboss::StatusCode::UNSPECIFIED_ERROR; + case fuchsia_hardware_bluetooth::HciError:: + kUnsupportedLmpOrLlParameterValue: + return pw::bluetooth::emboss::StatusCode:: + UNSUPPORTED_LMP_OR_LL_PARAMETER_VALUE; + case fuchsia_hardware_bluetooth::HciError::kRoleChangeNotAllowed: + return pw::bluetooth::emboss::StatusCode::ROLE_CHANGE_NOT_ALLOWED; + case fuchsia_hardware_bluetooth::HciError::kLmpOrLlResponseTimeout: + return pw::bluetooth::emboss::StatusCode::LMP_OR_LL_RESPONSE_TIMEOUT; + case fuchsia_hardware_bluetooth::HciError::kLmpErrorTransactionCollision: + return pw::bluetooth::emboss::StatusCode::LMP_ERROR_TRANSACTION_COLLISION; + case fuchsia_hardware_bluetooth::HciError::kLmpPduNotAllowed: + return pw::bluetooth::emboss::StatusCode::LMP_PDU_NOT_ALLOWED; + case fuchsia_hardware_bluetooth::HciError::kEncryptionModeNotAcceptable: + return pw::bluetooth::emboss::StatusCode::ENCRYPTION_MODE_NOT_ACCEPTABLE; + case fuchsia_hardware_bluetooth::HciError::kLinkKeyCannotBeChanged: + return pw::bluetooth::emboss::StatusCode::LINK_KEY_CANNOT_BE_CHANGED; + case fuchsia_hardware_bluetooth::HciError::kRequestedQosNotSupported: + return pw::bluetooth::emboss::StatusCode::REQUESTED_QOS_NOT_SUPPORTED; + case fuchsia_hardware_bluetooth::HciError::kInstantPassed: + return pw::bluetooth::emboss::StatusCode::INSTANT_PASSED; + case fuchsia_hardware_bluetooth::HciError::kPairingWithUnitKeyNotSupported: + return pw::bluetooth::emboss::StatusCode:: + PAIRING_WITH_UNIT_KEY_NOT_SUPPORTED; + case fuchsia_hardware_bluetooth::HciError::kDifferentTransactionCollision: + return pw::bluetooth::emboss::StatusCode::DIFFERENT_TRANSACTION_COLLISION; + case fuchsia_hardware_bluetooth::HciError::kReserved0: + return pw::bluetooth::emboss::StatusCode::RESERVED_0; + case fuchsia_hardware_bluetooth::HciError::kQosUnacceptableParameter: + return pw::bluetooth::emboss::StatusCode::QOS_UNACCEPTABLE_PARAMETER; + case fuchsia_hardware_bluetooth::HciError::kQosRejected: + return pw::bluetooth::emboss::StatusCode::QOS_REJECTED; + case fuchsia_hardware_bluetooth::HciError:: + kChannelClassificationNotSupported: + return pw::bluetooth::emboss::StatusCode:: + CHANNEL_CLASSIFICATION_NOT_SUPPORTED; + case fuchsia_hardware_bluetooth::HciError::kInsufficientSecurity: + return pw::bluetooth::emboss::StatusCode::INSUFFICIENT_SECURITY; + case fuchsia_hardware_bluetooth::HciError::kParameterOutOfMandatoryRange: + return pw::bluetooth::emboss::StatusCode:: + PARAMETER_OUT_OF_MANDATORY_RANGE; + case fuchsia_hardware_bluetooth::HciError::kReserved1: + return pw::bluetooth::emboss::StatusCode::RESERVED_1; + case fuchsia_hardware_bluetooth::HciError::kRoleSwitchPending: + return pw::bluetooth::emboss::StatusCode::ROLE_SWITCH_PENDING; + case fuchsia_hardware_bluetooth::HciError::kReserved2: + return pw::bluetooth::emboss::StatusCode::RESERVED_2; + case fuchsia_hardware_bluetooth::HciError::kReservedSlotViolation: + return pw::bluetooth::emboss::StatusCode::RESERVED_SLOT_VIOLATION; + case fuchsia_hardware_bluetooth::HciError::kRoleSwitchFailed: + return pw::bluetooth::emboss::StatusCode::ROLE_SWITCH_FAILED; + case fuchsia_hardware_bluetooth::HciError::kExtendedInquiryResponseTooLarge: + return pw::bluetooth::emboss::StatusCode:: + EXTENDED_INQUIRY_RESPONSE_TOO_LARGE; + case fuchsia_hardware_bluetooth::HciError:: + kSecureSimplePairingNotSupportedByHost: + return pw::bluetooth::emboss::StatusCode:: + SECURE_SIMPLE_PAIRING_NOT_SUPPORTED_BY_HOST; + case fuchsia_hardware_bluetooth::HciError::kHostBusyPairing: + return pw::bluetooth::emboss::StatusCode::HOST_BUSY_PAIRING; + case fuchsia_hardware_bluetooth::HciError:: + kConnectionRejectedNoSuitableChannelFound: + return pw::bluetooth::emboss::StatusCode:: + CONNECTION_REJECTED_NO_SUITABLE_CHANNEL_FOUND; + case fuchsia_hardware_bluetooth::HciError::kControllerBusy: + return pw::bluetooth::emboss::StatusCode::CONTROLLER_BUSY; + case fuchsia_hardware_bluetooth::HciError:: + kUnacceptableConnectionParameters: + return pw::bluetooth::emboss::StatusCode:: + UNACCEPTABLE_CONNECTION_PARAMETERS; + case fuchsia_hardware_bluetooth::HciError::kDirectedAdvertisingTimeout: + return pw::bluetooth::emboss::StatusCode::DIRECTED_ADVERTISING_TIMEOUT; + case fuchsia_hardware_bluetooth::HciError::kConnectionTerminatedMicFailure: + return pw::bluetooth::emboss::StatusCode:: + CONNECTION_TERMINATED_MIC_FAILURE; + case fuchsia_hardware_bluetooth::HciError::kConnectionFailedToBeEstablished: + return pw::bluetooth::emboss::StatusCode:: + CONNECTION_FAILED_TO_BE_ESTABLISHED; + case fuchsia_hardware_bluetooth::HciError::kMacConnectionFailed: + return pw::bluetooth::emboss::StatusCode::MAC_CONNECTION_FAILED; + case fuchsia_hardware_bluetooth::HciError::kCoarseClockAdjustmentRejected: + return pw::bluetooth::emboss::StatusCode:: + COARSE_CLOCK_ADJUSTMENT_REJECTED; + case fuchsia_hardware_bluetooth::HciError::kType0SubmapNotDefined: + return pw::bluetooth::emboss::StatusCode::TYPE_0_SUBMAP_NOT_DEFINED; + case fuchsia_hardware_bluetooth::HciError::kUnknownAdvertisingIdentifier: + return pw::bluetooth::emboss::StatusCode::UNKNOWN_ADVERTISING_IDENTIFIER; + case fuchsia_hardware_bluetooth::HciError::kLimitReached: + return pw::bluetooth::emboss::StatusCode::LIMIT_REACHED; + case fuchsia_hardware_bluetooth::HciError::kOperationCancelledByHost: + return pw::bluetooth::emboss::StatusCode::OPERATION_CANCELLED_BY_HOST; + case fuchsia_hardware_bluetooth::HciError::kPacketTooLong: + return pw::bluetooth::emboss::StatusCode::PACKET_TOO_LONG; + case fuchsia_hardware_bluetooth::HciError::kTooLate: + return pw::bluetooth::emboss::StatusCode::TOO_LATE; + case fuchsia_hardware_bluetooth::HciError::kTooEarly: + return pw::bluetooth::emboss::StatusCode::TOO_EARLY; + default: + return pw::bluetooth::emboss::StatusCode::UNKNOWN_COMMAND; + } +} + +fuchsia::bluetooth::le::CisEstablishedParameters CisEstablishedParametersToFidl( + const bt::iso::CisEstablishedParameters& params_in) { + fuchsia::bluetooth::le::CisEstablishedParameters params_out; + + // General parameters + zx::duration cig_sync_delay = zx::usec(params_in.cig_sync_delay); + params_out.set_cig_sync_delay(cig_sync_delay.get()); + zx::duration cis_sync_delay = zx::usec(params_in.cis_sync_delay); + params_out.set_cis_sync_delay(cis_sync_delay.get()); + params_out.set_max_subevents(params_in.max_subevents); + zx::duration iso_interval = + zx::usec(params_in.iso_interval * + bt::iso::CisEstablishedParameters::kIsoIntervalToMicroseconds); + params_out.set_iso_interval(iso_interval.get()); + + // Central => Peripheral parameters + // phy and max_pdu_size are not passed back to FIDL client + if (params_in.c_to_p_params.burst_number > 0) { + fuchsia::bluetooth::le::CisUnidirectionalParams c_to_p_params; + zx::duration transport_latency = + zx::usec(params_in.c_to_p_params.transport_latency); + c_to_p_params.set_transport_latency(transport_latency.get()); + c_to_p_params.set_burst_number(params_in.c_to_p_params.burst_number); + c_to_p_params.set_flush_timeout(params_in.c_to_p_params.flush_timeout); + params_out.set_central_to_peripheral_params(std::move(c_to_p_params)); + } + + // Peripheral => Central parameters + // phy and max_pdu_size are not passed back to FIDL client + if (params_in.p_to_c_params.burst_number > 0) { + fuchsia::bluetooth::le::CisUnidirectionalParams p_to_c_params; + zx::duration transport_latency = + zx::usec(params_in.p_to_c_params.transport_latency); + p_to_c_params.set_transport_latency(transport_latency.get()); + p_to_c_params.set_burst_number(params_in.p_to_c_params.burst_number); + p_to_c_params.set_flush_timeout(params_in.p_to_c_params.flush_timeout); + params_out.set_peripheral_to_central_params(std::move(p_to_c_params)); + } + + return params_out; +} + +} // namespace bthost::fidl_helpers + +// static +std::vector +fidl::TypeConverter, bt::ByteBuffer>::Convert( + const bt::ByteBuffer& from) { + std::vector to(from.size()); + bt::MutableBufferView view(to.data(), to.size()); + view.Write(from); + return to; +} diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/helpers_test.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/helpers_test.cc new file mode 100644 index 0000000000..73fb04c6ae --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/helpers_test.cc @@ -0,0 +1,2734 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/fidl/helpers.h" + +#include +#include + +#include +#include + +#include "fuchsia/bluetooth/bredr/cpp/fidl.h" +#include "fuchsia/bluetooth/cpp/fidl.h" +#include "fuchsia/bluetooth/le/cpp/fidl.h" +#include "fuchsia/bluetooth/sys/cpp/fidl.h" +#include "fuchsia/media/cpp/fidl.h" +#include "lib/fidl/cpp/comparison.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/adapter_test_fixture.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/fake_adapter_test_fixture.h" +#include "pw_bluetooth_sapphire/internal/host/common/advertising_data.h" +#include "pw_bluetooth_sapphire/internal/host/common/device_address.h" +#include "pw_bluetooth_sapphire/internal/host/common/uuid.h" +#include "pw_bluetooth_sapphire/internal/host/gap/gap.h" +#include "pw_bluetooth_sapphire/internal/host/iso/iso_common.h" +#include "pw_bluetooth_sapphire/internal/host/sco/sco.h" +#include "pw_bluetooth_sapphire/internal/host/sdp/data_element.h" +#include "pw_bluetooth_sapphire/internal/host/sdp/sdp.h" +#include "pw_bluetooth_sapphire/internal/host/sdp/service_record.h" +#include "pw_bluetooth_sapphire/internal/host/sm/types.h" +#include "pw_bluetooth_sapphire/internal/host/testing/loop_fixture.h" +#include "pw_bluetooth_sapphire/internal/host/testing/test_helpers.h" +#include "pw_unit_test/framework.h" + +namespace fble = fuchsia::bluetooth::le; +namespace fbt = fuchsia::bluetooth; +namespace fsys = fuchsia::bluetooth::sys; +namespace fbg = fuchsia::bluetooth::gatt; +namespace fbg2 = fuchsia::bluetooth::gatt2; +namespace fbredr = fuchsia::bluetooth::bredr; +namespace faudio = fuchsia::hardware::audio; + +namespace fuchsia::bluetooth { +// Make UUIDs equality comparable for advanced testing matchers. ADL rules +// mandate the namespace. +bool operator==(const Uuid& a, const Uuid& b) { return fidl::Equals(a, b); } +} // namespace fuchsia::bluetooth + +namespace bthost::fidl_helpers { +namespace { + +// Constants as BT stack types +const bt::UInt128 kTestKeyValue{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; +const bt::sm::SecurityProperties kTestSecurity( + bt::sm::SecurityLevel::kSecureAuthenticated, + 16, + /*secure_connections=*/true); +const bt::sm::LTK kTestLtk(kTestSecurity, + bt::hci_spec::LinkKey(kTestKeyValue, 0, 0)); +const bt::sm::Key kTestKey(kTestSecurity, kTestKeyValue); + +// Constants as FIDL types +const fbt::Address kPublicAddrFidl = + fbt::Address{fbt::AddressType::PUBLIC, {1, 0, 0, 0, 0, 0}}; +const fbt::Address kRandomAddrFidl = + fbt::Address{fbt::AddressType::RANDOM, {2, 0, 0, 0, 0, 0b11000011}}; +const fbt::Address kRandomAddrResolvableFidl{ + fbt::AddressType::RANDOM, {0x55, 0x44, 0x33, 0x22, 0x11, 0b01000011}}; +const fbt::Address kRandomAddrNonResolvableFidl{ + fbt::AddressType::RANDOM, {0x55, 0x44, 0x33, 0x22, 0x11, 0x00}}; + +const bt::DeviceAddress kTestPeerAddr(bt::DeviceAddress::Type::kBREDR, + {1, 0, 0, 0, 0, 0}); +const bt::DeviceAddress kLePublicAddress(bt::DeviceAddress::Type::kLEPublic, + {1, 0, 0, 0, 0, 0}); + +const fsys::PeerKey kTestKeyFidl{ + .security = + fsys::SecurityProperties{ + .authenticated = true, + .secure_connections = true, + .encryption_key_size = 16, + }, + .data = fsys::Key{.value = kTestKeyValue}, +}; +const fsys::Ltk kTestLtkFidl{.key = kTestKeyFidl, .ediv = 0, .rand = 0}; + +// Minimum values permitted for the CIS_ESTABLISHED unidirectional parameters +bt::iso::CisEstablishedParameters::CisUnidirectionalParams kMinUniParams = { + .transport_latency = 0x0000ea, + .phy = pw::bluetooth::emboss::IsoPhyType::LE_1M, + .burst_number = 0x01, + .flush_timeout = 0x01, + .max_pdu_size = 0x00, +}; + +// Maximum values permitted for the CIS_ESTABLISHED unidirectional parameters +bt::iso::CisEstablishedParameters::CisUnidirectionalParams kMaxUniParams = { + .transport_latency = 0x7fffff, + .phy = pw::bluetooth::emboss::IsoPhyType::LE_CODED, + .burst_number = 0x0f, + .flush_timeout = 0xff, + .max_pdu_size = 0xfb, +}; + +class HelpersTestWithLoop : public bt::testing::TestLoopFixture { + public: + pw::async::Dispatcher& pw_dispatcher() { return pw_dispatcher_; } + + private: + pw::async_fuchsia::FuchsiaDispatcher pw_dispatcher_{dispatcher()}; +}; + +TEST(HelpersTest, HostErrorToFidl) { + EXPECT_EQ(fsys::Error::FAILED, HostErrorToFidl(bt::HostError::kFailed)); + EXPECT_EQ(fsys::Error::TIMED_OUT, HostErrorToFidl(bt::HostError::kTimedOut)); + EXPECT_EQ(fsys::Error::INVALID_ARGUMENTS, + HostErrorToFidl(bt::HostError::kInvalidParameters)); + EXPECT_EQ(fsys::Error::CANCELED, HostErrorToFidl(bt::HostError::kCanceled)); + EXPECT_EQ(fsys::Error::IN_PROGRESS, + HostErrorToFidl(bt::HostError::kInProgress)); + EXPECT_EQ(fsys::Error::NOT_SUPPORTED, + HostErrorToFidl(bt::HostError::kNotSupported)); + EXPECT_EQ(fsys::Error::PEER_NOT_FOUND, + HostErrorToFidl(bt::HostError::kNotFound)); + + // All other errors currently map to FAILED. + EXPECT_EQ(fsys::Error::FAILED, HostErrorToFidl(bt::HostError::kNotReady)); +} + +TEST(HelpersTest, GattErrorToFidl) { + // Host errors + EXPECT_EQ(fbg::Error::INVALID_RESPONSE, + GattErrorToFidl(bt::Error(bt::HostError::kPacketMalformed))); + EXPECT_EQ(fbg::Error::FAILURE, + GattErrorToFidl(bt::Error(bt::HostError::kTimedOut))); + + // Protocol errors + EXPECT_EQ(fbg::Error::INSUFFICIENT_AUTHORIZATION, + GattErrorToFidl(bt::att::Error( + bt::att::ErrorCode::kInsufficientAuthorization))); + EXPECT_EQ(fbg::Error::INSUFFICIENT_AUTHENTICATION, + GattErrorToFidl(bt::att::Error( + bt::att::ErrorCode::kInsufficientAuthentication))); + EXPECT_EQ(fbg::Error::INSUFFICIENT_ENCRYPTION_KEY_SIZE, + GattErrorToFidl(bt::att::Error( + bt::att::ErrorCode::kInsufficientEncryptionKeySize))); + EXPECT_EQ(fbg::Error::INSUFFICIENT_ENCRYPTION, + GattErrorToFidl( + bt::att::Error(bt::att::ErrorCode::kInsufficientEncryption))); + EXPECT_EQ( + fbg::Error::READ_NOT_PERMITTED, + GattErrorToFidl(bt::att::Error(bt::att::ErrorCode::kReadNotPermitted))); + EXPECT_EQ( + fbg::Error::FAILURE, + GattErrorToFidl(bt::att::Error(bt::att::ErrorCode::kUnlikelyError))); +} + +TEST(HelpersTest, AttErrorToGattFidlError) { + // Host errors + EXPECT_EQ( + fbg2::Error::INVALID_PDU, + AttErrorToGattFidlError(bt::Error(bt::HostError::kPacketMalformed))); + EXPECT_EQ( + fbg2::Error::INVALID_PARAMETERS, + AttErrorToGattFidlError(bt::Error(bt::HostError::kInvalidParameters))); + EXPECT_EQ(fbg2::Error::UNLIKELY_ERROR, + AttErrorToGattFidlError(bt::Error(bt::HostError::kTimedOut))); + + // Protocol errors + EXPECT_EQ(fbg2::Error::INSUFFICIENT_AUTHORIZATION, + AttErrorToGattFidlError(bt::att::Error( + bt::att::ErrorCode::kInsufficientAuthorization))); + EXPECT_EQ(fbg2::Error::INSUFFICIENT_AUTHENTICATION, + AttErrorToGattFidlError(bt::att::Error( + bt::att::ErrorCode::kInsufficientAuthentication))); + EXPECT_EQ(fbg2::Error::INSUFFICIENT_ENCRYPTION_KEY_SIZE, + AttErrorToGattFidlError(bt::att::Error( + bt::att::ErrorCode::kInsufficientEncryptionKeySize))); + EXPECT_EQ(fbg2::Error::INSUFFICIENT_ENCRYPTION, + AttErrorToGattFidlError( + bt::att::Error(bt::att::ErrorCode::kInsufficientEncryption))); + EXPECT_EQ(fbg2::Error::READ_NOT_PERMITTED, + AttErrorToGattFidlError( + bt::att::Error(bt::att::ErrorCode::kReadNotPermitted))); + EXPECT_EQ(fbg2::Error::INVALID_HANDLE, + AttErrorToGattFidlError( + bt::att::Error(bt::att::ErrorCode::kInvalidHandle))); + EXPECT_EQ(fbg2::Error::UNLIKELY_ERROR, + AttErrorToGattFidlError( + bt::att::Error(bt::att::ErrorCode::kUnlikelyError))); +} + +TEST(HelpersTest, AdvertisingIntervalFromFidl) { + EXPECT_EQ(bt::gap::AdvertisingInterval::FAST1, + AdvertisingIntervalFromFidl(fble::AdvertisingModeHint::VERY_FAST)); + EXPECT_EQ(bt::gap::AdvertisingInterval::FAST2, + AdvertisingIntervalFromFidl(fble::AdvertisingModeHint::FAST)); + EXPECT_EQ(bt::gap::AdvertisingInterval::SLOW, + AdvertisingIntervalFromFidl(fble::AdvertisingModeHint::SLOW)); +} + +TEST(HelpersTest, UuidFromFidl) { + // Test HLCPP FIDL bindings with fuchsia::bluetooth::Uuid + fbt::Uuid input; + input.value = {{0xFB, + 0x34, + 0x9B, + 0x5F, + 0x80, + 0x00, + 0x00, + 0x80, + 0x00, + 0x10, + 0x00, + 0x00, + 0x0d, + 0x18, + 0x00, + 0x00}}; + + // We expect the input bytes to be carried over directly. + bt::UUID output = UuidFromFidl(input); + EXPECT_EQ("0000180d-0000-1000-8000-00805f9b34fb", output.ToString()); + EXPECT_EQ(2u, output.CompactSize()); + + // Test new C++ FIDL bindings with fuchsia_bluetooth::Uuid + fuchsia_bluetooth::Uuid input2; + input2.value({0xFB, + 0x34, + 0x9B, + 0x5F, + 0x80, + 0x00, + 0x00, + 0x80, + 0x00, + 0x10, + 0x00, + 0x00, + 0x0d, + 0x18, + 0x00, + 0x00}); + + // We expect the input bytes to be carried over directly. + output = NewUuidFromFidl(input2); + EXPECT_EQ("0000180d-0000-1000-8000-00805f9b34fb", output.ToString()); + EXPECT_EQ(2u, output.CompactSize()); +} + +TEST(HelpersTest, FidlToScmsTEnableTest) { + bt::StaticPacket result_enable = + FidlToScmsTEnable(true); + EXPECT_EQ(result_enable.view().enabled().Read(), + pw::bluetooth::emboss::GenericEnableParam::ENABLE); + EXPECT_EQ(result_enable.view().header().Read(), 0x0); + + bt::StaticPacket result_disable = + FidlToScmsTEnable(false); + EXPECT_EQ(result_disable.view().enabled().Read(), + pw::bluetooth::emboss::GenericEnableParam::DISABLE); + EXPECT_EQ(result_disable.view().header().Read(), 0x0); +} + +TEST(HelpersTest, FidlToEncoderSettingsSbcTest) { + std::unique_ptr encoder_settings = + std::make_unique(); + std::unique_ptr value = + fuchsia::media::SbcEncoderSettings::New(); + encoder_settings->set_sbc(*value); + + fbredr::AudioSamplingFrequency sampling_frequency = + fbredr::AudioSamplingFrequency::HZ_44100; + fbredr::AudioChannelMode channel_mode = fbredr::AudioChannelMode::MONO; + + bt::StaticPacket result = + FidlToEncoderSettingsSbc( + *encoder_settings, sampling_frequency, channel_mode); + + EXPECT_EQ(android_emb::SbcAllocationMethod::LOUDNESS, + result.view().allocation_method().Read()); + EXPECT_EQ(android_emb::SbcSubBands::SUBBANDS_8, + result.view().subbands().Read()); + EXPECT_EQ(android_emb::SbcBlockLen::BLOCK_LEN_4, + result.view().block_length().Read()); + EXPECT_EQ(0, result.view().min_bitpool_value().Read()); + EXPECT_EQ(0, result.view().max_bitpool_value().Read()); + EXPECT_EQ(android_emb::SbcSamplingFrequency::HZ_44100, + result.view().sampling_frequency().Read()); + EXPECT_EQ(android_emb::SbcChannelMode::MONO, + result.view().channel_mode().Read()); +} + +TEST(HelpersTest, FidlToEncoderSettingsAacTest) { + std::unique_ptr encoder_settings = + std::make_unique(); + std::unique_ptr value = + fuchsia::media::AacEncoderSettings::New(); + value->aot = fuchsia::media::AacAudioObjectType::MPEG4_AAC_LC; + value->bit_rate.set_variable(::fuchsia::media::AacVariableBitRate::V1); + encoder_settings->set_aac(std::move(*value)); + + fbredr::AudioSamplingFrequency sampling_frequency = + fbredr::AudioSamplingFrequency::HZ_44100; + fbredr::AudioChannelMode channel_mode = fbredr::AudioChannelMode::MONO; + + bt::StaticPacket result = + FidlToEncoderSettingsAac( + *encoder_settings, sampling_frequency, channel_mode); + + EXPECT_EQ(result.view().object_type().Read(), 1); + EXPECT_EQ(result.view().variable_bit_rate().Read(), + android_emb::AacEnableVariableBitRate::ENABLE); +} + +template +void FidlToDataElementIntegerTest( + const std::function& func, + bt::sdp::DataElement::Type type) { + fbredr::DataElement data_element = func(std::numeric_limits::max()); + std::optional result = FidlToDataElement(data_element); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(type, result->type()); + EXPECT_EQ(std::numeric_limits::max(), result->Get()); + + // result->size() returns an enum member of DataElement::Size indicating the + // number of bytes + uint8_t exponent = static_cast(result->size()); + EXPECT_EQ(sizeof(T), std::pow(2, exponent)); +} + +TEST(HelpersTest, FidlToDataElementInt8Test) { + FidlToDataElementIntegerTest(std::function(fbredr::DataElement::WithInt8), + bt::sdp::DataElement::Type::kSignedInt); +} + +TEST(HelpersTest, FidlToDataElementInt16Test) { + FidlToDataElementIntegerTest(std::function(fbredr::DataElement::WithInt16), + bt::sdp::DataElement::Type::kSignedInt); +} + +TEST(HelpersTest, FidlToDataElementInt32Test) { + FidlToDataElementIntegerTest(std::function(fbredr::DataElement::WithInt32), + bt::sdp::DataElement::Type::kSignedInt); +} + +TEST(HelpersTest, FidlToDataElementInt64Test) { + FidlToDataElementIntegerTest(std::function(fbredr::DataElement::WithInt64), + bt::sdp::DataElement::Type::kSignedInt); +} + +TEST(HelpersTest, FidlToDataElementUint8Test) { + FidlToDataElementIntegerTest(std::function(fbredr::DataElement::WithUint8), + bt::sdp::DataElement::Type::kUnsignedInt); +} + +TEST(HelpersTest, FidlToDataElementUint16Test) { + FidlToDataElementIntegerTest(std::function(fbredr::DataElement::WithUint16), + bt::sdp::DataElement::Type::kUnsignedInt); +} + +TEST(HelpersTest, FidlToDataElementUint32Test) { + FidlToDataElementIntegerTest(std::function(fbredr::DataElement::WithUint32), + bt::sdp::DataElement::Type::kUnsignedInt); +} + +TEST(HelpersTest, FidlToDataElementUint64Test) { + FidlToDataElementIntegerTest(std::function(fbredr::DataElement::WithUint64), + bt::sdp::DataElement::Type::kUnsignedInt); +} + +TEST(HelpersTest, FidlToDataElementEmptyStringTest) { + std::vector data; + ASSERT_EQ(0u, data.size()); + + fbredr::DataElement data_element = + fbredr::DataElement::WithStr(std::move(data)); + std::optional result = FidlToDataElement(data_element); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(bt::sdp::DataElement::Type::kString, result->type()); + EXPECT_EQ("", result->Get()); + EXPECT_EQ(bt::sdp::DataElement::Size::kNextOne, result->size()); +} + +TEST(HelpersTest, FidlToDataElementStringTest) { + std::string expected_str = "foobarbaz"; + std::vector data(expected_str.size(), 0); + std::memcpy(data.data(), expected_str.data(), expected_str.size()); + + fbredr::DataElement data_element = + fbredr::DataElement::WithStr(std::move(data)); + std::optional result = FidlToDataElement(data_element); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(bt::sdp::DataElement::Type::kString, result->type()); + EXPECT_EQ(expected_str, result->Get()); + EXPECT_EQ(bt::sdp::DataElement::Size::kNextOne, result->size()); +} + +TEST(HelpersTest, FidlToDataElementUrlTest) { + std::string url = "http://www.google.com"; + std::string moved = url; + fbredr::DataElement data_element = + fbredr::DataElement::WithUrl(std::move(moved)); + std::optional result = FidlToDataElement(data_element); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(bt::sdp::DataElement::Type::kUrl, result->type()); + EXPECT_EQ(url, result->GetUrl()); + EXPECT_EQ(bt::sdp::DataElement::Size::kNextOne, result->size()); +} + +TEST(HelpersTest, FidlToDataElementBooleanTest) { + bool expected = true; + bool moved = expected; + fbredr::DataElement data_element = + fbredr::DataElement::WithB(std::move(moved)); + std::optional result = FidlToDataElement(data_element); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(bt::sdp::DataElement::Type::kBoolean, result->type()); + EXPECT_EQ(expected, result->Get()); + EXPECT_EQ(bt::sdp::DataElement::Size::kOneByte, result->size()); +} + +TEST(HelpersTest, FidlToDataElementUuidTest) { + std::unique_ptr uuid = fbt::Uuid::New(); + uuid->value.fill(123); + + fbredr::DataElement data_element = + fbredr::DataElement::WithUuid(std::move(*uuid)); + std::optional result = FidlToDataElement(data_element); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(bt::sdp::DataElement::Type::kUuid, result->type()); + EXPECT_EQ(bt::sdp::DataElement::Size::kSixteenBytes, result->size()); + + bt::DynamicByteBuffer bytes(16); + bytes.Fill(123); + + bt::UUID expected; + ASSERT_TRUE(bt::UUID::FromBytes(bytes, &expected)); + EXPECT_EQ(expected, result->Get()); +} + +TEST(HelpersTest, FidlToDataElementSequenceTest) { + int8_t size = 3; + std::vector> moved; + std::vector expected; + + for (int16_t i = 0; i < size; i++) { + expected.emplace_back(i); + moved.push_back(std::make_unique( + fbredr::DataElement::WithInt16(std::move(i)))); + } + + fbredr::DataElement data_element = + fbredr::DataElement::WithSequence(std::move(moved)); + std::optional result = FidlToDataElement(data_element); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(bt::sdp::DataElement::Type::kSequence, result->type()); + EXPECT_EQ(bt::sdp::DataElement::Size::kNextOne, result->size()); + + std::optional> actual = + result->Get>(); + EXPECT_TRUE(actual); + + for (int8_t i = 0; i < size; i++) { + EXPECT_EQ(expected[i].Get(), actual.value()[i].Get()); + } +} + +template +void NewFidlToDataElementIntegerTest( + const std::function& func, + bt::sdp::DataElement::Type type) { + fuchsia_bluetooth_bredr::DataElement data_element = + func(std::numeric_limits::max()); + std::optional result = + NewFidlToDataElement(data_element); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(type, result->type()); + EXPECT_EQ(std::numeric_limits::max(), result->Get()); + + // result->size() returns an enum member of DataElement::Size indicating the + // number of bytes + uint8_t exponent = static_cast(result->size()); + EXPECT_EQ(sizeof(T), std::pow(2, exponent)); +} + +TEST(HelpersTest, NewFidlToDataElementInt8Test) { + NewFidlToDataElementIntegerTest( + std::function(fuchsia_bluetooth_bredr::DataElement::WithInt8), + bt::sdp::DataElement::Type::kSignedInt); +} + +TEST(HelpersTest, NewFidlToDataElementInt16Test) { + NewFidlToDataElementIntegerTest( + std::function(fuchsia_bluetooth_bredr::DataElement::WithInt16), + bt::sdp::DataElement::Type::kSignedInt); +} + +TEST(HelpersTest, NewFidlToDataElementInt32Test) { + NewFidlToDataElementIntegerTest( + std::function(fuchsia_bluetooth_bredr::DataElement::WithInt32), + bt::sdp::DataElement::Type::kSignedInt); +} + +TEST(HelpersTest, NewFidlToDataElementInt64Test) { + NewFidlToDataElementIntegerTest( + std::function(fuchsia_bluetooth_bredr::DataElement::WithInt64), + bt::sdp::DataElement::Type::kSignedInt); +} + +TEST(HelpersTest, NewFidlToDataElementUint8Test) { + NewFidlToDataElementIntegerTest( + std::function(fuchsia_bluetooth_bredr::DataElement::WithUint8), + bt::sdp::DataElement::Type::kUnsignedInt); +} + +TEST(HelpersTest, NewFidlToDataElementUint16Test) { + NewFidlToDataElementIntegerTest( + std::function(fuchsia_bluetooth_bredr::DataElement::WithUint16), + bt::sdp::DataElement::Type::kUnsignedInt); +} + +TEST(HelpersTest, NewFidlToDataElementUint32Test) { + NewFidlToDataElementIntegerTest( + std::function(fuchsia_bluetooth_bredr::DataElement::WithUint32), + bt::sdp::DataElement::Type::kUnsignedInt); +} + +TEST(HelpersTest, NewFidlToDataElementUint64Test) { + NewFidlToDataElementIntegerTest( + std::function(fuchsia_bluetooth_bredr::DataElement::WithUint64), + bt::sdp::DataElement::Type::kUnsignedInt); +} + +TEST(HelpersTest, NewFidlToDataElementEmptyStringTest) { + std::vector data; + ASSERT_EQ(0u, data.size()); + + fuchsia_bluetooth_bredr::DataElement data_element = + fuchsia_bluetooth_bredr::DataElement::WithStr(std::move(data)); + std::optional result = + NewFidlToDataElement(data_element); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(bt::sdp::DataElement::Type::kString, result->type()); + EXPECT_EQ("", result->Get()); + EXPECT_EQ(bt::sdp::DataElement::Size::kNextOne, result->size()); +} + +TEST(HelpersTest, NewFidlToDataElementStringTest) { + std::string expected_str = "foobarbaz"; + std::vector data(expected_str.size(), 0); + std::memcpy(data.data(), expected_str.data(), expected_str.size()); + + fuchsia_bluetooth_bredr::DataElement data_element = + fuchsia_bluetooth_bredr::DataElement::WithStr(std::move(data)); + std::optional result = + NewFidlToDataElement(data_element); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(bt::sdp::DataElement::Type::kString, result->type()); + EXPECT_EQ(expected_str, result->Get()); + EXPECT_EQ(bt::sdp::DataElement::Size::kNextOne, result->size()); +} + +TEST(HelpersTest, NewFidlToDataElementUrlTest) { + std::string url = "http://www.google.com"; + std::string moved = url; + fuchsia_bluetooth_bredr::DataElement data_element = + fuchsia_bluetooth_bredr::DataElement::WithUrl(std::move(moved)); + std::optional result = + NewFidlToDataElement(data_element); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(bt::sdp::DataElement::Type::kUrl, result->type()); + EXPECT_EQ(url, result->GetUrl()); + EXPECT_EQ(bt::sdp::DataElement::Size::kNextOne, result->size()); +} + +TEST(HelpersTest, NewFidlToDataElementBooleanTest) { + bool expected = true; + bool moved = expected; + fuchsia_bluetooth_bredr::DataElement data_element = + fuchsia_bluetooth_bredr::DataElement::WithB(std::move(moved)); + std::optional result = + NewFidlToDataElement(data_element); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(bt::sdp::DataElement::Type::kBoolean, result->type()); + EXPECT_EQ(expected, result->Get()); + EXPECT_EQ(bt::sdp::DataElement::Size::kOneByte, result->size()); +} + +TEST(HelpersTest, NewFidlToDataElementUuidTest) { + fuchsia_bluetooth::Uuid uuid; + uuid.value().fill(123); + + fuchsia_bluetooth_bredr::DataElement data_element = + fuchsia_bluetooth_bredr::DataElement::WithUuid(std::move(uuid)); + std::optional result = + NewFidlToDataElement(data_element); + + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(bt::sdp::DataElement::Type::kUuid, result->type()); + EXPECT_EQ(bt::sdp::DataElement::Size::kSixteenBytes, result->size()); + + bt::DynamicByteBuffer bytes(16); + bytes.Fill(123); + + bt::UUID expected; + ASSERT_TRUE(bt::UUID::FromBytes(bytes, &expected)); + EXPECT_EQ(expected, result->Get()); +} + +TEST(HelpersTest, NewFidlToDataElementSequenceTest) { + int8_t size = 3; + std::vector> moved; + std::vector expected; + + for (int16_t i = 0; i < size; i++) { + expected.emplace_back(i); + moved.push_back(std::make_unique( + fuchsia_bluetooth_bredr::DataElement::WithInt16(std::move(i)))); + } + + fuchsia_bluetooth_bredr::DataElement data_element = + fuchsia_bluetooth_bredr::DataElement::WithSequence(std::move(moved)); + std::optional result = + NewFidlToDataElement(data_element); + + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(bt::sdp::DataElement::Type::kSequence, result->type()); + EXPECT_EQ(bt::sdp::DataElement::Size::kNextOne, result->size()); + + std::optional> actual = + result->Get>(); + EXPECT_TRUE(actual); + + for (int8_t i = 0; i < size; i++) { + EXPECT_EQ(expected[i].Get(), actual.value()[i].Get()); + } +} + +TEST(HelpersTest, AdvertisingDataFromFidlEmpty) { + fble::AdvertisingData input; + ASSERT_TRUE(input.IsEmpty()); + + std::optional maybe_data = + AdvertisingDataFromFidl(input); + ASSERT_TRUE(maybe_data.has_value()); + auto output = std::move(*maybe_data); + + EXPECT_TRUE(output.service_uuids().empty()); + EXPECT_TRUE(output.service_data_uuids().empty()); + EXPECT_TRUE(output.manufacturer_data_ids().empty()); + EXPECT_TRUE(output.uris().empty()); + EXPECT_FALSE(output.appearance()); + EXPECT_FALSE(output.tx_power()); + EXPECT_FALSE(output.local_name()); +} + +TEST(HelpersTest, AdvertisingDataFromFidlName) { + constexpr char kTestName[] = "💩"; + fble::AdvertisingData input; + input.set_name(kTestName); + + std::optional maybe_data = + AdvertisingDataFromFidl(input); + ASSERT_TRUE(maybe_data.has_value()); + auto output = std::move(*maybe_data); + EXPECT_TRUE(output.local_name()); + EXPECT_EQ(kTestName, output.local_name()->name); +} + +TEST(HelpersTest, AdvertisingDataFromFidlAppearance) { + fble::AdvertisingData input; + input.set_appearance(fuchsia::bluetooth::Appearance::HID_DIGITIZER_TABLET); + + std::optional maybe_data = + AdvertisingDataFromFidl(input); + ASSERT_TRUE(maybe_data.has_value()); + auto output = std::move(*maybe_data); + + EXPECT_TRUE(output.appearance()); + + // Value comes from the standard Bluetooth "assigned numbers" document. + EXPECT_EQ(0x03C5, *output.appearance()); +} + +TEST(HelpersTest, AdvertisingDataFromFidlTxPower) { + constexpr int8_t kTxPower = -50; + fble::AdvertisingData input; + input.set_tx_power_level(kTxPower); + + std::optional maybe_data = + AdvertisingDataFromFidl(input); + ASSERT_TRUE(maybe_data.has_value()); + auto output = std::move(*maybe_data); + + EXPECT_TRUE(output.tx_power()); + EXPECT_EQ(kTxPower, *output.tx_power()); +} + +TEST(HelpersTest, AdvertisingDataFromFidlUuids) { + // The first two entries are duplicated. The resulting structure should + // contain no duplicates. + const fbt::Uuid kUuid1{ + {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}}; + const fbt::Uuid kUuid2{ + {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}}; + const fbt::Uuid kUuid3{ + {16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}}; + fble::AdvertisingData input; + input.set_service_uuids({{kUuid1, kUuid2, kUuid3}}); + + std::optional maybe_data = + AdvertisingDataFromFidl(input); + ASSERT_TRUE(maybe_data.has_value()); + auto output = std::move(*maybe_data); + + EXPECT_EQ(2u, output.service_uuids().size()); + EXPECT_EQ(1u, output.service_uuids().count(bt::UUID(kUuid1.value))); + EXPECT_EQ(1u, output.service_uuids().count(bt::UUID(kUuid2.value))); +} + +TEST(HelpersTest, AdvertisingDataFromFidlServiceData) { + const fbt::Uuid kUuid1{ + {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}}; + const fbt::Uuid kUuid2{ + {16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1}}; + const std::vector kData1{{'h', 'e', 'l', 'l', 'o'}}; + const std::vector kData2{{'b', 'y', 'e'}}; + + fble::AdvertisingData input; + input.set_service_data({{{kUuid1, kData1}, {kUuid2, kData2}}}); + + std::optional maybe_data = + AdvertisingDataFromFidl(input); + ASSERT_TRUE(maybe_data.has_value()); + auto output = std::move(*maybe_data); + EXPECT_EQ(2u, output.service_data_uuids().size()); + EXPECT_TRUE(ContainersEqual(bt::BufferView(kData1), + output.service_data(bt::UUID(kUuid1.value)))); + EXPECT_TRUE(ContainersEqual(bt::BufferView(kData2), + output.service_data(bt::UUID(kUuid2.value)))); +} + +TEST(HelpersTest, AdvertisingDataFromFidlManufacturerData) { + constexpr uint16_t kCompanyId1 = 1; + constexpr uint16_t kCompanyId2 = 2; + const std::vector kData1{{'h', 'e', 'l', 'l', 'o'}}; + const std::vector kData2{{'b', 'y', 'e'}}; + + fble::AdvertisingData input; + input.set_manufacturer_data({{{kCompanyId1, kData1}, {kCompanyId2, kData2}}}); + + std::optional maybe_data = + AdvertisingDataFromFidl(input); + ASSERT_TRUE(maybe_data.has_value()); + auto output = std::move(*maybe_data); + EXPECT_EQ(2u, output.manufacturer_data_ids().size()); + EXPECT_TRUE(ContainersEqual(bt::BufferView(kData1), + output.manufacturer_data(kCompanyId1))); + EXPECT_TRUE(ContainersEqual(bt::BufferView(kData2), + output.manufacturer_data(kCompanyId2))); +} + +std::string UuidToString(fbt::Uuid uuid) { + std::string s; + for (uint8_t byte : uuid.value) { + s += std::to_string(static_cast(byte)) + ", "; + } + return s; +} +// Each field for this test first attempts to perform the too-long conversion, +// and then verifies that the bounds are where expected by performing a +// successful conversion with a field that just fits in the encoded version. +// This also enables using the same `input` throughout the test. +TEST(HelpersTest, AdvertisingDataFromFidlWithFieldsTooLong) { + fble::AdvertisingData input; + // The length of the AD name field must be <= 248 bytes per v5.2, Vol 4, Part + // E, 7.3.11 and Vol 3, Part C, 12.1.` + { + std::string name_that_fits(bt::kMaxNameLength, 'a'); + std::string too_long_name(bt::kMaxNameLength + 1, 'b'); + input.set_name(too_long_name); + EXPECT_FALSE(AdvertisingDataFromFidl(input).has_value()); + input.set_name(name_that_fits); + EXPECT_TRUE(AdvertisingDataFromFidl(input).has_value()); + } + { + // This is the longest encoding scheme known to Fuchsia BT, so this + // represents the longest string allowed (and subsequently, too long to be + // allowed) by both FIDL and internal invariants. + std::string uri = "ms-settings-cloudstorage:"; + uri += std::string(fble::MAX_URI_LENGTH - uri.size(), '.'); + input.set_uris({uri}); + EXPECT_FALSE(AdvertisingDataFromFidl(input).has_value()); + // This string should fit when it is one character shorter. + uri.pop_back(); + input.set_uris({uri}); + EXPECT_TRUE(AdvertisingDataFromFidl(input).has_value()); + } + // Ensure encoded service data that is too long is rejected. + { + const fbt::Uuid kUuid1{ + .value = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}}; + // |kUuid1| = 16 bytes, i.e. 14 bytes longer than the shortest possible + // encoded UUID (2 bytes). + std::vector too_long_data(fble::MAX_SERVICE_DATA_LENGTH - 13, + uint8_t{0xAB}); + input.set_service_data(std::vector( + {fble::ServiceData{.uuid = kUuid1, .data = too_long_data}})); + EXPECT_FALSE(AdvertisingDataFromFidl(input).has_value()); + // A vector that is 1 byte shorter than too_long_data should convert + // successfully + std::vector data_that_fits(too_long_data.size() - 1, too_long_data[0]); + input.set_service_data(std::vector( + {fble::ServiceData{.uuid = kUuid1, .data = data_that_fits}})); + EXPECT_TRUE(AdvertisingDataFromFidl(input).has_value()); + } + // Ensure encoded manufacturer data that is too long is rejected. + { + uint16_t company_id = 0x1212; + std::vector too_long_data(fble::MAX_MANUFACTURER_DATA_LENGTH + 1, + uint8_t{0xAB}); + input.set_manufacturer_data(std::vector({fble::ManufacturerData{ + .company_id = company_id, .data = too_long_data}})); + EXPECT_FALSE(AdvertisingDataFromFidl(input).has_value()); + // A vector that is 1 byte shorter than too_long_data should convert + // successfully + std::vector data_that_fits(too_long_data.size() - 1, too_long_data[0]); + input.set_manufacturer_data(std::vector({fble::ManufacturerData{ + .company_id = company_id, .data = data_that_fits}})); + EXPECT_TRUE(AdvertisingDataFromFidl(input).has_value()); + } + // Ensure input with too many service UUIDs is truncated (NOT rejected). + { + std::vector fbt_uuids; + fbt::Uuid kBaseUuid{ + .value = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}}; + for (int i = 0; i < bt::kMax128BitUuids; ++i) { + fbt::Uuid next_uuid = kBaseUuid; + next_uuid.value[0] += i; + fbt_uuids.push_back(next_uuid); + } + input.set_service_uuids(fbt_uuids); + auto ad = AdvertisingDataFromFidl(input); + EXPECT_TRUE(ad.has_value()); + std::unordered_set converted_uuids = ad->service_uuids(); + for (auto& fbt_uuid : fbt_uuids) { + SCOPED_TRACE(UuidToString(fbt_uuid)); + EXPECT_TRUE(converted_uuids.find(bt::UUID(fbt_uuid.value)) != + converted_uuids.end()); + } + fbt::Uuid excessive_uuid = kBaseUuid; + excessive_uuid.value[0] += bt::kMax128BitUuids + 1; + fbt_uuids.push_back(excessive_uuid); + input.set_service_uuids(fbt_uuids); + ad = AdvertisingDataFromFidl(input); + EXPECT_TRUE(ad.has_value()); + converted_uuids = ad->service_uuids(); + EXPECT_TRUE(converted_uuids.find(bt::UUID(excessive_uuid.value)) == + converted_uuids.end()); + } +} + +TEST(HelpersTest, AdvertisingDataToFidlDeprecatedEmpty) { + bt::AdvertisingData input; + auto output = AdvertisingDataToFidlDeprecated(input); + + // All fields in |input| are not set. Therefore, output should have no set + // fields as well. + EXPECT_FALSE(output.name); + EXPECT_FALSE(output.tx_power_level); + EXPECT_FALSE(output.appearance); + EXPECT_FALSE(output.service_uuids); + EXPECT_FALSE(output.service_data); + EXPECT_FALSE(output.manufacturer_specific_data); + EXPECT_FALSE(output.solicited_service_uuids); + EXPECT_FALSE(output.uris); +} + +TEST(HelpersTest, AdvertisingDataToFidlDeprecated) { + bt::AdvertisingData input; + EXPECT_TRUE(input.SetLocalName("fuchsia")); + input.SetTxPower(4); + input.SetAppearance(0x1234); + + const uint16_t id = 0x5678; + const bt::UUID test_uuid = bt::UUID(id); + bt::StaticByteBuffer service_bytes(0x01, 0x02); + EXPECT_TRUE(input.AddServiceUuid(test_uuid)); + EXPECT_TRUE(input.SetServiceData(test_uuid, service_bytes.view())); + + uint16_t company_id = 0x98; + bt::StaticByteBuffer manufacturer_bytes(0x04, 0x03); + EXPECT_TRUE(input.SetManufacturerData(company_id, manufacturer_bytes.view())); + + auto uri = "http://fuchsia.cl"; + EXPECT_TRUE(input.AddUri(uri)); + + auto output = AdvertisingDataToFidlDeprecated(input); + + EXPECT_EQ("fuchsia", output.name); + + auto expected_power_level = std::make_unique(); + expected_power_level->value = 4; + EXPECT_EQ(expected_power_level->value, output.tx_power_level->value); + + auto expected_appearance = std::make_unique(); + expected_appearance->value = 0x1234; + EXPECT_EQ(expected_appearance->value, output.appearance->value); + + EXPECT_EQ(1u, output.service_uuids->size()); + EXPECT_EQ(test_uuid.ToString(), output.service_uuids->front()); + + EXPECT_EQ(1u, output.service_data->size()); + auto service_data = output.service_data->front(); + EXPECT_EQ(test_uuid.ToString(), service_data.uuid); + EXPECT_TRUE( + ContainersEqual(bt::BufferView(service_bytes), service_data.data)); + + EXPECT_EQ(1u, output.manufacturer_specific_data->size()); + auto manufacturer_data = output.manufacturer_specific_data->front(); + EXPECT_EQ(company_id, manufacturer_data.company_id); + EXPECT_TRUE(ContainersEqual(bt::BufferView(manufacturer_bytes), + manufacturer_data.data)); + + EXPECT_EQ(1u, output.uris->size()); + EXPECT_EQ(uri, output.uris->front()); +} + +TEST(HelpersTest, AdvertisingDataToFidlEmpty) { + bt::AdvertisingData input; + auto output = AdvertisingDataToFidl(input); + + // All fields in |input| are not set. Therefore, output should have no set + // fields as well. + EXPECT_FALSE(output.has_name()); + EXPECT_FALSE(output.has_tx_power_level()); + EXPECT_FALSE(output.has_appearance()); + EXPECT_FALSE(output.has_service_uuids()); + EXPECT_FALSE(output.has_service_data()); + EXPECT_FALSE(output.has_manufacturer_data()); + EXPECT_FALSE(output.has_uris()); +} + +TEST(HelpersTest, AdvertisingDataToFidl) { + bt::AdvertisingData input; + EXPECT_TRUE(input.SetLocalName("fuchsia")); + input.SetTxPower(4); + const uint16_t kAppearance = 193u; // WATCH_SPORTS + input.SetAppearance(kAppearance); + + const uint16_t id = 0x5678; + const bt::UUID test_uuid = bt::UUID(id); + bt::StaticByteBuffer service_bytes(0x01, 0x02); + EXPECT_TRUE(input.AddServiceUuid(test_uuid)); + EXPECT_TRUE(input.SetServiceData(test_uuid, service_bytes.view())); + + uint16_t company_id = 0x98; + bt::StaticByteBuffer manufacturer_bytes(0x04, 0x03); + EXPECT_TRUE(input.SetManufacturerData(company_id, manufacturer_bytes.view())); + + auto uri = "http://fuchsia.cl/461435"; + EXPECT_TRUE(input.AddUri(uri)); + + auto output = AdvertisingDataToFidl(input); + + EXPECT_EQ("fuchsia", output.name()); + + auto expected_power_level = std::make_unique(); + expected_power_level->value = 4; + EXPECT_EQ(expected_power_level->value, output.tx_power_level()); + + EXPECT_EQ(fbt::Appearance{kAppearance}, output.appearance()); + + EXPECT_EQ(1u, output.service_uuids().size()); + EXPECT_EQ(test_uuid, UuidFromFidl(output.service_uuids().front())); + + EXPECT_EQ(1u, output.service_data().size()); + auto service_data = output.service_data().front(); + EXPECT_EQ(test_uuid, UuidFromFidl(service_data.uuid)); + EXPECT_TRUE( + ContainersEqual(bt::BufferView(service_bytes), service_data.data)); + + EXPECT_EQ(1u, output.manufacturer_data().size()); + auto manufacturer_data = output.manufacturer_data().front(); + EXPECT_EQ(company_id, manufacturer_data.company_id); + EXPECT_TRUE(ContainersEqual(bt::BufferView(manufacturer_bytes), + manufacturer_data.data)); + + EXPECT_THAT(output.uris(), ::testing::ElementsAre(uri)); +} + +TEST(HelpersTest, AdvertisingDataToFidlOmitsNonEnumeratedAppearance) { + // There is an "unknown" appearance, which is why this isn't named that. + const uint16_t kNonEnumeratedAppearance = 0xFFFFu; + bt::AdvertisingData input; + input.SetAppearance(kNonEnumeratedAppearance); + + EXPECT_FALSE(AdvertisingDataToFidl(input).has_appearance()); + + const uint16_t kKnownAppearance = 832u; // HEART_RATE_SENSOR + input.SetAppearance(kKnownAppearance); + + EXPECT_TRUE(AdvertisingDataToFidl(input).has_appearance()); +} + +TEST(HelpersTest, BrEdrSecurityModeFromFidl) { + EXPECT_EQ(bt::gap::BrEdrSecurityMode::Mode4, + BrEdrSecurityModeFromFidl(fsys::BrEdrSecurityMode::MODE_4)); + EXPECT_EQ(bt::gap::BrEdrSecurityMode::SecureConnectionsOnly, + BrEdrSecurityModeFromFidl( + fsys::BrEdrSecurityMode::SECURE_CONNECTIONS_ONLY)); + auto nonexistent_security_mode = static_cast(0xFF); + EXPECT_EQ(std::nullopt, BrEdrSecurityModeFromFidl(nonexistent_security_mode)); +} + +TEST(HelpersTest, LeSecurityModeFromFidl) { + EXPECT_EQ(bt::gap::LESecurityMode::Mode1, + LeSecurityModeFromFidl(fsys::LeSecurityMode::MODE_1)); + EXPECT_EQ( + bt::gap::LESecurityMode::SecureConnectionsOnly, + LeSecurityModeFromFidl(fsys::LeSecurityMode::SECURE_CONNECTIONS_ONLY)); + auto nonexistent_security_mode = static_cast(0xFF); + EXPECT_EQ(bt::gap::LESecurityMode::SecureConnectionsOnly, + LeSecurityModeFromFidl(nonexistent_security_mode)); +} + +TEST(HelpersTest, TechnologyTypeToFidl) { + EXPECT_EQ(fsys::TechnologyType::LOW_ENERGY, + TechnologyTypeToFidl(bt::gap::TechnologyType::kLowEnergy)); + EXPECT_EQ(fsys::TechnologyType::CLASSIC, + TechnologyTypeToFidl(bt::gap::TechnologyType::kClassic)); + EXPECT_EQ(fsys::TechnologyType::DUAL_MODE, + TechnologyTypeToFidl(bt::gap::TechnologyType::kDualMode)); +} + +TEST(HelpersTest, SecurityLevelFromFidl) { + const fsys::PairingSecurityLevel level = + fsys::PairingSecurityLevel::AUTHENTICATED; + EXPECT_EQ(bt::sm::SecurityLevel::kAuthenticated, + SecurityLevelFromFidl(level)); +} + +TEST(HelpersTest, SecurityLevelFromBadFidlFails) { + int nonexistant_security_level = 500000; + auto level = + static_cast(nonexistant_security_level); + EXPECT_EQ(std::nullopt, SecurityLevelFromFidl(level)); +} + +TEST(HelpersTest, PeerToFidlMandatoryFields) { + // Required by PeerCache expiry functions. + async::TestLoop test_loop; + pw::async_fuchsia::FuchsiaDispatcher pw_dispatcher(test_loop.dispatcher()); + + bt::gap::PeerCache cache(pw_dispatcher); + bt::DeviceAddress addr(bt::DeviceAddress::Type::kLEPublic, + {0, 1, 2, 3, 4, 5}); + auto* peer = cache.NewPeer(addr, /*connectable=*/true); + auto fidl = PeerToFidl(*peer); + ASSERT_TRUE(fidl.has_id()); + EXPECT_EQ(peer->identifier().value(), fidl.id().value); + ASSERT_TRUE(fidl.has_address()); + EXPECT_TRUE( + fidl::Equals(fbt::Address{fbt::AddressType::PUBLIC, {{0, 1, 2, 3, 4, 5}}}, + fidl.address())); + ASSERT_TRUE(fidl.has_technology()); + EXPECT_EQ(fsys::TechnologyType::LOW_ENERGY, fidl.technology()); + ASSERT_TRUE(fidl.has_connected()); + EXPECT_FALSE(fidl.connected()); + ASSERT_TRUE(fidl.has_bonded()); + EXPECT_FALSE(fidl.bonded()); + + EXPECT_FALSE(fidl.has_name()); + EXPECT_FALSE(fidl.has_appearance()); + EXPECT_FALSE(fidl.has_rssi()); + EXPECT_FALSE(fidl.has_tx_power()); + EXPECT_FALSE(fidl.has_device_class()); + EXPECT_FALSE(fidl.has_services()); + EXPECT_FALSE(fidl.has_le_services()); + EXPECT_FALSE(fidl.has_bredr_services()); +} + +TEST(HelpersTest, PeerToFidlOptionalFields) { + // Required by PeerCache expiry functions. + async::TestLoop test_loop; + pw::async_fuchsia::FuchsiaDispatcher pw_dispatcher(test_loop.dispatcher()); + + const int8_t kRssi = 5; + const int8_t kTxPower = 6; + const auto kAdv = bt::StaticByteBuffer(0x02, + 0x01, + 0x01, // Flags: General Discoverable + 0x03, + 0x19, + 192, + 0, // Appearance: Watch + 0x02, + 0x0A, + 0x06, // Tx-Power: 5 + 0x05, + 0x09, + 't', + 'e', + 's', + 't' // Complete Local Name: "test" + ); + const std::vector kBrEdrServices = {bt::UUID(uint16_t{0x110a}), + bt::UUID(uint16_t{0x110b})}; + + bt::gap::PeerCache cache(pw_dispatcher); + bt::DeviceAddress addr(bt::DeviceAddress::Type::kLEPublic, + {0, 1, 2, 3, 4, 5}); + auto* peer = cache.NewPeer(addr, /*connectable=*/true); + peer->MutLe().SetAdvertisingData( + kRssi, kAdv, pw::chrono::SystemClock::time_point()); + bt::StaticPacket inquiry_result; + auto view = inquiry_result.view(); + view.bd_addr().CopyFrom(bt::DeviceAddressBytes{{0, 1, 2, 3, 4, 5}}.view()); + view.page_scan_repetition_mode().Write( + pw::bluetooth::emboss::PageScanRepetitionMode::R0_); + view.class_of_device().major_device_class().Write( + pw::bluetooth::emboss::MajorDeviceClass::PERIPHERAL); + peer->MutBrEdr().SetInquiryData(inquiry_result.view()); + for (auto& service : kBrEdrServices) { + peer->MutBrEdr().AddService(service); + } + + auto fidl = PeerToFidl(*peer); + ASSERT_TRUE(fidl.has_name()); + EXPECT_EQ("test", fidl.name()); + ASSERT_TRUE(fidl.has_appearance()); + EXPECT_EQ(fbt::Appearance::WATCH, fidl.appearance()); + ASSERT_TRUE(fidl.has_rssi()); + EXPECT_EQ(kRssi, fidl.rssi()); + ASSERT_TRUE(fidl.has_tx_power()); + EXPECT_EQ(kTxPower, fidl.tx_power()); + ASSERT_TRUE(fidl.has_device_class()); + EXPECT_EQ(fbt::MAJOR_DEVICE_CLASS_PERIPHERAL, fidl.device_class().value); + + // Deprecated and never implemented (see https://fxbug.dev/42135180). + EXPECT_FALSE(fidl.has_services()); + + // TODO(fxbug.dev/42135180): Add a test when this field gets populated. + EXPECT_FALSE(fidl.has_le_services()); + + ASSERT_TRUE(fidl.has_bredr_services()); + std::vector expected_uuids; + std::transform(kBrEdrServices.begin(), + kBrEdrServices.end(), + std::back_inserter(expected_uuids), + UuidToFidl); + EXPECT_THAT(fidl.bredr_services(), + ::testing::UnorderedElementsAreArray(expected_uuids)); +} + +TEST(HelpersTest, ReliableModeFromFidl) { + using WriteOptions = fuchsia::bluetooth::gatt::WriteOptions; + using ReliableMode = fuchsia::bluetooth::gatt::ReliableMode; + WriteOptions options; + + // No options set, so this should default to disabled. + EXPECT_EQ(bt::gatt::ReliableMode::kDisabled, ReliableModeFromFidl(options)); + + options.set_reliable_mode(ReliableMode::ENABLED); + EXPECT_EQ(bt::gatt::ReliableMode::kEnabled, ReliableModeFromFidl(options)); + + options.set_reliable_mode(ReliableMode::DISABLED); + EXPECT_EQ(bt::gatt::ReliableMode::kDisabled, ReliableModeFromFidl(options)); +} + +TEST(HelpersTest, InvalidServiceDefinitionToServiceRecordIsError) { + fuchsia::bluetooth::bredr::ServiceDefinition def_should_fail; + // Should fail to convert without service class UUIDs. + auto rec_no_uuids = ServiceDefinitionToServiceRecord(def_should_fail); + EXPECT_FALSE(rec_no_uuids.is_ok()); + // Should fail to convert when information set without language. + def_should_fail.mutable_service_class_uuids()->emplace_back( + fidl_helpers::UuidToFidl(bt::sdp::profile::kAudioSink)); + fuchsia::bluetooth::bredr::Information info_no_language; + def_should_fail.mutable_information()->emplace_back( + std::move(info_no_language)); + auto rec_no_language = ServiceDefinitionToServiceRecord(def_should_fail); + EXPECT_FALSE(rec_no_language.is_ok()); +} + +// - make sure the expected attributes are set and have the correct type +// - make sure the profile descriptor sets the right attributes +TEST(HelpersTest, ServiceDefinitionToServiceRecordRoundtrip) { + fuchsia::bluetooth::bredr::ServiceDefinition def; + def.mutable_service_class_uuids()->emplace_back( + fidl_helpers::UuidToFidl(bt::sdp::profile::kAudioSink)); + fuchsia::bluetooth::bredr::Information info; + info.set_language("en"); + info.set_name("TEST"); + def.mutable_information()->emplace_back(std::move(info)); + fuchsia::bluetooth::bredr::ProtocolDescriptor l2cap_proto; + l2cap_proto.set_protocol( + fuchsia::bluetooth::bredr::ProtocolIdentifier::L2CAP); + fuchsia::bluetooth::bredr::DataElement l2cap_data_el; + l2cap_data_el.set_uint16(fuchsia::bluetooth::bredr::PSM_SDP); + std::vector l2cap_params; + l2cap_params.emplace_back(std::move(l2cap_data_el)); + l2cap_proto.set_params(std::move(l2cap_params)); + def.mutable_protocol_descriptor_list()->emplace_back(std::move(l2cap_proto)); + fuchsia::bluetooth::bredr::ProtocolDescriptor avdtp_proto; + avdtp_proto.set_protocol( + fuchsia::bluetooth::bredr::ProtocolIdentifier::AVDTP); + fuchsia::bluetooth::bredr::DataElement avdtp_data_el; + avdtp_data_el.set_uint16(0x0103); // Version 1 + std::vector avdtp_params; + avdtp_params.emplace_back(std::move(avdtp_data_el)); + avdtp_proto.set_params(std::move(avdtp_params)); + def.mutable_protocol_descriptor_list()->emplace_back(std::move(avdtp_proto)); + fuchsia::bluetooth::bredr::ProfileDescriptor prof_desc; + prof_desc.set_profile_id( + fuchsia::bluetooth::bredr::ServiceClassProfileIdentifier:: + ADVANCED_AUDIO_DISTRIBUTION); + prof_desc.set_major_version(1); + prof_desc.set_minor_version(3); + def.mutable_profile_descriptors()->emplace_back(std::move(prof_desc)); + bt::sdp::AttributeId valid_att_id = 0x1111; + fuchsia::bluetooth::bredr::Attribute valid_attribute; + valid_attribute.set_element( + ::fuchsia::bluetooth::bredr::DataElement::WithUint8(0x01)); + valid_attribute.set_id(valid_att_id); + def.mutable_additional_attributes()->emplace_back(std::move(valid_attribute)); + // Add a URL attribute. + bt::sdp::AttributeId url_attr_id = 0x1112; // Random ID + fuchsia::bluetooth::bredr::Attribute url_attribute; + url_attribute.set_element( + ::fuchsia::bluetooth::bredr::DataElement::WithUrl("foobar.dev")); + url_attribute.set_id(url_attr_id); + def.mutable_additional_attributes()->emplace_back(std::move(url_attribute)); + + // Confirm converted ServiceRecord fields match ServiceDefinition + auto rec = ServiceDefinitionToServiceRecord(def); + EXPECT_TRUE(rec.is_ok()); + + // Confirm UUIDs match + std::unordered_set attribute_uuid = {bt::sdp::profile::kAudioSink}; + EXPECT_TRUE(rec.value().FindUUID(attribute_uuid)); + + // Confirm information fields match + EXPECT_TRUE(rec.value().HasAttribute(bt::sdp::kLanguageBaseAttributeIdList)); + const bt::sdp::DataElement& lang_val = + rec.value().GetAttribute(bt::sdp::kLanguageBaseAttributeIdList); + auto triplets = lang_val.Get>(); + EXPECT_TRUE(triplets); + EXPECT_TRUE(triplets->size() % 3 == 0); + EXPECT_EQ(bt::sdp::DataElement::Type::kUnsignedInt, triplets->at(0).type()); + EXPECT_EQ(bt::sdp::DataElement::Type::kUnsignedInt, triplets->at(1).type()); + EXPECT_EQ(bt::sdp::DataElement::Type::kUnsignedInt, triplets->at(2).type()); + auto lang = triplets->at(0).Get(); + EXPECT_TRUE(lang); + EXPECT_EQ(0x656e, *lang); // should be 'en' in ascii (but big-endian) + auto encoding = triplets->at(1).Get(); + EXPECT_TRUE(encoding); + EXPECT_EQ(106, *encoding); // should always be UTF-8 + auto base_attrid = triplets->at(2).Get(); + EXPECT_TRUE(base_attrid); + EXPECT_EQ(0x0100, *base_attrid); // The primary language must be at 0x0100. + EXPECT_TRUE( + rec.value().HasAttribute(*base_attrid + bt::sdp::kServiceNameOffset)); + const bt::sdp::DataElement& name_elem = + rec.value().GetAttribute(*base_attrid + bt::sdp::kServiceNameOffset); + auto name = name_elem.Get(); + EXPECT_TRUE(name); + EXPECT_EQ("TEST", *name); + + // Confirm protocol + descriptor list + EXPECT_TRUE(rec.value().HasAttribute(bt::sdp::kProtocolDescriptorList)); + const bt::sdp::DataElement& protocol_val = + rec.value().GetAttribute(bt::sdp::kProtocolDescriptorList); + bt::DynamicByteBuffer protocol_block(protocol_val.WriteSize()); + protocol_val.Write(&protocol_block); + auto expected_protocol_list = + bt::StaticByteBuffer(0x35, + 0x10, // Data Element Sequence (10 bytes) + 0x35, + 0x06, // Data Element Sequence (6 bytes) + 0x19, // UUID (16 bits) + 0x01, + 0x00, // L2CAP Profile UUID + 0x09, // uint16_t + 0x00, + 0x01, // PSM = SDP + 0x35, + 0x06, // Data Element Sequence (6 bytes) + 0x19, // UUID + 0x00, + 0x19, // AVTDP Profile UUID + 0x09, // uint16_t + 0x01, + 0x03 // PSM_AVDTP + ); + EXPECT_EQ(expected_protocol_list.size(), protocol_block.size()); + EXPECT_TRUE(ContainersEqual(expected_protocol_list, protocol_block)); + + // Confirm profile descriptor list + EXPECT_TRUE( + rec.value().HasAttribute(bt::sdp::kBluetoothProfileDescriptorList)); + const bt::sdp::DataElement& profile_val = + rec.value().GetAttribute(bt::sdp::kBluetoothProfileDescriptorList); + bt::DynamicByteBuffer profile_block(profile_val.WriteSize()); + profile_val.Write(&profile_block); + auto expected_profile_list = + bt::StaticByteBuffer(0x35, + 0x08, // Data Element Sequence (8 bytes) + 0x35, + 0x06, // Data Element Sequence (6 bytes) + 0x19, // UUID + 0x11, + 0x0d, // Advanced Audio Identifier + 0x09, // uint16_t + 0x01, + 0x03 // Major and minor version + ); + EXPECT_EQ(expected_profile_list.size(), profile_block.size()); + EXPECT_TRUE(ContainersEqual(expected_profile_list, profile_block)); + + // Confirm additional attributes + EXPECT_TRUE(rec.value().HasAttribute(valid_att_id)); + EXPECT_TRUE(rec.value().HasAttribute(url_attr_id)); + EXPECT_EQ(*rec.value().GetAttribute(url_attr_id).GetUrl(), "foobar.dev"); + + // Can go back to the original FIDL service definition. + auto fidl_def = ServiceRecordToServiceDefinition(rec.value()); + ASSERT_TRUE(fidl_def.is_ok()); + ASSERT_TRUE(::fidl::Equals(fidl_def.value(), def)); +} + +TEST(HelpersTest, ObexServiceDefinitionToServiceRecordRoundtrip) { + // Create an OBEX definition for a successful conversion. MAP MSE uses both + // L2CAP and RFCOMM. + fuchsia::bluetooth::bredr::ServiceDefinition def; + def.mutable_service_class_uuids()->emplace_back( + fidl_helpers::UuidToFidl(bt::sdp::profile::kMessageAccessServer)); + + // The primary protocol contains L2CAP, RFCOMM, & OBEX. + fuchsia::bluetooth::bredr::ProtocolDescriptor l2cap_proto; + l2cap_proto.set_protocol( + fuchsia::bluetooth::bredr::ProtocolIdentifier::L2CAP); + l2cap_proto.set_params({}); + def.mutable_protocol_descriptor_list()->emplace_back(std::move(l2cap_proto)); + + fuchsia::bluetooth::bredr::ProtocolDescriptor rfcomm_proto; + rfcomm_proto.set_protocol( + fuchsia::bluetooth::bredr::ProtocolIdentifier::RFCOMM); + fuchsia::bluetooth::bredr::DataElement rfcomm_data_el; + rfcomm_data_el.set_uint8(10); // Random ServerChannel number + std::vector rfcomm_params; + rfcomm_params.emplace_back(std::move(rfcomm_data_el)); + rfcomm_proto.set_params(std::move(rfcomm_params)); + def.mutable_protocol_descriptor_list()->emplace_back(std::move(rfcomm_proto)); + + fuchsia::bluetooth::bredr::ProtocolDescriptor obex_proto; + obex_proto.set_protocol(fuchsia::bluetooth::bredr::ProtocolIdentifier::OBEX); + obex_proto.set_params({}); + def.mutable_protocol_descriptor_list()->emplace_back(std::move(obex_proto)); + + // Profile version = 1.4. + fuchsia::bluetooth::bredr::ProfileDescriptor prof_desc; + prof_desc.set_profile_id( + fuchsia::bluetooth::bredr::ServiceClassProfileIdentifier:: + MESSAGE_ACCESS_PROFILE); + prof_desc.set_major_version(1); + prof_desc.set_minor_version(4); + def.mutable_profile_descriptors()->emplace_back(std::move(prof_desc)); + + // ServiceName = "MAP MAS" + fuchsia::bluetooth::bredr::Information info; + info.set_language("en"); + info.set_name("MAP MAS"); + def.mutable_information()->emplace_back(std::move(info)); + + // GoepL2capPsm Attribute with a dynamic PSM. + fuchsia::bluetooth::bredr::Attribute goep_l2cap_psm_attribute; + goep_l2cap_psm_attribute.set_id(bt::sdp::kGoepL2capPsm); + fuchsia::bluetooth::bredr::DataElement geop_psm; + geop_psm.set_uint16(fuchsia::bluetooth::bredr::PSM_DYNAMIC); + goep_l2cap_psm_attribute.set_element(std::move(geop_psm)); + def.mutable_additional_attributes()->emplace_back( + std::move(goep_l2cap_psm_attribute)); + + // Add MASInstanceID Attribute = 1 + fuchsia::bluetooth::bredr::Attribute instance_id_attribute; + instance_id_attribute.set_id(0x0315); + instance_id_attribute.set_element( + fuchsia::bluetooth::bredr::DataElement::WithUint16(1)); + def.mutable_additional_attributes()->emplace_back( + std::move(instance_id_attribute)); + // Add SupportedMessagesTypes Attribute = Email (0) + fuchsia::bluetooth::bredr::Attribute message_types_attribute; + message_types_attribute.set_id(0x0316); + message_types_attribute.set_element( + fuchsia::bluetooth::bredr::DataElement::WithUint16(0)); + def.mutable_additional_attributes()->emplace_back( + std::move(message_types_attribute)); + // Add MapSupportedFeatures Attribute = Notification Registration only (1) + fuchsia::bluetooth::bredr::Attribute features_attribute; + features_attribute.set_id(0x0317); + features_attribute.set_element( + fuchsia::bluetooth::bredr::DataElement::WithUint16(1)); + def.mutable_additional_attributes()->emplace_back( + std::move(features_attribute)); + + // Confirm converted ServiceRecord fields match ServiceDefinition + const auto rec = ServiceDefinitionToServiceRecord(def); + ASSERT_TRUE(rec.is_ok()); + + // Confirm UUIDs match + std::unordered_set attribute_uuid = { + bt::sdp::profile::kMessageAccessServer}; + EXPECT_TRUE(rec.value().FindUUID(attribute_uuid)); + + // Confirm information fields match + EXPECT_TRUE(rec.value().HasAttribute(bt::sdp::kLanguageBaseAttributeIdList)); + const bt::sdp::DataElement& lang_val = + rec.value().GetAttribute(bt::sdp::kLanguageBaseAttributeIdList); + auto triplets = lang_val.Get>(); + EXPECT_TRUE(triplets); + EXPECT_TRUE(triplets->size() % 3 == 0); + EXPECT_EQ(bt::sdp::DataElement::Type::kUnsignedInt, triplets->at(0).type()); + EXPECT_EQ(bt::sdp::DataElement::Type::kUnsignedInt, triplets->at(1).type()); + EXPECT_EQ(bt::sdp::DataElement::Type::kUnsignedInt, triplets->at(2).type()); + auto lang = triplets->at(0).Get(); + EXPECT_TRUE(lang); + EXPECT_EQ(0x656e, *lang); // should be 'en' in ascii (but big-endian) + auto encoding = triplets->at(1).Get(); + EXPECT_TRUE(encoding); + EXPECT_EQ(106, *encoding); // should always be UTF-8 + auto base_attrid = triplets->at(2).Get(); + EXPECT_TRUE(base_attrid); + EXPECT_EQ(0x0100, *base_attrid); // The primary language must be at 0x0100. + EXPECT_TRUE( + rec.value().HasAttribute(*base_attrid + bt::sdp::kServiceNameOffset)); + const bt::sdp::DataElement& name_elem = + rec.value().GetAttribute(*base_attrid + bt::sdp::kServiceNameOffset); + auto name = name_elem.Get(); + EXPECT_TRUE(name); + EXPECT_EQ("MAP MAS", *name); + + // Confirm protocol + descriptor list + EXPECT_TRUE(rec.value().HasAttribute(bt::sdp::kProtocolDescriptorList)); + const bt::sdp::DataElement& protocol_val = + rec.value().GetAttribute(bt::sdp::kProtocolDescriptorList); + bt::DynamicByteBuffer protocol_block(protocol_val.WriteSize()); + protocol_val.Write(&protocol_block); + auto expected_protocol_list = + bt::StaticByteBuffer(0x35, + 0x11, // Data Element Sequence (11 bytes) + 0x35, + 0x03, // Data Element Sequence (3 bytes) + 0x19, // UUID (16 bits) + 0x01, + 0x00, // L2CAP Profile UUID + 0x35, + 0x05, // Data Element Sequence (5 bytes) + 0x19, // UUID + 0x00, + 0x03, // RFCOMM Profile UUID + 0x08, // uint8_t + 0x0a, // ServerChannel = 10 + 0x35, + 0x03, // Data Element Sequence (3 bytes) + 0x19, // UUID + 0x00, + 0x08 // OBEX Profile UUID + ); + EXPECT_EQ(expected_protocol_list.size(), protocol_block.size()); + EXPECT_TRUE(ContainersEqual(expected_protocol_list, protocol_block)); + + // Confirm profile descriptor list + EXPECT_TRUE( + rec.value().HasAttribute(bt::sdp::kBluetoothProfileDescriptorList)); + const bt::sdp::DataElement& profile_val = + rec.value().GetAttribute(bt::sdp::kBluetoothProfileDescriptorList); + bt::DynamicByteBuffer profile_block(profile_val.WriteSize()); + profile_val.Write(&profile_block); + auto expected_profile_list = + bt::StaticByteBuffer(0x35, + 0x08, // Data Element Sequence (8 bytes) + 0x35, + 0x06, // Data Element Sequence (6 bytes) + 0x19, // UUID + 0x11, + 0x34, // Message Access Profile ID + 0x09, // uint16_t + 0x01, + 0x04 // Major and minor version + ); + EXPECT_EQ(expected_profile_list.size(), profile_block.size()); + EXPECT_TRUE(ContainersEqual(expected_profile_list, profile_block)); + + // Confirm additional attributes + EXPECT_TRUE(rec.value().HasAttribute(0x0315)); + EXPECT_TRUE(rec.value().HasAttribute(0x0316)); + EXPECT_TRUE(rec.value().HasAttribute(0x0317)); + // The GoepL2capPsm value should be saved. + EXPECT_EQ(*rec.value().GetAttribute(bt::sdp::kGoepL2capPsm).Get(), + fuchsia::bluetooth::bredr::PSM_DYNAMIC); + + // Can go back to the original FIDL service definition. + auto fidl_def = ServiceRecordToServiceDefinition(rec.value()); + ASSERT_TRUE(fidl_def.is_ok()); + ASSERT_TRUE(::fidl::Equals(fidl_def.value(), def)); +} + +TEST(HelpersTest, MinimalServiceRecordToServiceDefinition) { + // Minimal record has only service UUIDs. Everything else is optional. + bt::sdp::ServiceRecord minimal; + const std::vector uuids = {bt::sdp::profile::kSerialPort}; + minimal.SetServiceClassUUIDs(uuids); + auto minimal_rec = ServiceRecordToServiceDefinition(minimal); + EXPECT_TRUE(minimal_rec.is_ok()); +} + +TEST(HelpersTest, InvalidServiceRecordToServiceDefinitionIsError) { + const bt::sdp::ServiceRecord empty_record; + // Should fail to convert without service class UUIDs. + auto rec_no_uuids = ServiceRecordToServiceDefinition(empty_record); + EXPECT_FALSE(rec_no_uuids.is_ok()); + + bt::sdp::ServiceRecord invalid_record; + const std::vector uuids = {bt::sdp::profile::kAudioSink}; + invalid_record.SetServiceClassUUIDs(uuids); + // Invalid profile descriptor + bt::sdp::DataElement invalid_profile_descriptors; + invalid_record.SetAttribute(bt::sdp::kBluetoothProfileDescriptorList, + std::move(invalid_profile_descriptors)); + auto invalid_profile_rec = ServiceRecordToServiceDefinition(invalid_record); + EXPECT_FALSE(invalid_profile_rec.is_ok()); + invalid_record.RemoveAttribute(bt::sdp::kBluetoothProfileDescriptorList); + + // Invalid protocol descriptor + bt::sdp::DataElement invalid_protocol_descriptor; + invalid_record.SetAttribute(bt::sdp::kProtocolDescriptorList, + std::move(invalid_protocol_descriptor)); + auto invalid_protocol_rec = ServiceRecordToServiceDefinition(invalid_record); + EXPECT_FALSE(invalid_protocol_rec.is_ok()); +} + +TEST(HelpersTest, FidlToBrEdrSecurityRequirements) { + fbt::ChannelParameters params; + EXPECT_EQ(FidlToBrEdrSecurityRequirements(params), + (bt::gap::BrEdrSecurityRequirements{.authentication = false, + .secure_connections = false})); + + params.set_security_requirements(fbt::SecurityRequirements()); + EXPECT_EQ(FidlToBrEdrSecurityRequirements(params), + (bt::gap::BrEdrSecurityRequirements{.authentication = false, + .secure_connections = false})); + + params.mutable_security_requirements()->set_secure_connections_required( + false); + EXPECT_EQ(FidlToBrEdrSecurityRequirements(params), + (bt::gap::BrEdrSecurityRequirements{.authentication = false, + .secure_connections = false})); + params.mutable_security_requirements()->clear_secure_connections_required(); + + params.mutable_security_requirements()->set_authentication_required(false); + EXPECT_EQ(FidlToBrEdrSecurityRequirements(params), + (bt::gap::BrEdrSecurityRequirements{.authentication = false, + .secure_connections = false})); + + params.mutable_security_requirements()->set_secure_connections_required( + false); + EXPECT_EQ(FidlToBrEdrSecurityRequirements(params), + (bt::gap::BrEdrSecurityRequirements{.authentication = false, + .secure_connections = false})); + + params.mutable_security_requirements()->set_authentication_required(true); + EXPECT_EQ(FidlToBrEdrSecurityRequirements(params), + (bt::gap::BrEdrSecurityRequirements{.authentication = true, + .secure_connections = false})); + + params.mutable_security_requirements()->set_secure_connections_required(true); + EXPECT_EQ(FidlToBrEdrSecurityRequirements(params), + (bt::gap::BrEdrSecurityRequirements{.authentication = true, + .secure_connections = true})); +} + +TEST(HelpersTest, AddressFromFidlBondingDataRandomAddressRejectedIfBredr) { + fsys::BondingData bond; + bond.set_address(kRandomAddrFidl); + bond.set_bredr_bond(fsys::BredrBondData()); + + EXPECT_EQ(std::nullopt, AddressFromFidlBondingData(bond)); +} + +TEST(HelpersTest, AddressFromFidlBondingDataBredr) { + fsys::BondingData bond; + bond.set_address(kPublicAddrFidl); + bond.set_bredr_bond(fsys::BredrBondData()); + + auto addr = AddressFromFidlBondingData(bond); + ASSERT_TRUE(addr); + EXPECT_EQ(addr->type(), bt::DeviceAddress::Type::kBREDR); +} + +TEST(HelpersTest, AddressFromFidlBondingDataDualMode) { + fsys::BondingData bond; + bond.set_address(kPublicAddrFidl); + bond.set_bredr_bond(fsys::BredrBondData()); + bond.set_le_bond(fsys::LeBondData()); + + auto addr = AddressFromFidlBondingData(bond); + ASSERT_TRUE(addr); + EXPECT_EQ(addr->type(), bt::DeviceAddress::Type::kBREDR); +} + +TEST(HelpersTest, AddressFromFidlBondingDataLePublic) { + fsys::BondingData bond; + bond.set_address(kPublicAddrFidl); + bond.set_le_bond(fsys::LeBondData()); + + auto addr = AddressFromFidlBondingData(bond); + ASSERT_TRUE(addr); + EXPECT_EQ(addr->type(), bt::DeviceAddress::Type::kLEPublic); +} + +TEST(HelpersTest, AddressFromFidlBondingDataLeRandom) { + fsys::BondingData bond; + bond.set_address(kRandomAddrFidl); + bond.set_le_bond(fsys::LeBondData()); + + auto addr = AddressFromFidlBondingData(bond); + ASSERT_TRUE(addr); + EXPECT_EQ(addr->type(), bt::DeviceAddress::Type::kLERandom); +} + +TEST(HelpersTest, AddressFromFidlBondingDataLeRandomResolvable) { + fsys::BondingData bond; + bond.set_address(kRandomAddrResolvableFidl); + bond.set_le_bond(fsys::LeBondData()); + + auto addr = AddressFromFidlBondingData(bond); + EXPECT_FALSE(addr); +} + +TEST(HelpersTest, AddressFromFidlBondingDataLeRandomNonResolvable) { + fsys::BondingData bond; + bond.set_address(kRandomAddrNonResolvableFidl); + bond.set_le_bond(fsys::LeBondData()); + + auto addr = AddressFromFidlBondingData(bond); + EXPECT_FALSE(addr); +} + +TEST(HelpersTest, LePairingDataFromFidlEmpty) { + bt::sm::PairingData result = + LePairingDataFromFidl(kLePublicAddress, fsys::LeBondData()); + EXPECT_FALSE(result.identity_address); + EXPECT_FALSE(result.local_ltk); + EXPECT_FALSE(result.peer_ltk); + EXPECT_FALSE(result.irk); + EXPECT_FALSE(result.csrk); +} + +TEST(HelpersTest, LePairingDataFromFidl) { + fsys::LeBondData le; + le.set_local_ltk(kTestLtkFidl); + le.set_peer_ltk(kTestLtkFidl); + le.set_irk(kTestKeyFidl); + le.set_csrk(kTestKeyFidl); + + bt::sm::PairingData result = + LePairingDataFromFidl(kLePublicAddress, std::move(le)); + ASSERT_TRUE(result.local_ltk); + ASSERT_TRUE(result.peer_ltk); + ASSERT_TRUE(result.irk); + ASSERT_TRUE(result.identity_address); + ASSERT_TRUE(result.csrk); + + EXPECT_EQ(kTestLtk, *result.local_ltk); + EXPECT_EQ(kTestLtk, *result.peer_ltk); + EXPECT_EQ(kTestKey, *result.irk); + EXPECT_EQ(kLePublicAddress, *result.identity_address); + EXPECT_EQ(kTestKey, *result.csrk); +} + +TEST(HelpersTest, BredrKeyFromFidlEmpty) { + EXPECT_FALSE(BredrKeyFromFidl(fsys::BredrBondData())); +} + +TEST(HelpersTest, BredrKeyFromFidl) { + const bt::sm::SecurityProperties kTestSecurity( + bt::sm::SecurityLevel::kSecureAuthenticated, + 16, + /*secure_connections=*/true); + const bt::sm::LTK kTestLtk(kTestSecurity, + bt::hci_spec::LinkKey(kTestKeyValue, 0, 0)); + + fsys::BredrBondData bredr; + bredr.set_link_key(kTestKeyFidl); + std::optional result = BredrKeyFromFidl(bredr); + ASSERT_TRUE(result); + EXPECT_EQ(kTestLtk, *result); +} + +TEST(HelpersTest, BredrServicesFromFidlEmpty) { + EXPECT_TRUE(BredrServicesFromFidl(fsys::BredrBondData()).empty()); +} + +TEST(HelpersTest, BredrServicesFromFidl) { + fsys::BredrBondData bredr; + bredr.mutable_services()->push_back(UuidToFidl(bt::sdp::profile::kAudioSink)); + bredr.mutable_services()->push_back( + UuidToFidl(bt::sdp::profile::kAudioSource)); + auto bredr_services = BredrServicesFromFidl(bredr); + EXPECT_THAT(bredr_services, + ::testing::UnorderedElementsAre(bt::sdp::profile::kAudioSource, + bt::sdp::profile::kAudioSink)); +} + +class HelpersAdapterTest : public bthost::testing::AdapterTestFixture {}; + +TEST_F(HelpersAdapterTest, PeerToFidlBondingData_NoTransportData) { + auto* peer = + adapter()->peer_cache()->NewPeer(kTestPeerAddr, /*connectable=*/true); + fsys::BondingData data = PeerToFidlBondingData(adapter().get(), *peer); + ASSERT_TRUE(data.has_identifier()); + ASSERT_TRUE(data.has_local_address()); + ASSERT_TRUE(data.has_address()); + EXPECT_FALSE(data.has_name()); + EXPECT_FALSE(data.has_le_bond()); + EXPECT_FALSE(data.has_bredr_bond()); + + EXPECT_EQ(peer->identifier().value(), data.identifier().value); + EXPECT_TRUE(fidl::Equals( + (fbt::Address{fbt::AddressType::PUBLIC, std::array{0}}), + data.local_address())); + EXPECT_TRUE(fidl::Equals(kPublicAddrFidl, data.address())); +} + +TEST_F(HelpersAdapterTest, + PeerToFidlBondingData_BothTransportsPresentButNotBonded) { + auto* peer = + adapter()->peer_cache()->NewPeer(kTestPeerAddr, /*connectable=*/true); + peer->MutLe(); + peer->MutBrEdr(); + + fsys::BondingData data = PeerToFidlBondingData(adapter().get(), *peer); + ASSERT_TRUE(data.has_identifier()); + ASSERT_TRUE(data.has_local_address()); + ASSERT_TRUE(data.has_address()); + EXPECT_FALSE(data.has_le_bond()); + EXPECT_FALSE(data.has_bredr_bond()); + + EXPECT_EQ(peer->identifier().value(), data.identifier().value); + EXPECT_TRUE(fidl::Equals( + (fbt::Address{fbt::AddressType::PUBLIC, std::array{0}}), + data.local_address())); + EXPECT_TRUE(fidl::Equals(kPublicAddrFidl, data.address())); +} + +TEST_F(HelpersAdapterTest, + PeerToFidlBondingData_BredrServicesDiscoveredNotBonded) { + auto* peer = + adapter()->peer_cache()->NewPeer(kTestPeerAddr, /*connectable=*/true); + peer->MutBrEdr().AddService(bt::UUID(uint16_t{0x1234})); + + fsys::BondingData data = PeerToFidlBondingData(adapter().get(), *peer); + EXPECT_FALSE(data.has_bredr_bond()); +} + +TEST_F(HelpersAdapterTest, PeerToFidlBondingData_EmptyLeData) { + auto* peer = + adapter()->peer_cache()->NewPeer(kTestPeerAddr, /*connectable=*/true); + peer->MutLe().SetBondData(bt::sm::PairingData()); + + fsys::BondingData data = PeerToFidlBondingData(adapter().get(), *peer); + EXPECT_FALSE(data.has_bredr_bond()); + ASSERT_TRUE(data.has_le_bond()); + EXPECT_FALSE(data.le_bond().has_local_ltk()); + EXPECT_FALSE(data.le_bond().has_peer_ltk()); + EXPECT_FALSE(data.le_bond().has_irk()); + EXPECT_FALSE(data.le_bond().has_csrk()); +} + +TEST_F(HelpersAdapterTest, PeerToFidlBondingData_LeData) { + auto* peer = + adapter()->peer_cache()->NewPeer(kTestPeerAddr, /*connectable=*/true); + peer->MutLe().SetBondData(bt::sm::PairingData{ + .local_ltk = {kTestLtk}, + .peer_ltk = {kTestLtk}, + .irk = {kTestKey}, + .csrk = {kTestKey}, + }); + + fsys::BondingData data = PeerToFidlBondingData(adapter().get(), *peer); + EXPECT_FALSE(data.has_bredr_bond()); + ASSERT_TRUE(data.has_le_bond()); + ASSERT_TRUE(data.le_bond().has_local_ltk()); + ASSERT_TRUE(data.le_bond().has_peer_ltk()); + ASSERT_TRUE(data.le_bond().has_irk()); + ASSERT_TRUE(data.le_bond().has_csrk()); + + EXPECT_TRUE(fidl::Equals(kTestLtkFidl, data.le_bond().local_ltk())); + EXPECT_TRUE(fidl::Equals(kTestLtkFidl, data.le_bond().peer_ltk())); + EXPECT_TRUE(fidl::Equals(kTestKeyFidl, data.le_bond().irk())); + EXPECT_TRUE(fidl::Equals(kTestKeyFidl, data.le_bond().csrk())); +} + +TEST_F(HelpersAdapterTest, PeerToFidlBondingData_BredrData) { + auto* peer = + adapter()->peer_cache()->NewPeer(kTestPeerAddr, /*connectable=*/true); + peer->MutBrEdr().SetBondData(kTestLtk); + + fsys::BondingData data = PeerToFidlBondingData(adapter().get(), *peer); + EXPECT_FALSE(data.has_le_bond()); + ASSERT_TRUE(data.has_bredr_bond()); + ASSERT_TRUE(data.bredr_bond().has_link_key()); + EXPECT_TRUE(fidl::Equals(kTestKeyFidl, data.bredr_bond().link_key())); +} + +TEST_F(HelpersAdapterTest, PeerToFidlBondingData_IncludesBredrServices) { + auto* peer = + adapter()->peer_cache()->NewPeer(kTestPeerAddr, /*connectable=*/true); + peer->MutBrEdr().SetBondData(kTestLtk); + peer->MutBrEdr().AddService(bt::sdp::profile::kAudioSink); + peer->MutBrEdr().AddService(bt::sdp::profile::kAudioSource); + + fsys::BondingData data = PeerToFidlBondingData(adapter().get(), *peer); + ASSERT_TRUE(data.has_bredr_bond()); + ASSERT_TRUE(data.bredr_bond().has_services()); + + EXPECT_THAT(data.bredr_bond().services(), + ::testing::UnorderedElementsAre( + UuidToFidl(bt::sdp::profile::kAudioSink), + UuidToFidl(bt::sdp::profile::kAudioSource))); +} + +// Returns a copy to avoid forming references to misaligned fields, which is UB. +template +T copy(T t) { + return t; +} + +TEST_F(HelpersAdapterTest, FidlToScoParameters) { + fbredr::ScoConnectionParameters params; + EXPECT_TRUE(FidlToScoParameters(params).is_error()); + params.set_parameter_set(fbredr::HfpParameterSet::T2); + EXPECT_TRUE(FidlToScoParameters(params).is_error()); + params.set_air_coding_format(fbt::AssignedCodingFormat::MSBC); + EXPECT_TRUE(FidlToScoParameters(params).is_error()); + params.set_air_frame_size(8u); + EXPECT_TRUE(FidlToScoParameters(params).is_error()); + params.set_io_bandwidth(32000); + EXPECT_TRUE(FidlToScoParameters(params).is_error()); + params.set_io_coding_format(fbt::AssignedCodingFormat::LINEAR_PCM); + EXPECT_TRUE(FidlToScoParameters(params).is_error()); + params.set_io_frame_size(16u); + EXPECT_TRUE(FidlToScoParameters(params).is_error()); + params.set_io_pcm_data_format(faudio::SampleFormat::PCM_SIGNED); + EXPECT_TRUE(FidlToScoParameters(params).is_error()); + params.set_io_pcm_sample_payload_msb_position(3u); + EXPECT_TRUE(FidlToScoParameters(params).is_error()); + params.set_path(fbredr::DataPath::OFFLOAD); + ASSERT_TRUE(FidlToScoParameters(params).is_ok()); + + bt::StaticPacket + out = FidlToScoParameters(params).take_value(); + auto view = out.view(); + EXPECT_EQ(view.transmit_bandwidth().Read(), 8000u); + EXPECT_EQ(view.receive_bandwidth().Read(), 8000u); + + EXPECT_EQ(view.transmit_coding_format().coding_format().Read(), + pw::bluetooth::emboss::CodingFormat::MSBC); + EXPECT_EQ(view.transmit_coding_format().company_id().Read(), 0u); + EXPECT_EQ(view.transmit_coding_format().vendor_codec_id().Read(), 0u); + + EXPECT_EQ(view.receive_coding_format().coding_format().Read(), + pw::bluetooth::emboss::CodingFormat::MSBC); + EXPECT_EQ(view.receive_coding_format().company_id().Read(), 0u); + EXPECT_EQ(view.receive_coding_format().vendor_codec_id().Read(), 0u); + + EXPECT_EQ(view.transmit_codec_frame_size_bytes().Read(), 8u); + EXPECT_EQ(view.receive_codec_frame_size_bytes().Read(), 8u); + + EXPECT_EQ(view.input_bandwidth().Read(), 32000u); + EXPECT_EQ(view.output_bandwidth().Read(), 32000u); + + EXPECT_EQ(view.input_coding_format().coding_format().Read(), + pw::bluetooth::emboss::CodingFormat::LINEAR_PCM); + EXPECT_EQ(view.input_coding_format().company_id().Read(), 0u); + EXPECT_EQ(view.input_coding_format().vendor_codec_id().Read(), 0u); + + EXPECT_EQ(view.output_coding_format().coding_format().Read(), + pw::bluetooth::emboss::CodingFormat::LINEAR_PCM); + EXPECT_EQ(view.output_coding_format().company_id().Read(), 0u); + EXPECT_EQ(view.output_coding_format().vendor_codec_id().Read(), 0u); + + EXPECT_EQ(view.input_coded_data_size_bits().Read(), 16u); + EXPECT_EQ(view.output_coded_data_size_bits().Read(), 16u); + + EXPECT_EQ(view.input_pcm_data_format().Read(), + pw::bluetooth::emboss::PcmDataFormat::TWOS_COMPLEMENT); + EXPECT_EQ(view.output_pcm_data_format().Read(), + pw::bluetooth::emboss::PcmDataFormat::TWOS_COMPLEMENT); + + EXPECT_EQ(view.input_pcm_sample_payload_msb_position().Read(), 3u); + EXPECT_EQ(view.output_pcm_sample_payload_msb_position().Read(), 3u); + + EXPECT_EQ(view.input_data_path().Read(), + static_cast(6)); + EXPECT_EQ(view.output_data_path().Read(), + static_cast(6)); + + EXPECT_EQ(view.input_transport_unit_size_bits().Read(), 0u); + EXPECT_EQ(view.output_transport_unit_size_bits().Read(), 0u); + + EXPECT_EQ(view.max_latency_ms().Read(), + bt::sco::kParameterSetT2.max_latency_ms); + EXPECT_EQ(view.packet_types().BackingStorage().ReadUInt(), + bt::sco::kParameterSetT2.packet_types); + EXPECT_EQ(view.retransmission_effort().Read(), + static_cast( + bt::sco::kParameterSetT2.retransmission_effort)); + + // When the IO coding format is Linear PCM, the PCM data format is required. + params.clear_io_pcm_data_format(); + EXPECT_TRUE(FidlToScoParameters(params).is_error()); + + // PCM_FLOAT is not a supported PCM format. + params.set_io_pcm_data_format(faudio::SampleFormat::PCM_FLOAT); + EXPECT_TRUE(FidlToScoParameters(params).is_error()); + + // PCM format for non-PCM IO coding formats is kNotApplicable and MSB is 0. + params.set_io_coding_format(fbt::AssignedCodingFormat::TRANSPARENT); + ASSERT_TRUE(FidlToScoParameters(params).is_ok()); + out = FidlToScoParameters(params).value(); + EXPECT_EQ(view.input_pcm_data_format().Read(), + pw::bluetooth::emboss::PcmDataFormat::NOT_APPLICABLE); + EXPECT_EQ(view.input_pcm_sample_payload_msb_position().Read(), 0u); +} + +class HelpersTestFakeAdapter + : public bt::fidl::testing::FakeAdapterTestFixture { + public: + HelpersTestFakeAdapter() = default; + ~HelpersTestFakeAdapter() override = default; + + void SetUp() override { FakeAdapterTestFixture::SetUp(); } + + void TearDown() override { FakeAdapterTestFixture::TearDown(); } + + private: + BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(HelpersTestFakeAdapter); +}; + +TEST_F(HelpersTestFakeAdapter, HostInfoToFidl) { + // Verify that the default parameters are populated as expected. + auto host_info = HostInfoToFidl(*adapter()); + ASSERT_TRUE(host_info.has_id()); + ASSERT_TRUE(host_info.has_technology()); + ASSERT_TRUE(host_info.has_local_name()); + ASSERT_TRUE(host_info.has_discoverable()); + ASSERT_TRUE(host_info.has_discovering()); + ASSERT_TRUE(host_info.has_addresses()); + + EXPECT_EQ(adapter()->identifier().value(), host_info.id().value); + EXPECT_EQ(fsys::TechnologyType::DUAL_MODE, host_info.technology()); + EXPECT_EQ("", host_info.local_name()); + EXPECT_TRUE(host_info.discoverable()); + EXPECT_TRUE(host_info.discovering()); + // Since privacy is not enabled by default, only the public address should be + // reported. + ASSERT_EQ(host_info.addresses().size(), 1u); + EXPECT_EQ(fbt::AddressType::PUBLIC, host_info.addresses()[0].type); + EXPECT_TRUE(ContainersEqual(adapter()->state().controller_address.bytes(), + host_info.addresses()[0].bytes)); +} + +TEST_F(HelpersTestFakeAdapter, HostInfoWithLEAddressToFidl) { + // Enabling privacy does not result in the LE random address being set + // immediately. Therefore, only the public address should be reported. + adapter()->fake_le()->EnablePrivacy(/*enabled=*/true); + auto host_info = HostInfoToFidl(*adapter()); + EXPECT_EQ(adapter()->identifier().value(), host_info.id().value); + EXPECT_EQ(fsys::TechnologyType::DUAL_MODE, host_info.technology()); + EXPECT_EQ("", host_info.local_name()); + EXPECT_TRUE(host_info.discoverable()); + EXPECT_TRUE(host_info.discovering()); + ASSERT_EQ(host_info.addresses().size(), 1u); + EXPECT_EQ(fbt::AddressType::PUBLIC, host_info.addresses()[0].type); + EXPECT_TRUE(ContainersEqual(adapter()->state().controller_address.bytes(), + host_info.addresses()[0].bytes)); + + // Setting a RPA should result in the LE random address being reported. + auto resolvable_address = bt::DeviceAddress( + bt::DeviceAddress::Type::kLERandom, {0x55, 0x44, 0x33, 0x22, 0x11, 0x43}); + adapter()->fake_le()->UpdateRandomAddress(resolvable_address); + host_info = HostInfoToFidl(*adapter()); + ASSERT_EQ(host_info.addresses().size(), 2u); + EXPECT_EQ(fbt::AddressType::PUBLIC, host_info.addresses()[0].type); + EXPECT_TRUE(ContainersEqual(adapter()->state().controller_address.bytes(), + host_info.addresses()[0].bytes)); + EXPECT_EQ(fbt::AddressType::RANDOM, host_info.addresses()[1].type); + EXPECT_TRUE(ContainersEqual(adapter()->le()->CurrentAddress().value().bytes(), + host_info.addresses()[1].bytes)); + + // Setting a static address should result in the LE random address being + // reported. + auto static_address = bt::DeviceAddress(bt::DeviceAddress::Type::kLERandom, + {0x55, 0x44, 0x33, 0x22, 0x11, 0x43}); + adapter()->fake_le()->UpdateRandomAddress(static_address); + host_info = HostInfoToFidl(*adapter()); + ASSERT_EQ(host_info.addresses().size(), 2u); + EXPECT_EQ(fbt::AddressType::RANDOM, host_info.addresses()[1].type); + EXPECT_TRUE(ContainersEqual(adapter()->le()->CurrentAddress().value().bytes(), + host_info.addresses()[1].bytes)); +} + +TEST(HelpersTest, DiscoveryFilterFromEmptyFidlFilter) { + bt::gap::DiscoveryFilter filter = DiscoveryFilterFromFidl(fble::Filter()); + EXPECT_TRUE(filter.service_uuids().empty()); + EXPECT_TRUE(filter.service_data_uuids().empty()); + EXPECT_FALSE(filter.manufacturer_code().has_value()); + EXPECT_FALSE(filter.connectable().has_value()); + EXPECT_TRUE(filter.name_substring().empty()); + EXPECT_FALSE(filter.pathloss().has_value()); +} + +TEST(HelpersTest, DiscoveryFilterFromFidlFilter) { + fble::Filter fidl_filter; + fuchsia::bluetooth::Uuid service_uuid; + service_uuid.value[0] = 1; + fuchsia::bluetooth::Uuid service_data_uuid; + service_uuid.value[0] = 2; + fidl_filter.set_service_uuid(service_uuid); + fidl_filter.set_service_data_uuid(service_data_uuid); + fidl_filter.set_manufacturer_id(2); + fidl_filter.set_connectable(true); + fidl_filter.set_name("name"); + fidl_filter.set_max_path_loss(3); + bt::gap::DiscoveryFilter filter = DiscoveryFilterFromFidl(fidl_filter); + EXPECT_THAT(filter.service_uuids(), + ::testing::ElementsAre(service_uuid.value)); + EXPECT_THAT(filter.service_data_uuids(), + ::testing::ElementsAre(service_data_uuid.value)); + ASSERT_TRUE(filter.manufacturer_code().has_value()); + EXPECT_EQ(filter.manufacturer_code().value(), 2u); + ASSERT_TRUE(filter.connectable().has_value()); + EXPECT_EQ(filter.connectable().value(), true); + EXPECT_EQ(filter.name_substring(), "name"); + ASSERT_TRUE(filter.pathloss().has_value()); + ASSERT_EQ(filter.pathloss().value(), 3); +} + +TEST(HelpersTest, EmptyAdvertisingDataToFidlScanData) { + bt::AdvertisingData input; + fble::ScanData output = AdvertisingDataToFidlScanData( + input, pw::chrono::SystemClock::time_point(std::chrono::nanoseconds(1))); + EXPECT_FALSE(output.has_tx_power()); + EXPECT_FALSE(output.has_appearance()); + EXPECT_FALSE(output.has_service_uuids()); + EXPECT_FALSE(output.has_service_data()); + EXPECT_FALSE(output.has_manufacturer_data()); + EXPECT_FALSE(output.has_uris()); + ASSERT_TRUE(output.has_timestamp()); + EXPECT_EQ(output.timestamp(), zx::time(1).get()); +} + +TEST(HelpersTest, AdvertisingDataToFidlScanData) { + bt::AdvertisingData input; + input.SetTxPower(4); + const uint16_t kAppearance = 193u; // WATCH_SPORTS + input.SetAppearance(kAppearance); + + const uint16_t id = 0x5678; + const bt::UUID kServiceUuid = bt::UUID(id); + auto service_bytes = bt::StaticByteBuffer(0x01, 0x02); + EXPECT_TRUE(input.AddServiceUuid(kServiceUuid)); + EXPECT_TRUE(input.SetServiceData(kServiceUuid, service_bytes.view())); + + const uint16_t kManufacturer = 0x98; + auto manufacturer_bytes = bt::StaticByteBuffer(0x04, 0x03); + EXPECT_TRUE( + input.SetManufacturerData(kManufacturer, manufacturer_bytes.view())); + + const char* const kUri = "http://fuchsia.cl/461435"; + EXPECT_TRUE(input.AddUri(kUri)); + + fble::ScanData output = AdvertisingDataToFidlScanData( + input, pw::chrono::SystemClock::time_point(std::chrono::nanoseconds(1))); + EXPECT_EQ(4, output.tx_power()); + EXPECT_EQ(fbt::Appearance{kAppearance}, output.appearance()); + ASSERT_EQ(1u, output.service_uuids().size()); + EXPECT_EQ(kServiceUuid, UuidFromFidl(output.service_uuids().front())); + ASSERT_EQ(1u, output.service_data().size()); + ASSERT_TRUE(output.has_timestamp()); + EXPECT_EQ(output.timestamp(), zx::time(1).get()); + auto service_data = output.service_data().front(); + EXPECT_EQ(kServiceUuid, UuidFromFidl(service_data.uuid)); + EXPECT_TRUE( + ContainersEqual(bt::BufferView(service_bytes), service_data.data)); + EXPECT_EQ(1u, output.manufacturer_data().size()); + auto manufacturer_data = output.manufacturer_data().front(); + EXPECT_EQ(kManufacturer, manufacturer_data.company_id); + EXPECT_TRUE(ContainersEqual(bt::BufferView(manufacturer_bytes), + manufacturer_data.data)); + EXPECT_THAT(output.uris(), ::testing::ElementsAre(kUri)); +} + +TEST(HelpersTest, AdvertisingDataToFidlScanDataOmitsNonEnumeratedAppearance) { + // There is an "unknown" appearance, which is why this isn't named that. + const uint16_t kNonEnumeratedAppearance = 0xFFFFu; + bt::AdvertisingData input; + input.SetAppearance(kNonEnumeratedAppearance); + + EXPECT_FALSE(AdvertisingDataToFidlScanData( + input, pw::chrono::SystemClock::time_point()) + .has_appearance()); + + const uint16_t kKnownAppearance = 832u; // HEART_RATE_SENSOR + input.SetAppearance(kKnownAppearance); + + EXPECT_TRUE(AdvertisingDataToFidlScanData( + input, pw::chrono::SystemClock::time_point()) + .has_appearance()); +} + +TEST_F(HelpersTestWithLoop, PeerToFidlLeWithAllFields) { + RunLoopFor(zx::duration(2)); + + const bt::PeerId kPeerId(1); + const bt::DeviceAddress kAddr(bt::DeviceAddress::Type::kLEPublic, + {1, 0, 0, 0, 0, 0}); + bt::gap::PeerMetrics metrics; + bt::gap::Peer peer([](auto&, auto) {}, + [](auto&) {}, + [](auto&) {}, + [](auto&) { return false; }, + kPeerId, + kAddr, + /*connectable=*/true, + &metrics, + pw_dispatcher()); + peer.RegisterName("name"); + const int8_t kRssi = 1; + auto adv_bytes = bt::StaticByteBuffer( + // Uri: "https://abc.xyz" + 0x0B, + bt::DataType::kURI, + 0x17, + '/', + '/', + 'a', + 'b', + 'c', + '.', + 'x', + 'y', + 'z'); + peer.MutLe().SetAdvertisingData( + kRssi, + adv_bytes, + pw::chrono::SystemClock::time_point(std::chrono::nanoseconds(1))); + + fble::Peer fidl_peer = PeerToFidlLe(peer); + ASSERT_TRUE(fidl_peer.has_id()); + EXPECT_EQ(fidl_peer.id().value, kPeerId.value()); + ASSERT_TRUE(fidl_peer.has_bonded()); + EXPECT_FALSE(fidl_peer.bonded()); + ASSERT_TRUE(fidl_peer.has_name()); + EXPECT_EQ(fidl_peer.name(), "name"); + ASSERT_TRUE(fidl_peer.has_rssi()); + EXPECT_EQ(fidl_peer.rssi(), kRssi); + ASSERT_TRUE(fidl_peer.has_data()); + EXPECT_THAT(fidl_peer.data().uris(), + ::testing::ElementsAre("https://abc.xyz")); + ASSERT_TRUE(fidl_peer.has_last_updated()); + EXPECT_EQ(fidl_peer.last_updated(), 2); + ASSERT_TRUE(fidl_peer.data().has_timestamp()); + EXPECT_EQ(fidl_peer.data().timestamp(), 1); +} + +TEST_F(HelpersTestWithLoop, PeerToFidlLeWithoutAdvertisingData) { + const bt::PeerId kPeerId(1); + const bt::DeviceAddress kAddr(bt::DeviceAddress::Type::kLEPublic, + {1, 0, 0, 0, 0, 0}); + bt::gap::PeerMetrics metrics; + bt::gap::Peer peer([](auto&, auto) {}, + [](auto&) {}, + [](auto&) {}, + [](auto&) { return false; }, + kPeerId, + kAddr, + /*connectable=*/true, + &metrics, + pw_dispatcher()); + + fble::Peer fidl_peer = PeerToFidlLe(peer); + ASSERT_TRUE(fidl_peer.has_id()); + EXPECT_EQ(fidl_peer.id().value, kPeerId.value()); + ASSERT_TRUE(fidl_peer.has_bonded()); + EXPECT_FALSE(fidl_peer.bonded()); + EXPECT_FALSE(fidl_peer.has_name()); + EXPECT_FALSE(fidl_peer.has_rssi()); + EXPECT_FALSE(fidl_peer.has_data()); + ASSERT_TRUE(fidl_peer.has_last_updated()); + EXPECT_EQ(fidl_peer.last_updated(), 0); +} + +TEST(HelpersTest, PeerIdFromString) { + EXPECT_FALSE(PeerIdFromString(std::string())); + EXPECT_FALSE(PeerIdFromString("-1")); + EXPECT_FALSE(PeerIdFromString("g")); + EXPECT_EQ(PeerIdFromString("0"), std::optional(bt::PeerId(0))); + EXPECT_EQ(PeerIdFromString("FFFFFFFFFFFFFFFF"), + std::optional(bt::PeerId(std::numeric_limits::max()))); + // ID is 1 more than max. + EXPECT_FALSE(PeerIdFromString("10000000000000000")); + EXPECT_EQ(PeerIdFromString("0123456789"), + std::optional(bt::PeerId(0x123456789))); + EXPECT_EQ(PeerIdFromString("abcdef"), std::optional(bt::PeerId(0xabcdef))); + EXPECT_EQ(PeerIdFromString("ABCDEF"), std::optional(bt::PeerId(0xabcdef))); +} + +TEST(HelpersTest, ScoPacketStatusToFidl) { + EXPECT_EQ( + ScoPacketStatusToFidl( + bt::hci_spec::SynchronousDataPacketStatusFlag::kCorrectlyReceived), + fuchsia::bluetooth::bredr::RxPacketStatus::CORRECTLY_RECEIVED_DATA); + EXPECT_EQ( + ScoPacketStatusToFidl( + bt::hci_spec::SynchronousDataPacketStatusFlag::kPossiblyInvalid), + fuchsia::bluetooth::bredr::RxPacketStatus::POSSIBLY_INVALID_DATA); + EXPECT_EQ(ScoPacketStatusToFidl( + bt::hci_spec::SynchronousDataPacketStatusFlag::kNoDataReceived), + fuchsia::bluetooth::bredr::RxPacketStatus::NO_DATA_RECEIVED); + EXPECT_EQ( + ScoPacketStatusToFidl( + bt::hci_spec::SynchronousDataPacketStatusFlag::kDataPartiallyLost), + fuchsia::bluetooth::bredr::RxPacketStatus::DATA_PARTIALLY_LOST); +} + +TEST(HelpersTest, Gatt2DescriptorFromFidlNoPermissions) { + fbg2::Descriptor desc; + fbt::Uuid fidl_uuid{{}}; + desc.set_type(fidl_uuid); + desc.set_handle(fbg2::Handle{3}); + EXPECT_EQ(Gatt2DescriptorFromFidl(desc), nullptr); +} + +TEST(HelpersTest, Gatt2DescriptorFromFidlNoType) { + fbg2::Descriptor desc; + fbg2::AttributePermissions permissions; + desc.set_permissions(std::move(permissions)); + desc.set_handle(fbg2::Handle{3}); + EXPECT_EQ(Gatt2DescriptorFromFidl(desc), nullptr); +} + +TEST(HelpersTest, Gatt2DescriptorFromFidlNoHandle) { + fbg2::Descriptor desc; + fbg2::AttributePermissions permissions; + desc.set_permissions(std::move(permissions)); + fbt::Uuid fidl_uuid{{}}; + desc.set_type(fidl_uuid); + EXPECT_EQ(Gatt2DescriptorFromFidl(desc), nullptr); +} + +TEST(HelpersTest, Gatt2DescriptorFromFidlSuccessWithEmptyPermissions) { + bt::UInt128 type = {2}; + + fbg2::Descriptor desc; + fbg2::AttributePermissions permissions; + desc.set_permissions(std::move(permissions)); + fbt::Uuid fidl_uuid{type}; + desc.set_type(fidl_uuid); + desc.set_handle(fbg2::Handle{3}); + + std::unique_ptr out = Gatt2DescriptorFromFidl(desc); + ASSERT_TRUE(out); + EXPECT_EQ(out->id(), 3u); + EXPECT_EQ(out->type(), bt::UUID(type)); + EXPECT_FALSE(out->read_permissions().allowed()); + EXPECT_FALSE(out->write_permissions().allowed()); +} + +TEST(HelpersTest, Gatt2DescriptorFromFidlSuccessWithEmptyReadWritePermissions) { + fbg2::Descriptor desc; + fbg2::AttributePermissions permissions; + permissions.set_read(fbg2::SecurityRequirements()); + permissions.set_write(fbg2::SecurityRequirements()); + desc.set_permissions(std::move(permissions)); + fbt::Uuid fidl_uuid{{}}; + desc.set_type(fidl_uuid); + desc.set_handle(fbg2::Handle{3}); + + std::unique_ptr out = Gatt2DescriptorFromFidl(desc); + ASSERT_TRUE(out); + EXPECT_TRUE(out->read_permissions().allowed_without_security()); + EXPECT_TRUE(out->write_permissions().allowed_without_security()); +} + +TEST(HelpersTest, Gatt2DescriptorFromFidlSuccessWithReadPermissions) { + fbg2::Descriptor desc; + fbg2::AttributePermissions permissions; + fbg2::SecurityRequirements read_reqs; + read_reqs.set_authentication_required(true); + read_reqs.set_authorization_required(true); + read_reqs.set_encryption_required(true); + permissions.set_read(std::move(read_reqs)); + desc.set_permissions(std::move(permissions)); + desc.set_type({{}}); + desc.set_handle(fbg2::Handle{3}); + + std::unique_ptr out = Gatt2DescriptorFromFidl(desc); + ASSERT_TRUE(out); + EXPECT_TRUE(out->read_permissions().encryption_required()); + EXPECT_TRUE(out->read_permissions().authentication_required()); + EXPECT_TRUE(out->read_permissions().authorization_required()); + EXPECT_FALSE(out->write_permissions().allowed()); +} + +TEST(HelpersTest, Gatt2DescriptorFromFidlSuccessWithWritePermissions) { + fbg2::Descriptor desc; + fbg2::AttributePermissions permissions; + fbg2::SecurityRequirements write_reqs; + write_reqs.set_authentication_required(true); + write_reqs.set_authorization_required(true); + write_reqs.set_encryption_required(true); + permissions.set_write(std::move(write_reqs)); + desc.set_permissions(std::move(permissions)); + desc.set_type({{}}); + desc.set_handle(fbg2::Handle{3}); + + std::unique_ptr out = Gatt2DescriptorFromFidl(desc); + ASSERT_TRUE(out); + EXPECT_FALSE(out->read_permissions().allowed()); + EXPECT_TRUE(out->write_permissions().encryption_required()); + EXPECT_TRUE(out->write_permissions().authentication_required()); + EXPECT_TRUE(out->write_permissions().authorization_required()); +} + +TEST(HelpersTest, Gatt2CharacteristicFromFidlNoProperties) { + fbg2::Characteristic chrc; + chrc.set_permissions(fbg2::AttributePermissions()); + chrc.set_type(fbt::Uuid{{}}); + chrc.set_handle(fbg2::Handle{3}); + EXPECT_FALSE(Gatt2CharacteristicFromFidl(chrc)); +} + +TEST(HelpersTest, Gatt2CharacteristicFromFidlNoPermissions) { + fbg2::Characteristic chrc; + chrc.mutable_properties(); + chrc.set_type(fbt::Uuid{{}}); + chrc.set_handle(fbg2::Handle{3}); + EXPECT_FALSE(Gatt2CharacteristicFromFidl(chrc)); +} + +TEST(HelpersTest, + Gatt2CharacteristicFromFidlSuccessWithPropertiesAndEmptyPermissions) { + fbg2::Characteristic chrc; + chrc.set_properties(fbg2::CharacteristicPropertyBits::WRITE | + fbg2::CharacteristicPropertyBits::RELIABLE_WRITE); + chrc.set_permissions(fbg2::AttributePermissions()); + bt::UInt128 type = {2}; + chrc.set_type(fbt::Uuid{type}); + chrc.set_handle(fbg2::Handle{3}); + std::unique_ptr out = + Gatt2CharacteristicFromFidl(chrc); + ASSERT_TRUE(out); + EXPECT_EQ( + out->properties(), + bt::gatt::Property::kWrite | bt::gatt::Property::kExtendedProperties); + EXPECT_EQ(out->extended_properties(), + bt::gatt::ExtendedProperty::kReliableWrite); + EXPECT_FALSE(out->write_permissions().allowed()); + EXPECT_FALSE(out->read_permissions().allowed()); + EXPECT_FALSE(out->update_permissions().allowed()); + EXPECT_EQ(out->type(), bt::UUID(type)); + EXPECT_EQ(out->id(), 3u); +} + +TEST(HelpersTest, + Gatt2CharacteristicFromFidlSupportsNotifyButDoesNotHavePermission) { + fbg2::Characteristic chrc; + chrc.set_properties(fbg2::CharacteristicPropertyBits::NOTIFY); + chrc.set_permissions(fbg2::AttributePermissions()); + bt::UInt128 type = {2}; + chrc.set_type(fbt::Uuid{type}); + chrc.set_handle(fbg2::Handle{3}); + std::unique_ptr out = + Gatt2CharacteristicFromFidl(chrc); + ASSERT_FALSE(out); +} + +TEST( + HelpersTest, + Gatt2CharacteristicFromFidlDoesNotSupportNotifyButDoesHaveUpdatePermission) { + fbg2::Characteristic chrc; + chrc.mutable_properties(); + fbg2::AttributePermissions permissions; + permissions.set_update(fbg2::SecurityRequirements()); + chrc.set_permissions(std::move(permissions)); + bt::UInt128 type = {2}; + chrc.set_type(fbt::Uuid{type}); + chrc.set_handle(fbg2::Handle{3}); + std::unique_ptr out = + Gatt2CharacteristicFromFidl(chrc); + ASSERT_FALSE(out); +} + +TEST(HelpersTest, + Gatt2CharacteristicFromFidlSuccessWithEmptySecurityRequirements) { + fbg2::Characteristic chrc; + chrc.set_properties(fbg2::CharacteristicPropertyBits::NOTIFY); + fbg2::AttributePermissions permissions; + permissions.set_read(fbg2::SecurityRequirements()); + permissions.set_write(fbg2::SecurityRequirements()); + permissions.set_update(fbg2::SecurityRequirements()); + chrc.set_permissions(std::move(permissions)); + bt::UInt128 type = {2}; + chrc.set_type(fbt::Uuid{type}); + chrc.set_handle(fbg2::Handle{3}); + std::unique_ptr out = + Gatt2CharacteristicFromFidl(chrc); + ASSERT_TRUE(out); + EXPECT_TRUE(out->write_permissions().allowed_without_security()); + EXPECT_TRUE(out->read_permissions().allowed_without_security()); + EXPECT_TRUE(out->update_permissions().allowed_without_security()); +} + +TEST(HelpersTest, + Gatt2CharacteristicFromFidlSuccessWithAllSecurityRequirements) { + fbg2::Characteristic chrc; + chrc.set_properties(fbg2::CharacteristicPropertyBits::NOTIFY); + fbg2::AttributePermissions permissions; + fbg2::SecurityRequirements read_reqs; + read_reqs.set_authentication_required(true); + read_reqs.set_authorization_required(true); + read_reqs.set_encryption_required(true); + permissions.set_read(std::move(read_reqs)); + fbg2::SecurityRequirements write_reqs; + write_reqs.set_authentication_required(true); + write_reqs.set_authorization_required(true); + write_reqs.set_encryption_required(true); + permissions.set_write(std::move(write_reqs)); + fbg2::SecurityRequirements update_reqs; + update_reqs.set_authentication_required(true); + update_reqs.set_authorization_required(true); + update_reqs.set_encryption_required(true); + permissions.set_update(std::move(update_reqs)); + chrc.set_permissions(std::move(permissions)); + bt::UInt128 type = {2}; + chrc.set_type(fbt::Uuid{type}); + chrc.set_handle(fbg2::Handle{3}); + std::unique_ptr out = + Gatt2CharacteristicFromFidl(chrc); + ASSERT_TRUE(out); + EXPECT_TRUE(out->write_permissions().authentication_required()); + EXPECT_TRUE(out->write_permissions().authorization_required()); + EXPECT_TRUE(out->write_permissions().encryption_required()); + EXPECT_TRUE(out->read_permissions().authentication_required()); + EXPECT_TRUE(out->read_permissions().authorization_required()); + EXPECT_TRUE(out->read_permissions().encryption_required()); + EXPECT_TRUE(out->update_permissions().authentication_required()); + EXPECT_TRUE(out->update_permissions().authorization_required()); + EXPECT_TRUE(out->update_permissions().encryption_required()); +} + +TEST(HelpersTest, Gatt2CharacteristicFromFidlWithInvalidDescriptor) { + fbg2::Characteristic chrc; + chrc.mutable_properties(); + chrc.set_permissions(fbg2::AttributePermissions()); + bt::UInt128 type = {2}; + chrc.set_type(fbt::Uuid{type}); + chrc.set_handle(fbg2::Handle{3}); + std::vector descriptors; + descriptors.emplace_back(fbg2::Descriptor()); + chrc.set_descriptors(std::move(descriptors)); + std::unique_ptr out = + Gatt2CharacteristicFromFidl(chrc); + ASSERT_FALSE(out); +} + +TEST(HelpersTest, Gatt2CharacteristicFromFidlWith2Descriptors) { + fbg2::Characteristic chrc; + chrc.mutable_properties(); + chrc.set_permissions(fbg2::AttributePermissions()); + bt::UInt128 chrc_type = {2}; + chrc.set_type(fbt::Uuid{chrc_type}); + chrc.set_handle(fbg2::Handle{3}); + + std::vector descriptors; + + fbg2::Descriptor desc_0; + desc_0.set_permissions(fbg2::AttributePermissions()); + bt::UInt128 desc_type_0 = {4}; + desc_0.set_type(fbt::Uuid{desc_type_0}); + desc_0.set_handle(fbg2::Handle{5}); + descriptors.push_back(std::move(desc_0)); + + fbg2::Descriptor desc_1; + desc_1.set_permissions(fbg2::AttributePermissions()); + bt::UInt128 desc_type_1 = {6}; + desc_1.set_type(fbt::Uuid{desc_type_1}); + desc_1.set_handle(fbg2::Handle{7}); + descriptors.push_back(std::move(desc_1)); + + chrc.set_descriptors(std::move(descriptors)); + + std::unique_ptr out = + Gatt2CharacteristicFromFidl(chrc); + ASSERT_TRUE(out); + ASSERT_EQ(out->descriptors().size(), 2u); + EXPECT_EQ(out->descriptors()[0]->id(), 5u); + EXPECT_EQ(out->descriptors()[0]->type(), bt::UUID(desc_type_0)); + EXPECT_FALSE(out->descriptors()[0]->read_permissions().allowed()); + EXPECT_FALSE(out->descriptors()[0]->write_permissions().allowed()); + EXPECT_EQ(out->descriptors()[1]->id(), 7u); + EXPECT_EQ(out->descriptors()[1]->type(), bt::UUID(desc_type_1)); + EXPECT_FALSE(out->descriptors()[1]->read_permissions().allowed()); + EXPECT_FALSE(out->descriptors()[1]->write_permissions().allowed()); +} + +TEST(HelpersTest, DataPathDirectionFromFidl) { + EXPECT_EQ(DataPathDirectionFromFidl(fuchsia::bluetooth::DataDirection::INPUT), + pw::bluetooth::emboss::DataPathDirection::INPUT); + EXPECT_EQ( + DataPathDirectionFromFidl(fuchsia::bluetooth::DataDirection::OUTPUT), + pw::bluetooth::emboss::DataPathDirection::OUTPUT); +} + +TEST(HelpersTest, CodingFormatFromFidl) { + EXPECT_EQ( + CodingFormatFromFidl(fuchsia::bluetooth::AssignedCodingFormat::U_LAW_LOG), + pw::bluetooth::emboss::CodingFormat::U_LAW); + EXPECT_EQ( + CodingFormatFromFidl(fuchsia::bluetooth::AssignedCodingFormat::A_LAW_LOG), + pw::bluetooth::emboss::CodingFormat::A_LAW); + EXPECT_EQ( + CodingFormatFromFidl(fuchsia::bluetooth::AssignedCodingFormat::CVSD), + pw::bluetooth::emboss::CodingFormat::CVSD); + EXPECT_EQ(CodingFormatFromFidl( + fuchsia::bluetooth::AssignedCodingFormat::TRANSPARENT), + pw::bluetooth::emboss::CodingFormat::TRANSPARENT); + EXPECT_EQ(CodingFormatFromFidl( + fuchsia::bluetooth::AssignedCodingFormat::LINEAR_PCM), + pw::bluetooth::emboss::CodingFormat::LINEAR_PCM); + EXPECT_EQ( + CodingFormatFromFidl(fuchsia::bluetooth::AssignedCodingFormat::MSBC), + pw::bluetooth::emboss::CodingFormat::MSBC); + EXPECT_EQ(CodingFormatFromFidl(fuchsia::bluetooth::AssignedCodingFormat::LC3), + pw::bluetooth::emboss::CodingFormat::LC3); + EXPECT_EQ( + CodingFormatFromFidl(fuchsia::bluetooth::AssignedCodingFormat::G_729A), + pw::bluetooth::emboss::CodingFormat::G729A); +} + +TEST(HelpersTest, AssignedCodecIdFromFidl) { + // Assigned format + fuchsia::bluetooth::CodecId fidl_codec_id; + fidl_codec_id.set_assigned_format( + fuchsia::bluetooth::AssignedCodingFormat::LC3); + auto codec_id = CodecIdFromFidl(fidl_codec_id); + auto view = codec_id.view(); + EXPECT_EQ(view.coding_format().Read(), + pw::bluetooth::emboss::CodingFormat::LC3); +} + +TEST(HelpersTest, VendorCodecIdFromFidl) { + // Vendor-specific format + uint16_t kCompanyId = 0x1234; + uint16_t kVendorId = 0xfedc; + fuchsia::bluetooth::CodecId fidl_codec_id; + fuchsia::bluetooth::VendorCodingFormat vendor_format; + vendor_format.set_company_id(kCompanyId); + vendor_format.set_vendor_id(kVendorId); + fidl_codec_id.set_vendor_format(std::move(vendor_format)); + auto codec_id = CodecIdFromFidl(fidl_codec_id); + auto view = codec_id.view(); + EXPECT_EQ(view.coding_format().Read(), + pw::bluetooth::emboss::CodingFormat::VENDOR_SPECIFIC); + EXPECT_EQ(view.company_id().Read(), kCompanyId); + EXPECT_EQ(view.vendor_codec_id().Read(), kVendorId); +} + +TEST(HelpersTest, LogicalTransportTypeFromFidl) { + EXPECT_EQ(LogicalTransportTypeFromFidl( + fuchsia::bluetooth::LogicalTransportType::LE_CIS), + pw::bluetooth::emboss::LogicalTransportType::LE_CIS); + EXPECT_EQ(LogicalTransportTypeFromFidl( + fuchsia::bluetooth::LogicalTransportType::LE_BIS), + pw::bluetooth::emboss::LogicalTransportType::LE_BIS); +} + +static void ValidateCisEstablishedUnidirectionalParams( + const fuchsia::bluetooth::le::CisUnidirectionalParams& fidl_params, + const bt::iso::CisEstablishedParameters::CisUnidirectionalParams& + iso_params) { + zx::duration transport_latency = zx::usec(iso_params.transport_latency); + EXPECT_EQ(fidl_params.transport_latency(), transport_latency.get()); + // phy is not included in FIDL output + EXPECT_EQ(fidl_params.burst_number(), iso_params.burst_number); + EXPECT_EQ(fidl_params.flush_timeout(), iso_params.flush_timeout); + // max_pdu_size is not included in FIDL output +} + +// Helper function to validate a single set of test values +static void ValidateCisEstablishedConversion( + const bt::iso::CisEstablishedParameters& params_in) { + auto params_out = CisEstablishedParametersToFidl(params_in); + + zx::duration cig_sync_delay(params_out.cig_sync_delay()); + EXPECT_EQ(cig_sync_delay, zx::usec(params_in.cig_sync_delay)); + + zx::duration cis_sync_delay(params_out.cis_sync_delay()); + EXPECT_EQ(cis_sync_delay, zx::usec(params_in.cis_sync_delay)); + + EXPECT_EQ(params_out.max_subevents(), params_in.max_subevents); + + zx::duration iso_interval(params_out.iso_interval()); + EXPECT_EQ( + iso_interval, + zx::usec(params_in.iso_interval * + bt::iso::CisEstablishedParameters::kIsoIntervalToMicroseconds)); + + if (params_in.c_to_p_params.burst_number > 0) { + ValidateCisEstablishedUnidirectionalParams( + params_out.central_to_peripheral_params(), params_in.c_to_p_params); + } else { + EXPECT_FALSE(params_out.has_central_to_peripheral_params()); + } + + if (params_in.p_to_c_params.burst_number > 0) { + ValidateCisEstablishedUnidirectionalParams( + params_out.peripheral_to_central_params(), params_in.p_to_c_params); + } else { + EXPECT_FALSE(params_out.has_peripheral_to_central_params()); + } +} + +TEST(HelpersTest, CisEstablishedParametersToFidl) { + // Min values for all bidirectional params + bt::iso::CisEstablishedParameters params1 = { + .cig_sync_delay = 0x0000ea, + .cis_sync_delay = 0x0000ea, + .max_subevents = 0x01, + .iso_interval = 0x0004, + .c_to_p_params = kMinUniParams, + .p_to_c_params = kMaxUniParams, + }; + ValidateCisEstablishedConversion(params1); + + // Values somewhere between min and max + bt::iso::CisEstablishedParameters params2 = { + .cig_sync_delay = 0x001234, + .cis_sync_delay = 0x005678, + .max_subevents = 0x10, + .iso_interval = 0x246, + .c_to_p_params = + { + .transport_latency = 0x55aa55, + .phy = pw::bluetooth::emboss::IsoPhyType::LE_2M, + .burst_number = 0x02, + .flush_timeout = 0x42, + .max_pdu_size = 0xa3, + }, + .p_to_c_params = + { + .transport_latency = 0xabc123, + .phy = pw::bluetooth::emboss::IsoPhyType::LE_CODED, + .burst_number = 0x0F, + .flush_timeout = 0x53, + .max_pdu_size = 0x37, + }, + }; + ValidateCisEstablishedConversion(params2); + + // Max values for all bidirectional params + bt::iso::CisEstablishedParameters params3 = { + .cig_sync_delay = 0x7fffff, + .cis_sync_delay = 0x7fffff, + .max_subevents = 0x1f, + .iso_interval = 0x0c80, + .c_to_p_params = kMaxUniParams, + .p_to_c_params = kMinUniParams, + }; + ValidateCisEstablishedConversion(params3); + + // Parameters where the burst_number is zero, indicating that this direction + // is not configured. All other values should be ignored and the corresponding + // FIDL unidirectional parameters should not be instantiated. + bt::iso::CisEstablishedParameters::CisUnidirectionalParams + unconfigured_params = { + .transport_latency = 0, + .phy = pw::bluetooth::emboss::IsoPhyType::LE_1M, + .burst_number = 0x00, + .flush_timeout = 0x00, + .max_pdu_size = 0x00, + }; + + // Unconfigured central => peripheral + bt::iso::CisEstablishedParameters params4 = params2; + params4.c_to_p_params = unconfigured_params; + ValidateCisEstablishedConversion(params4); + + // Unconfigured peripheral => central + params4 = params2; + params4.p_to_c_params = unconfigured_params; + ValidateCisEstablishedConversion(params4); +} + +} // namespace +} // namespace bthost::fidl_helpers diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/host_server.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/host_server.cc new file mode 100644 index 0000000000..89235bb8dc --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/host_server.cc @@ -0,0 +1,1176 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/fidl/host_server.h" + +#include +#include +#include + +#include +#include + +#include "fuchsia/bluetooth/host/cpp/fidl.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_server_server.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_server_server.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/helpers.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_central_server.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_peripheral_server.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/profile_server.h" +#include "pw_bluetooth_sapphire/internal/host/common/assert.h" +#include "pw_bluetooth_sapphire/internal/host/common/identifier.h" +#include "pw_bluetooth_sapphire/internal/host/common/log.h" +#include "pw_bluetooth_sapphire/internal/host/gap/adapter.h" +#include "pw_bluetooth_sapphire/internal/host/gap/bonding_data.h" +#include "pw_bluetooth_sapphire/internal/host/gap/bredr_connection_manager.h" +#include "pw_bluetooth_sapphire/internal/host/gap/bredr_discovery_manager.h" +#include "pw_bluetooth_sapphire/internal/host/gap/gap.h" +#include "pw_bluetooth_sapphire/internal/host/gap/low_energy_discovery_manager.h" +#include "pw_bluetooth_sapphire/internal/host/sm/types.h" +#include "pw_bluetooth_sapphire/internal/host/sm/util.h" + +namespace bthost { + +namespace fbt = fuchsia::bluetooth; +namespace fsys = fuchsia::bluetooth::sys; +namespace fhost = fuchsia::bluetooth::host; + +using bt::PeerId; +using bt::gap::BrEdrSecurityModeToString; +using bt::gap::LeSecurityModeToString; +using bt::sm::IOCapability; +using fidl_helpers::BrEdrSecurityModeFromFidl; +using fidl_helpers::HostErrorToFidl; +using fidl_helpers::LeSecurityModeFromFidl; +using fidl_helpers::NewFidlError; +using fidl_helpers::PeerIdFromString; +using fidl_helpers::ResultToFidl; +using fidl_helpers::SecurityLevelFromFidl; + +HostServer::HostServer(zx::channel channel, + const bt::gap::Adapter::WeakPtr& adapter, + bt::gatt::GATT::WeakPtr gatt) + : AdapterServerBase(adapter, this, std::move(channel)), + pairing_delegate_(nullptr), + gatt_(std::move(gatt)), + requesting_background_scan_(false), + requesting_discoverable_(false), + io_capability_(IOCapability::kNoInputNoOutput), + weak_self_(this), + weak_pairing_(this) { + BT_ASSERT(gatt_.is_alive()); + + auto self = weak_self_.GetWeakPtr(); + adapter->peer_cache()->set_peer_bonded_callback([self](const auto& peer) { + if (self.is_alive()) { + self->OnPeerBonded(peer); + } + }); + adapter->set_auto_connect_callback([self](auto conn_ref) { + if (self.is_alive()) { + self->RegisterLowEnergyConnection(std::move(conn_ref), + /*auto_connect=*/true); + } + }); + + // Watch for changes in LE address. + adapter->le()->register_address_changed_callback([self]() { + if (self.is_alive()) { + self->NotifyInfoChange(); + } + }); + + // Initialize the HostInfo getter with the initial state. + NotifyInfoChange(); +} + +HostServer::~HostServer() { Shutdown(); } + +void HostServer::RequestProtocol(fhost::ProtocolRequest request) { + switch (request.Which()) { + case fhost::ProtocolRequest::Tag::kCentral: + BindServer( + adapter()->AsWeakPtr(), std::move(request.central()), gatt_); + break; + case fhost::ProtocolRequest::Tag::kPeripheral: + BindServer( + adapter()->AsWeakPtr(), gatt_, std::move(request.peripheral())); + break; + case fhost::ProtocolRequest::Tag::kGattServer: + BindServer(gatt_->GetWeakPtr(), + std::move(request.gatt_server())); + break; + case fhost::ProtocolRequest::Tag::kGatt2Server: + BindServer(gatt_->GetWeakPtr(), + std::move(request.gatt2_server())); + break; + case fhost::ProtocolRequest::Tag::kProfile: + BindServer(adapter()->AsWeakPtr(), + std::move(request.profile())); + break; + default: + bt_log(WARN, "fidl", "received unknown protocol request"); + // The unknown protocol will be closed when `request` is destroyed. + break; + } +} + +void HostServer::WatchState(WatchStateCallback callback) { + info_getter_.Watch([cb = std::move(callback)](fsys::HostInfo info) { + cb(fhost::Host_WatchState_Result::WithResponse( + fhost::Host_WatchState_Response(std::move(info)))); + }); +} + +void HostServer::SetLocalData(fsys::HostData host_data) { + if (host_data.has_irk()) { + bt_log(DEBUG, "fidl", "assign IRK"); + if (adapter()->le()) { + adapter()->le()->set_irk(host_data.irk().value); + } + } +} + +void HostServer::SetPeerWatcher( + ::fidl::InterfaceRequest<::fuchsia::bluetooth::host::PeerWatcher> + peer_watcher) { + if (peer_watcher_server_.has_value()) { + peer_watcher.Close(ZX_ERR_ALREADY_BOUND); + return; + } + peer_watcher_server_.emplace( + std::move(peer_watcher), adapter()->peer_cache(), this); +} + +void HostServer::SetLocalName(::std::string local_name, + SetLocalNameCallback callback) { + BT_DEBUG_ASSERT(!local_name.empty()); + adapter()->SetLocalName(std::move(local_name), + [self = weak_self_.GetWeakPtr(), + callback = std::move(callback)](auto status) { + // Send adapter state update on success and if the + // connection is still open. + if (status.is_ok() && self.is_alive()) { + self->NotifyInfoChange(); + } + callback(ResultToFidl(status)); + }); +} + +// TODO(fxbug.dev/42110379): Add a unit test for this method. +void HostServer::SetDeviceClass(fbt::DeviceClass device_class, + SetDeviceClassCallback callback) { + // Device Class values must only contain data in the lower 3 bytes. + if (device_class.value >= 1 << 24) { + callback(fpromise::error(fsys::Error::INVALID_ARGUMENTS)); + return; + } + bt::DeviceClass dev_class(device_class.value); + adapter()->SetDeviceClass(dev_class, + [callback = std::move(callback)](auto status) { + callback(ResultToFidl(status)); + }); +} + +void HostServer::StartLEDiscovery() { + if (!adapter()->le()) { + StopDiscovery(ZX_ERR_INTERNAL); + return; + } + + adapter()->le()->StartDiscovery( + /*active=*/true, [self = weak_self_.GetWeakPtr()](auto session) { + // End the new session if this AdapterServer got destroyed in the + // meantime (e.g. because the client disconnected). + if (!self.is_alive() || self->discovery_session_servers_.empty()) { + return; + } + + if (!session) { + bt_log(ERROR, "fidl", "failed to start active LE discovery session"); + self->StopDiscovery(ZX_ERR_INTERNAL); + return; + } + + // Set up a general-discovery filter for connectable devices. + // NOTE(armansito): This currently has no effect since peer updates + // are driven by PeerCache events. |session|'s "result callback" is + // unused. + session->filter()->set_connectable(true); + session->filter()->SetGeneralDiscoveryFlags(); + + self->le_discovery_session_ = std::move(session); + + // Send the adapter state update. + self->NotifyInfoChange(); + }); +} + +void HostServer::StartDiscovery( + ::fuchsia::bluetooth::host::HostStartDiscoveryRequest request) { + bt_log(DEBUG, "fidl", "%s", __FUNCTION__); + BT_DEBUG_ASSERT(adapter().is_alive()); + + if (!request.has_token()) { + bt_log(WARN, "fidl", "missing Discovery token"); + return; + } + fidl::InterfaceRequest token = + std::move(*request.mutable_token()); + + std::unique_ptr server = + std::make_unique(std::move(token), this); + DiscoverySessionServer* server_ptr = server.get(); + discovery_session_servers_.emplace(server_ptr, std::move(server)); + + // If there were existing sessions, then discovery is already + // starting/started. + if (discovery_session_servers_.size() != 1) { + return; + } + + if (!adapter()->bredr()) { + StartLEDiscovery(); + return; + } + // TODO(jamuraa): start these in parallel instead of sequence + adapter()->bredr()->RequestDiscovery([self = weak_self_.GetWeakPtr(), + func = __FUNCTION__]( + bt::hci::Result<> result, + auto session) mutable { + if (!self.is_alive() || self->discovery_session_servers_.empty()) { + return; + } + + if (result.is_error() || !session) { + bt_log( + ERROR, "fidl", "%s: failed to start BR/EDR discovery session", func); + self->StopDiscovery(ZX_ERR_INTERNAL); + return; + } + + self->bredr_discovery_session_ = std::move(session); + self->StartLEDiscovery(); + }); +} + +void HostServer::StopDiscovery(zx_status_t epitaph, bool notify_info_change) { + bool discovering = le_discovery_session_ || bredr_discovery_session_; + bredr_discovery_session_ = nullptr; + le_discovery_session_ = nullptr; + for (auto& [server, _] : discovery_session_servers_) { + server->Close(epitaph); + } + discovery_session_servers_.clear(); + + if (discovering && notify_info_change) { + NotifyInfoChange(); + } +} + +void HostServer::OnDiscoverySessionServerClose(DiscoverySessionServer* server) { + server->Close(ZX_ERR_CANCELED); + discovery_session_servers_.erase(server); + if (discovery_session_servers_.empty()) { + StopDiscovery(ZX_ERR_CANCELED); + } +} + +void HostServer::SetConnectable(bool connectable, + SetConnectableCallback callback) { + bt_log(INFO, "fidl", "%s: %s", __FUNCTION__, connectable ? "true" : "false"); + + auto classic = adapter()->bredr(); + if (!classic) { + callback(fpromise::error(fsys::Error::NOT_SUPPORTED)); + return; + } + classic->SetConnectable(connectable, + [callback = std::move(callback)](const auto& result) { + callback(ResultToFidl(result)); + }); +} + +void HostServer::RestoreBonds( + ::std::vector bonds, + fhost::BondingDelegate::RestoreBondsCallback callback) { + bt_log(INFO, "fidl", "%s", __FUNCTION__); + + if (bonds.empty()) { + // Nothing to do. Reply with an empty list. + callback(fhost::BondingDelegate_RestoreBonds_Result::WithResponse( + fhost::BondingDelegate_RestoreBonds_Response())); + return; + } + + std::vector errors; + for (auto& bond : bonds) { + if (!bond.has_identifier() || !bond.has_address() || + !(bond.has_le_bond() || bond.has_bredr_bond())) { + bt_log(ERROR, + "fidl", + "%s: BondingData mandatory fields missing!", + __FUNCTION__); + errors.push_back(std::move(bond)); + continue; + } + + auto address = fidl_helpers::AddressFromFidlBondingData(bond); + if (!address) { + bt_log(ERROR, + "fidl", + "%s: BondingData address missing or invalid!", + __FUNCTION__); + errors.push_back(std::move(bond)); + continue; + } + + bt::gap::BondingData bd; + bd.identifier = bt::PeerId{bond.identifier().value}; + bd.address = *address; + if (bond.has_name()) { + bd.name = {bond.name()}; + } + + if (bond.has_le_bond()) { + bd.le_pairing_data = + fidl_helpers::LePairingDataFromFidl(*address, bond.le_bond()); + } + if (bond.has_bredr_bond()) { + bd.bredr_link_key = fidl_helpers::BredrKeyFromFidl(bond.bredr_bond()); + bd.bredr_services = + fidl_helpers::BredrServicesFromFidl(bond.bredr_bond()); + } + + // TODO(fxbug.dev/42137736): Convert bond.bredr.services to + // BondingData::bredr_services + if (!adapter()->AddBondedPeer(bd)) { + bt_log(ERROR, + "fidl", + "%s: failed to restore bonding data entry", + __FUNCTION__); + errors.push_back(std::move(bond)); + } + } + + callback(fhost::BondingDelegate_RestoreBonds_Result::WithResponse( + fhost::BondingDelegate_RestoreBonds_Response(std::move(errors)))); +} + +void HostServer::OnPeerBonded(const bt::gap::Peer& peer) { + bt_log(DEBUG, "fidl", "%s", __FUNCTION__); + if (bonding_delegate_server_) { + bonding_delegate_server_->OnNewBondingData(peer); + } +} + +void HostServer::RegisterLowEnergyConnection( + std::unique_ptr conn_ref, + bool auto_connect) { + BT_DEBUG_ASSERT(conn_ref); + + bt::PeerId id = conn_ref->peer_identifier(); + auto iter = le_connections_.find(id); + if (iter != le_connections_.end()) { + bt_log( + WARN, + "fidl", + "%s: peer already connected; connection reference dropped (peer: %s)", + __FUNCTION__, + bt_str(id)); + return; + } + + bt_log(DEBUG, + "fidl", + "LE peer connected (%s): %s ", + (auto_connect ? "auto" : "direct"), + bt_str(id)); + conn_ref->set_closed_callback([self = weak_self_.GetWeakPtr(), id] { + if (self.is_alive()) + self->le_connections_.erase(id); + }); + le_connections_[id] = std::move(conn_ref); +} + +void HostServer::SetDiscoverable(bool discoverable, + SetDiscoverableCallback callback) { + bt_log(INFO, "fidl", "%s(%s)", __FUNCTION__, discoverable ? "true" : "false"); + // TODO(fxbug.dev/42177512): advertise LE here + if (!discoverable) { + bredr_discoverable_session_ = nullptr; + NotifyInfoChange(); + callback(fpromise::ok()); + return; + } + if (discoverable && requesting_discoverable_) { + bt_log(DEBUG, "fidl", "%s already in progress", __FUNCTION__); + callback(fpromise::error(fsys::Error::IN_PROGRESS)); + return; + } + requesting_discoverable_ = true; + if (!adapter()->bredr()) { + callback(fpromise::error(fsys::Error::FAILED)); + return; + } + adapter()->bredr()->RequestDiscoverable([self = weak_self_.GetWeakPtr(), + callback = std::move(callback), + func = __FUNCTION__]( + bt::hci::Result<> result, + auto session) { + if (!self.is_alive()) { + callback(fpromise::error(fsys::Error::FAILED)); + return; + } + + if (!self->requesting_discoverable_) { + callback(fpromise::error(fsys::Error::CANCELED)); + return; + } + + if (result.is_error() || !session) { + bt_log(ERROR, "fidl", "%s: failed (result: %s)", func, bt_str(result)); + fpromise::result fidl_result = ResultToFidl(result); + if (result.is_ok()) { + BT_ASSERT(session == nullptr); + fidl_result = fpromise::error(fsys::Error::FAILED); + } + self->requesting_discoverable_ = false; + callback(std::move(fidl_result)); + return; + } + + self->bredr_discoverable_session_ = std::move(session); + self->requesting_discoverable_ = false; + self->NotifyInfoChange(); + callback(fpromise::ok()); + }); +} + +void HostServer::EnableBackgroundScan(bool enabled) { + bt_log(INFO, "fidl", "%s background scan", (enabled ? "enable" : "disable")); + if (!adapter()->le()) { + bt_log(ERROR, "fidl", "%s: adapter does not support LE", __FUNCTION__); + return; + } + + if (!enabled) { + requesting_background_scan_ = false; + le_background_scan_ = nullptr; + return; + } + + // If a scan is already starting or is in progress, there is nothing to do to + // enable the scan. + if (requesting_background_scan_ || le_background_scan_) { + return; + } + + requesting_background_scan_ = true; + adapter()->le()->StartDiscovery( + /*active=*/false, [self = weak_self_.GetWeakPtr()](auto session) { + if (!self.is_alive()) { + return; + } + + // Background scan may have been disabled while discovery was starting. + if (!self->requesting_background_scan_) { + return; + } + + if (!session) { + bt_log(ERROR, "fidl", "failed to start LE background scan"); + self->le_background_scan_ = nullptr; + self->requesting_background_scan_ = false; + return; + } + + self->le_background_scan_ = std::move(session); + self->requesting_background_scan_ = false; + }); +} + +void HostServer::EnablePrivacy(bool enabled) { + bt_log(INFO, + "fidl", + "%s: %s LE privacy", + __FUNCTION__, + (enabled ? "enable" : "disable")); + if (adapter()->le()) { + adapter()->le()->EnablePrivacy(enabled); + } +} + +void HostServer::SetBrEdrSecurityMode(fsys::BrEdrSecurityMode mode) { + std::optional gap_mode = + BrEdrSecurityModeFromFidl(mode); + if (!gap_mode.has_value()) { + bt_log(WARN, "fidl", "%s: Unrecognized BR/EDR security mode", __FUNCTION__); + return; + } + + bt_log(INFO, + "fidl", + "%s: %s", + __FUNCTION__, + BrEdrSecurityModeToString(gap_mode.value())); + if (adapter()->bredr()) { + adapter()->bredr()->SetBrEdrSecurityMode(gap_mode.value()); + } +} + +void HostServer::SetLeSecurityMode(fsys::LeSecurityMode mode) { + bt::gap::LESecurityMode gap_mode = LeSecurityModeFromFidl(mode); + bt_log( + INFO, "fidl", "%s: %s", __FUNCTION__, LeSecurityModeToString(gap_mode)); + if (adapter()->le()) { + adapter()->le()->SetLESecurityMode(gap_mode); + } +} + +void HostServer::SetPairingDelegate( + fsys::InputCapability input, + fsys::OutputCapability output, + ::fidl::InterfaceHandle delegate) { + bool cleared = !delegate; + pairing_delegate_.Bind(std::move(delegate)); + + if (cleared) { + bt_log(INFO, "fidl", "%s: PairingDelegate cleared", __FUNCTION__); + ResetPairingDelegate(); + return; + } + + io_capability_ = fidl_helpers::IoCapabilityFromFidl(input, output); + bt_log(INFO, + "fidl", + "%s: PairingDelegate assigned (I/O capability: %s)", + __FUNCTION__, + bt::sm::util::IOCapabilityToString(io_capability_).c_str()); + + auto pairing = weak_pairing_.GetWeakPtr(); + auto self = weak_self_.GetWeakPtr(); + adapter()->SetPairingDelegate(pairing); + pairing_delegate_.set_error_handler([self, func = __FUNCTION__]( + zx_status_t status) { + bt_log( + INFO, "fidl", "%s error handler: PairingDelegate disconnected", func); + if (self.is_alive()) { + self->ResetPairingDelegate(); + } + }); +} + +// Attempt to connect to peer identified by |peer_id|. The peer must be +// in our peer cache. We will attempt to connect technologies (LowEnergy, +// Classic or Dual-Mode) as the peer claims to support when discovered +void HostServer::Connect(fbt::PeerId peer_id, ConnectCallback callback) { + bt::PeerId id{peer_id.value}; + bt_log(INFO, "fidl", "%s: (peer: %s)", __FUNCTION__, bt_str(id)); + + auto peer = adapter()->peer_cache()->FindById(id); + if (!peer) { + // We don't support connecting to peers that are not in our cache + bt_log(WARN, + "fidl", + "%s: peer not found in peer cache (peer: %s)", + __FUNCTION__, + bt_str(id)); + callback(fpromise::error(fsys::Error::PEER_NOT_FOUND)); + return; + } + + // TODO(fxbug.dev/42075069): Dual-mode currently not supported; if the peer + // supports BR/EDR we prefer BR/EDR. If a dual-mode peer, we should attempt to + // connect both protocols. + if (peer->bredr()) { + ConnectBrEdr(id, std::move(callback)); + return; + } + + ConnectLowEnergy(id, std::move(callback)); +} + +// Attempt to disconnect the peer identified by |peer_id| from all transports. +// If the peer is already not connected, return success. If the peer is +// disconnected succesfully, return success. +void HostServer::Disconnect(fbt::PeerId peer_id, DisconnectCallback callback) { + bt::PeerId id{peer_id.value}; + bt_log(INFO, "fidl", "%s: (peer: %s)", __FUNCTION__, bt_str(id)); + + bool le_disc = adapter()->le() ? adapter()->le()->Disconnect(id) : true; + bool bredr_disc = adapter()->bredr() + ? adapter()->bredr()->Disconnect( + id, bt::gap::DisconnectReason::kApiRequest) + : true; + if (le_disc && bredr_disc) { + callback(fpromise::ok()); + } else { + bt_log(WARN, "fidl", "%s: failed (peer: %s)", __FUNCTION__, bt_str(id)); + callback(fpromise::error(fsys::Error::FAILED)); + } +} + +void HostServer::ConnectLowEnergy(PeerId peer_id, ConnectCallback callback) { + auto self = weak_self_.GetWeakPtr(); + auto on_complete = [self, + callback = std::move(callback), + peer_id, + func = __FUNCTION__](auto result) { + if (result.is_error()) { + bt_log(INFO, + "fidl", + "%s: failed to connect LE transport to peer (peer: %s)", + func, + bt_str(peer_id)); + callback(fpromise::error(HostErrorToFidl(result.error_value()))); + return; + } + + // We must be connected and to the right peer + auto connection = std::move(result).value(); + BT_ASSERT(connection); + BT_ASSERT(peer_id == connection->peer_identifier()); + + callback(fpromise::ok()); + + if (self.is_alive()) + self->RegisterLowEnergyConnection(std::move(connection), + /*auto_connect=*/false); + }; + + adapter()->le()->Connect( + peer_id, std::move(on_complete), bt::gap::LowEnergyConnectionOptions()); +} + +// Initiate an outgoing Br/Edr connection, unless already connected +// Br/Edr connections are host-wide, and stored in BrEdrConnectionManager +void HostServer::ConnectBrEdr(PeerId peer_id, ConnectCallback callback) { + auto on_complete = [callback = std::move(callback), + peer_id, + func = __FUNCTION__](auto status, auto connection) { + if (status.is_error()) { + BT_ASSERT(!connection); + bt_log(INFO, + "fidl", + "%s: failed to connect BR/EDR transport to peer (peer: %s)", + func, + bt_str(peer_id)); + callback(fpromise::error(HostErrorToFidl(status.error_value()))); + return; + } + + // We must be connected and to the right peer + BT_ASSERT(connection); + BT_ASSERT(peer_id == connection->peer_id()); + + callback(fpromise::ok()); + }; + + if (!adapter()->bredr()->Connect(peer_id, std::move(on_complete))) { + bt_log( + INFO, + "fidl", + "%s: failed to initiate BR/EDR transport connection to peer (peer: %s)", + __FUNCTION__, + bt_str(peer_id)); + callback(fpromise::error(fsys::Error::FAILED)); + } +} + +void HostServer::Forget(fbt::PeerId peer_id, ForgetCallback callback) { + bt::PeerId id{peer_id.value}; + auto peer = adapter()->peer_cache()->FindById(id); + if (!peer) { + bt_log(DEBUG, "fidl", "peer %s to forget wasn't found", bt_str(id)); + callback(fpromise::ok()); + return; + } + + const bool le_disconnected = + adapter()->le() ? adapter()->le()->Disconnect(id) : true; + const bool bredr_disconnected = + adapter()->bredr() ? adapter()->bredr()->Disconnect( + id, bt::gap::DisconnectReason::kApiRequest) + : true; + const bool peer_removed = adapter()->peer_cache()->RemoveDisconnectedPeer(id); + + if (!le_disconnected || !bredr_disconnected) { + const auto message = + bt_lib_cpp_string::StringPrintf("link(s) failed to close:%s%s", + le_disconnected ? "" : " LE", + bredr_disconnected ? "" : " BR/EDR"); + callback(fpromise::error(fsys::Error::FAILED)); + } else { + BT_ASSERT(peer_removed); + callback(fpromise::ok()); + } +} + +void HostServer::Pair(fbt::PeerId id, + fsys::PairingOptions options, + PairCallback callback) { + auto peer_id = bt::PeerId(id.value); + auto peer = adapter()->peer_cache()->FindById(peer_id); + if (!peer) { + bt_log(WARN, "fidl", "%s: unknown peer %s", __FUNCTION__, bt_str(peer_id)); + // We don't support pairing to peers that are not in our cache + callback(fpromise::error(fsys::Error::PEER_NOT_FOUND)); + return; + } + // If options specifies a transport preference for LE or BR/EDR, we use that. + // Otherwise, we use whatever transport exists, defaulting to LE for dual-mode + // connections. + bool pair_bredr = !peer->le(); + if (options.has_transport() && + options.transport() != fsys::TechnologyType::DUAL_MODE) { + pair_bredr = (options.transport() == fsys::TechnologyType::CLASSIC); + } + if (pair_bredr) { + PairBrEdr(peer_id, std::move(callback)); + return; + } + PairLowEnergy(peer_id, std::move(options), std::move(callback)); +} + +void HostServer::PairLowEnergy(PeerId peer_id, + fsys::PairingOptions options, + PairCallback callback) { + std::optional security_level; + if (options.has_le_security_level()) { + security_level = SecurityLevelFromFidl(options.le_security_level()); + if (!security_level.has_value()) { + bt_log(WARN, + "fidl", + "%s: pairing options missing LE security level (peer: %s)", + __FUNCTION__, + bt_str(peer_id)); + callback(fpromise::error(fsys::Error::INVALID_ARGUMENTS)); + return; + } + } else { + security_level = bt::sm::SecurityLevel::kAuthenticated; + } + bt::sm::BondableMode bondable_mode = bt::sm::BondableMode::Bondable; + if (options.has_bondable_mode() && + options.bondable_mode() == fsys::BondableMode::NON_BONDABLE) { + bondable_mode = bt::sm::BondableMode::NonBondable; + } + auto on_complete = [peer_id, + callback = std::move(callback), + func = __FUNCTION__](bt::sm::Result<> status) { + if (status.is_error()) { + bt_log( + WARN, "fidl", "%s: failed to pair (peer: %s)", func, bt_str(peer_id)); + callback(fpromise::error(HostErrorToFidl(status.error_value()))); + } else { + callback(fpromise::ok()); + } + }; + BT_ASSERT(adapter()->le()); + adapter()->le()->Pair( + peer_id, *security_level, bondable_mode, std::move(on_complete)); +} + +void HostServer::PairBrEdr(PeerId peer_id, PairCallback callback) { + auto on_complete = [peer_id, + callback = std::move(callback), + func = __FUNCTION__](bt::hci::Result<> status) { + if (status.is_error()) { + bt_log( + WARN, "fidl", "%s: failed to pair (peer: %s)", func, bt_str(peer_id)); + callback(fpromise::error(HostErrorToFidl(status.error_value()))); + } else { + callback(fpromise::ok()); + } + }; + // TODO(fxbug.dev/42135898): Add security parameter to Pair and use that here + // instead of hardcoding default. + bt::gap::BrEdrSecurityRequirements security{.authentication = false, + .secure_connections = false}; + BT_ASSERT(adapter()->bredr()); + adapter()->bredr()->Pair(peer_id, security, std::move(on_complete)); +} + +void HostServer::Shutdown() { + bt_log(INFO, "fidl", "closing FIDL handles"); + + // Invalidate all weak pointers. This will guarantee that all pending tasks + // that reference this HostServer will return early if they run in the future. + weak_self_.InvalidatePtrs(); + + // Destroy all FIDL bindings. + servers_.clear(); + + // Cancel pending requests. + requesting_discoverable_ = false; + requesting_background_scan_ = false; + + le_background_scan_ = nullptr; + bredr_discoverable_session_ = nullptr; + + StopDiscovery(ZX_ERR_CANCELED, /*notify_info_change=*/false); + + // Drop all connections that are attached to this HostServer. + le_connections_.clear(); + + if (adapter()->le()) { + // Stop background scan if enabled. + adapter()->le()->EnablePrivacy(false); + adapter()->le()->set_irk(std::nullopt); + } + + // Disallow future pairing. + pairing_delegate_ = nullptr; + ResetPairingDelegate(); + + // Send adapter state change. + if (binding()->is_bound()) { + NotifyInfoChange(); + } +} + +void HostServer::SetBondingDelegate( + ::fidl::InterfaceRequest<::fuchsia::bluetooth::host::BondingDelegate> + request) { + if (bonding_delegate_server_.has_value()) { + request.Close(ZX_ERR_ALREADY_BOUND); + return; + } + bonding_delegate_server_.emplace(std::move(request), this); +} + +void HostServer::handle_unknown_method(uint64_t ordinal, + bool method_has_response) { + bt_log(WARN, "fidl", "Received unknown method with ordinal: %lu", ordinal); +} + +void HostServer::DiscoverySessionServer::Stop() { + host_->OnDiscoverySessionServerClose(this); +} + +void HostServer::DiscoverySessionServer::handle_unknown_method( + uint64_t ordinal, bool method_has_response) { + bt_log(WARN, "fidl", "Received unknown method with ordinal: %lu", ordinal); +} + +HostServer::DiscoverySessionServer::DiscoverySessionServer( + fidl::InterfaceRequest<::fuchsia::bluetooth::host::DiscoverySession> + request, + HostServer* host) + : ServerBase(this, std::move(request)), host_(host) { + binding()->set_error_handler([this, host](zx_status_t /*status*/) { + host->OnDiscoverySessionServerClose(this); + }); +} + +HostServer::PeerWatcherServer::PeerWatcherServer( + ::fidl::InterfaceRequest<::fuchsia::bluetooth::host::PeerWatcher> request, + bt::gap::PeerCache* peer_cache, + HostServer* host) + : ServerBase(this, std::move(request)), + peer_cache_(peer_cache), + host_(host), + weak_self_(this) { + auto self = weak_self_.GetWeakPtr(); + + peer_updated_callback_id_ = + peer_cache_->add_peer_updated_callback([self](const auto& peer) { + if (self.is_alive()) { + self->OnPeerUpdated(peer); + } + }); + peer_cache_->set_peer_removed_callback([self](const auto& identifier) { + if (self.is_alive()) { + self->OnPeerRemoved(identifier); + } + }); + + // Initialize the peer watcher with all known connectable peers that are in + // the cache. + peer_cache_->ForEach( + [this](const bt::gap::Peer& peer) { OnPeerUpdated(peer); }); + + binding()->set_error_handler( + [this](zx_status_t /*status*/) { host_->peer_watcher_server_.reset(); }); +} + +HostServer::PeerWatcherServer::~PeerWatcherServer() { + // Unregister PeerCache callbacks. + peer_cache_->remove_peer_updated_callback(peer_updated_callback_id_); + peer_cache_->set_peer_removed_callback(nullptr); +} + +void HostServer::PeerWatcherServer::OnPeerUpdated(const bt::gap::Peer& peer) { + if (!peer.connectable()) { + return; + } + + updated_.insert(peer.identifier()); + removed_.erase(peer.identifier()); + MaybeCallCallback(); +} + +void HostServer::PeerWatcherServer::OnPeerRemoved(bt::PeerId id) { + updated_.erase(id); + removed_.insert(id); + MaybeCallCallback(); +} + +void HostServer::PeerWatcherServer::MaybeCallCallback() { + if (!callback_) { + return; + } + + if (!removed_.empty()) { + Removed removed_fidl; + for (const bt::PeerId& id : removed_) { + removed_fidl.push_back(fbt::PeerId{id.value()}); + } + removed_.clear(); + callback_(fhost::PeerWatcher_GetNext_Result::WithResponse( + fhost::PeerWatcher_GetNext_Response::WithRemoved( + std::move(removed_fidl)))); + callback_ = nullptr; + return; + } + + if (!updated_.empty()) { + Updated updated_fidl; + for (const bt::PeerId& id : updated_) { + bt::gap::Peer* peer = peer_cache_->FindById(id); + // All ids in |updated_| are assumed to be valid as they would otherwise + // be in |removed_|. + BT_ASSERT(peer); + updated_fidl.push_back(fidl_helpers::PeerToFidl(*peer)); + } + updated_.clear(); + callback_(fhost::PeerWatcher_GetNext_Result::WithResponse( + fhost::PeerWatcher_GetNext_Response::WithUpdated( + std::move(updated_fidl)))); + callback_ = nullptr; + return; + } +} + +void HostServer::PeerWatcherServer::GetNext( + ::fuchsia::bluetooth::host::PeerWatcher::GetNextCallback callback) { + if (callback_) { + binding()->Close(ZX_ERR_BAD_STATE); + host_->peer_watcher_server_.reset(); + return; + } + callback_ = std::move(callback); + MaybeCallCallback(); +} + +void HostServer::PeerWatcherServer::handle_unknown_method( + uint64_t ordinal, bool method_has_response) { + bt_log(WARN, + "fidl", + "PeerWatcher received unknown method with ordinal %lu", + ordinal); +} + +HostServer::BondingDelegateServer::BondingDelegateServer( + ::fidl::InterfaceRequest<::fuchsia::bluetooth::host::BondingDelegate> + request, + HostServer* host) + : ServerBase(this, std::move(request)), host_(host) { + binding()->set_error_handler( + [this](zx_status_t status) { host_->bonding_delegate_server_.reset(); }); + // Initialize the peer watcher with all known bonded peers that are in the + // cache. + host_->adapter()->peer_cache()->ForEach([this](const bt::gap::Peer& peer) { + if (peer.bonded()) { + OnNewBondingData(peer); + } + }); +} + +void HostServer::BondingDelegateServer::OnNewBondingData( + const bt::gap::Peer& peer) { + updated_.push( + fidl_helpers::PeerToFidlBondingData(host_->adapter().get(), peer)); + MaybeNotifyWatchBonds(); +} + +void HostServer::BondingDelegateServer::RestoreBonds( + ::std::vector<::fuchsia::bluetooth::sys::BondingData> bonds, + RestoreBondsCallback callback) { + host_->RestoreBonds(std::move(bonds), std::move(callback)); +} +void HostServer::BondingDelegateServer::WatchBonds( + WatchBondsCallback callback) { + if (watch_bonds_cb_) { + binding()->Close(ZX_ERR_ALREADY_EXISTS); + host_->bonding_delegate_server_.reset(); + return; + } + watch_bonds_cb_ = std::move(callback); + MaybeNotifyWatchBonds(); +} + +void HostServer::BondingDelegateServer::handle_unknown_method( + uint64_t ordinal, bool method_has_response) { + bt_log(WARN, + "fidl", + "BondingDelegate received unknown method with ordinal %lu", + ordinal); +} + +// TODO(fxbug.dev/42158854): Support notifying removed bonds. +void HostServer::BondingDelegateServer::MaybeNotifyWatchBonds() { + if (!watch_bonds_cb_ || updated_.empty()) { + return; + } + + watch_bonds_cb_(fhost::BondingDelegate_WatchBonds_Result::WithResponse( + ::fuchsia::bluetooth::host::BondingDelegate_WatchBonds_Response:: + WithUpdated(std::move(updated_.front())))); + updated_.pop(); +} + +bt::sm::IOCapability HostServer::io_capability() const { + bt_log(DEBUG, + "fidl", + "I/O capability: %s", + bt::sm::util::IOCapabilityToString(io_capability_).c_str()); + return io_capability_; +} + +void HostServer::CompletePairing(PeerId id, bt::sm::Result<> status) { + bt_log(DEBUG, + "fidl", + "pairing complete for peer: %s, status: %s", + bt_str(id), + bt_str(status)); + BT_DEBUG_ASSERT(pairing_delegate_); + pairing_delegate_->OnPairingComplete(fbt::PeerId{id.value()}, status.is_ok()); +} + +void HostServer::ConfirmPairing(PeerId id, ConfirmCallback confirm) { + bt_log( + DEBUG, "fidl", "pairing confirmation request for peer: %s", bt_str(id)); + DisplayPairingRequest( + id, std::nullopt, fsys::PairingMethod::CONSENT, std::move(confirm)); +} + +void HostServer::DisplayPasskey(PeerId id, + uint32_t passkey, + DisplayMethod method, + ConfirmCallback confirm) { + auto fidl_method = fsys::PairingMethod::PASSKEY_DISPLAY; + if (method == DisplayMethod::kComparison) { + bt_log( + DEBUG, "fidl", "compare passkey %06u on peer: %s", passkey, bt_str(id)); + fidl_method = fsys::PairingMethod::PASSKEY_COMPARISON; + } else { + bt_log( + DEBUG, "fidl", "enter passkey %06u on peer: %s", passkey, bt_str(id)); + } + DisplayPairingRequest(id, passkey, fidl_method, std::move(confirm)); +} + +void HostServer::RequestPasskey(PeerId id, PasskeyResponseCallback respond) { + bt_log(DEBUG, "fidl", "passkey request for peer: %s", bt_str(id)); + auto found_peer = adapter()->peer_cache()->FindById(id); + BT_ASSERT(found_peer); + auto peer = fidl_helpers::PeerToFidl(*found_peer); + + BT_ASSERT(pairing_delegate_); + pairing_delegate_->OnPairingRequest( + std::move(peer), + fsys::PairingMethod::PASSKEY_ENTRY, + 0u, + [respond = std::move(respond), id, func = __FUNCTION__]( + const bool accept, uint32_t entered_passkey) mutable { + if (!respond) { + bt_log(WARN, + "fidl", + "%s: The PairingDelegate invoked the Pairing Request callback " + "more than once, which " + "should not happen (peer: %s)", + func, + bt_str(id)); + return; + } + bt_log(INFO, + "fidl", + "%s: got PairingDelegate response: %s with passkey code \"%u\" " + "(peer: %s)", + func, + accept ? "accept" : "reject", + entered_passkey, + bt_str(id)); + if (!accept) { + respond(-1); + } else { + respond(entered_passkey); + } + }); +} + +void HostServer::DisplayPairingRequest(bt::PeerId id, + std::optional passkey, + fsys::PairingMethod method, + ConfirmCallback confirm) { + auto found_peer = adapter()->peer_cache()->FindById(id); + BT_ASSERT(found_peer); + auto peer = fidl_helpers::PeerToFidl(*found_peer); + + BT_ASSERT(pairing_delegate_); + uint32_t displayed_passkey = passkey ? *passkey : 0u; + pairing_delegate_->OnPairingRequest( + std::move(peer), + method, + displayed_passkey, + [confirm = std::move(confirm), id, func = __FUNCTION__]( + const bool accept, uint32_t entered_passkey) mutable { + if (!confirm) { + bt_log(WARN, + "fidl", + "%s: The PairingDelegate invoked the Pairing Request callback " + "more than once, which " + "should not happen (peer: %s)", + func, + bt_str(id)); + return; + } + bt_log(INFO, + "fidl", + "%s: got PairingDelegate response: %s, \"%u\" (peer: %s)", + func, + accept ? "accept" : "reject", + entered_passkey, + bt_str(id)); + confirm(accept); + }); +} + +void HostServer::OnConnectionError(Server* server) { + BT_DEBUG_ASSERT(server); + servers_.erase(server); +} + +void HostServer::ResetPairingDelegate() { + io_capability_ = IOCapability::kNoInputNoOutput; + adapter()->SetPairingDelegate(PairingDelegate::WeakPtr()); +} + +void HostServer::NotifyInfoChange() { + info_getter_.Set(fidl_helpers::HostInfoToFidl(adapter().get())); +} + +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/host_server_test.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/host_server_test.cc new file mode 100644 index 0000000000..6ee320976f --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/host_server_test.cc @@ -0,0 +1,1682 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/fidl/host_server.h" + +#include +#include +#include +#include +#include +#include + +#include "fuchsia/bluetooth/host/cpp/fidl.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/adapter_test_fixture.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/fake_adapter_test_fixture.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/helpers.h" +#include "pw_bluetooth_sapphire/internal/host/common/byte_buffer.h" +#include "pw_bluetooth_sapphire/internal/host/common/device_address.h" +#include "pw_bluetooth_sapphire/internal/host/gap/gap.h" +#include "pw_bluetooth_sapphire/internal/host/gap/low_energy_address_manager.h" +#include "pw_bluetooth_sapphire/internal/host/gatt/fake_layer.h" +#include "pw_bluetooth_sapphire/internal/host/l2cap/fake_channel.h" +#include "pw_bluetooth_sapphire/internal/host/l2cap/fake_l2cap.h" +#include "pw_bluetooth_sapphire/internal/host/sm/smp.h" +#include "pw_bluetooth_sapphire/internal/host/testing/controller_test.h" +#include "pw_bluetooth_sapphire/internal/host/testing/fake_peer.h" +#include "pw_bluetooth_sapphire/internal/host/testing/test_helpers.h" +#include "pw_bluetooth_sapphire/internal/host/testing/test_packets.h" +#include "pw_unit_test/framework.h" + +namespace fuchsia::bluetooth { +// Make PeerIds equality comparable for advanced testing matchers. ADL rules +// mandate the namespace. +bool operator==(const PeerId& a, const PeerId& b) { return fidl::Equals(a, b); } +} // namespace fuchsia::bluetooth + +namespace bthost { +namespace { + +// Limiting the de-scoped aliases here helps test cases be more specific about +// whether they're using FIDL names or bt-host internal names. +using bt::LowerBits; +using bt::StaticByteBuffer; +using bt::UpperBits; +using bt::l2cap::testing::FakeChannel; +using bt::sm::AuthReq; +using bt::sm::KeyDistGen; +using bt::testing::FakePeer; + +namespace fbt = fuchsia::bluetooth; +namespace fsys = fuchsia::bluetooth::sys; +namespace fhost = fuchsia::bluetooth::host; + +const bt::PeerId kTestId(1); +const bt::DeviceAddress kLeTestAddr(bt::DeviceAddress::Type::kLEPublic, + {0x01, 0, 0, 0, 0, 0}); +const bt::DeviceAddress kBredrTestAddr(bt::DeviceAddress::Type::kBREDR, + {0x01, 0, 0, 0, 0, 0}); + +const fbt::Address kTestFidlAddrPublic{fbt::AddressType::PUBLIC, + {1, 0, 0, 0, 0, 0}}; +const fbt::Address kTestFidlAddrRandom{ + fbt::AddressType::RANDOM, {0x55, 0x44, 0x33, 0x22, 0x11, 0b11000011}}; +const fbt::Address kTestFidlAddrResolvable{ + fbt::AddressType::RANDOM, {0x55, 0x44, 0x33, 0x22, 0x11, 0b01000011}}; +const fbt::Address kTestFidlAddrNonResolvable{ + fbt::AddressType::RANDOM, {0x55, 0x44, 0x33, 0x22, 0x11, 0x00}}; + +class MockFidlPairingDelegate : public fsys::testing::PairingDelegate_TestBase { + public: + using PairingRequestCallback = fit::function; + using PairingCompleteCallback = fit::function; + using RemoteKeypressCallback = + fit::function; + + MockFidlPairingDelegate(fidl::InterfaceRequest request, + async_dispatcher_t* dispatcher) + : binding_(this, std::move(request), dispatcher) {} + + ~MockFidlPairingDelegate() override = default; + + void OnPairingRequest(fsys::Peer device, + fsys::PairingMethod method, + uint32_t displayed_passkey, + OnPairingRequestCallback callback) override { + pairing_request_cb_( + std::move(device), method, displayed_passkey, std::move(callback)); + } + + void OnPairingComplete(fbt::PeerId id, bool success) override { + pairing_complete_cb_(id, success); + } + + void OnRemoteKeypress(fbt::PeerId id, + fsys::PairingKeypress keypress) override { + remote_keypress_cb_(id, keypress); + } + + void set_pairing_request_cb(PairingRequestCallback cb) { + pairing_request_cb_ = std::move(cb); + } + void set_pairing_complete_cb(PairingCompleteCallback cb) { + pairing_complete_cb_ = std::move(cb); + } + void set_remote_keypress_cb(RemoteKeypressCallback cb) { + remote_keypress_cb_ = std::move(cb); + } + + private: + void NotImplemented_(const std::string& name) override { + FAIL() << name << " is not implemented"; + } + + fidl::Binding binding_; + PairingRequestCallback pairing_request_cb_; + PairingCompleteCallback pairing_complete_cb_; + RemoteKeypressCallback remote_keypress_cb_; +}; + +class HostServerTest : public bthost::testing::AdapterTestFixture { + public: + HostServerTest() = default; + ~HostServerTest() override = default; + + void SetUp() override { + AdapterTestFixture::SetUp(); + + gatt_ = take_gatt(); + ResetHostServer(); + } + + void ResetHostServer() { + fidl::InterfaceHandle host_handle; + host_server_ = + std::make_unique(host_handle.NewRequest().TakeChannel(), + adapter()->AsWeakPtr(), + gatt_->GetWeakPtr()); + host_.Bind(std::move(host_handle)); + } + + void TearDown() override { + RunLoopUntilIdle(); + + host_ = nullptr; + host_server_ = nullptr; + gatt_ = nullptr; + + AdapterTestFixture::TearDown(); + } + + protected: + HostServer* host_server() const { return host_server_.get(); } + + fuchsia::bluetooth::host::Host* host_client() const { return host_.get(); } + + // Mutable reference to the Host client interface pointer. + fuchsia::bluetooth::host::HostPtr& host_client_ptr() { return host_; } + + // Create and bind a MockFidlPairingDelegate and attach it to the HostServer + // under test. It is heap-allocated to permit its explicit destruction. + [[nodiscard]] std::unique_ptr + SetMockFidlPairingDelegate(fsys::InputCapability input_capability, + fsys::OutputCapability output_capability) { + using ::testing::StrictMock; + fidl::InterfaceHandle pairing_delegate_handle; + auto pairing_delegate = std::make_unique( + pairing_delegate_handle.NewRequest(), dispatcher()); + host_client()->SetPairingDelegate(input_capability, + output_capability, + std::move(pairing_delegate_handle)); + + // Wait for the Access/SetPairingDelegate message to process. + RunLoopUntilIdle(); + return pairing_delegate; + } + + bt::gap::Peer* AddFakePeer(const bt::DeviceAddress& address) { + bt::gap::Peer* peer = + adapter()->peer_cache()->NewPeer(address, /*connectable=*/true); + BT_ASSERT(peer); + BT_ASSERT(peer->temporary()); + + test_device()->AddPeer( + std::make_unique(address, pw_dispatcher())); + + return peer; + } + + using ConnectResult = fhost::Host_Connect_Result; + std::optional ConnectFakePeer(bt::PeerId id) { + std::optional result; + host_client()->Connect(fbt::PeerId{id.value()}, [&](ConnectResult _result) { + result = std::move(_result); + }); + RunLoopUntilIdle(); + return result; + } + + std::tuple CreateAndConnectFakePeer( + bool connect_le = true) { + auto address = connect_le ? kLeTestAddr : kBredrTestAddr; + bt::gap::Peer* peer = AddFakePeer(address); + + // This is to capture the channel created during the Connection process + FakeChannel::WeakPtr fake_chan = FakeChannel::WeakPtr(); + l2cap()->set_channel_callback( + [&fake_chan](FakeChannel::WeakPtr new_fake_chan) { + fake_chan = std::move(new_fake_chan); + }); + + auto connect_result = ConnectFakePeer(peer->identifier()); + + if (!connect_result || connect_result->is_err()) { + peer = nullptr; + fake_chan.reset(); + } + return std::make_tuple(peer, fake_chan); + } + + // Calls the RestoreBonds method and verifies that the callback is run with + // the expected output. + void TestRestoreBonds(fhost::BondingDelegatePtr& delegate, + std::vector bonds, + std::vector expected) { + bool called = false; + delegate->RestoreBonds( + std::move(bonds), + [&](fhost::BondingDelegate_RestoreBonds_Result result) { + ASSERT_TRUE(result.is_response()); + called = true; + ASSERT_EQ(expected.size(), result.response().errors.size()); + for (size_t i = 0; i < result.response().errors.size(); i++) { + SCOPED_TRACE(i); + EXPECT_TRUE(fidl::Equals(result.response().errors[i], expected[i])); + } + }); + RunLoopUntilIdle(); + EXPECT_TRUE(called); + } + + fidl::InterfacePtr SetPeerWatcher() { + fidl::InterfaceHandle handle; + host_server()->SetPeerWatcher(handle.NewRequest()); + return handle.Bind(); + } + + private: + std::unique_ptr host_server_; + std::unique_ptr gatt_; + fuchsia::bluetooth::host::HostPtr host_; + + BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(HostServerTest); +}; + +// The main role of this sub-suite is improved test object lifecycle management +// (see TearDown for more details). An additional convenience it provides is +// fake peer/channel and mock pairing delegate setup, which all tests of the +// full pairing stack need. +class HostServerPairingTest : public HostServerTest { + public: + void SetUp() override { + HostServerTest::SetUp(); + NewPairingTest(fsys::InputCapability::NONE, fsys::OutputCapability::NONE); + } + + void NewPairingTest(fsys::InputCapability input_cap, + fsys::OutputCapability output_cap, + bool is_le = true) { + pairing_delegate_ = SetMockFidlPairingDelegate(input_cap, output_cap); + if (!fake_peer_ || !fake_chan_.is_alive()) { + ASSERT_FALSE(fake_peer_); + ASSERT_FALSE(fake_chan_.is_alive()); + std::tie(fake_peer_, fake_chan_) = CreateAndConnectFakePeer(is_le); + ASSERT_TRUE(fake_peer_); + ASSERT_TRUE(fake_chan_.is_alive()); + ASSERT_EQ(bt::gap::Peer::ConnectionState::kConnected, + fake_peer_->le()->connection_state()); + } + } + + // With the base HostServerTest, it is too easy to set up callbacks related to + // fake channels or the mock pairing delegate that lead to unexpected failure + // callbacks, or worse, use-after- frees. These failures mostly stem from the + // Host server notifying the client upon pairing delegate destruction, which + // is not important behavior for many tests. + void TearDown() override { + fake_chan_->SetSendCallback(/*callback=*/nullptr); + host_client_ptr().Unbind(); + HostServerTest::TearDown(); + } + + bt::gap::Peer* peer() { return fake_peer_; } + FakeChannel::WeakPtr fake_chan() { return fake_chan_; } + + private: + std::unique_ptr pairing_delegate_; + bt::gap::Peer* fake_peer_ = nullptr; + FakeChannel::WeakPtr fake_chan_; +}; + +// Constructs a vector of a fidl::Clone'able data type that contains a copy of +// the input |data|. This allows move-only FIDL types to be re-used in test +// cases that need to refer to such data. +// +// Returns an empty vector if |data| could not be copied, e.g. because it +// contains handles that cannot be duplicated. +template +std::vector MakeClonedVector(const T& data) { + std::vector output; + T clone; + + zx_status_t status = fidl::Clone(data, &clone); + EXPECT_EQ(ZX_OK, status); + if (status == ZX_OK) { + output.push_back(std::move(clone)); + } + + return output; +} + +// Construct bonding data structure for testing using the given ID and address +// and an empty LE bond structure. +fsys::BondingData MakeTestBond(bt::PeerId id, fbt::Address address) { + fsys::BondingData bond; + bond.set_identifier(fbt::PeerId{id.value()}); + bond.set_address(address); + bond.set_le_bond(fsys::LeBondData()); + return bond; +} + +TEST_F(HostServerTest, FidlIoCapabilitiesMapToHostIoCapability) { + // Isolate HostServer's private bt::gap::PairingDelegate implementation. + auto host_pairing_delegate = + static_cast(host_server()); + + // Getter should be safe to call when no PairingDelegate assigned. + EXPECT_EQ(bt::sm::IOCapability::kNoInputNoOutput, + host_pairing_delegate->io_capability()); + + auto fidl_pairing_delegate = SetMockFidlPairingDelegate( + fsys::InputCapability::KEYBOARD, fsys::OutputCapability::DISPLAY); + EXPECT_EQ(bt::sm::IOCapability::kKeyboardDisplay, + host_pairing_delegate->io_capability()); +} + +TEST_F(HostServerTest, HostCompletePairingCallsFidlOnPairingComplete) { + using namespace ::testing; + + // Isolate HostServer's private bt::gap::PairingDelegate implementation. + auto host_pairing_delegate = + static_cast(host_server()); + auto fidl_pairing_delegate = SetMockFidlPairingDelegate( + fsys::InputCapability::KEYBOARD, fsys::OutputCapability::DISPLAY); + + // fuchsia::bluetooth::PeerId has no equality operator + std::optional actual_id; + fidl_pairing_delegate->set_pairing_complete_cb( + [&actual_id](fbt::PeerId id, Unused) { actual_id = id; }); + auto id = bt::PeerId(0xc0decafe); + host_pairing_delegate->CompletePairing( + id, fit::error(bt::Error(bt::sm::ErrorCode::kConfirmValueFailed))); + + // Wait for the PairingDelegate/OnPairingComplete message to process. + RunLoopUntilIdle(); + + EXPECT_TRUE(actual_id.has_value()); + EXPECT_EQ(id.value(), actual_id.value().value); +} + +TEST_F(HostServerTest, HostConfirmPairingRequestsConsentPairingOverFidl) { + using namespace ::testing; + auto host_pairing_delegate = + static_cast(host_server()); + auto fidl_pairing_delegate = SetMockFidlPairingDelegate( + fsys::InputCapability::KEYBOARD, fsys::OutputCapability::DISPLAY); + + auto* const peer = + adapter()->peer_cache()->NewPeer(kLeTestAddr, /*connectable=*/true); + ASSERT_TRUE(peer); + + fidl_pairing_delegate->set_pairing_request_cb( + [id = peer->identifier()]( + fsys::Peer peer, + fsys::PairingMethod method, + uint32_t displayed_passkey, + fsys::PairingDelegate::OnPairingRequestCallback callback) { + ASSERT_TRUE(peer.has_id()); + EXPECT_EQ(id.value(), peer.id().value); + EXPECT_EQ(method, fsys::PairingMethod::CONSENT); + EXPECT_EQ(displayed_passkey, 0u); + callback(/*accept=*/true, /*entered_passkey=*/0u); + }); + + bool confirm_cb_value = false; + bt::gap::PairingDelegate::ConfirmCallback confirm_cb = + [&confirm_cb_value](bool confirmed) { confirm_cb_value = confirmed; }; + host_pairing_delegate->ConfirmPairing(peer->identifier(), + std::move(confirm_cb)); + + // Wait for the PairingDelegate/OnPairingRequest message to process. + RunLoopUntilIdle(); + EXPECT_TRUE(confirm_cb_value); +} + +TEST_F( + HostServerTest, + HostDisplayPasskeyRequestsPasskeyDisplayOrNumericComparisonPairingOverFidl) { + using namespace ::testing; + auto host_pairing_delegate = + static_cast(host_server()); + auto fidl_pairing_delegate = SetMockFidlPairingDelegate( + fsys::InputCapability::KEYBOARD, fsys::OutputCapability::DISPLAY); + + auto* const peer = + adapter()->peer_cache()->NewPeer(kLeTestAddr, /*connectable=*/true); + ASSERT_TRUE(peer); + + // This call should use PASSKEY_DISPLAY to request that the user perform peer + // passkey entry. + fidl_pairing_delegate->set_pairing_request_cb( + [id = peer->identifier()]( + fsys::Peer peer, + fsys::PairingMethod method, + uint32_t displayed_passkey, + fsys::PairingDelegate::OnPairingRequestCallback callback) { + ASSERT_TRUE(peer.has_id()); + EXPECT_EQ(id.value(), peer.id().value); + EXPECT_EQ(method, fsys::PairingMethod::PASSKEY_DISPLAY); + EXPECT_EQ(displayed_passkey, 12345u); + callback(/*accept=*/false, /*entered_passkey=*/0u); + }); + + bool confirm_cb_called = false; + auto confirm_cb = [&confirm_cb_called](bool confirmed) { + EXPECT_FALSE(confirmed); + confirm_cb_called = true; + }; + using DisplayMethod = bt::gap::PairingDelegate::DisplayMethod; + host_pairing_delegate->DisplayPasskey( + peer->identifier(), 12345, DisplayMethod::kPeerEntry, confirm_cb); + + // Wait for the PairingDelegate/OnPairingRequest message to process. + RunLoopUntilIdle(); + EXPECT_TRUE(confirm_cb_called); + + // This call should use PASSKEY_COMPARISON to request that the user compare + // the passkeys shown on the local and peer devices. + fidl_pairing_delegate->set_pairing_request_cb( + [id = peer->identifier()]( + fsys::Peer peer, + fsys::PairingMethod method, + uint32_t displayed_passkey, + fsys::PairingDelegate::OnPairingRequestCallback callback) { + ASSERT_TRUE(peer.has_id()); + EXPECT_EQ(id.value(), peer.id().value); + EXPECT_EQ(method, fsys::PairingMethod::PASSKEY_COMPARISON); + EXPECT_EQ(displayed_passkey, 12345u); + callback(/*accept=*/false, /*entered_passkey=*/0u); + }); + + confirm_cb_called = false; + host_pairing_delegate->DisplayPasskey( + peer->identifier(), 12345, DisplayMethod::kComparison, confirm_cb); + + // Wait for the PairingDelegate/OnPairingRequest message to process. + RunLoopUntilIdle(); + EXPECT_TRUE(confirm_cb_called); +} + +TEST_F(HostServerTest, HostRequestPasskeyRequestsPasskeyEntryPairingOverFidl) { + using namespace ::testing; + auto host_pairing_delegate = + static_cast(host_server()); + auto fidl_pairing_delegate = SetMockFidlPairingDelegate( + fsys::InputCapability::KEYBOARD, fsys::OutputCapability::DISPLAY); + + auto* const peer = + adapter()->peer_cache()->NewPeer(kLeTestAddr, /*connectable=*/true); + ASSERT_TRUE(peer); + + std::optional passkey_response; + auto response_cb = [&passkey_response](int64_t passkey) { + passkey_response = passkey; + }; + + // The first request is rejected and should receive a negative passkey value, + // regardless what was passed over FIDL (i.e. 12345). + fidl_pairing_delegate->set_pairing_request_cb( + [id = peer->identifier()]( + fsys::Peer peer, + fsys::PairingMethod method, + uint32_t displayed_passkey, + fsys::PairingDelegate::OnPairingRequestCallback callback) { + ASSERT_TRUE(peer.has_id()); + EXPECT_EQ(id.value(), peer.id().value); + EXPECT_EQ(method, fsys::PairingMethod::PASSKEY_ENTRY); + EXPECT_EQ(displayed_passkey, 0u); + callback(/*accept=*/false, /*entered_passkey=*/12345); + }); + + host_pairing_delegate->RequestPasskey(peer->identifier(), response_cb); + RunLoopUntilIdle(); + ASSERT_TRUE(passkey_response.has_value()); + EXPECT_LT(passkey_response.value(), 0); + + // The second request should be accepted with the passkey set to "0". + fidl_pairing_delegate->set_pairing_request_cb( + [id = peer->identifier()]( + fsys::Peer peer, + Unused, + Unused, + fsys::PairingDelegate::OnPairingRequestCallback callback) { + ASSERT_TRUE(peer.has_id()); + EXPECT_EQ(id.value(), peer.id().value); + callback(/*accept=*/true, /*entered_passkey=*/0); + }); + + passkey_response.reset(); + host_pairing_delegate->RequestPasskey(peer->identifier(), response_cb); + RunLoopUntilIdle(); + ASSERT_TRUE(passkey_response.has_value()); + EXPECT_EQ(0, passkey_response.value()); + + // The third request should be accepted with the passkey set to "12345". + fidl_pairing_delegate->set_pairing_request_cb( + [id = peer->identifier()]( + fsys::Peer peer, + Unused, + Unused, + fsys::PairingDelegate::OnPairingRequestCallback callback) { + ASSERT_TRUE(peer.has_id()); + EXPECT_EQ(id.value(), peer.id().value); + callback(/*accept=*/true, /*entered_passkey=*/12345); + }); + + passkey_response.reset(); + host_pairing_delegate->RequestPasskey(peer->identifier(), response_cb); + RunLoopUntilIdle(); + ASSERT_TRUE(passkey_response.has_value()); + EXPECT_EQ(12345, passkey_response.value()); +} + +TEST_F(HostServerTest, SysDelegateInvokesCallbackMultipleTimesIgnored) { + using namespace ::testing; + auto host_pairing_delegate = + static_cast(host_server()); + auto fidl_pairing_delegate = SetMockFidlPairingDelegate( + fsys::InputCapability::KEYBOARD, fsys::OutputCapability::DISPLAY); + + auto* const peer = + adapter()->peer_cache()->NewPeer(kLeTestAddr, /*connectable=*/true); + ASSERT_TRUE(peer); + + using OnPairingRequestCallback = + fsys::PairingDelegate::OnPairingRequestCallback; + OnPairingRequestCallback fidl_passkey_req_cb = nullptr, + fidl_confirm_req_cb = nullptr; + fidl_pairing_delegate->set_pairing_request_cb( + [id = peer->identifier(), &fidl_passkey_req_cb, &fidl_confirm_req_cb]( + fsys::Peer peer, + fsys::PairingMethod method, + auto /*ignore*/, + OnPairingRequestCallback callback) { + ASSERT_TRUE(peer.has_id()); + EXPECT_EQ(id.value(), peer.id().value); + if (method == fsys::PairingMethod::PASSKEY_ENTRY) { + fidl_passkey_req_cb = std::move(callback); + } else if (method == fsys::PairingMethod::CONSENT) { + fidl_confirm_req_cb = std::move(callback); + } else { + FAIL() << "unexpected pairing request method!"; + } + }); + + size_t passkey_req_cb_count = 0, confirm_req_cb_count = 0; + auto passkey_response_cb = [&passkey_req_cb_count](int64_t /*ignore*/) { + passkey_req_cb_count++; + ; + }; + auto confirm_req_cb = [&confirm_req_cb_count](bool /*ignore*/) { + confirm_req_cb_count++; + }; + + host_pairing_delegate->RequestPasskey(peer->identifier(), + passkey_response_cb); + host_pairing_delegate->ConfirmPairing(peer->identifier(), confirm_req_cb); + RunLoopUntilIdle(); + ASSERT_TRUE(fidl_passkey_req_cb); + ASSERT_TRUE(fidl_confirm_req_cb); + + ASSERT_EQ(0u, passkey_req_cb_count); + ASSERT_EQ(0u, confirm_req_cb_count); + + fidl_passkey_req_cb(true, 12345); + fidl_confirm_req_cb(true, 0); + RunLoopUntilIdle(); + ASSERT_EQ(1u, passkey_req_cb_count); + ASSERT_EQ(1u, confirm_req_cb_count); + + fidl_passkey_req_cb(true, 456789); + fidl_confirm_req_cb(true, 0); + RunLoopUntilIdle(); + ASSERT_EQ(1u, passkey_req_cb_count); + ASSERT_EQ(1u, confirm_req_cb_count); +} + +TEST_F(HostServerTest, WatchState) { + std::optional info; + host_server()->WatchState([&](fhost::Host_WatchState_Result result) { + ASSERT_TRUE(result.is_response()); + info = std::move(result.response().info); + }); + ASSERT_TRUE(info.has_value()); + ASSERT_TRUE(info->has_id()); + ASSERT_TRUE(info->has_technology()); + ASSERT_TRUE(info->has_local_name()); + ASSERT_TRUE(info->has_discoverable()); + ASSERT_TRUE(info->has_discovering()); + ASSERT_TRUE(info->has_addresses()); + + EXPECT_EQ(adapter()->identifier().value(), info->id().value); + EXPECT_EQ(fsys::TechnologyType::DUAL_MODE, info->technology()); + EXPECT_EQ("fuchsia", info->local_name()); + EXPECT_FALSE(info->discoverable()); + EXPECT_FALSE(info->discovering()); + EXPECT_EQ(fbt::AddressType::PUBLIC, info->addresses()[0].type); + EXPECT_TRUE(ContainersEqual(adapter()->state().controller_address.bytes(), + info->addresses()[0].bytes)); +} + +TEST_F(HostServerTest, WatchDiscoveryState) { + std::optional info; + + // Make initial watch call so that subsequent calls remain pending. + host_client()->WatchState([&](fhost::Host_WatchState_Result result) { + ASSERT_TRUE(result.is_response()); + info = std::move(result.response().info); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(info.has_value()); + ASSERT_TRUE(info->has_discovering()); + EXPECT_FALSE(info->discovering()); + info.reset(); + + // Watch for updates. + host_client()->WatchState([&](fhost::Host_WatchState_Result result) { + ASSERT_TRUE(result.is_response()); + info = std::move(result.response().info); + }); + RunLoopUntilIdle(); + EXPECT_FALSE(info.has_value()); + + fhost::DiscoverySessionHandle discovery; + fhost::HostStartDiscoveryRequest start_request; + start_request.set_token(discovery.NewRequest()); + fidl::InterfacePtr discovery_client = discovery.Bind(); + std::optional discovery_error; + discovery_client.set_error_handler( + [&](zx_status_t error) { discovery_error = error; }); + host_client()->StartDiscovery(std::move(start_request)); + RunLoopUntilIdle(); + EXPECT_FALSE(discovery_error); + ASSERT_TRUE(info.has_value()); + ASSERT_TRUE(info->has_discovering()); + EXPECT_TRUE(info->discovering()); + + info.reset(); + host_client()->WatchState([&](fhost::Host_WatchState_Result result) { + ASSERT_TRUE(result.is_response()); + info = std::move(result.response().info); + }); + RunLoopUntilIdle(); + EXPECT_FALSE(info.has_value()); + discovery_client->Stop(); + RunLoopUntilIdle(); + ASSERT_TRUE(discovery_error); + EXPECT_EQ(discovery_error.value(), ZX_ERR_CANCELED); + ASSERT_TRUE(info.has_value()); + ASSERT_TRUE(info->has_discovering()); + EXPECT_FALSE(info->discovering()); +} + +TEST_F(HostServerTest, StartDiscoveryWithMissingToken) { + fhost::HostStartDiscoveryRequest start_request; + host_client()->StartDiscovery(std::move(start_request)); + RunLoopUntilIdle(); +} + +TEST_F(HostServerTest, StartDiscoveryTwiceAndCloseTwice) { + fhost::DiscoverySessionHandle discovery_0; + fhost::HostStartDiscoveryRequest start_request_0; + start_request_0.set_token(discovery_0.NewRequest()); + fidl::InterfacePtr discovery_client_0 = discovery_0.Bind(); + std::optional discovery_error_0; + discovery_client_0.set_error_handler( + [&](zx_status_t error) { discovery_error_0 = error; }); + host_client()->StartDiscovery(std::move(start_request_0)); + RunLoopUntilIdle(); + EXPECT_FALSE(discovery_error_0); + + fhost::DiscoverySessionHandle discovery_1; + fhost::HostStartDiscoveryRequest start_request_1; + start_request_1.set_token(discovery_1.NewRequest()); + fidl::InterfacePtr discovery_client_1 = discovery_1.Bind(); + std::optional discovery_error_1; + discovery_client_1.set_error_handler( + [&](zx_status_t error) { discovery_error_1 = error; }); + host_client()->StartDiscovery(std::move(start_request_1)); + RunLoopUntilIdle(); + EXPECT_FALSE(discovery_error_0); + EXPECT_FALSE(discovery_error_1); + + std::optional info; + host_client()->WatchState([&](fhost::Host_WatchState_Result result) { + info = std::move(result.response().info); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(info.has_value()); + EXPECT_TRUE(info->discovering()); + info.reset(); + + host_client()->WatchState([&](fhost::Host_WatchState_Result result) { + info = std::move(result.response().info); + }); + RunLoopUntilIdle(); + EXPECT_FALSE(info.has_value()); + + discovery_client_0.Unbind(); + RunLoopUntilIdle(); + // Client 1 is still open, so discovery should still be enabled. + EXPECT_FALSE(info.has_value()); + + discovery_client_1.Unbind(); + RunLoopUntilIdle(); + ASSERT_TRUE(info.has_value()); + EXPECT_FALSE(info->discovering()); +} + +TEST_F(HostServerTest, WatchDiscoverableState) { + std::optional info; + + // Make initial watch call so that subsequent calls remain pending. + host_server()->WatchState([&](fhost::Host_WatchState_Result result) { + ASSERT_TRUE(result.is_response()); + info = std::move(result.response().info); + }); + ASSERT_TRUE(info.has_value()); + info.reset(); + + // Watch for updates. + host_server()->WatchState([&](fhost::Host_WatchState_Result result) { + ASSERT_TRUE(result.is_response()); + info = std::move(result.response().info); + }); + EXPECT_FALSE(info.has_value()); + + host_server()->SetDiscoverable(/*discoverable=*/true, [](auto) {}); + RunLoopUntilIdle(); + ASSERT_TRUE(info.has_value()); + ASSERT_TRUE(info->has_discoverable()); + EXPECT_TRUE(info->discoverable()); + + info.reset(); + host_server()->WatchState([&](fhost::Host_WatchState_Result result) { + ASSERT_TRUE(result.is_response()); + info = std::move(result.response().info); + }); + EXPECT_FALSE(info.has_value()); + host_server()->SetDiscoverable(/*discoverable=*/false, [](auto) {}); + RunLoopUntilIdle(); + ASSERT_TRUE(info.has_value()); + ASSERT_TRUE(info->has_discoverable()); + EXPECT_FALSE(info->discoverable()); +} + +TEST_F(HostServerPairingTest, InitiatePairingLeDefault) { + const StaticByteBuffer kExpected( + 0x01, // code: "Pairing Request" + 0x04, // IO cap.: KeyboardDisplay + 0x00, // OOB: not present + // inclusive-language: ignore + AuthReq::kBondingFlag | AuthReq::kMITM | AuthReq::kSC | AuthReq::kCT2, + 0x10, // encr. key size: 16 (default max) + KeyDistGen::kEncKey | KeyDistGen::kLinkKey, // initiator keys + KeyDistGen::kEncKey | KeyDistGen::kIdKey | + KeyDistGen::kLinkKey // responder keys + ); + + // inclusive-language: ignore + // IOCapabilities must be KeyboardDisplay to support default MITM pairing + // request. + NewPairingTest(fsys::InputCapability::KEYBOARD, + fsys::OutputCapability::DISPLAY); + + bool pairing_request_sent = false; + // This test only checks that SecureSimplePairingState kicks off an LE pairing + // feature exchange correctly, as the call to Pair is only responsible for + // starting pairing, not for completing it. + auto expect_default_bytebuffer = [&pairing_request_sent, + kExpected](bt::ByteBufferPtr sent) { + ASSERT_TRUE(sent); + ASSERT_EQ(*sent, kExpected); + pairing_request_sent = true; + }; + fake_chan()->SetSendCallback(expect_default_bytebuffer, pw_dispatcher()); + + std::optional pair_result; + fsys::PairingOptions opts; + host_client()->Pair( + fbt::PeerId{peer()->identifier().value()}, + std::move(opts), + [&](fhost::Host_Pair_Result result) { pair_result = std::move(result); }); + RunLoopUntilIdle(); + + // TODO(fxbug.dev/42169848): We don't have a good mechanism for driving + // pairing to completion without faking the entire SMP exchange. We should add + // SMP mocks that allows us to propagate a result up to the FIDL layer. For + // now we assert that pairing has started and remains pending. + ASSERT_FALSE(pair_result); // Pairing request is pending + ASSERT_TRUE(pairing_request_sent); +} + +TEST_F(HostServerPairingTest, InitiatePairingLeEncrypted) { + const StaticByteBuffer kExpected( + 0x01, // code: "Pairing Request" + 0x03, // IO cap.: NoInputNoOutput + 0x00, // OOB: not present + AuthReq::kBondingFlag | AuthReq::kSC | AuthReq::kCT2, + 0x10, // encr. key size: 16 (default max) + KeyDistGen::kEncKey | KeyDistGen::kLinkKey, // initiator keys + KeyDistGen::kEncKey | KeyDistGen::kIdKey | + KeyDistGen::kLinkKey // responder keys + ); + + bool pairing_request_sent = false; + // This test only checks that SecureSimplePairingState kicks off an LE pairing + // feature exchange correctly, as the call to Pair is only responsible for + // starting pairing, not for completing it. + auto expect_default_bytebuffer = [&pairing_request_sent, + kExpected](bt::ByteBufferPtr sent) { + ASSERT_TRUE(sent); + ASSERT_EQ(*sent, kExpected); + pairing_request_sent = true; + }; + fake_chan()->SetSendCallback(expect_default_bytebuffer, pw_dispatcher()); + + std::optional pair_result; + fsys::PairingOptions opts; + opts.set_le_security_level(fsys::PairingSecurityLevel::ENCRYPTED); + host_client()->Pair(fbt::PeerId{peer()->identifier().value()}, + std::move(opts), + [&](auto result) { pair_result = std::move(result); }); + RunLoopUntilIdle(); + + // TODO(fxbug.dev/42169848): We don't have a good mechanism for driving + // pairing to completion without faking the entire SMP exchange. We should add + // SMP mocks that allows us to propagate a result up to the FIDL layer. For + // now we assert that pairing has started and remains pending. + ASSERT_FALSE(pair_result); // Pairing request is pending + ASSERT_TRUE(pairing_request_sent); +} + +TEST_F(HostServerPairingTest, InitiatePairingNonBondableLe) { + const StaticByteBuffer kExpected( + 0x01, // code: "Pairing Request" + 0x04, // IO cap.: KeyboardDisplay + 0x00, // OOB: not present + // inclusive-language: ignore + AuthReq::kMITM | AuthReq::kSC | AuthReq::kCT2, + 0x10, // encr. key size: 16 (default max) + 0x00, // initiator keys: none + 0x00 // responder keys: none + ); + + // inclusive-language: ignore + // IOCapabilities must be KeyboardDisplay to support default MITM pairing + // request. + NewPairingTest(fsys::InputCapability::KEYBOARD, + fsys::OutputCapability::DISPLAY); + + bool pairing_request_sent = false; + // This test only checks that SecureSimplePairingState kicks off an LE pairing + // feature exchange correctly, as the call to Pair is only responsible for + // starting pairing, not for completing it. + auto expect_default_bytebuffer = [&pairing_request_sent, + kExpected](bt::ByteBufferPtr sent) { + ASSERT_TRUE(sent); + ASSERT_EQ(*sent, kExpected); + pairing_request_sent = true; + }; + fake_chan()->SetSendCallback(expect_default_bytebuffer, pw_dispatcher()); + + std::optional pair_result; + fsys::PairingOptions opts; + opts.set_bondable_mode(fsys::BondableMode::NON_BONDABLE); + host_client()->Pair(fbt::PeerId{peer()->identifier().value()}, + std::move(opts), + [&](auto result) { pair_result = std::move(result); }); + RunLoopUntilIdle(); + + // TODO(fxbug.dev/42169848): We don't have a good mechanism for driving + // pairing to completion without faking the entire SMP exchange. We should add + // SMP mocks that allows us to propagate a result up to the FIDL layer. For + // now we assert that pairing has started and remains pending. + ASSERT_FALSE(pair_result); // Pairing request is pending + ASSERT_TRUE(pairing_request_sent); +} + +TEST_F(HostServerTest, InitiateBrEdrPairingLePeerFails) { + auto [peer, fake_chan] = CreateAndConnectFakePeer(); + ASSERT_TRUE(peer); + ASSERT_TRUE(fake_chan.is_alive()); + ASSERT_EQ(bt::gap::Peer::ConnectionState::kConnected, + peer->le()->connection_state()); + + std::optional pair_result; + fsys::PairingOptions opts; + // Set pairing option with classic + opts.set_transport(fsys::TechnologyType::CLASSIC); + auto pair_cb = [&](auto result) { + ASSERT_TRUE(result.is_err()); + pair_result = std::move(result); + }; + host_client()->Pair(fbt::PeerId{peer->identifier().value()}, + std::move(opts), + std::move(pair_cb)); + RunLoopUntilIdle(); + ASSERT_TRUE(pair_result); + ASSERT_TRUE(pair_result->is_err()); + ASSERT_EQ(pair_result->err(), fsys::Error::PEER_NOT_FOUND); +} + +TEST_F(HostServerTest, PeerWatcherGetNextHangsOnFirstCallWithNoExistingPeers) { + // By default the peer cache contains no entries when HostServer is first + // constructed. The first call to GetNext should hang. + bool replied = false; + fidl::InterfacePtr client = SetPeerWatcher(); + client->GetNext([&](auto) { replied = true; }); + RunLoopUntilIdle(); + EXPECT_FALSE(replied); +} + +TEST_F(HostServerTest, PeerWatcherGetNextRepliesOnFirstCallWithExistingPeers) { + [[maybe_unused]] bt::gap::Peer* peer = + adapter()->peer_cache()->NewPeer(kLeTestAddr, /*connectable=*/true); + ResetHostServer(); + + // The first call to GetNext immediately resolves with the contents of the + // peer cache. + bool replied = false; + fidl::InterfacePtr client = SetPeerWatcher(); + client->GetNext([&](fhost::PeerWatcher_GetNext_Result result) { + ASSERT_TRUE(result.is_response()); + ASSERT_TRUE(result.response().is_updated()); + EXPECT_EQ(1u, result.response().updated().size()); + replied = true; + }); + RunLoopUntilIdle(); + EXPECT_TRUE(replied); +} + +TEST_F(HostServerTest, PeerWatcherHandlesNonEnumeratedAppearanceInPeer) { + using namespace ::testing; + bt::gap::Peer* const peer = + adapter()->peer_cache()->NewPeer(kLeTestAddr, /*connectable=*/true); + ASSERT_TRUE(peer); + bt::AdvertisingData adv_data; + + // Invalid appearance. + adv_data.SetAppearance(0xFFFFu); + bt::DynamicByteBuffer write_buf( + adv_data.CalculateBlockSize(/*include_flags=*/true)); + ASSERT_TRUE( + adv_data.WriteBlock(&write_buf, bt::AdvFlag::kLEGeneralDiscoverableMode)); + peer->MutLe().SetAdvertisingData( + /*rssi=*/0, write_buf, pw::chrono::SystemClock::time_point()); + + ResetHostServer(); + + bool replied = false; + fidl::InterfacePtr client = SetPeerWatcher(); + client->GetNext([&](fhost::PeerWatcher_GetNext_Result result) { + // Client should still receive updates to this peer. + replied = true; + const fbt::PeerId id = {peer->identifier().value()}; + ASSERT_TRUE(result.is_response()); + ASSERT_THAT(result.response().updated(), + Contains(Property(&fsys::Peer::id, id))); + EXPECT_FALSE(result.response().updated().front().has_appearance()); + }); + RunLoopUntilIdle(); + EXPECT_TRUE(replied); +} + +TEST_F(HostServerTest, PeerWatcherStateMachine) { + std::optional response; + + // Initial watch call hangs as the cache is empty. + fidl::InterfacePtr client = SetPeerWatcher(); + client->GetNext([&](fhost::PeerWatcher_GetNext_Result result) { + ASSERT_TRUE(result.is_response()); + response = std::move(result.response()); + }); + ASSERT_FALSE(response.has_value()); + + // Adding a new peer should resolve the hanging get. + bt::gap::Peer* peer = + adapter()->peer_cache()->NewPeer(kLeTestAddr, /*connectable=*/true); + RunLoopUntilIdle(); + ASSERT_TRUE(response.has_value()); + ASSERT_TRUE(response->is_updated()); + EXPECT_EQ(1u, response->updated().size()); + EXPECT_TRUE( + fidl::Equals(fidl_helpers::PeerToFidl(*peer), response->updated()[0])); + response.reset(); + + // The next call should hang. + client->GetNext([&](fhost::PeerWatcher_GetNext_Result result) { + ASSERT_TRUE(result.is_response()); + response = std::move(result.response()); + }); + RunLoopUntilIdle(); + ASSERT_FALSE(response.has_value()); + + // Removing the peer should resolve the hanging get. + auto peer_id = peer->identifier(); + [[maybe_unused]] auto result = + adapter()->peer_cache()->RemoveDisconnectedPeer(peer_id); + RunLoopUntilIdle(); + ASSERT_TRUE(response.has_value()); + ASSERT_TRUE(response->is_removed()); + EXPECT_EQ(1u, response->removed().size()); + EXPECT_TRUE( + fidl::Equals(fbt::PeerId{peer_id.value()}, response->removed()[0])); +} + +TEST_F(HostServerTest, WatchPeersUpdatedThenRemoved) { + fidl::InterfacePtr client = SetPeerWatcher(); + RunLoopUntilIdle(); + + // Add then remove a peer. The watcher should only report the removal. + bt::PeerId id; + { + bt::gap::Peer* peer = + adapter()->peer_cache()->NewPeer(kLeTestAddr, /*connectable=*/true); + id = peer->identifier(); + + // |peer| becomes a dangling pointer after the call to + // RemoveDisconnectedPeer. We scoped the binding of |peer| so that it + // doesn't exist beyond this point. + [[maybe_unused]] auto result = + adapter()->peer_cache()->RemoveDisconnectedPeer(id); + } + + bool replied = false; + client->GetNext([&](fhost::PeerWatcher_GetNext_Result result) { + ASSERT_TRUE(result.is_response()); + ASSERT_TRUE(result.response().is_removed()); + EXPECT_EQ(1u, result.response().removed().size()); + EXPECT_TRUE( + fidl::Equals(fbt::PeerId{id.value()}, result.response().removed()[0])); + replied = true; + }); + RunLoopUntilIdle(); + EXPECT_TRUE(replied); +} + +TEST_F(HostServerTest, SetBrEdrSecurityMode) { + // Default BR/EDR security mode is Mode 4 + ASSERT_EQ( + fidl_helpers::BrEdrSecurityModeFromFidl(fsys::BrEdrSecurityMode::MODE_4), + adapter()->bredr()->security_mode()); + + // Set the HostServer to SecureConnectionsOnly mode first + host_client()->SetBrEdrSecurityMode( + fsys::BrEdrSecurityMode::SECURE_CONNECTIONS_ONLY); + RunLoopUntilIdle(); + ASSERT_EQ(fidl_helpers::BrEdrSecurityModeFromFidl( + fsys::BrEdrSecurityMode::SECURE_CONNECTIONS_ONLY), + adapter()->bredr()->security_mode()); + + // Set the HostServer back to Mode 4 and verify that the change takes place + host_client()->SetBrEdrSecurityMode(fsys::BrEdrSecurityMode::MODE_4); + RunLoopUntilIdle(); + ASSERT_EQ( + fidl_helpers::BrEdrSecurityModeFromFidl(fsys::BrEdrSecurityMode::MODE_4), + adapter()->bredr()->security_mode()); +} + +TEST_F(HostServerTest, SetLeSecurityMode) { + // Set the HostServer to SecureConnectionsOnly mode first + host_client()->SetLeSecurityMode( + fsys::LeSecurityMode::SECURE_CONNECTIONS_ONLY); + RunLoopUntilIdle(); + ASSERT_EQ(fidl_helpers::LeSecurityModeFromFidl( + fsys::LeSecurityMode::SECURE_CONNECTIONS_ONLY), + adapter()->le()->security_mode()); + + // Set the HostServer back to Mode 1 and verify that the change takes place + host_client()->SetLeSecurityMode(fsys::LeSecurityMode::MODE_1); + RunLoopUntilIdle(); + ASSERT_EQ(fidl_helpers::LeSecurityModeFromFidl(fsys::LeSecurityMode::MODE_1), + adapter()->le()->security_mode()); +} + +TEST_F(HostServerTest, ConnectLowEnergy) { + bt::gap::Peer* peer = AddFakePeer(kLeTestAddr); + EXPECT_EQ(bt::gap::TechnologyType::kLowEnergy, peer->technology()); + + auto result = ConnectFakePeer(peer->identifier()); + ASSERT_TRUE(result); + ASSERT_FALSE(result->is_err()); + + EXPECT_FALSE(peer->bredr()); + ASSERT_TRUE(peer->le()); + EXPECT_TRUE(peer->le()->connected()); + + // bt-host should only attempt to connect the LE transport. + EXPECT_EQ(1, test_device()->le_create_connection_command_count()); + EXPECT_EQ(0, test_device()->acl_create_connection_command_count()); +} + +TEST_F(HostServerTest, ConnectBredr) { + bt::gap::Peer* peer = AddFakePeer(kBredrTestAddr); + EXPECT_EQ(bt::gap::TechnologyType::kClassic, peer->technology()); + + auto result = ConnectFakePeer(peer->identifier()); + ASSERT_TRUE(result); + ASSERT_FALSE(result->is_err()); + + EXPECT_FALSE(peer->le()); + ASSERT_TRUE(peer->bredr()); + + // bt-host should only attempt to connect the BR/EDR transport. + EXPECT_EQ(0, test_device()->le_create_connection_command_count()); + EXPECT_EQ(1, test_device()->acl_create_connection_command_count()); +} + +TEST_F(HostServerTest, ConnectDualMode) { + // Initialize the peer with data for both transport types. + bt::gap::Peer* peer = AddFakePeer(kBredrTestAddr); + peer->MutLe(); + ASSERT_TRUE(peer->le()); + peer->MutBrEdr(); + ASSERT_TRUE(peer->bredr()); + EXPECT_EQ(bt::gap::TechnologyType::kDualMode, peer->technology()); + + auto result = ConnectFakePeer(peer->identifier()); + ASSERT_TRUE(result); + ASSERT_FALSE(result->is_err()); + + // bt-host should only attempt to connect the BR/EDR transport. + EXPECT_FALSE(peer->le()->connected()); + EXPECT_EQ(0, test_device()->le_create_connection_command_count()); + EXPECT_EQ(1, test_device()->acl_create_connection_command_count()); +} + +TEST_F(HostServerTest, RestoreBondsErrorDataMissing) { + fidl::InterfaceHandle delegate_handle; + host_client()->SetBondingDelegate(delegate_handle.NewRequest()); + fhost::BondingDelegatePtr delegate = delegate_handle.Bind(); + + fsys::BondingData bond; + + // Empty bond. + TestRestoreBonds(delegate, MakeClonedVector(bond), MakeClonedVector(bond)); + + // ID missing. + bond = MakeTestBond(kTestId, kTestFidlAddrPublic); + bond.clear_identifier(); + TestRestoreBonds(delegate, MakeClonedVector(bond), MakeClonedVector(bond)); + + // Address missing. + bond = MakeTestBond(kTestId, kTestFidlAddrPublic); + bond.clear_address(); + TestRestoreBonds(delegate, MakeClonedVector(bond), MakeClonedVector(bond)); + + // Transport data missing. + bond = MakeTestBond(kTestId, kTestFidlAddrPublic); + bond.clear_le_bond(); + bond.clear_bredr_bond(); + TestRestoreBonds(delegate, MakeClonedVector(bond), MakeClonedVector(bond)); + + // Transport data missing keys. + bond = MakeTestBond(kTestId, kTestFidlAddrPublic); + TestRestoreBonds(delegate, MakeClonedVector(bond), MakeClonedVector(bond)); +} + +TEST_F(HostServerTest, RestoreBondsInvalidAddress) { + fidl::InterfaceHandle delegate_handle; + host_client()->SetBondingDelegate(delegate_handle.NewRequest()); + fhost::BondingDelegatePtr delegate = delegate_handle.Bind(); + + // LE Random address on dual-mode or BR/EDR-only bond should not be supported. + fsys::BondingData bond = MakeTestBond(kTestId, kTestFidlAddrRandom); + bond.set_bredr_bond(fsys::BredrBondData()); + TestRestoreBonds(delegate, MakeClonedVector(bond), MakeClonedVector(bond)); + + // BR/EDR only + bond.clear_le_bond(); + TestRestoreBonds(delegate, MakeClonedVector(bond), MakeClonedVector(bond)); + + // Resolvable Private address should not be supported + fsys::BondingData resolvable_bond = + MakeTestBond(kTestId, kTestFidlAddrResolvable); + TestRestoreBonds(delegate, + MakeClonedVector(resolvable_bond), + MakeClonedVector(resolvable_bond)); + + // Non-resolvable Private address should not be supported + fsys::BondingData non_resolvable_bond = + MakeTestBond(kTestId, kTestFidlAddrNonResolvable); + TestRestoreBonds(delegate, + MakeClonedVector(non_resolvable_bond), + MakeClonedVector(non_resolvable_bond)); +} + +TEST_F(HostServerTest, RestoreBondsLeOnlySuccess) { + fsys::BondingData bond = MakeTestBond(kTestId, kTestFidlAddrRandom); + auto ltk = fsys::Ltk{.key = + fsys::PeerKey{ + .security = + fsys::SecurityProperties{ + .authenticated = true, + .secure_connections = true, + .encryption_key_size = 16, + }, + .data = + fsys::Key{ + .value = {1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16}, + }, + }, + .ediv = 0, + .rand = 0}; + fsys::LeBondData le; + le.set_peer_ltk(ltk); + le.set_local_ltk(ltk); + bond.set_le_bond(std::move(le)); + + fidl::InterfaceHandle delegate_handle; + host_client()->SetBondingDelegate(delegate_handle.NewRequest()); + fhost::BondingDelegatePtr delegate = delegate_handle.Bind(); + + // This should succeed. + TestRestoreBonds( + delegate, MakeClonedVector(bond), {} /* no errors expected */); + + auto* peer = adapter()->peer_cache()->FindById(kTestId); + ASSERT_TRUE(peer); + EXPECT_TRUE(peer->le()); + EXPECT_FALSE(peer->bredr()); + EXPECT_EQ(bt::DeviceAddress::Type::kLERandom, peer->address().type()); +} + +TEST_F(HostServerTest, RestoreBondsBredrOnlySuccess) { + fidl::InterfaceHandle delegate_handle; + host_client()->SetBondingDelegate(delegate_handle.NewRequest()); + fhost::BondingDelegatePtr delegate = delegate_handle.Bind(); + + fsys::BondingData bond = MakeTestBond(kTestId, kTestFidlAddrPublic); + bond.clear_le_bond(); + + fsys::BredrBondData bredr; + bredr.set_link_key(fsys::PeerKey{ + .security = + fsys::SecurityProperties{ + .authenticated = true, + .secure_connections = true, + .encryption_key_size = 16, + }, + .data = + fsys::Key{ + .value = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, + }, + }); + constexpr bt::UUID kServiceId = bt::sdp::profile::kAudioSink; + bredr.set_services({fidl_helpers::UuidToFidl(kServiceId)}); + bond.set_bredr_bond(std::move(bredr)); + + // This should succeed. + TestRestoreBonds( + delegate, MakeClonedVector(bond), {} /* no errors expected */); + + auto* peer = adapter()->peer_cache()->FindById(kTestId); + ASSERT_TRUE(peer); + ASSERT_TRUE(peer->bredr()); + EXPECT_THAT(peer->bredr()->services(), ::testing::ElementsAre(kServiceId)); + EXPECT_FALSE(peer->le()); + EXPECT_EQ(bt::DeviceAddress::Type::kBREDR, peer->address().type()); +} + +TEST_F(HostServerTest, RestoreBondsDualModeSuccess) { + fidl::InterfaceHandle delegate_handle; + host_client()->SetBondingDelegate(delegate_handle.NewRequest()); + fhost::BondingDelegatePtr delegate = delegate_handle.Bind(); + + fsys::BondingData bond = MakeTestBond(kTestId, kTestFidlAddrPublic); + auto key = fsys::PeerKey{ + .security = + fsys::SecurityProperties{ + .authenticated = true, + .secure_connections = true, + .encryption_key_size = 16, + }, + .data = + fsys::Key{ + .value = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, + }, + }; + auto ltk = fsys::Ltk{.key = key, .ediv = 0, .rand = 0}; + fsys::LeBondData le; + le.set_peer_ltk(ltk); + le.set_local_ltk(ltk); + bond.set_le_bond(std::move(le)); + + fsys::BredrBondData bredr; + bredr.set_link_key(key); + constexpr bt::UUID kServiceId = bt::sdp::profile::kAudioSink; + bredr.set_services({fidl_helpers::UuidToFidl(kServiceId)}); + bond.set_bredr_bond(std::move(bredr)); + + // This should succeed. + TestRestoreBonds( + delegate, MakeClonedVector(bond), {} /* no errors expected */); + + auto* peer = adapter()->peer_cache()->FindById(kTestId); + ASSERT_TRUE(peer); + EXPECT_TRUE(peer->le()); + ASSERT_TRUE(peer->bredr()); + EXPECT_THAT(peer->bredr()->services(), ::testing::ElementsAre(kServiceId)); + EXPECT_EQ(bt::DeviceAddress::Type::kBREDR, peer->address().type()); +} + +TEST_F(HostServerTest, SetHostData) { + EXPECT_FALSE(adapter()->le()->irk()); + + fsys::Key irk{{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}}; + fsys::HostData data; + data.set_irk(irk); + + host_server()->SetLocalData(std::move(data)); + ASSERT_TRUE(adapter()->le()->irk()); + EXPECT_EQ(irk.value, adapter()->le()->irk().value()); +} + +TEST_F(HostServerTest, OnNewBondingData) { + const std::string kTestName = "florp"; + const bt::UInt128 kTestKeyValue{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; + const bt::sm::SecurityProperties kTestSecurity( + bt::sm::SecurityLevel::kSecureAuthenticated, + 16, + /*secure_connections=*/true); + const bt::sm::LTK kTestLtk(kTestSecurity, + bt::hci_spec::LinkKey(kTestKeyValue, 0, 0)); + const fsys::PeerKey kTestKeyFidl{ + .security = + fsys::SecurityProperties{ + .authenticated = true, + .secure_connections = true, + .encryption_key_size = 16, + }, + .data = fsys::Key{.value = kTestKeyValue}, + }; + const fsys::Ltk kTestLtkFidl{.key = kTestKeyFidl, .ediv = 0, .rand = 0}; + + auto* peer = + adapter()->peer_cache()->NewPeer(kBredrTestAddr, /*connectable=*/true); + peer->RegisterName(kTestName); + adapter()->peer_cache()->StoreLowEnergyBond( + peer->identifier(), bt::sm::PairingData{.peer_ltk = {kTestLtk}}); + + // Set the bonding delegate after the bond has already been stored. The + // delegate should still be notified. + fidl::InterfaceHandle delegate_handle; + host_client_ptr()->SetBondingDelegate(delegate_handle.NewRequest()); + fhost::BondingDelegatePtr delegate = delegate_handle.Bind(); + std::optional data; + delegate->WatchBonds( + [&data](fhost::BondingDelegate_WatchBonds_Result result) { + ASSERT_TRUE(result.is_response()); + data = std::move(result.response().updated()); + }); + + RunLoopUntilIdle(); + ASSERT_TRUE(data); + ASSERT_TRUE(data->has_identifier()); + ASSERT_TRUE(data->has_local_address()); + ASSERT_TRUE(data->has_address()); + ASSERT_TRUE(data->has_name()); + + EXPECT_TRUE(fidl::Equals( + (fbt::Address{fbt::AddressType::PUBLIC, std::array{0}}), + data->local_address())); + EXPECT_TRUE(fidl::Equals(kTestFidlAddrPublic, data->address())); + EXPECT_EQ(kTestName, data->name()); + + ASSERT_TRUE(data->has_le_bond()); + EXPECT_FALSE(data->has_bredr_bond()); + + ASSERT_TRUE(data->le_bond().has_peer_ltk()); + EXPECT_FALSE(data->le_bond().has_local_ltk()); + EXPECT_FALSE(data->le_bond().has_irk()); + EXPECT_FALSE(data->le_bond().has_csrk()); + EXPECT_TRUE(fidl::Equals(kTestLtkFidl, data->le_bond().peer_ltk())); + + // Add BR/EDR data. This time, set WatchBonds callback before storing the + // bond. + data.reset(); + delegate->WatchBonds( + [&data](fhost::BondingDelegate_WatchBonds_Result result) { + ASSERT_TRUE(result.is_response()); + data = std::move(result.response().updated()); + }); + RunLoopUntilIdle(); + + adapter()->peer_cache()->StoreBrEdrBond(kBredrTestAddr, kTestLtk); + RunLoopUntilIdle(); + + ASSERT_TRUE(data); + ASSERT_TRUE(data->has_identifier()); + ASSERT_TRUE(data->has_local_address()); + ASSERT_TRUE(data->has_address()); + ASSERT_TRUE(data->has_name()); + + EXPECT_TRUE(fidl::Equals( + (fbt::Address{fbt::AddressType::PUBLIC, std::array{0}}), + data->local_address())); + EXPECT_TRUE(fidl::Equals(kTestFidlAddrPublic, data->address())); + EXPECT_EQ(kTestName, data->name()); + + ASSERT_TRUE(data->has_le_bond()); + ASSERT_TRUE(data->le_bond().has_peer_ltk()); + EXPECT_FALSE(data->le_bond().has_local_ltk()); + EXPECT_FALSE(data->le_bond().has_irk()); + EXPECT_FALSE(data->le_bond().has_csrk()); + EXPECT_TRUE(fidl::Equals(kTestLtkFidl, data->le_bond().peer_ltk())); + + ASSERT_TRUE(data->has_bredr_bond()); + ASSERT_TRUE(data->bredr_bond().has_link_key()); + EXPECT_TRUE(fidl::Equals(kTestKeyFidl, data->bredr_bond().link_key())); +} + +TEST_F(HostServerTest, EnableBackgroundScan) { + host_server()->EnableBackgroundScan(true); + EXPECT_FALSE(test_device()->le_scan_state().enabled); + + RunLoopUntilIdle(); + EXPECT_TRUE(test_device()->le_scan_state().enabled); + EXPECT_EQ(pw::bluetooth::emboss::LEScanType::PASSIVE, + test_device()->le_scan_state().scan_type); + + host_server()->EnableBackgroundScan(false); + RunLoopUntilIdle(); + EXPECT_FALSE(test_device()->le_scan_state().enabled); +} + +TEST_F(HostServerTest, EnableBackgroundScanTwiceAtSameTime) { + host_server()->EnableBackgroundScan(true); + host_server()->EnableBackgroundScan(true); + EXPECT_FALSE(test_device()->le_scan_state().enabled); + + RunLoopUntilIdle(); + EXPECT_TRUE(test_device()->le_scan_state().enabled); + EXPECT_EQ(pw::bluetooth::emboss::LEScanType::PASSIVE, + test_device()->le_scan_state().scan_type); + + host_server()->EnableBackgroundScan(false); + RunLoopUntilIdle(); + EXPECT_FALSE(test_device()->le_scan_state().enabled); +} + +TEST_F(HostServerTest, EnableBackgroundScanTwiceSequentially) { + host_server()->EnableBackgroundScan(true); + EXPECT_FALSE(test_device()->le_scan_state().enabled); + + RunLoopUntilIdle(); + EXPECT_TRUE(test_device()->le_scan_state().enabled); + EXPECT_EQ(pw::bluetooth::emboss::LEScanType::PASSIVE, + test_device()->le_scan_state().scan_type); + + host_server()->EnableBackgroundScan(true); + RunLoopUntilIdle(); + EXPECT_TRUE(test_device()->le_scan_state().enabled); + EXPECT_EQ(pw::bluetooth::emboss::LEScanType::PASSIVE, + test_device()->le_scan_state().scan_type); + + host_server()->EnableBackgroundScan(false); + RunLoopUntilIdle(); + EXPECT_FALSE(test_device()->le_scan_state().enabled); +} + +TEST_F(HostServerTest, CancelEnableBackgroundScan) { + host_server()->EnableBackgroundScan(true); + host_server()->EnableBackgroundScan(false); + + RunLoopUntilIdle(); + EXPECT_FALSE(test_device()->le_scan_state().enabled); + + host_server()->EnableBackgroundScan(true); + RunLoopUntilIdle(); + EXPECT_TRUE(test_device()->le_scan_state().enabled); +} + +TEST_F(HostServerTest, DisableBackgroundScan) { + host_server()->EnableBackgroundScan(false); + RunLoopUntilIdle(); + EXPECT_FALSE(test_device()->le_scan_state().enabled); +} + +TEST_F(HostServerTest, EnableBackgroundScanFailsToStart) { + test_device()->SetDefaultCommandStatus( + bt::hci_spec::kLESetScanEnable, + pw::bluetooth::emboss::StatusCode::CONTROLLER_BUSY); + host_server()->EnableBackgroundScan(true); + EXPECT_FALSE(test_device()->le_scan_state().enabled); + + RunLoopUntilIdle(); + EXPECT_FALSE(test_device()->le_scan_state().enabled); + + test_device()->ClearDefaultCommandStatus(bt::hci_spec::kLESetScanEnable); + host_server()->EnableBackgroundScan(true); + RunLoopUntilIdle(); + EXPECT_TRUE(test_device()->le_scan_state().enabled); +} + +class HostServerTestFakeAdapter + : public bt::fidl::testing::FakeAdapterTestFixture { + public: + HostServerTestFakeAdapter() = default; + ~HostServerTestFakeAdapter() override = default; + + void SetUp() override { + FakeAdapterTestFixture::SetUp(); + gatt_ = std::make_unique(pw_dispatcher()); + fidl::InterfaceHandle host_handle; + host_server_ = + std::make_unique(host_handle.NewRequest().TakeChannel(), + adapter()->AsWeakPtr(), + gatt_->GetWeakPtr()); + host_.Bind(std::move(host_handle)); + } + + void TearDown() override { + RunLoopUntilIdle(); + host_ = nullptr; + host_server_ = nullptr; + gatt_ = nullptr; + FakeAdapterTestFixture::TearDown(); + } + + protected: + HostServer* host_server() const { return host_server_.get(); } + + fuchsia::bluetooth::host::Host* host_client() const { return host_.get(); } + + fuchsia::bluetooth::host::HostPtr& host_client_ptr() { return host_; } + + private: + std::unique_ptr host_server_; + fuchsia::bluetooth::host::HostPtr host_; + std::unique_ptr gatt_; + + BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(HostServerTestFakeAdapter); +}; + +TEST_F(HostServerTestFakeAdapter, SetLocalNameNotifiesWatchState) { + std::vector info; + // Consume initial state value. + host_client()->WatchState([&](fhost::Host_WatchState_Result result) { + ASSERT_TRUE(result.is_response()); + info.push_back(std::move(result.response().info)); + }); + RunLoopUntilIdle(); + EXPECT_EQ(info.size(), 1u); + // Second watch state will hang until state is updated. + host_client()->WatchState([&](fhost::Host_WatchState_Result result) { + ASSERT_TRUE(result.is_response()); + info.push_back(std::move(result.response().info)); + }); + RunLoopUntilIdle(); + EXPECT_EQ(info.size(), 1u); + + int cb_count = 0; + host_client()->SetLocalName("test", [&](auto result) { + EXPECT_TRUE(result.is_response()); + cb_count++; + }); + RunLoopUntilIdle(); + EXPECT_EQ(cb_count, 1); + EXPECT_EQ(adapter()->local_name(), "test"); + ASSERT_EQ(info.size(), 2u); + ASSERT_TRUE(info.back().has_local_name()); + EXPECT_EQ(info.back().local_name(), "test"); +} + +TEST_F(HostServerTestFakeAdapter, WatchAddressesState) { + std::optional info; + + // Make an initial watch call so that subsequent calls remain pending. + host_server()->WatchState([&](fhost::Host_WatchState_Result result) { + ASSERT_TRUE(result.is_response()); + info = std::move(result.response().info); + }); + ASSERT_TRUE(info.has_value()); + info.reset(); + + // Next request to watch should hang and not produce a result. + host_server()->WatchState([&](fhost::Host_WatchState_Result result) { + ASSERT_TRUE(result.is_response()); + info = std::move(result.response().info); + }); + EXPECT_FALSE(info.has_value()); + + host_server()->EnablePrivacy(/*enabled=*/true); + RunLoopUntilIdle(); + // The LE address change is an asynchronous operation. The state watcher + // should only update when the address changes. + EXPECT_FALSE(info.has_value()); + // Simulate a change in random LE address. + auto resolvable_address = bt::DeviceAddress( + bt::DeviceAddress::Type::kLERandom, {0x55, 0x44, 0x33, 0x22, 0x11, 0x43}); + adapter()->fake_le()->UpdateRandomAddress(resolvable_address); + RunLoopUntilIdle(); + ASSERT_TRUE(info.has_value()); + ASSERT_TRUE(info->has_addresses()); + // Both the public and private addresses should be reported. + EXPECT_EQ(info->addresses().size(), 2u); + EXPECT_TRUE(ContainersEqual(adapter()->state().controller_address.bytes(), + info->addresses()[0].bytes)); + EXPECT_EQ(fbt::AddressType::RANDOM, info->addresses()[1].type); + EXPECT_TRUE(ContainersEqual(adapter()->le()->CurrentAddress().value().bytes(), + info->addresses()[1].bytes)); + + info.reset(); + host_server()->WatchState([&](fhost::Host_WatchState_Result result) { + ASSERT_TRUE(result.is_response()); + info = std::move(result.response().info); + }); + EXPECT_FALSE(info.has_value()); + // Disabling privacy is a synchronous operation - the random LE address should + // no longer be used. + host_server()->EnablePrivacy(/*enabled=*/false); + RunLoopUntilIdle(); + + ASSERT_TRUE(info.has_value()); + ASSERT_TRUE(info->has_addresses()); + // Only the public address should be reported. + EXPECT_EQ(info->addresses().size(), 1u); + EXPECT_TRUE(ContainersEqual(adapter()->state().controller_address.bytes(), + info->addresses()[0].bytes)); +} + +} // namespace +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/host_server_watch_peers_fuzztest.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/host_server_watch_peers_fuzztest.cc new file mode 100644 index 0000000000..45f7058ded --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/host_server_watch_peers_fuzztest.cc @@ -0,0 +1,168 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 +#include + +#include "fuchsia/bluetooth/host/cpp/fidl.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/adapter_test_fixture.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/host_server.h" +#include "pw_bluetooth_sapphire/internal/host/common/random.h" +#include "pw_bluetooth_sapphire/internal/host/testing/peer_fuzzer.h" + +namespace bthost { +namespace { + +class HostServerFuzzTest final : public bthost::testing::AdapterTestFixture { + public: + HostServerFuzzTest() { SetUp(); } + + ~HostServerFuzzTest() override { + host_ = nullptr; + host_server_ = nullptr; + gatt_ = nullptr; + AdapterTestFixture::TearDown(); + } + + // Creates a peer with fuzzer-generated mutations that will be encoded as a + // fuchsia.bluetooth.sys.Peer and sent as a response to WatchPeers immediately + // (see HostServerTest.WatchPeersRepliesOnFirstCallWithExistingPeers). + void FuzzWatchPeers(FuzzedDataProvider& fuzzed_data_provider) { + // WatchPeers only tracks connectable peers by design + bt::gap::Peer* const peer = adapter()->peer_cache()->NewPeer( + bt::testing::MakePublicDeviceAddress(fuzzed_data_provider), + /*connectable=*/true); + BT_ASSERT(peer); + bt::gap::testing::PeerFuzzer peer_fuzzer(fuzzed_data_provider, *peer); + while (fuzzed_data_provider.remaining_bytes() != 0) { + peer_fuzzer.FuzzOneField(); + } + + // TODO(fxbug.dev/42144165): WatchPeers will trigger this test as a failure + // if we try to encode a lot of peers, even though fuzzing multiple peers + // would be helpful. + int watch_peers_responses = 0; + peer_watcher()->GetNext( + [this, peer, &watch_peers_responses]( + fuchsia::bluetooth::host::PeerWatcher_GetNext_Result result) { + BT_ASSERT(result.is_response()); + std::vector<::fuchsia::bluetooth::sys::Peer> updated; + if (result.response().is_updated()) { + updated = std::move(result.response().updated()); + } + std::vector<::fuchsia::bluetooth::PeerId> removed; + if (result.response().is_removed()) { + removed = std::move(result.response().removed()); + } + BT_ASSERT_MSG(updated.size() == 1, + "peer %s: peers updated = %zu", + bt_str(*peer), + updated.size()); + BT_ASSERT_MSG(removed.size() == 0, + "peer %s: peers removed = %zu", + bt_str(*peer), + removed.size()); + HandleWatchPeersResponse(*host_client(), + watch_peers_responses, + /*max_call_depth=*/1, + std::move(updated), + std::move(removed)); + }); + RunLoopUntilIdle(); + BT_ASSERT_MSG(watch_peers_responses == 1, + "peer %s: WatchPeers returned %d times", + bt_str(*peer), + watch_peers_responses); + } + + private: + HostServer* host_server() const { return host_server_.get(); } + + fuchsia::bluetooth::host::Host* host_client() const { return host_.get(); } + + fuchsia::bluetooth::host::PeerWatcher* peer_watcher() const { + return peer_watcher_client_.get(); + } + + void SetUp() override { + AdapterTestFixture::SetUp(); + gatt_ = take_gatt(); + fidl::InterfaceHandle host_handle; + host_server_ = + std::make_unique(host_handle.NewRequest().TakeChannel(), + adapter()->AsWeakPtr(), + gatt_->GetWeakPtr()); + host_.Bind(std::move(host_handle)); + + fidl::InterfaceHandle + peer_watcher_handle; + host_client()->SetPeerWatcher(peer_watcher_handle.NewRequest()); + peer_watcher_client_ = peer_watcher_handle.Bind(); + } + + // WatchPeers response handler that can re-initiate the call per the "hanging + // get" pattern up to |max_call_depth| times, like a normal client might. + template + void HandleWatchPeersResponse( + Host& host, + int& call_counter, + int max_call_depth, + std::vector updated, + std::vector removed) { + call_counter++; + BT_ASSERT_MSG(call_counter <= max_call_depth, + "max depth (%d) exceeded", + call_counter); + peer_watcher()->GetNext( + [this, &host, &call_counter, max_call_depth]( + fuchsia::bluetooth::host::PeerWatcher_GetNext_Result result) { + BT_ASSERT(result.is_response()); + std::vector<::fuchsia::bluetooth::sys::Peer> updated; + if (result.response().is_updated()) { + updated = std::move(result.response().updated()); + } + std::vector<::fuchsia::bluetooth::PeerId> removed; + if (result.response().is_removed()) { + removed = std::move(result.response().removed()); + } + this->HandleWatchPeersResponse(host, + call_counter, + max_call_depth, + std::move(updated), + std::move(removed)); + }); + } + + // No-op impl for the GoogleTest base class + void TestBody() override {} + + std::unique_ptr gatt_; + std::unique_ptr host_server_; + fuchsia::bluetooth::host::HostPtr host_; + fuchsia::bluetooth::host::PeerWatcherPtr peer_watcher_client_; +}; + +} // namespace +} // namespace bthost + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + FuzzedDataProvider fuzzed_data_provider(data, size); + + pw::random::FuzzerRandomGenerator rng(&fuzzed_data_provider); + bt::set_random_generator(&rng); + + bthost::HostServerFuzzTest host_server_fuzz_test; + host_server_fuzz_test.FuzzWatchPeers(fuzzed_data_provider); + return 0; +} diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/iso_stream_server.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/iso_stream_server.cc new file mode 100644 index 0000000000..aba3fffd5b --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/iso_stream_server.cc @@ -0,0 +1,170 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/fidl/iso_stream_server.h" + +#include + +#include + +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/helpers.h" + +namespace bthost { + +IsoStreamServer::IsoStreamServer( + fidl::InterfaceRequest request, + fit::callback on_closed_cb) + : ServerBase(this, std::move(request)), + on_closed_cb_(std::move(on_closed_cb)), + weak_self_(this) { + set_error_handler([this](zx_status_t) { OnClosed(); }); +} + +void IsoStreamServer::OnStreamEstablished( + bt::iso::IsoStream::WeakPtr stream_ptr, + const bt::iso::CisEstablishedParameters& connection_params) { + bt_log(INFO, "fidl", "CIS established"); + iso_stream_ = stream_ptr; + fuchsia::bluetooth::le::IsochronousStreamOnEstablishedRequest request; + request.set_result(ZX_OK); + fuchsia::bluetooth::le::CisEstablishedParameters params = + bthost::fidl_helpers::CisEstablishedParametersToFidl(connection_params); + request.set_established_params(std::move(params)); + binding()->events().OnEstablished(std::move(request)); +} + +void IsoStreamServer::OnStreamEstablishmentFailed( + pw::bluetooth::emboss::StatusCode status) { + BT_ASSERT(status != pw::bluetooth::emboss::StatusCode::SUCCESS); + bt_log(WARN, + "fidl", + "CIS failed to be established: %u", + static_cast(status)); + fuchsia::bluetooth::le::IsochronousStreamOnEstablishedRequest request; + request.set_result(ZX_ERR_INTERNAL); + binding()->events().OnEstablished(std::move(request)); +} + +void IsoStreamServer::SetupDataPath( + fuchsia::bluetooth::le::IsochronousStreamSetupDataPathRequest parameters, + SetupDataPathCallback callback) { + pw::bluetooth::emboss::DataPathDirection direction = + fidl_helpers::DataPathDirectionFromFidl(parameters.data_direction()); + const char* direction_as_str = + fidl_helpers::DataPathDirectionToString(direction); + bt_log(INFO, + "fidl", + "Request received to set up data path (direction: %s)", + direction_as_str); + if (direction != pw::bluetooth::emboss::DataPathDirection::OUTPUT) { + // We only support Controller => Host at the moment + bt_log(WARN, + "fidl", + "Attempt to set up data path with unsupported direction: %s", + direction_as_str); + callback(fpromise::error(ZX_ERR_NOT_SUPPORTED)); + return; + } + + bt::StaticPacket codec_id = + fidl_helpers::CodecIdFromFidl(parameters.codec_attributes().codec_id()); + std::optional> codec_configuration; + if (parameters.codec_attributes().has_codec_configuration()) { + codec_configuration = parameters.codec_attributes().codec_configuration(); + } + + zx::duration delay(parameters.controller_delay()); + uint32_t delay_in_us = delay.to_usecs(); + if (!iso_stream_.has_value()) { + bt_log(WARN, "fidl", "data path setup failed (CIS not established)"); + callback(fpromise::error(ZX_ERR_BAD_STATE)); + return; + } + if (!iso_stream_->is_alive()) { + bt_log(INFO, "fidl", "Attempt to set data path after CIS closed"); + callback(fpromise::error(ZX_ERR_BAD_STATE)); + return; + } + + (*iso_stream_) + ->SetupDataPath( + direction, + codec_id, + codec_configuration, + delay_in_us, + [callback = std::move(callback)]( + bt::iso::IsoStream::SetupDataPathError error) { + switch (error) { + case bt::iso::IsoStream::kSuccess: + bt_log(INFO, "fidl", "data path successfully setup"); + callback(fpromise::ok()); + break; + case bt::iso::IsoStream::kStreamAlreadyExists: + bt_log(WARN, + "fidl", + "data path setup failed (stream already setup)"); + callback(fpromise::error(ZX_ERR_ALREADY_EXISTS)); + break; + case bt::iso::IsoStream::kCisNotEstablished: + bt_log(WARN, + "fidl", + "data path setup failed (CIS not established)"); + callback(fpromise::error(ZX_ERR_BAD_STATE)); + break; + case bt::iso::IsoStream::kInvalidArgs: + bt_log(WARN, + "fidl", + "data path setup failed (invalid parameters)"); + callback(fpromise::error(ZX_ERR_INVALID_ARGS)); + break; + case bt::iso::IsoStream::kStreamClosed: + bt_log(WARN, "fidl", "data path setup failed (stream closed)"); + callback(fpromise::error(ZX_ERR_BAD_STATE)); + break; + default: + bt_log(ERROR, + "fidl", + "Unsupported case in SetupDataPathError: %u", + static_cast(error)); + callback(fpromise::error(ZX_ERR_INTERNAL)); + break; + } + }); +} + +void IsoStreamServer::Read(ReadCallback callback) {} + +void IsoStreamServer::OnClosed() { + if (iso_stream_.has_value() && iso_stream_->is_alive()) { + (*iso_stream_)->Close(); + } + // This may free our instance. + on_closed_cb_(); +} + +void IsoStreamServer::Close(zx_status_t epitaph) { + binding()->Close(epitaph); + OnClosed(); +} + +void IsoStreamServer::handle_unknown_method(uint64_t ordinal, + bool has_response) { + bt_log(WARN, + "fidl", + "Received unknown fidl call %#" PRIx64 " (%s responses)", + ordinal, + has_response ? "with" : "without"); +} + +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/iso_stream_server_test.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/iso_stream_server_test.cc new file mode 100644 index 0000000000..a29af22ca0 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/iso_stream_server_test.cc @@ -0,0 +1,287 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/fidl/iso_stream_server.h" + +#include "pw_bluetooth_sapphire/internal/host/iso/fake_iso_stream.h" +#include "pw_bluetooth_sapphire/internal/host/testing/loop_fixture.h" + +namespace bthost { +namespace { + +const bt::iso::CisEstablishedParameters kCisParameters = { + .cig_sync_delay = 1000000, + .cis_sync_delay = 2000000, + .max_subevents = 5, + .iso_interval = 15, + .c_to_p_params = + { + .transport_latency = 5000, + .phy = pw::bluetooth::emboss::IsoPhyType::LE_1M, + .burst_number = 3, + .flush_timeout = 100, + .max_pdu_size = 120, + }, + .p_to_c_params = + { + .transport_latency = 6000, + .phy = pw::bluetooth::emboss::IsoPhyType::LE_CODED, + .burst_number = 4, + .flush_timeout = 60, + .max_pdu_size = 70, + }, +}; + +using TestingBase = bt::testing::TestLoopFixture; +class IsoStreamServerTest : public TestingBase { + public: + IsoStreamServerTest() = default; + ~IsoStreamServerTest() override = default; + + void SetUp() override { + TestingBase::SetUp(); + + fidl::InterfaceHandle handle; + server_ = std::make_unique(handle.NewRequest(), + [this]() { OnClosed(); }); + client_.Bind(std::move(handle), dispatcher()); + client_.set_error_handler( + [this](zx_status_t status) { epitaph_ = status; }); + client_.events().OnEstablished = + [this](::fuchsia::bluetooth::le::IsochronousStreamOnEstablishedRequest + request) { + on_established_events_.push(std::move(request)); + }; + fake_iso_stream_ = std::make_unique(); + } + + void TearDown() override { + RunLoopUntilIdle(); + CloseProxy(); + server_ = nullptr; + TestingBase::TearDown(); + } + + void CallSetupDataPath(fuchsia::bluetooth::DataDirection data_direction, + fuchsia::bluetooth::CodecAttributes codec_attributes, + std::optional* status_out); + + protected: + void OnClosed() { on_closed_called_times_++; } + void CloseProxy() { client_ = nullptr; } + IsoStreamServer* server() const { return server_.get(); } + std::optional epitaph() const { return epitaph_; } + bt::iso::testing::FakeIsoStream* fake_iso_stream() { + return fake_iso_stream_.get(); + } + + std::queue<::fuchsia::bluetooth::le::IsochronousStreamOnEstablishedRequest> + on_established_events_; + uint32_t on_closed_called_times_ = 0; + fuchsia::bluetooth::le::IsochronousStreamPtr client_; + + private: + std::unique_ptr server_; + std::optional epitaph_; + std::unique_ptr fake_iso_stream_; + + BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(IsoStreamServerTest); +}; + +void IsoStreamServerTest::CallSetupDataPath( + fuchsia::bluetooth::DataDirection data_direction, + fuchsia::bluetooth::CodecAttributes codec_attributes, + std::optional* status_out) { + fuchsia::bluetooth::le::IsochronousStreamSetupDataPathRequest request; + request.set_data_direction(data_direction); + request.set_codec_attributes(std::move(codec_attributes)); + request.set_controller_delay(0); + client_->SetupDataPath(std::move(request), [status_out](auto result) { + if (result.is_err()) + *status_out = result.err(); + }); + RunLoopUntilIdle(); +} + +TEST_F(IsoStreamServerTest, ClosedServerSide) { + server()->Close(ZX_ERR_WRONG_TYPE); + RunLoopUntilIdle(); + auto status = epitaph(); + ASSERT_TRUE(status); + EXPECT_EQ(*status, ZX_ERR_WRONG_TYPE); + EXPECT_EQ(on_closed_called_times_, 1u); +} + +TEST_F(IsoStreamServerTest, ClosedClientSide) { + CloseProxy(); + RunLoopUntilIdle(); + EXPECT_EQ(on_closed_called_times_, 1u); +} + +// Verify that when an IsoStreamServer receives notification of a successful +// stream establishment it sends the stream parameters back to the client. +TEST_F(IsoStreamServerTest, StreamEstablishedSuccessfully) { + EXPECT_EQ(on_established_events_.size(), (size_t)0); + server()->OnStreamEstablished(fake_iso_stream()->GetWeakPtr(), + kCisParameters); + RunLoopUntilIdle(); + ASSERT_EQ(on_established_events_.size(), (size_t)1); + + auto& event = on_established_events_.front(); + ASSERT_TRUE(event.has_result()); + EXPECT_EQ(event.result(), ZX_OK); + + ASSERT_TRUE(event.has_established_params()); + auto& established_params = event.established_params(); + ASSERT_TRUE(established_params.has_cig_sync_delay()); + EXPECT_EQ(established_params.cig_sync_delay(), + zx::usec(kCisParameters.cig_sync_delay).get()); + ASSERT_TRUE(established_params.has_cis_sync_delay()); + EXPECT_EQ(established_params.cis_sync_delay(), + zx::usec(kCisParameters.cis_sync_delay).get()); + ASSERT_TRUE(established_params.has_max_subevents()); + EXPECT_EQ(established_params.max_subevents(), kCisParameters.max_subevents); + ASSERT_TRUE(established_params.has_iso_interval()); + // Each increment represent 1.25ms + EXPECT_EQ(established_params.iso_interval(), + zx::usec(kCisParameters.iso_interval * 1250).get()); + + ASSERT_TRUE(established_params.has_central_to_peripheral_params()); + auto& c_to_p_params = established_params.central_to_peripheral_params(); + ASSERT_TRUE(c_to_p_params.has_transport_latency()); + EXPECT_EQ(c_to_p_params.transport_latency(), + zx::usec(kCisParameters.c_to_p_params.transport_latency).get()); + ASSERT_TRUE(c_to_p_params.has_burst_number()); + EXPECT_EQ(c_to_p_params.burst_number(), + kCisParameters.c_to_p_params.burst_number); + ASSERT_TRUE(c_to_p_params.has_flush_timeout()); + EXPECT_EQ(c_to_p_params.flush_timeout(), + kCisParameters.c_to_p_params.flush_timeout); + + ASSERT_TRUE(established_params.has_peripheral_to_central_params()); + auto& p_to_c_params = established_params.peripheral_to_central_params(); + ASSERT_TRUE(p_to_c_params.has_transport_latency()); + EXPECT_EQ(p_to_c_params.transport_latency(), + zx::usec(kCisParameters.p_to_c_params.transport_latency).get()); + ASSERT_TRUE(p_to_c_params.has_burst_number()); + EXPECT_EQ(p_to_c_params.burst_number(), + kCisParameters.p_to_c_params.burst_number); + ASSERT_TRUE(p_to_c_params.has_flush_timeout()); + EXPECT_EQ(p_to_c_params.flush_timeout(), + kCisParameters.p_to_c_params.flush_timeout); +} + +// Verify that on failure we properly notify the client, set status code to +// ZX_ERR_INTERNAL, and don't pass back any stream parameters. +TEST_F(IsoStreamServerTest, StreamNotEstablished) { + EXPECT_EQ(on_established_events_.size(), 0u); + server()->OnStreamEstablishmentFailed( + pw::bluetooth::emboss::StatusCode::UNSPECIFIED_ERROR); + RunLoopUntilIdle(); + ASSERT_EQ(on_established_events_.size(), 1u); + auto& event1 = on_established_events_.front(); + ASSERT_TRUE(event1.has_result()); + EXPECT_EQ(event1.result(), ZX_ERR_INTERNAL); + ASSERT_FALSE(event1.has_established_params()); + on_established_events_.pop(); + + server()->OnStreamEstablishmentFailed( + pw::bluetooth::emboss::StatusCode::UNKNOWN_COMMAND); + RunLoopUntilIdle(); + ASSERT_EQ(on_established_events_.size(), 1u); + auto& event2 = on_established_events_.front(); + ASSERT_TRUE(event2.has_result()); + EXPECT_EQ(event2.result(), ZX_ERR_INTERNAL); + ASSERT_FALSE(event2.has_established_params()); + on_established_events_.pop(); +} + +fuchsia::bluetooth::CodecAttributes BuildCodecAttributes() { + fuchsia::bluetooth::CodecAttributes codec_attributes; + fuchsia::bluetooth::CodecId codec_id; + codec_id.set_assigned_format(fuchsia::bluetooth::AssignedCodingFormat::MSBC); + codec_attributes.set_codec_id(std::move(codec_id)); + return codec_attributes; +} + +TEST_F(IsoStreamServerTest, SetupDataPathInvalidDirection) { + fuchsia::bluetooth::CodecAttributes codec_attributes = BuildCodecAttributes(); + std::optional status; + CallSetupDataPath(fuchsia::bluetooth::DataDirection::INPUT, + std::move(codec_attributes), + &status); + EXPECT_TRUE(status.has_value()); + EXPECT_EQ(*status, ZX_ERR_NOT_SUPPORTED); +} + +TEST_F(IsoStreamServerTest, SetupDataPathBeforeCisEstablished) { + fuchsia::bluetooth::CodecAttributes codec_attributes = BuildCodecAttributes(); + std::optional status; + CallSetupDataPath(fuchsia::bluetooth::DataDirection::OUTPUT, + std::move(codec_attributes), + &status); + EXPECT_TRUE(status.has_value()); + EXPECT_EQ(*status, ZX_ERR_BAD_STATE); +} + +// Verify that return code from SetupDataPath() callback is properly translated +// into result of FIDL call. +TEST_F(IsoStreamServerTest, SetupDataPathStatusCodes) { + server()->OnStreamEstablished(fake_iso_stream()->GetWeakPtr(), + kCisParameters); + RunLoopUntilIdle(); + fuchsia::bluetooth::CodecAttributes codec_attributes = BuildCodecAttributes(); + + // kSuccess => no error + fake_iso_stream()->SetSetupDataPathReturnStatus( + bt::iso::IsoStream::SetupDataPathError::kSuccess); + std::optional status1; + CallSetupDataPath(fuchsia::bluetooth::DataDirection::OUTPUT, + std::move(codec_attributes), + &status1); + EXPECT_FALSE(status1.has_value()); + + // kStreamAlreadyExists => ZX_ERR_ALREADY_EXISTS + fake_iso_stream()->SetSetupDataPathReturnStatus( + bt::iso::IsoStream::SetupDataPathError::kStreamAlreadyExists); + std::optional status2; + CallSetupDataPath(fuchsia::bluetooth::DataDirection::OUTPUT, + std::move(codec_attributes), + &status2); + EXPECT_TRUE(status2.has_value()); + EXPECT_EQ(*status2, ZX_ERR_ALREADY_EXISTS); + + // kCisNotEstablished => ZX_ERR_BAD_STATE + fake_iso_stream()->SetSetupDataPathReturnStatus( + bt::iso::IsoStream::SetupDataPathError::kCisNotEstablished); + status2 = std::nullopt; + CallSetupDataPath(fuchsia::bluetooth::DataDirection::OUTPUT, + std::move(codec_attributes), + &status2); + EXPECT_TRUE(status2.has_value()); + EXPECT_EQ(*status2, ZX_ERR_BAD_STATE); + + // kInvalidArgs => ZX_ERR_INVALID_ARGS + fake_iso_stream()->SetSetupDataPathReturnStatus( + bt::iso::IsoStream::SetupDataPathError::kInvalidArgs); + status2 = std::nullopt; + CallSetupDataPath(fuchsia::bluetooth::DataDirection::OUTPUT, + std::move(codec_attributes), + &status2); + EXPECT_TRUE(status2.has_value()); + EXPECT_EQ(*status2, ZX_ERR_INVALID_ARGS); +} + +} // namespace +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_central_server.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_central_server.cc new file mode 100644 index 0000000000..91b4fc1f9b --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_central_server.cc @@ -0,0 +1,703 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_central_server.h" + +#include + +#include + +#include "fuchsia/bluetooth/le/cpp/fidl.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_client_server.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/helpers.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/measure_tape/hlcpp_measure_tape_for_peer.h" +#include "pw_bluetooth_sapphire/internal/host/common/assert.h" +#include "pw_bluetooth_sapphire/internal/host/common/error.h" +#include "pw_bluetooth_sapphire/internal/host/common/log.h" +#include "pw_bluetooth_sapphire/internal/host/hci-spec/constants.h" +#include "pw_bluetooth_sapphire/internal/host/sm/types.h" + +using fuchsia::bluetooth::ErrorCode; +using fuchsia::bluetooth::Int8; +using fuchsia::bluetooth::Status; + +using bt::sm::BondableMode; +using fuchsia::bluetooth::gatt::Client; +using fuchsia::bluetooth::le::ScanFilterPtr; +namespace fble = fuchsia::bluetooth::le; +namespace measure_fble = measure_tape::fuchsia::bluetooth::le; + +namespace bthost { + +namespace { + +bt::gap::LowEnergyConnectionOptions ConnectionOptionsFromFidl( + const fble::ConnectionOptions& options) { + BondableMode bondable_mode = + (!options.has_bondable_mode() || options.bondable_mode()) + ? BondableMode::Bondable + : BondableMode::NonBondable; + + std::optional service_uuid = + options.has_service_filter() + ? std::optional(fidl_helpers::UuidFromFidl(options.service_filter())) + : std::nullopt; + + return bt::gap::LowEnergyConnectionOptions{.bondable_mode = bondable_mode, + .service_uuid = service_uuid}; +} + +} // namespace + +LowEnergyCentralServer::LowEnergyCentralServer( + bt::gap::Adapter::WeakPtr adapter, + fidl::InterfaceRequest request, + bt::gatt::GATT::WeakPtr gatt) + : AdapterServerBase(std::move(adapter), this, std::move(request)), + gatt_(std::move(gatt)), + requesting_scan_deprecated_(false), + weak_self_(this) { + BT_ASSERT(gatt_.is_alive()); +} + +LowEnergyCentralServer::~LowEnergyCentralServer() { + if (scan_instance_) { + scan_instance_->Close(ZX_OK); + scan_instance_.reset(); + } +} + +std::optional +LowEnergyCentralServer::FindConnectionForTesting(bt::PeerId identifier) { + auto conn_iter = connections_deprecated_.find(identifier); + if (conn_iter != connections_deprecated_.end()) { + return conn_iter->second.get(); + } + return std::nullopt; +} + +LowEnergyCentralServer::ScanResultWatcherServer::ScanResultWatcherServer( + bt::gap::Adapter::WeakPtr adapter, + fidl::InterfaceRequest watcher, + fit::callback error_cb) + : ServerBase(this, std::move(watcher)), + adapter_(std::move(adapter)), + error_callback_(std::move(error_cb)) { + set_error_handler([this](auto) { + bt_log(DEBUG, "fidl", "ScanResultWatcher client closed, stopping scan"); + BT_ASSERT(error_callback_); + error_callback_(); + }); +} + +void LowEnergyCentralServer::ScanResultWatcherServer::Close( + zx_status_t epitaph) { + binding()->Close(epitaph); +} + +void LowEnergyCentralServer::ScanResultWatcherServer::AddPeers( + std::unordered_set peers) { + while (!peers.empty() && + updated_peers_.size() < kMaxPendingScanResultWatcherPeers) { + updated_peers_.insert(peers.extract(peers.begin())); + } + + if (!peers.empty()) { + bt_log( + WARN, + "fidl", + "Maximum pending peers (%zu) reached, dropping %zu peers from results", + kMaxPendingScanResultWatcherPeers, + peers.size()); + } + + MaybeSendPeers(); +} + +void LowEnergyCentralServer::ScanResultWatcherServer::Watch( + WatchCallback callback) { + bt_log(TRACE, "fidl", "%s", __FUNCTION__); + if (watch_callback_) { + bt_log(WARN, + "fidl", + "%s: called before previous call completed", + __FUNCTION__); + Close(ZX_ERR_CANCELED); + BT_ASSERT(error_callback_); + error_callback_(); + return; + } + watch_callback_ = std::move(callback); + MaybeSendPeers(); +} + +void LowEnergyCentralServer::ScanResultWatcherServer::MaybeSendPeers() { + if (updated_peers_.empty() || !watch_callback_) { + return; + } + + // Send as many peers as will fit in the channel. + const size_t kVectorOverhead = + sizeof(fidl_message_header_t) + sizeof(fidl_vector_t); + const size_t kMaxBytes = ZX_CHANNEL_MAX_MSG_BYTES - kVectorOverhead; + size_t bytes_used = 0; + std::vector peers; + while (!updated_peers_.empty()) { + bt::PeerId peer_id = *updated_peers_.begin(); + bt::gap::Peer* peer = adapter_->peer_cache()->FindById(peer_id); + if (!peer) { + // The peer has been removed from the peer cache since it was queued, so + // the stale peer ID should not be sent to the client. + updated_peers_.erase(peer_id); + continue; + } + + fble::Peer fidl_peer = fidl_helpers::PeerToFidlLe(*peer); + measure_fble::Size peer_size = measure_fble::Measure(fidl_peer); + BT_ASSERT_MSG(peer_size.num_handles == 0, + "Expected fuchsia.bluetooth.le/Peer to not have handles, but " + "%zu handles found", + peer_size.num_handles); + bytes_used += peer_size.num_bytes; + if (bytes_used > kMaxBytes) { + // Don't remove the peer that exceeded the size limit. It will be sent in + // the next batch. + break; + } + + updated_peers_.erase(peer_id); + peers.emplace_back(std::move(fidl_peer)); + } + + // It is possible that all queued peers were stale, so there is nothing to + // send. + if (peers.empty()) { + return; + } + + watch_callback_(std::move(peers)); +} + +LowEnergyCentralServer::ScanInstance::ScanInstance( + bt::gap::Adapter::WeakPtr adapter, + LowEnergyCentralServer* central_server, + std::vector fidl_filters, + fidl::InterfaceRequest watcher, + ScanCallback cb) + : result_watcher_(adapter, + std::move(watcher), + /*error_cb=*/ + [this] { + Close(ZX_OK); + central_server_->ClearScan(); + }), + scan_complete_callback_(std::move(cb)), + central_server_(central_server), + adapter_(std::move(adapter)), + weak_self_(this) { + std::transform(fidl_filters.begin(), + fidl_filters.end(), + std::back_inserter(filters_), + fidl_helpers::DiscoveryFilterFromFidl); + + // Send all current peers in peer cache that match filters. + std::unordered_set initial_peers; + adapter_->peer_cache()->ForEach([&](const bt::gap::Peer& peer) { + initial_peers.emplace(peer.identifier()); + }); + FilterAndAddPeers(std::move(initial_peers)); + + // Subscribe to updated peers. + peer_updated_callback_id_ = adapter_->peer_cache()->add_peer_updated_callback( + [this](const bt::gap::Peer& peer) { + FilterAndAddPeers({peer.identifier()}); + }); + + auto self = weak_self_.GetWeakPtr(); + adapter_->le()->StartDiscovery( + /*active=*/true, [self](auto session) { + if (!self.is_alive()) { + bt_log( + TRACE, "fidl", "ignoring LE discovery session for canceled Scan"); + return; + } + + if (!session) { + bt_log(WARN, "fidl", "failed to start LE discovery session"); + self->Close(ZX_ERR_INTERNAL); + self->central_server_->ClearScan(); + return; + } + + session->set_error_callback([self] { + if (!self.is_alive()) { + bt_log(TRACE, + "fidl", + "ignoring LE discovery session error for canceled Scan"); + return; + } + + bt_log(DEBUG, + "fidl", + "canceling Scan due to LE discovery session error"); + self->Close(ZX_ERR_INTERNAL); + self->central_server_->ClearScan(); + }); + + self->scan_session_ = std::move(session); + }); +} + +LowEnergyCentralServer::ScanInstance::~ScanInstance() { + // If this scan instance has not already been closed with a more specific + // status, close with an error status. + Close(ZX_ERR_INTERNAL); + adapter_->peer_cache()->remove_peer_updated_callback( + peer_updated_callback_id_); +} + +void LowEnergyCentralServer::ScanInstance::Close(zx_status_t status) { + if (scan_complete_callback_) { + result_watcher_.Close(status); + scan_complete_callback_(); + } +} + +void LowEnergyCentralServer::ScanInstance::FilterAndAddPeers( + std::unordered_set peers) { + // Remove peers that don't match any filters. + for (auto peers_iter = peers.begin(); peers_iter != peers.end();) { + bt::gap::Peer* peer = adapter_->peer_cache()->FindById(*peers_iter); + if (!peer || !peer->le()) { + peers_iter = peers.erase(peers_iter); + continue; + } + bool matches_any = false; + for (const bt::gap::DiscoveryFilter& filter : filters_) { + // TODO(fxbug.dev/42111894): Match peer names that are not in advertising + // data. This might require implementing a new peer filtering class, as + // DiscoveryFilter only filters advertising data. + if (filter.MatchLowEnergyResult(peer->le()->parsed_advertising_data(), + peer->connectable(), + peer->rssi())) { + matches_any = true; + break; + } + } + if (!matches_any) { + peers_iter = peers.erase(peers_iter); + continue; + } + peers_iter++; + } + + result_watcher_.AddPeers(std::move(peers)); +} + +void LowEnergyCentralServer::Scan( + fuchsia::bluetooth::le::ScanOptions options, + fidl::InterfaceRequest + result_watcher, + ScanCallback callback) { + bt_log(DEBUG, "fidl", "%s", __FUNCTION__); + + if (scan_instance_ || requesting_scan_deprecated_ || + scan_session_deprecated_) { + bt_log(INFO, "fidl", "%s: scan already in progress", __FUNCTION__); + result_watcher.Close(ZX_ERR_ALREADY_EXISTS); + callback(); + return; + } + + if (!options.has_filters() || options.filters().empty()) { + bt_log(INFO, "fidl", "%s: no scan filters specified", __FUNCTION__); + result_watcher.Close(ZX_ERR_INVALID_ARGS); + callback(); + return; + } + + scan_instance_ = + std::make_unique(adapter()->AsWeakPtr(), + this, + std::move(*options.mutable_filters()), + std::move(result_watcher), + std::move(callback)); +} + +void LowEnergyCentralServer::Connect( + fuchsia::bluetooth::PeerId id, + fble::ConnectionOptions options, + fidl::InterfaceRequest request) { + bt::PeerId peer_id(id.value); + bt_log(INFO, "fidl", "%s: (peer: %s)", __FUNCTION__, bt_str(peer_id)); + + auto conn_iter = connections_.find(peer_id); + if (conn_iter != connections_.end()) { + bt_log( + INFO, + "fidl", + "%s: connection %s (peer: %s)", + __FUNCTION__, + (conn_iter->second == nullptr ? "request pending" : "already exists"), + bt_str(peer_id)); + request.Close(ZX_ERR_ALREADY_BOUND); + return; + } + + auto self = weak_self_.GetWeakPtr(); + auto conn_cb = + [self, peer_id, request = std::move(request)]( + bt::gap::Adapter::LowEnergy::ConnectionResult result) mutable { + if (!self.is_alive()) + return; + + auto conn_iter = self->connections_.find(peer_id); + BT_ASSERT(conn_iter != self->connections_.end()); + BT_ASSERT(conn_iter->second == nullptr); + + if (result.is_error()) { + bt_log(INFO, + "fidl", + "Connect: failed to connect to peer (peer: %s)", + bt_str(peer_id)); + self->connections_.erase(peer_id); + request.Close(ZX_ERR_NOT_CONNECTED); + return; + } + + auto conn_ref = std::move(result).value(); + BT_ASSERT(conn_ref); + BT_ASSERT(peer_id == conn_ref->peer_identifier()); + + auto closed_cb = [self, peer_id] { + if (self.is_alive()) { + self->connections_.erase(peer_id); + } + }; + auto server = + std::make_unique(self->adapter(), + self->gatt_, + std::move(conn_ref), + request.TakeChannel(), + std::move(closed_cb)); + + BT_ASSERT(!conn_iter->second); + conn_iter->second = std::move(server); + }; + + // An entry for the connection must be created here so that a synchronous call + // to conn_cb below does not cause conn_cb to treat the connection as + // cancelled. + connections_[peer_id] = nullptr; + + adapter()->le()->Connect( + peer_id, std::move(conn_cb), ConnectionOptionsFromFidl(options)); +} + +void LowEnergyCentralServer::GetPeripherals( + ::fidl::VectorPtr<::std::string> service_uuids, + GetPeripheralsCallback callback) { + bt_log(ERROR, "fidl", "GetPeripherals() not implemented"); +} + +void LowEnergyCentralServer::GetPeripheral(::std::string identifier, + GetPeripheralCallback callback) { + bt_log(ERROR, "fidl", "GetPeripheral() not implemented"); +} + +void LowEnergyCentralServer::StartScan(ScanFilterPtr filter, + StartScanCallback callback) { + bt_log(DEBUG, "fidl", "%s", __FUNCTION__); + + if (requesting_scan_deprecated_) { + bt_log(DEBUG, "fidl", "%s: scan request already in progress", __FUNCTION__); + callback(fidl_helpers::NewFidlError(ErrorCode::IN_PROGRESS, + "Scan request in progress")); + return; + } + + if (filter && !fidl_helpers::IsScanFilterValid(*filter)) { + bt_log(WARN, "fidl", "%s: invalid scan filter given", __FUNCTION__); + callback(fidl_helpers::NewFidlError(ErrorCode::INVALID_ARGUMENTS, + "ScanFilter contains an invalid UUID")); + return; + } + + if (scan_session_deprecated_) { + // A scan is already in progress. Update its filter and report success. + scan_session_deprecated_->filter()->Reset(); + fidl_helpers::PopulateDiscoveryFilter(*filter, + scan_session_deprecated_->filter()); + callback(Status()); + return; + } + + requesting_scan_deprecated_ = true; + adapter()->le()->StartDiscovery( + /*active=*/true, + [self = weak_self_.GetWeakPtr(), + filter = std::move(filter), + callback = std::move(callback), + func = __FUNCTION__](auto session) { + if (!self.is_alive()) + return; + + self->requesting_scan_deprecated_ = false; + + if (!session) { + bt_log( + WARN, "fidl", "%s: failed to start LE discovery session", func); + callback(fidl_helpers::NewFidlError( + ErrorCode::FAILED, "Failed to start discovery session")); + return; + } + + // Assign the filter contents if a filter was provided. + if (filter) + fidl_helpers::PopulateDiscoveryFilter(*filter, session->filter()); + + session->SetResultCallback([self](const auto& peer) { + if (self.is_alive()) + self->OnScanResult(peer); + }); + + session->set_error_callback([self] { + if (self.is_alive()) { + // Clean up the session and notify the delegate. + self->StopScan(); + } + }); + + self->scan_session_deprecated_ = std::move(session); + self->NotifyScanStateChanged(true); + callback(Status()); + }); +} + +void LowEnergyCentralServer::StopScan() { + bt_log(DEBUG, "fidl", "StopScan()"); + + if (!scan_session_deprecated_) { + bt_log(DEBUG, + "fidl", + "%s: no active discovery session; nothing to do", + __FUNCTION__); + return; + } + + scan_session_deprecated_ = nullptr; + NotifyScanStateChanged(false); +} + +void LowEnergyCentralServer::ConnectPeripheral( + ::std::string identifier, + fuchsia::bluetooth::le::ConnectionOptions connection_options, + ::fidl::InterfaceRequest client_request, + ConnectPeripheralCallback callback) { + bt_log(INFO, "fidl", "%s: (peer: %s)", __FUNCTION__, identifier.c_str()); + + auto peer_id = fidl_helpers::PeerIdFromString(identifier); + if (!peer_id.has_value()) { + bt_log(WARN, + "fidl", + "%s: invalid peer id : %s", + __FUNCTION__, + identifier.c_str()); + callback(fidl_helpers::NewFidlError(ErrorCode::INVALID_ARGUMENTS, + "invalid peer ID")); + return; + } + + auto iter = connections_deprecated_.find(*peer_id); + if (iter != connections_deprecated_.end()) { + if (iter->second) { + bt_log(INFO, + "fidl", + "%s: already connected to %s", + __FUNCTION__, + bt_str(*peer_id)); + callback(fidl_helpers::NewFidlError( + ErrorCode::ALREADY, "Already connected to requested peer")); + } else { + bt_log(INFO, + "fidl", + "%s: connect request pending (peer: %s)", + __FUNCTION__, + bt_str(*peer_id)); + callback(fidl_helpers::NewFidlError(ErrorCode::IN_PROGRESS, + "Connect request pending")); + } + return; + } + + auto self = weak_self_.GetWeakPtr(); + auto conn_cb = [self, + callback = callback.share(), + peer_id = *peer_id, + request = std::move(client_request), + func = __FUNCTION__](auto result) mutable { + if (!self.is_alive()) + return; + + auto iter = self->connections_deprecated_.find(peer_id); + if (iter == self->connections_deprecated_.end()) { + bt_log( + INFO, + "fidl", + "%s: connect request canceled during connection procedure (peer: %s)", + func, + bt_str(peer_id)); + auto error = fidl_helpers::NewFidlError(ErrorCode::FAILED, + "Connect request canceled"); + callback(std::move(error)); + return; + } + + if (result.is_error()) { + bt_log(INFO, + "fidl", + "%s: failed to connect to peer (peer: %s)", + func, + bt_str(peer_id)); + self->connections_deprecated_.erase(peer_id); + callback(fidl_helpers::ResultToFidlDeprecated( + bt::ToResult(result.error_value()), "failed to connect")); + return; + } + + auto conn_ref = std::move(result).value(); + BT_ASSERT(conn_ref); + BT_ASSERT(peer_id == conn_ref->peer_identifier()); + + if (self->gatt_client_servers_.find(peer_id) != + self->gatt_client_servers_.end()) { + bt_log(WARN, + "fidl", + "only 1 gatt.Client FIDL handle allowed per peer (%s)", + bt_str(peer_id)); + // The handle owned by |request| will be closed. + return; + } + + auto server = std::make_unique( + peer_id, self->gatt_, std::move(request)); + server->set_error_handler([self, peer_id](zx_status_t status) { + if (self.is_alive()) { + bt_log(DEBUG, "bt-host", "GATT client disconnected"); + self->gatt_client_servers_.erase(peer_id); + } + }); + self->gatt_client_servers_.emplace(peer_id, std::move(server)); + + conn_ref->set_closed_callback([self, peer_id] { + if (self.is_alive() && + self->connections_deprecated_.erase(peer_id) != 0) { + bt_log(INFO, + "fidl", + "peripheral connection closed (peer: %s)", + bt_str(peer_id)); + self->gatt_client_servers_.erase(peer_id); + self->NotifyPeripheralDisconnected(peer_id); + } + }); + + BT_ASSERT(!iter->second); + iter->second = std::move(conn_ref); + callback(Status()); + }; + + // An entry for the connection must be created here so that a synchronous call + // to conn_cb below does not cause conn_cb to treat the connection as + // cancelled. + connections_deprecated_[*peer_id] = nullptr; + + adapter()->le()->Connect(*peer_id, + std::move(conn_cb), + ConnectionOptionsFromFidl(connection_options)); +} + +void LowEnergyCentralServer::DisconnectPeripheral( + ::std::string identifier, DisconnectPeripheralCallback callback) { + auto peer_id = fidl_helpers::PeerIdFromString(identifier); + if (!peer_id.has_value()) { + bt_log(WARN, + "fidl", + "%s: invalid peer id : %s", + __FUNCTION__, + identifier.c_str()); + callback(fidl_helpers::NewFidlError(ErrorCode::INVALID_ARGUMENTS, + "invalid peer ID")); + return; + } + + auto iter = connections_deprecated_.find(*peer_id); + if (iter == connections_deprecated_.end()) { + bt_log(INFO, + "fidl", + "%s: client not connected to peer (peer: %s)", + __FUNCTION__, + identifier.c_str()); + callback(Status()); + return; + } + + // If a request to this peer is pending then the request will be canceled. + bool was_pending = !iter->second; + connections_deprecated_.erase(iter); + + if (was_pending) { + bt_log(INFO, + "fidl", + "%s: canceling connection request (peer: %s)", + __FUNCTION__, + bt_str(*peer_id)); + } else { + gatt_client_servers_.erase(*peer_id); + NotifyPeripheralDisconnected(*peer_id); + } + + callback(Status()); +} + +void LowEnergyCentralServer::ListenL2cap( + fble::ChannelListenerRegistryListenL2capRequest request, + ListenL2capCallback callback) { + // TODO(fxbug.dev/42178956): Implement ListenL2cap. + fble::ChannelListenerRegistry_ListenL2cap_Result result; + callback(std::move(result.set_err(ZX_ERR_NOT_SUPPORTED))); +} + +void LowEnergyCentralServer::OnScanResult(const bt::gap::Peer& peer) { + auto fidl_device = fidl_helpers::NewLERemoteDevice(peer); + if (!fidl_device) { + return; + } + + if (peer.rssi() != bt::hci_spec::kRSSIInvalid) { + fidl_device->rssi = std::make_unique(); + fidl_device->rssi->value = peer.rssi(); + } + + binding()->events().OnDeviceDiscovered(std::move(*fidl_device)); +} + +void LowEnergyCentralServer::NotifyScanStateChanged(bool scanning) { + binding()->events().OnScanStateChanged(scanning); +} + +void LowEnergyCentralServer::NotifyPeripheralDisconnected(bt::PeerId peer_id) { + binding()->events().OnPeripheralDisconnected(peer_id.ToString()); +} + +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_central_server_test.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_central_server_test.cc new file mode 100644 index 0000000000..13b0b9570e --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_central_server_test.cc @@ -0,0 +1,1400 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_central_server.h" + +#include +#include + +#include + +#include "fuchsia/bluetooth/gatt/cpp/fidl.h" +#include "fuchsia/bluetooth/le/cpp/fidl.h" +#include "lib/fidl/cpp/interface_request.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/adapter_test_fixture.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/fake_adapter_test_fixture.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/helpers.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/measure_tape/hlcpp_measure_tape_for_peer.h" +#include "pw_bluetooth_sapphire/internal/host/sm/types.h" +#include "pw_bluetooth_sapphire/internal/host/testing/fake_controller.h" +#include "pw_bluetooth_sapphire/internal/host/testing/fake_peer.h" + +namespace bthost { +namespace { + +namespace fble = fuchsia::bluetooth::le; +namespace fgatt = fuchsia::bluetooth::gatt; + +const bt::DeviceAddress kTestAddr(bt::DeviceAddress::Type::kLEPublic, + {0x01, 0, 0, 0, 0, 0}); +const size_t kLEMaxNumPackets = 10; +const bt::hci::DataBufferInfo kLEDataBufferInfo( + bt::hci_spec::kMaxACLPayloadSize, kLEMaxNumPackets); + +fble::ScanOptions ScanOptionsWithEmptyFilter() { + fble::ScanOptions options; + fble::Filter filter; + std::vector filters; + filters.emplace_back(std::move(filter)); + options.set_filters(std::move(filters)); + return options; +} + +size_t MaxPeersPerScanResultWatcherChannel(const bt::gap::Peer& peer) { + const size_t kPeerSize = measure_tape::fuchsia::bluetooth::le::Measure( + fidl_helpers::PeerToFidlLe(peer)) + .num_bytes; + const size_t kVectorOverhead = + sizeof(fidl_message_header_t) + sizeof(fidl_vector_t); + const size_t kMaxBytes = ZX_CHANNEL_MAX_MSG_BYTES - kVectorOverhead; + return kMaxBytes / kPeerSize; +} + +using TestingBase = bthost::testing::AdapterTestFixture; + +class LowEnergyCentralServerTest : public TestingBase { + public: + LowEnergyCentralServerTest() = default; + ~LowEnergyCentralServerTest() override = default; + + void SetUp() override { + AdapterTestFixture::SetUp(); + + // Create a LowEnergyCentralServer and bind it to a local client. + fidl::InterfaceHandle handle; + gatt_ = take_gatt(); + server_ = std::make_unique( + adapter(), handle.NewRequest(), gatt_->GetWeakPtr()); + proxy_.Bind(std::move(handle)); + + bt::testing::FakeController::Settings settings; + settings.ApplyLegacyLEConfig(); + test_device()->set_settings(settings); + } + + void TearDown() override { + RunLoopUntilIdle(); + + proxy_ = nullptr; + server_ = nullptr; + gatt_ = nullptr; + + RunLoopUntilIdle(); + AdapterTestFixture::TearDown(); + } + + protected: + // Returns true if the given gatt.Client handle was closed after the event + // loop finishes processing. Returns false if the handle was not closed. + // Ownership of |handle| remains with the caller when this method returns. + bool IsClientHandleClosedAfterLoop( + fidl::InterfaceHandle* handle) { + BT_ASSERT(handle); + + fgatt::ClientPtr proxy; + proxy.Bind(std::move(*handle)); + + bool closed = false; + proxy.set_error_handler([&](zx_status_t s) { + EXPECT_EQ(ZX_ERR_PEER_CLOSED, s); + closed = true; + }); + RunLoopUntilIdle(); + + *handle = proxy.Unbind(); + return closed; + } + + // Destroys the FIDL server. The le.Central proxy will be shut down and + // subsequent calls to `server()` will return nullptr. + void DestroyServer() { server_ = nullptr; } + + LowEnergyCentralServer* server() const { return server_.get(); } + fuchsia::bluetooth::le::Central* central_proxy() const { + return proxy_.get(); + } + + private: + std::unique_ptr server_; + fble::CentralPtr proxy_; + std::unique_ptr gatt_; + + BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(LowEnergyCentralServerTest); +}; + +class LowEnergyCentralServerTestFakeAdapter + : public bt::fidl::testing::FakeAdapterTestFixture { + public: + void SetUp() override { + bt::fidl::testing::FakeAdapterTestFixture::SetUp(); + + // Create a LowEnergyCentralServer and bind it to a local client. + fidl::InterfaceHandle handle; + gatt_ = std::make_unique(pw_dispatcher()); + server_ = std::make_unique( + adapter()->AsWeakPtr(), handle.NewRequest(), gatt_->GetWeakPtr()); + proxy_.Bind(std::move(handle)); + } + + fuchsia::bluetooth::le::Central* central_proxy() const { + return proxy_.get(); + } + + private: + std::unique_ptr server_; + fble::CentralPtr proxy_; + std::unique_ptr gatt_; +}; + +class LowEnergyCentralServerTestFakeAdapterBoolParam + : public LowEnergyCentralServerTestFakeAdapter, + public ::testing::WithParamInterface {}; + +// Tests that connecting to a peripheral with +// LowEnergyConnectionOptions.bondable_mode unset results in a bondable +// connection ref being stored in LowEnergyConnectionManager +TEST_F(LowEnergyCentralServerTest, ConnectDefaultResultsBondableConnectionRef) { + auto* const peer = + adapter()->peer_cache()->NewPeer(kTestAddr, /*connectable=*/true); + ASSERT_TRUE(peer); + + test_device()->AddPeer( + std::make_unique(kTestAddr, pw_dispatcher())); + + fble::ConnectionOptions options; + + fidl::InterfaceHandle gatt_client; + fidl::InterfaceRequest gatt_client_req = + gatt_client.NewRequest(); + + auto status = fidl_helpers::NewFidlError( + fuchsia::bluetooth::ErrorCode::BAD_STATE, "this should change"); + auto callback = [&status](::fuchsia::bluetooth::Status cb_status) { + ASSERT_EQ(cb_status.error, nullptr); + status = std::move(cb_status); + }; + central_proxy()->ConnectPeripheral(peer->identifier().ToString(), + std::move(options), + std::move(gatt_client_req), + callback); + ASSERT_FALSE(server()->FindConnectionForTesting(peer->identifier())); + RunLoopUntilIdle(); + auto conn_ref = server()->FindConnectionForTesting(peer->identifier()); + ASSERT_EQ(status.error, nullptr); + ASSERT_TRUE(conn_ref.has_value()); + ASSERT_TRUE(conn_ref.value()); + ASSERT_EQ(conn_ref.value()->bondable_mode(), bt::sm::BondableMode::Bondable); +} + +// Tests that setting LowEnergyConnectionOptions.bondable_mode to true and +// connecting to a peer in bondable mode results in a bondable connection ref +// being stored in LowEnergyConnectionManager +TEST_F(LowEnergyCentralServerTest, + ConnectBondableResultsBondableConnectionRef) { + auto* const peer = + adapter()->peer_cache()->NewPeer(kTestAddr, /*connectable=*/true); + ASSERT_TRUE(peer); + + test_device()->AddPeer( + std::make_unique(kTestAddr, pw_dispatcher())); + + fble::ConnectionOptions options; + options.set_bondable_mode(true); + + fidl::InterfaceHandle gatt_client; + fidl::InterfaceRequest gatt_client_req = + gatt_client.NewRequest(); + + auto status = fidl_helpers::NewFidlError( + fuchsia::bluetooth::ErrorCode::BAD_STATE, "this should change"); + auto callback = [&status](::fuchsia::bluetooth::Status cb_status) { + ASSERT_EQ(cb_status.error, nullptr); + status = std::move(cb_status); + }; + central_proxy()->ConnectPeripheral(peer->identifier().ToString(), + std::move(options), + std::move(gatt_client_req), + callback); + ASSERT_FALSE(server()->FindConnectionForTesting(peer->identifier())); + RunLoopUntilIdle(); + auto conn_ref = server()->FindConnectionForTesting(peer->identifier()); + ASSERT_EQ(status.error, nullptr); + ASSERT_TRUE(conn_ref.has_value()); + ASSERT_TRUE(conn_ref.value()); + ASSERT_EQ(conn_ref.value()->bondable_mode(), bt::sm::BondableMode::Bondable); +} + +// Tests that setting LowEnergyConnectionOptions.bondable_mode to false and +// connecting to a peer results in a non-bondable connection ref being stored in +// LowEnergyConnectionManager. +TEST_F(LowEnergyCentralServerTest, + ConnectNonBondableResultsNonBondableConnectionRef) { + auto* const peer = + adapter()->peer_cache()->NewPeer(kTestAddr, /*connectable=*/true); + ASSERT_TRUE(peer); + + test_device()->AddPeer( + std::make_unique(kTestAddr, pw_dispatcher())); + + fble::ConnectionOptions options; + options.set_bondable_mode(false); + + fidl::InterfaceHandle gatt_client; + fidl::InterfaceRequest gatt_client_req = + gatt_client.NewRequest(); + + auto status = fidl_helpers::NewFidlError( + fuchsia::bluetooth::ErrorCode::BAD_STATE, "this should change"); + auto callback = [&status](::fuchsia::bluetooth::Status cb_status) { + ASSERT_EQ(cb_status.error, nullptr); + status = std::move(cb_status); + }; + central_proxy()->ConnectPeripheral(peer->identifier().ToString(), + std::move(options), + std::move(gatt_client_req), + callback); + ASSERT_FALSE(server()->FindConnectionForTesting(peer->identifier())); + RunLoopUntilIdle(); + auto conn_ref = server()->FindConnectionForTesting(peer->identifier()); + ASSERT_EQ(status.error, nullptr); + ASSERT_TRUE(conn_ref.has_value()); + ASSERT_TRUE(conn_ref.value()); + ASSERT_EQ(conn_ref.value()->bondable_mode(), + bt::sm::BondableMode::NonBondable); +} + +TEST_F(LowEnergyCentralServerTest, + DisconnectUnconnectedPeripheralReturnsSuccess) { + auto status = fidl_helpers::NewFidlError( + fuchsia::bluetooth::ErrorCode::BAD_STATE, "this should change"); + auto callback = [&status](::fuchsia::bluetooth::Status cb_status) { + status = std::move(cb_status); + }; + central_proxy()->DisconnectPeripheral(bt::PeerId(1).ToString(), + std::move(callback)); + RunLoopUntilIdle(); + EXPECT_EQ(status.error, nullptr); +} + +TEST_F(LowEnergyCentralServerTest, FailedConnectionCleanedUp) { + auto* const peer = + adapter()->peer_cache()->NewPeer(kTestAddr, /*connectable=*/true); + ASSERT_TRUE(peer); + + test_device()->AddPeer( + std::make_unique(kTestAddr, pw_dispatcher())); + + fble::ConnectionOptions options; + + fidl::InterfaceHandle gatt_client; + fidl::InterfaceRequest gatt_client_req = + gatt_client.NewRequest(); + + fuchsia::bluetooth::Status status; + auto callback = [&status](::fuchsia::bluetooth::Status cb_status) { + status = std::move(cb_status); + }; + + test_device()->SetDefaultCommandStatus( + bt::hci_spec::kReadRemoteVersionInfo, + pw::bluetooth::emboss::StatusCode::CONNECTION_LIMIT_EXCEEDED); + + ASSERT_FALSE( + server()->FindConnectionForTesting(peer->identifier()).has_value()); + central_proxy()->ConnectPeripheral(peer->identifier().ToString(), + std::move(options), + std::move(gatt_client_req), + callback); + RunLoopUntilIdle(); + auto conn = server()->FindConnectionForTesting(peer->identifier()); + EXPECT_NE(status.error, nullptr); + EXPECT_FALSE(conn.has_value()); +} + +TEST_F(LowEnergyCentralServerTest, ConnectPeripheralAlreadyConnectedInLecm) { + auto* const peer = + adapter()->peer_cache()->NewPeer(kTestAddr, /*connectable=*/true); + test_device()->AddPeer( + std::make_unique(kTestAddr, pw_dispatcher())); + + std::unique_ptr le_conn; + adapter()->le()->Connect( + peer->identifier(), + [&le_conn](auto result) { + ASSERT_EQ(fit::ok(), result); + le_conn = std::move(result).value(); + }, + bt::gap::LowEnergyConnectionOptions()); + RunLoopUntilIdle(); + ASSERT_TRUE(le_conn); + ASSERT_FALSE( + server()->FindConnectionForTesting(peer->identifier()).has_value()); + + fuchsia::bluetooth::Status status; + auto callback = [&status](::fuchsia::bluetooth::Status cb_status) { + status = std::move(cb_status); + }; + + fble::ConnectionOptions options; + fidl::InterfaceHandle gatt_client; + fidl::InterfaceRequest gatt_client_req = + gatt_client.NewRequest(); + central_proxy()->ConnectPeripheral(peer->identifier().ToString(), + std::move(options), + std::move(gatt_client_req), + callback); + RunLoopUntilIdle(); + EXPECT_EQ(status.error, nullptr); + auto server_conn = server()->FindConnectionForTesting(peer->identifier()); + ASSERT_TRUE(server_conn.has_value()); + EXPECT_NE(server_conn.value(), nullptr); +} + +TEST_F(LowEnergyCentralServerTest, ConnectPeripheralUnknownPeer) { + fuchsia::bluetooth::Status status; + auto callback = [&status](::fuchsia::bluetooth::Status cb_status) { + status = std::move(cb_status); + }; + + const bt::PeerId peer_id(1); + + fble::ConnectionOptions options; + fidl::InterfaceHandle gatt_client; + fidl::InterfaceRequest gatt_client_req = + gatt_client.NewRequest(); + central_proxy()->ConnectPeripheral(peer_id.ToString(), + std::move(options), + std::move(gatt_client_req), + callback); + RunLoopUntilIdle(); + ASSERT_TRUE(status.error); + EXPECT_EQ(status.error->error_code, fuchsia::bluetooth::ErrorCode::NOT_FOUND); + auto server_conn = server()->FindConnectionForTesting(peer_id); + EXPECT_FALSE(server_conn.has_value()); +} + +TEST_F(LowEnergyCentralServerTest, + DisconnectPeripheralClosesCorrectGattHandle) { + const bt::DeviceAddress kAddr1 = kTestAddr; + const bt::DeviceAddress kAddr2(bt::DeviceAddress::Type::kLEPublic, + {2, 0, 0, 0, 0, 0}); + auto* const peer1 = + adapter()->peer_cache()->NewPeer(kAddr1, /*connectable=*/true); + auto* const peer2 = + adapter()->peer_cache()->NewPeer(kAddr2, /*connectable=*/true); + + test_device()->AddPeer( + std::make_unique(kAddr1, pw_dispatcher())); + test_device()->AddPeer( + std::make_unique(kAddr2, pw_dispatcher())); + + // Establish two connections. + fidl::InterfaceHandle handle1, handle2; + central_proxy()->ConnectPeripheral(peer1->identifier().ToString(), + fble::ConnectionOptions{}, + handle1.NewRequest(), + [](auto) {}); + central_proxy()->ConnectPeripheral(peer2->identifier().ToString(), + fble::ConnectionOptions{}, + handle2.NewRequest(), + [](auto) {}); + RunLoopUntilIdle(); + ASSERT_TRUE(server()->FindConnectionForTesting(peer1->identifier())); + ASSERT_TRUE(server()->FindConnectionForTesting(peer2->identifier())); + EXPECT_FALSE(IsClientHandleClosedAfterLoop(&handle1)); + EXPECT_FALSE(IsClientHandleClosedAfterLoop(&handle2)); + + // Disconnect peer1. Only its gatt.Client handle should close. + central_proxy()->DisconnectPeripheral(peer1->identifier().ToString(), + [](auto) {}); + EXPECT_TRUE(IsClientHandleClosedAfterLoop(&handle1)); + EXPECT_FALSE(IsClientHandleClosedAfterLoop(&handle2)); + + // Disconnect peer2. Its handle should close now. + central_proxy()->DisconnectPeripheral(peer2->identifier().ToString(), + [](auto) {}); + EXPECT_TRUE(IsClientHandleClosedAfterLoop(&handle2)); +} + +TEST_F(LowEnergyCentralServerTest, PeerDisconnectClosesCorrectHandle) { + const bt::DeviceAddress kAddr1 = kTestAddr; + const bt::DeviceAddress kAddr2(bt::DeviceAddress::Type::kLEPublic, + {2, 0, 0, 0, 0, 0}); + auto* const peer1 = + adapter()->peer_cache()->NewPeer(kAddr1, /*connectable=*/true); + auto* const peer2 = + adapter()->peer_cache()->NewPeer(kAddr2, /*connectable=*/true); + + test_device()->AddPeer( + std::make_unique(kAddr1, pw_dispatcher())); + test_device()->AddPeer( + std::make_unique(kAddr2, pw_dispatcher())); + + // Establish two connections. + fidl::InterfaceHandle handle1, handle2; + central_proxy()->ConnectPeripheral(peer1->identifier().ToString(), + fble::ConnectionOptions{}, + handle1.NewRequest(), + [](auto) {}); + central_proxy()->ConnectPeripheral(peer2->identifier().ToString(), + fble::ConnectionOptions{}, + handle2.NewRequest(), + [](auto) {}); + RunLoopUntilIdle(); + ASSERT_TRUE(server()->FindConnectionForTesting(peer1->identifier())); + ASSERT_TRUE(server()->FindConnectionForTesting(peer2->identifier())); + EXPECT_FALSE(IsClientHandleClosedAfterLoop(&handle1)); + EXPECT_FALSE(IsClientHandleClosedAfterLoop(&handle2)); + + // Disconnect peer1. Only its gatt.Client handle should close. + test_device()->Disconnect(kAddr1); + EXPECT_TRUE(IsClientHandleClosedAfterLoop(&handle1)); + EXPECT_FALSE(IsClientHandleClosedAfterLoop(&handle2)); + + // Disconnect peer2. Its handle should close now. + test_device()->Disconnect(kAddr2); + EXPECT_TRUE(IsClientHandleClosedAfterLoop(&handle2)); +} + +TEST_F(LowEnergyCentralServerTest, + ClosingCentralHandleClosesAssociatedGattClientHandles) { + const bt::DeviceAddress kAddr1 = kTestAddr; + const bt::DeviceAddress kAddr2(bt::DeviceAddress::Type::kLEPublic, + {2, 0, 0, 0, 0, 0}); + auto* const peer1 = + adapter()->peer_cache()->NewPeer(kAddr1, /*connectable=*/true); + auto* const peer2 = + adapter()->peer_cache()->NewPeer(kAddr2, /*connectable=*/true); + + test_device()->AddPeer( + std::make_unique(kAddr1, pw_dispatcher())); + test_device()->AddPeer( + std::make_unique(kAddr2, pw_dispatcher())); + + // Establish two connections. + fidl::InterfaceHandle handle1, handle2; + central_proxy()->ConnectPeripheral(peer1->identifier().ToString(), + fble::ConnectionOptions{}, + handle1.NewRequest(), + [](auto) {}); + central_proxy()->ConnectPeripheral(peer2->identifier().ToString(), + fble::ConnectionOptions{}, + handle2.NewRequest(), + [](auto) {}); + RunLoopUntilIdle(); + ASSERT_TRUE(server()->FindConnectionForTesting(peer1->identifier())); + ASSERT_TRUE(server()->FindConnectionForTesting(peer2->identifier())); + EXPECT_FALSE(IsClientHandleClosedAfterLoop(&handle1)); + EXPECT_FALSE(IsClientHandleClosedAfterLoop(&handle2)); + + DestroyServer(); + EXPECT_TRUE(IsClientHandleClosedAfterLoop(&handle1)); + EXPECT_TRUE(IsClientHandleClosedAfterLoop(&handle2)); +} + +TEST_F(LowEnergyCentralServerTest, ScanWithEmptyScanOptionsFails) { + fidl::InterfaceHandle result_watcher_handle; + auto result_watcher_server = result_watcher_handle.NewRequest(); + + auto result_watcher_client = result_watcher_handle.Bind(); + std::optional result_watcher_epitaph; + result_watcher_client.set_error_handler( + [&](zx_status_t epitaph) { result_watcher_epitaph = epitaph; }); + + bool scan_stopped = false; + central_proxy()->Scan(fble::ScanOptions(), + std::move(result_watcher_server), + [&]() { scan_stopped = true; }); + RunLoopUntilIdle(); + EXPECT_TRUE(scan_stopped); + ASSERT_TRUE(result_watcher_epitaph.has_value()); + EXPECT_EQ(result_watcher_epitaph.value(), ZX_ERR_INVALID_ARGS); +} + +TEST_F(LowEnergyCentralServerTest, ScanWithNoFiltersFails) { + fble::ScanOptions options; + std::vector filters; + options.set_filters(std::move(filters)); + + fidl::InterfaceHandle result_watcher_handle; + auto result_watcher_server = result_watcher_handle.NewRequest(); + + auto result_watcher_client = result_watcher_handle.Bind(); + std::optional result_watcher_epitaph; + result_watcher_client.set_error_handler( + [&](zx_status_t epitaph) { result_watcher_epitaph = epitaph; }); + + bool scan_stopped = false; + central_proxy()->Scan(std::move(options), + std::move(result_watcher_server), + [&]() { scan_stopped = true; }); + RunLoopUntilIdle(); + EXPECT_TRUE(scan_stopped); + ASSERT_TRUE(result_watcher_epitaph.has_value()); + EXPECT_EQ(result_watcher_epitaph.value(), ZX_ERR_INVALID_ARGS); +} + +TEST_F(LowEnergyCentralServerTest, ScanReceivesPeerPreviouslyAddedToPeerCache) { + bt::gap::Peer* peer = + adapter()->peer_cache()->NewPeer(kTestAddr, /*connectable=*/false); + + fidl::InterfaceHandle result_watcher_handle; + auto result_watcher_server = result_watcher_handle.NewRequest(); + auto result_watcher_client = result_watcher_handle.Bind(); + std::optional epitaph; + result_watcher_client.set_error_handler( + [&](zx_status_t cb_epitaph) { epitaph = cb_epitaph; }); + + bool scan_stopped = false; + central_proxy()->Scan(ScanOptionsWithEmptyFilter(), + std::move(result_watcher_server), + [&]() { scan_stopped = true; }); + + RunLoopUntilIdle(); + EXPECT_FALSE(scan_stopped); + EXPECT_FALSE(epitaph); + + std::optional> peers; + result_watcher_client->Watch( + [&](std::vector updated) { peers = std::move(updated); }); + RunLoopUntilIdle(); + ASSERT_TRUE(peers.has_value()); + ASSERT_EQ(peers->size(), 1u); + ASSERT_TRUE(peers->front().has_id()); + EXPECT_EQ(peers->front().id().value, peer->identifier().value()); + + result_watcher_client.Unbind(); + RunLoopUntilIdle(); + EXPECT_TRUE(scan_stopped); +} + +TEST_F(LowEnergyCentralServerTest, + ScanReceivesPeerAddedToPeerCacheAfterScanStart) { + fidl::InterfaceHandle result_watcher_handle; + auto result_watcher_server = result_watcher_handle.NewRequest(); + auto result_watcher_client = result_watcher_handle.Bind(); + std::optional epitaph; + result_watcher_client.set_error_handler( + [&](zx_status_t cb_epitaph) { epitaph = cb_epitaph; }); + + bool scan_stopped = false; + central_proxy()->Scan(ScanOptionsWithEmptyFilter(), + std::move(result_watcher_server), + [&]() { scan_stopped = true; }); + + RunLoopUntilIdle(); + EXPECT_FALSE(scan_stopped); + EXPECT_FALSE(epitaph); + + std::optional> peers; + result_watcher_client->Watch( + [&](std::vector updated) { peers = std::move(updated); }); + RunLoopUntilIdle(); + ASSERT_FALSE(peers.has_value()); + + bt::gap::Peer* peer = + adapter()->peer_cache()->NewPeer(kTestAddr, /*connectable=*/false); + RunLoopUntilIdle(); + ASSERT_TRUE(peers.has_value()); + ASSERT_EQ(peers->size(), 1u); + ASSERT_TRUE(peers->front().has_id()); + EXPECT_EQ(peers->front().id().value, peer->identifier().value()); + + result_watcher_client.Unbind(); + RunLoopUntilIdle(); + EXPECT_TRUE(scan_stopped); +} + +TEST_F(LowEnergyCentralServerTest, + PeerAddedToPeerCacheAfterScanEndDoesNotCrash) { + fidl::InterfaceHandle result_watcher_handle; + auto result_watcher_server = result_watcher_handle.NewRequest(); + auto result_watcher_client = result_watcher_handle.Bind(); + std::optional epitaph; + result_watcher_client.set_error_handler( + [&](zx_status_t cb_epitaph) { epitaph = cb_epitaph; }); + + bool scan_stopped = false; + central_proxy()->Scan(ScanOptionsWithEmptyFilter(), + std::move(result_watcher_server), + [&]() { scan_stopped = true; }); + + RunLoopUntilIdle(); + EXPECT_FALSE(scan_stopped); + EXPECT_FALSE(epitaph); + + RunLoopUntilIdle(); + + result_watcher_client.Unbind(); + RunLoopUntilIdle(); + EXPECT_TRUE(scan_stopped); + + adapter()->peer_cache()->NewPeer(kTestAddr, /*connectable=*/false); + RunLoopUntilIdle(); +} + +TEST_F(LowEnergyCentralServerTest, ConcurrentScansFail) { + fidl::InterfaceHandle result_watcher_handle_0; + auto result_watcher_server_0 = result_watcher_handle_0.NewRequest(); + auto result_watcher_client_0 = result_watcher_handle_0.Bind(); + bool scan_stopped_0 = false; + central_proxy()->Scan(ScanOptionsWithEmptyFilter(), + std::move(result_watcher_server_0), + [&]() { scan_stopped_0 = true; }); + RunLoopUntilIdle(); + EXPECT_FALSE(scan_stopped_0); + + fidl::InterfaceHandle result_watcher_handle_1; + auto result_watcher_server_1 = result_watcher_handle_1.NewRequest(); + auto result_watcher_client_1 = result_watcher_handle_1.Bind(); + std::optional epitaph_1; + result_watcher_client_1.set_error_handler( + [&](zx_status_t cb_epitaph) { epitaph_1 = cb_epitaph; }); + + bool scan_stopped_1 = false; + central_proxy()->Scan(ScanOptionsWithEmptyFilter(), + std::move(result_watcher_server_1), + [&]() { scan_stopped_1 = true; }); + RunLoopUntilIdle(); + EXPECT_FALSE(scan_stopped_0); + EXPECT_TRUE(scan_stopped_1); + ASSERT_TRUE(epitaph_1); + EXPECT_EQ(epitaph_1.value(), ZX_ERR_ALREADY_EXISTS); + + result_watcher_client_0.Unbind(); + RunLoopUntilIdle(); + EXPECT_TRUE(scan_stopped_0); +} + +TEST_F(LowEnergyCentralServerTest, SequentialScansSucceed) { + fidl::InterfaceHandle result_watcher_handle_0; + auto result_watcher_server_0 = result_watcher_handle_0.NewRequest(); + auto result_watcher_client_0 = result_watcher_handle_0.Bind(); + bool scan_stopped_0 = false; + central_proxy()->Scan(ScanOptionsWithEmptyFilter(), + std::move(result_watcher_server_0), + [&]() { scan_stopped_0 = true; }); + RunLoopUntilIdle(); + EXPECT_FALSE(scan_stopped_0); + + result_watcher_client_0.Unbind(); + RunLoopUntilIdle(); + EXPECT_TRUE(scan_stopped_0); + + fidl::InterfaceHandle result_watcher_handle_1; + auto result_watcher_server_1 = result_watcher_handle_1.NewRequest(); + auto result_watcher_client_1 = result_watcher_handle_1.Bind(); + bool scan_stopped_1 = false; + central_proxy()->Scan(ScanOptionsWithEmptyFilter(), + std::move(result_watcher_server_1), + [&]() { scan_stopped_1 = true; }); + RunLoopUntilIdle(); + EXPECT_FALSE(scan_stopped_1); + + result_watcher_client_1.Unbind(); + RunLoopUntilIdle(); + EXPECT_TRUE(scan_stopped_1); +} + +TEST_F(LowEnergyCentralServerTest, IgnorePeersThatDoNotMatchFilter) { + fble::ScanOptions options; + fble::Filter filter; + filter.set_connectable(true); + std::vector filters; + filters.emplace_back(std::move(filter)); + options.set_filters(std::move(filters)); + + fidl::InterfaceHandle result_watcher_handle; + auto result_watcher_server = result_watcher_handle.NewRequest(); + auto result_watcher_client = result_watcher_handle.Bind(); + std::optional epitaph; + result_watcher_client.set_error_handler( + [&](zx_status_t cb_epitaph) { epitaph = cb_epitaph; }); + + bool scan_stopped = false; + central_proxy()->Scan(std::move(options), + std::move(result_watcher_server), + [&]() { scan_stopped = true; }); + + RunLoopUntilIdle(); + EXPECT_FALSE(scan_stopped); + EXPECT_FALSE(epitaph); + + std::optional> peers; + result_watcher_client->Watch( + [&](std::vector updated) { peers = std::move(updated); }); + RunLoopUntilIdle(); + ASSERT_FALSE(peers.has_value()); + + // Peer is not LE + adapter()->peer_cache()->NewPeer( + bt::DeviceAddress(bt::DeviceAddress::Type::kBREDR, {1, 0, 0, 0, 0, 0}), + /*connectable=*/true); + // Peer is not connectable + adapter()->peer_cache()->NewPeer( + bt::DeviceAddress(bt::DeviceAddress::Type::kLEPublic, {2, 0, 0, 0, 0, 0}), + /*connectable=*/false); + + RunLoopUntilIdle(); + EXPECT_FALSE(peers.has_value()); + + result_watcher_client.Unbind(); + RunLoopUntilIdle(); + EXPECT_TRUE(scan_stopped); +} + +TEST_F(LowEnergyCentralServerTest, + IgnorePeerThatDoesNotMatchServiceDataFilter) { + fble::ScanOptions options; + fble::Filter filter; + const bt::UUID kServiceUuid(static_cast(2)); + filter.set_connectable(true); + filter.set_service_data_uuid(fuchsia::bluetooth::Uuid{kServiceUuid.value()}); + std::vector filters; + filters.emplace_back(std::move(filter)); + options.set_filters(std::move(filters)); + + fidl::InterfaceHandle result_watcher_handle; + auto result_watcher_server = result_watcher_handle.NewRequest(); + auto result_watcher_client = result_watcher_handle.Bind(); + std::optional epitaph; + result_watcher_client.set_error_handler( + [&](zx_status_t cb_epitaph) { epitaph = cb_epitaph; }); + + bool scan_stopped = false; + central_proxy()->Scan(std::move(options), + std::move(result_watcher_server), + [&]() { scan_stopped = true; }); + + RunLoopUntilIdle(); + EXPECT_FALSE(scan_stopped); + EXPECT_FALSE(epitaph); + + std::optional> peers; + result_watcher_client->Watch( + [&](std::vector updated) { peers = std::move(updated); }); + RunLoopUntilIdle(); + ASSERT_FALSE(peers.has_value()); + + // Peer is connectable but doesn't have any service data. + adapter()->peer_cache()->NewPeer( + bt::DeviceAddress(bt::DeviceAddress::Type::kLEPublic, {2, 0, 0, 0, 0, 0}), + /*connectable=*/true); + + RunLoopUntilIdle(); + EXPECT_FALSE(peers.has_value()); + + result_watcher_client.Unbind(); + RunLoopUntilIdle(); + EXPECT_TRUE(scan_stopped); +} + +TEST_F(LowEnergyCentralServerTest, + DoNotNotifyResultWatcherWithPeerThatWasRemovedFromPeerCacheWhileQueued) { + bt::gap::Peer* peer = + adapter()->peer_cache()->NewPeer(kTestAddr, /*connectable=*/false); + + fidl::InterfaceHandle result_watcher_handle; + auto result_watcher_server = result_watcher_handle.NewRequest(); + auto result_watcher_client = result_watcher_handle.Bind(); + std::optional epitaph; + result_watcher_client.set_error_handler( + [&](zx_status_t cb_epitaph) { epitaph = cb_epitaph; }); + + bool scan_stopped = false; + central_proxy()->Scan(ScanOptionsWithEmptyFilter(), + std::move(result_watcher_server), + [&]() { scan_stopped = true; }); + + RunLoopUntilIdle(); + EXPECT_FALSE(scan_stopped); + EXPECT_FALSE(epitaph); + + // Peer is in ScanResultWatcher queue. Remove it from PeerCache before Watch() + // is called. + EXPECT_TRUE( + adapter()->peer_cache()->RemoveDisconnectedPeer(peer->identifier())); + + std::optional> peers; + result_watcher_client->Watch( + [&](std::vector updated) { peers = std::move(updated); }); + RunLoopUntilIdle(); + EXPECT_FALSE(peers.has_value()); + + result_watcher_client.Unbind(); + RunLoopUntilIdle(); + EXPECT_TRUE(scan_stopped); +} + +TEST_F(LowEnergyCentralServerTest, MaxQueuedScanResultWatcherPeers) { + // Create smallest possible peer + bt::gap::Peer* peer_0 = adapter()->peer_cache()->NewPeer( + bt::DeviceAddress(bt::DeviceAddress::Type::kLEPublic, {0, 0, 0, 0, 0, 0}), + /*connectable=*/false); + const size_t kMaxPeersPerChannel = + MaxPeersPerScanResultWatcherChannel(*peer_0); + ASSERT_GT(kMaxPeersPerChannel, + LowEnergyCentralServer::kMaxPendingScanResultWatcherPeers); + + // Queue 1 more peer than queue size limit. + ASSERT_LE(LowEnergyCentralServer::kMaxPendingScanResultWatcherPeers, + std::numeric_limits::max()); + for (size_t i = 1; + i < LowEnergyCentralServer::kMaxPendingScanResultWatcherPeers + 1; + i++) { + SCOPED_TRACE(i); + ASSERT_TRUE(adapter()->peer_cache()->NewPeer( + bt::DeviceAddress(bt::DeviceAddress::Type::kLEPublic, + {static_cast(i), 0, 0, 0, 0, 0}), + /*connectable=*/false)); + } + + fidl::InterfaceHandle result_watcher_handle; + auto result_watcher_server = result_watcher_handle.NewRequest(); + auto result_watcher_client = result_watcher_handle.Bind(); + std::optional epitaph; + result_watcher_client.set_error_handler( + [&](zx_status_t cb_epitaph) { epitaph = cb_epitaph; }); + + bool scan_stopped = false; + central_proxy()->Scan(ScanOptionsWithEmptyFilter(), + std::move(result_watcher_server), + [&]() { scan_stopped = true; }); + + RunLoopUntilIdle(); + EXPECT_FALSE(scan_stopped); + EXPECT_FALSE(epitaph); + + std::optional> peers; + result_watcher_client->Watch( + [&](std::vector updated) { peers = std::move(updated); }); + RunLoopUntilIdle(); + ASSERT_TRUE(peers.has_value()); + EXPECT_EQ(peers->size(), + LowEnergyCentralServer::kMaxPendingScanResultWatcherPeers); + peers.reset(); + + // Additional calls to Watch should hang + result_watcher_client->Watch( + [&](std::vector updated) { peers = std::move(updated); }); + RunLoopUntilIdle(); + EXPECT_FALSE(peers.has_value()); + + result_watcher_client.Unbind(); + RunLoopUntilIdle(); + EXPECT_TRUE(scan_stopped); +} + +TEST_F(LowEnergyCentralServerTest, ScanResultWatcherMeasureTape) { + // Create a very large Peer + bt::gap::Peer* peer_0 = adapter()->peer_cache()->NewPeer( + bt::DeviceAddress(bt::DeviceAddress::Type::kLEPublic, {0, 0, 0, 0, 0, 0}), + /*connectable=*/true); + bt::AdvertisingData adv_data; + for (int i = 0; i < 100; i++) { + SCOPED_TRACE(i); + ASSERT_TRUE(adv_data.AddUri( + bt_lib_cpp_string::StringPrintf("uri:a-really-long-uri-%d", i))); + } + adv_data.CalculateBlockSize(); + bt::DynamicByteBuffer adv_buffer(adv_data.CalculateBlockSize()); + adv_data.WriteBlock(&adv_buffer, std::nullopt); + peer_0->MutLe().SetAdvertisingData( + /*rssi=*/0, adv_buffer, pw::chrono::SystemClock::time_point()); + + const size_t kMaxPeersPerChannel = + MaxPeersPerScanResultWatcherChannel(*peer_0); + + // Queue 1 more peer than will fit in the channel. + // Start at i = 1 because peer_0 was created above. + ASSERT_LE(kMaxPeersPerChannel, std::numeric_limits::max()); + ASSERT_GT(LowEnergyCentralServer::kMaxPendingScanResultWatcherPeers, + kMaxPeersPerChannel); + for (size_t i = 1; i < kMaxPeersPerChannel + 1; i++) { + SCOPED_TRACE(i); + bt::gap::Peer* peer = adapter()->peer_cache()->NewPeer( + bt::DeviceAddress(bt::DeviceAddress::Type::kLEPublic, + {static_cast(i), 0, 0, 0, 0, 0}), + /*connectable=*/false); + ASSERT_TRUE(peer); + peer->MutLe().SetAdvertisingData( + /*rssi=*/0, adv_buffer, pw::chrono::SystemClock::time_point()); + } + + fidl::InterfaceHandle result_watcher_handle; + auto result_watcher_server = result_watcher_handle.NewRequest(); + auto result_watcher_client = result_watcher_handle.Bind(); + std::optional epitaph; + result_watcher_client.set_error_handler( + [&](zx_status_t cb_epitaph) { epitaph = cb_epitaph; }); + + bool scan_stopped = false; + central_proxy()->Scan(ScanOptionsWithEmptyFilter(), + std::move(result_watcher_server), + [&]() { scan_stopped = true; }); + + RunLoopUntilIdle(); + EXPECT_FALSE(scan_stopped); + EXPECT_FALSE(epitaph); + + std::optional> peers; + result_watcher_client->Watch( + [&](std::vector updated) { peers = std::move(updated); }); + RunLoopUntilIdle(); + ASSERT_TRUE(peers.has_value()); + EXPECT_EQ(peers->size(), kMaxPeersPerChannel); + peers.reset(); + + // Additional call to Watch should return the 1 peer that exceeded the channel + // size limit. + result_watcher_client->Watch( + [&](std::vector updated) { peers = std::move(updated); }); + RunLoopUntilIdle(); + ASSERT_TRUE(peers.has_value()); + EXPECT_EQ(peers->size(), 1u); + + result_watcher_client.Unbind(); + RunLoopUntilIdle(); + EXPECT_TRUE(scan_stopped); +} + +TEST_F(LowEnergyCentralServerTest, ScanResultsMatchPeerFromAnyFilter) { + const int8_t kRssi = 0; + // Peer that matches neither filter + adapter()->peer_cache()->NewPeer( + bt::DeviceAddress(bt::DeviceAddress::Type::kLEPublic, {0, 0, 0, 0, 0, 0}), + /*connectable=*/false); + + // Peer that matches filter_0 + bt::gap::Peer* peer_0 = adapter()->peer_cache()->NewPeer( + bt::DeviceAddress(bt::DeviceAddress::Type::kLEPublic, {1, 0, 0, 0, 0, 0}), + /*connectable=*/true); + ASSERT_TRUE(peer_0); + const auto kAdvData0 = + bt::StaticByteBuffer(0x02, // Length + 0x09, // AD type: Complete Local Name + '0'); + peer_0->MutLe().SetAdvertisingData( + kRssi, kAdvData0, pw::chrono::SystemClock::time_point()); + // Peer that matches filter_1 + bt::gap::Peer* peer_1 = adapter()->peer_cache()->NewPeer( + bt::DeviceAddress(bt::DeviceAddress::Type::kLEPublic, {2, 0, 0, 0, 0, 0}), + /*connectable=*/false); + ASSERT_TRUE(peer_1); + const auto kAdvData1 = + bt::StaticByteBuffer(0x02, // Length + 0x09, // AD type: Complete Local Name + '1'); + peer_1->MutLe().SetAdvertisingData( + kRssi, kAdvData1, pw::chrono::SystemClock::time_point()); + + fble::ScanOptions options; + fble::Filter filter_0; + filter_0.set_connectable(true); + filter_0.set_name("0"); + fble::Filter filter_1; + filter_1.set_connectable(false); + filter_1.set_name("1"); + std::vector filters; + filters.emplace_back(std::move(filter_0)); + filters.emplace_back(std::move(filter_1)); + options.set_filters(std::move(filters)); + + fidl::InterfaceHandle result_watcher_handle; + auto result_watcher_server = result_watcher_handle.NewRequest(); + auto result_watcher_client = result_watcher_handle.Bind(); + std::optional epitaph; + result_watcher_client.set_error_handler( + [&](zx_status_t cb_epitaph) { epitaph = cb_epitaph; }); + + bool scan_stopped = false; + central_proxy()->Scan(std::move(options), + std::move(result_watcher_server), + [&]() { scan_stopped = true; }); + + RunLoopUntilIdle(); + EXPECT_FALSE(scan_stopped); + EXPECT_FALSE(epitaph); + + std::optional> peers; + result_watcher_client->Watch([&](std::vector updated) { + peers = std::vector(); + std::transform(updated.begin(), + updated.end(), + std::back_inserter(*peers), + [](auto& p) { return bt::PeerId(p.id().value); }); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(peers.has_value()); + EXPECT_THAT( + peers.value(), + ::testing::UnorderedElementsAre(::testing::Eq(peer_0->identifier()), + ::testing::Eq(peer_1->identifier()))); + result_watcher_client.Unbind(); + RunLoopUntilIdle(); + EXPECT_TRUE(scan_stopped); +} + +TEST_F(LowEnergyCentralServerTest, + DiscoveryStartJustAfterScanCanceledShouldBeIgnored) { + // Pause discovery so that we can cancel scanning before resuming discovery. + fit::closure start_discovery; + test_device()->pause_responses_for_opcode( + bt::hci_spec::kLESetScanEnable, [&](auto resume_set_scan_enable) { + start_discovery = std::move(resume_set_scan_enable); + }); + + fidl::InterfaceHandle result_watcher_handle; + auto result_watcher_server = result_watcher_handle.NewRequest(); + auto result_watcher_client = result_watcher_handle.Bind(); + + bool scan_stopped = false; + central_proxy()->Scan(ScanOptionsWithEmptyFilter(), + std::move(result_watcher_server), + [&]() { scan_stopped = true; }); + + RunLoopUntilIdle(); + EXPECT_FALSE(scan_stopped); + EXPECT_TRUE(start_discovery); + + result_watcher_client.Unbind(); + RunLoopUntilIdle(); + EXPECT_TRUE(scan_stopped); + + start_discovery(); + RunLoopUntilIdle(); +} + +TEST_F(LowEnergyCentralServerTest, ScanFailsToStart) { + test_device()->SetDefaultResponseStatus( + bt::hci_spec::kLESetScanEnable, + pw::bluetooth::emboss::StatusCode::CONTROLLER_BUSY); + + fidl::InterfaceHandle result_watcher_handle; + auto result_watcher_server = result_watcher_handle.NewRequest(); + auto result_watcher_client = result_watcher_handle.Bind(); + std::optional epitaph; + result_watcher_client.set_error_handler( + [&](zx_status_t cb_epitaph) { epitaph = cb_epitaph; }); + + bool scan_stopped = false; + central_proxy()->Scan(ScanOptionsWithEmptyFilter(), + std::move(result_watcher_server), + [&]() { scan_stopped = true; }); + + RunLoopUntilIdle(); + EXPECT_TRUE(scan_stopped); + ASSERT_TRUE(epitaph); + EXPECT_EQ(*epitaph, ZX_ERR_INTERNAL); +} + +TEST_F(LowEnergyCentralServerTest, ScanSessionErrorCancelsScan) { + zx::duration kTestScanPeriod = zx::sec(1); + pw::chrono::SystemClock::duration kPwTestScanPeriod = std::chrono::seconds(1); + adapter()->le()->set_scan_period_for_testing(kPwTestScanPeriod); + std::vector scan_states; + test_device()->set_scan_state_callback([&](bool enabled) { + scan_states.push_back(enabled); + // Wait for 2 state transitions: -> enabled -> disabled. + // Then disable restarting scanning, so that an error is sent to sessions. + if (scan_states.size() == 2u) { + EXPECT_FALSE(enabled); + test_device()->SetDefaultResponseStatus( + bt::hci_spec::kLESetScanEnable, + pw::bluetooth::emboss::StatusCode::COMMAND_DISALLOWED); + } + }); + + fidl::InterfaceHandle result_watcher_handle; + auto result_watcher_server = result_watcher_handle.NewRequest(); + auto result_watcher_client = result_watcher_handle.Bind(); + std::optional epitaph; + result_watcher_client.set_error_handler( + [&](zx_status_t cb_epitaph) { epitaph = cb_epitaph; }); + + bool scan_stopped = false; + central_proxy()->Scan(ScanOptionsWithEmptyFilter(), + std::move(result_watcher_server), + [&]() { scan_stopped = true; }); + RunLoopFor(kTestScanPeriod); + EXPECT_TRUE(scan_stopped); + ASSERT_TRUE(epitaph); + EXPECT_EQ(*epitaph, ZX_ERR_INTERNAL); +} + +TEST_F(LowEnergyCentralServerTest, + ScanResultWatcherWatchCalledBeforePreviousWatchReceivedResponse) { + fidl::InterfaceHandle result_watcher_handle; + auto result_watcher_server = result_watcher_handle.NewRequest(); + auto result_watcher_client = result_watcher_handle.Bind(); + std::optional epitaph; + result_watcher_client.set_error_handler( + [&](zx_status_t cb_epitaph) { epitaph = cb_epitaph; }); + + bool scan_stopped = false; + central_proxy()->Scan(ScanOptionsWithEmptyFilter(), + std::move(result_watcher_server), + [&]() { scan_stopped = true; }); + bool watch_response_0 = false; + result_watcher_client->Watch([&](auto) { watch_response_0 = true; }); + bool watch_response_1 = false; + result_watcher_client->Watch([&](auto) { watch_response_1 = true; }); + RunLoopUntilIdle(); + EXPECT_FALSE(watch_response_0); + EXPECT_FALSE(watch_response_1); + EXPECT_TRUE(scan_stopped); + ASSERT_TRUE(epitaph); + EXPECT_EQ(*epitaph, ZX_ERR_CANCELED); +} + +TEST_F(LowEnergyCentralServerTest, ConnectToAlreadyConnectedPeerFails) { + auto* const peer = + adapter()->peer_cache()->NewPeer(kTestAddr, /*connectable=*/true); + ASSERT_TRUE(peer); + test_device()->AddPeer( + std::make_unique(kTestAddr, pw_dispatcher())); + + fble::ConnectionPtr conn_client_0; + std::optional epitaph_0; + conn_client_0.set_error_handler( + [&](zx_status_t cb_epitaph) { epitaph_0 = cb_epitaph; }); + + const fuchsia::bluetooth::PeerId peer_id{peer->identifier().value()}; + fble::ConnectionOptions options_0; + central_proxy()->Connect( + peer_id, std::move(options_0), conn_client_0.NewRequest()); + RunLoopUntilIdle(); + EXPECT_FALSE(epitaph_0.has_value()); + + fble::ConnectionPtr conn_client_1; + std::optional epitaph_1; + conn_client_1.set_error_handler( + [&](zx_status_t cb_epitaph) { epitaph_1 = cb_epitaph; }); + + fble::ConnectionOptions options_1; + central_proxy()->Connect( + peer_id, std::move(options_1), conn_client_1.NewRequest()); + RunLoopUntilIdle(); + EXPECT_FALSE(epitaph_0.has_value()); + ASSERT_TRUE(epitaph_1.has_value()); + EXPECT_EQ(epitaph_1.value(), ZX_ERR_ALREADY_BOUND); +} + +TEST_F(LowEnergyCentralServerTest, ConnectToPeerWithRequestPending) { + auto* const peer = + adapter()->peer_cache()->NewPeer(kTestAddr, /*connectable=*/true); + ASSERT_TRUE(peer); + + auto fake_peer = + std::make_unique(kTestAddr, pw_dispatcher()); + fake_peer->force_pending_connect(); + test_device()->AddPeer(std::move(fake_peer)); + + fble::ConnectionPtr conn_client_0; + std::optional epitaph_0; + conn_client_0.set_error_handler( + [&](zx_status_t cb_epitaph) { epitaph_0 = cb_epitaph; }); + + const fuchsia::bluetooth::PeerId peer_id{peer->identifier().value()}; + fble::ConnectionOptions options_0; + central_proxy()->Connect( + peer_id, std::move(options_0), conn_client_0.NewRequest()); + RunLoopUntilIdle(); + EXPECT_FALSE(epitaph_0.has_value()); + + fble::ConnectionPtr conn_client_1; + std::optional epitaph_1; + conn_client_1.set_error_handler( + [&](zx_status_t cb_epitaph) { epitaph_1 = cb_epitaph; }); + + fble::ConnectionOptions options_1; + central_proxy()->Connect( + peer_id, std::move(options_1), conn_client_1.NewRequest()); + RunLoopUntilIdle(); + EXPECT_FALSE(epitaph_0.has_value()); + ASSERT_TRUE(epitaph_1.has_value()); + EXPECT_EQ(epitaph_1.value(), ZX_ERR_ALREADY_BOUND); +} + +TEST_F(LowEnergyCentralServerTest, + ConnectToPeerAlreadyConnectedInLowEnergyConnectionManager) { + auto* const peer = + adapter()->peer_cache()->NewPeer(kTestAddr, /*connectable=*/true); + ASSERT_TRUE(peer); + test_device()->AddPeer( + std::make_unique(kTestAddr, pw_dispatcher())); + + std::unique_ptr le_conn; + adapter()->le()->Connect( + peer->identifier(), + [&le_conn](auto result) { + ASSERT_EQ(fit::ok(), result); + le_conn = std::move(result).value(); + }, + bt::gap::LowEnergyConnectionOptions()); + RunLoopUntilIdle(); + ASSERT_TRUE(le_conn); + + fble::ConnectionPtr conn_client1; + std::optional epitaph1; + conn_client1.set_error_handler( + [&](zx_status_t cb_epitaph) { epitaph1 = cb_epitaph; }); + + const fuchsia::bluetooth::PeerId kFidlPeerId{peer->identifier().value()}; + fble::ConnectionOptions options1; + central_proxy()->Connect( + kFidlPeerId, std::move(options1), conn_client1.NewRequest()); + RunLoopUntilIdle(); + EXPECT_FALSE(epitaph1.has_value()); +} + +TEST_F(LowEnergyCentralServerTest, ConnectThenPeerDisconnectThenReconnect) { + auto* const peer = + adapter()->peer_cache()->NewPeer(kTestAddr, /*connectable=*/true); + ASSERT_TRUE(peer); + const fuchsia::bluetooth::PeerId kFidlPeerId{peer->identifier().value()}; + + std::unique_ptr fake_peer = + std::make_unique(kTestAddr, pw_dispatcher()); + test_device()->AddPeer(std::move(fake_peer)); + + fble::ConnectionPtr conn_client_0; + std::optional epitaph_0; + conn_client_0.set_error_handler( + [&](zx_status_t cb_epitaph) { epitaph_0 = cb_epitaph; }); + + fble::ConnectionOptions options_0; + central_proxy()->Connect( + kFidlPeerId, std::move(options_0), conn_client_0.NewRequest()); + RunLoopUntilIdle(); + EXPECT_FALSE(epitaph_0.has_value()); + + test_device()->Disconnect(kTestAddr); + RunLoopUntilIdle(); + EXPECT_TRUE(epitaph_0.has_value()); + + fble::ConnectionPtr conn_client_1; + std::optional epitaph_1; + conn_client_1.set_error_handler( + [&](zx_status_t cb_epitaph) { epitaph_1 = cb_epitaph; }); + + fble::ConnectionOptions options_1; + central_proxy()->Connect( + kFidlPeerId, std::move(options_1), conn_client_1.NewRequest()); + RunLoopUntilIdle(); + EXPECT_FALSE(epitaph_1.has_value()); +} + +TEST_F(LowEnergyCentralServerTest, + ConnectFailsDueToPeerNotConnectableThenConnectSuceeds) { + bt::gap::Peer* peer = + adapter()->peer_cache()->NewPeer(kTestAddr, /*connectable=*/false); + ASSERT_TRUE(peer); + auto fake_peer = + std::make_unique(kTestAddr, pw_dispatcher()); + test_device()->AddPeer(std::move(fake_peer)); + + fble::ConnectionPtr conn_client_0; + std::optional epitaph_0; + conn_client_0.set_error_handler( + [&](zx_status_t cb_epitaph) { epitaph_0 = cb_epitaph; }); + + central_proxy()->Connect( + fuchsia::bluetooth::PeerId{peer->identifier().value()}, + fble::ConnectionOptions{}, + conn_client_0.NewRequest()); + RunLoopUntilIdle(); + ASSERT_TRUE(epitaph_0.has_value()); + EXPECT_EQ(epitaph_0.value(), ZX_ERR_NOT_CONNECTED); + + // Connect to peer to verify connection state was cleaned up on previous + // error. + peer->set_connectable(true); + + fble::ConnectionPtr conn_client_1; + std::optional epitaph_1; + conn_client_1.set_error_handler( + [&](zx_status_t cb_epitaph) { epitaph_1 = cb_epitaph; }); + + central_proxy()->Connect( + fuchsia::bluetooth::PeerId{peer->identifier().value()}, + fble::ConnectionOptions{}, + conn_client_1.NewRequest()); + RunLoopUntilIdle(); + EXPECT_FALSE(epitaph_1.has_value()); +} + +TEST_F(LowEnergyCentralServerTestFakeAdapter, + ConnectWithConnectionOptionsNonBondableAndServiceFilter) { + const bt::PeerId kPeerId(1); + const bt::UUID kServiceUuid(static_cast(2)); + + fble::ConnectionPtr conn_client; + std::optional epitaph; + conn_client.set_error_handler( + [&](zx_status_t cb_epitaph) { epitaph = cb_epitaph; }); + + fble::ConnectionOptions options; + options.set_bondable_mode(false); + options.set_service_filter(fuchsia::bluetooth::Uuid{kServiceUuid.value()}); + central_proxy()->Connect(fuchsia::bluetooth::PeerId{kPeerId.value()}, + std::move(options), + conn_client.NewRequest()); + RunLoopUntilIdle(); + EXPECT_FALSE(epitaph.has_value()); + + auto& connections = adapter()->fake_le()->connections(); + auto conn_iter = connections.find(kPeerId); + ASSERT_NE(conn_iter, connections.end()); + EXPECT_EQ(conn_iter->second.options.bondable_mode, + bt::sm::BondableMode::NonBondable); + ASSERT_TRUE(conn_iter->second.options.service_uuid.has_value()); + EXPECT_EQ(conn_iter->second.options.service_uuid, kServiceUuid); + EXPECT_EQ(conn_iter->second.options.auto_connect, false); +} + +TEST_P(LowEnergyCentralServerTestFakeAdapterBoolParam, + ConnectConnectionOptionsBondable) { + const bt::PeerId kPeerId(1); + + fble::ConnectionPtr conn_client; + std::optional epitaph; + conn_client.set_error_handler( + [&](zx_status_t cb_epitaph) { epitaph = cb_epitaph; }); + + fble::ConnectionOptions options; + // Bondable mode option defaults to true, so behavior shouldn't change whether + // or not it is explicitly set to true. + if (GetParam()) { + options.set_bondable_mode(true); + } + central_proxy()->Connect(fuchsia::bluetooth::PeerId{kPeerId.value()}, + std::move(options), + conn_client.NewRequest()); + RunLoopUntilIdle(); + EXPECT_FALSE(epitaph.has_value()); + + auto& connections = adapter()->fake_le()->connections(); + auto conn_iter = connections.find(kPeerId); + ASSERT_NE(conn_iter, connections.end()); + EXPECT_EQ(conn_iter->second.options.bondable_mode, + bt::sm::BondableMode::Bondable); +} + +INSTANTIATE_TEST_SUITE_P(LowEnergyCentralServerTestFakeAdapterBoolParamTests, + LowEnergyCentralServerTestFakeAdapterBoolParam, + ::testing::Bool()); + +} // namespace +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_connection_server.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_connection_server.cc new file mode 100644 index 0000000000..89cbc1dd54 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_connection_server.cc @@ -0,0 +1,234 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_connection_server.h" + +#include + +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/helpers.h" + +namespace bthost { + +namespace fbg = fuchsia::bluetooth::gatt2; + +LowEnergyConnectionServer::LowEnergyConnectionServer( + bt::gap::Adapter::WeakPtr adapter, + bt::gatt::GATT::WeakPtr gatt, + std::unique_ptr connection, + zx::channel handle, + fit::callback closed_cb) + : ServerBase(this, std::move(handle)), + conn_(std::move(connection)), + closed_handler_(std::move(closed_cb)), + peer_id_(conn_->peer_identifier()), + adapter_(std::move(adapter)), + gatt_(std::move(gatt)) { + BT_DEBUG_ASSERT(conn_); + + set_error_handler([this](zx_status_t) { OnClosed(); }); + conn_->set_closed_callback( + fit::bind_member<&LowEnergyConnectionServer::OnClosed>(this)); +} + +void LowEnergyConnectionServer::OnClosed() { + if (closed_handler_) { + binding()->Close(ZX_ERR_CONNECTION_RESET); + closed_handler_(); + } +} + +void LowEnergyConnectionServer::RequestGattClient( + fidl::InterfaceRequest client) { + if (gatt_client_server_.has_value()) { + bt_log(INFO, + "fidl", + "%s: gatt client server already bound (peer: %s)", + __FUNCTION__, + bt_str(peer_id_)); + client.Close(ZX_ERR_ALREADY_BOUND); + return; + } + + fit::callback server_error_cb = [this] { + bt_log( + TRACE, "fidl", "gatt client server error (peer: %s)", bt_str(peer_id_)); + gatt_client_server_.reset(); + }; + gatt_client_server_.emplace( + peer_id_, gatt_, std::move(client), std::move(server_error_cb)); +} + +void LowEnergyConnectionServer::AcceptCis( + fuchsia::bluetooth::le::ConnectionAcceptCisRequest parameters) { + if (!parameters.has_connection_stream()) { + bt_log(WARN, "fidl", "AcceptCis invoked without a connection stream"); + return; + } + ::fidl::InterfaceRequest<::fuchsia::bluetooth::le::IsochronousStream>* + connection_stream = parameters.mutable_connection_stream(); + uint8_t cig_id = parameters.cig_id(); + uint8_t cis_id = parameters.cis_id(); + bt::iso::CigCisIdentifier id(cig_id, cis_id); + + // Check for existing stream with same CIG/CIS combination + if (iso_streams_.count(id) != 0) { + bt_log(WARN, + "fidl", + "AcceptCis invoked with duplicate ID (CIG: %u, CIS: %u)", + cig_id, + cis_id); + connection_stream->Close(ZX_ERR_INVALID_ARGS); + return; + } + auto stream_server = std::make_unique( + std::move(*connection_stream), [id, this]() { iso_streams_.erase(id); }); + auto weak_stream_server = stream_server->GetWeakPtr(); + iso_streams_[id] = std::move(stream_server); + + bt::iso::AcceptCisStatus result = conn_->AcceptCis( + id, + [weak_stream_server]( + pw::bluetooth::emboss::StatusCode status, + std::optional weak_stream_ptr, + const std::optional& + connection_params) { + if (weak_stream_server.is_alive()) { + if (status == pw::bluetooth::emboss::StatusCode::SUCCESS) { + BT_ASSERT(weak_stream_ptr.has_value()); + BT_ASSERT(connection_params.has_value()); + weak_stream_server->OnStreamEstablished(*weak_stream_ptr, + *connection_params); + } else { + weak_stream_server->OnStreamEstablishmentFailed(status); + } + } + }); + + switch (result) { + case bt::iso::AcceptCisStatus::kSuccess: + bt_log(INFO, + "fidl", + "waiting for incoming CIS connection (CIG: %u, CIS: %u)", + cig_id, + cis_id); + return; + case bt::iso::AcceptCisStatus::kNotPeripheral: + bt_log(WARN, + "fidl", + "attempt to wait for incoming CIS on Central not allowed"); + iso_streams_[id]->Close(ZX_ERR_NOT_SUPPORTED); + return; + case bt::iso::AcceptCisStatus::kAlreadyExists: + bt_log(WARN, + "fidl", + "redundant request to wait for incoming CIS (CIG: %u, CIS: %u)", + cig_id, + cis_id); + iso_streams_[id]->Close(ZX_ERR_INVALID_ARGS); + return; + default: + BT_PANIC("Invalid AcceptCisStatus value %d", static_cast(result)); + } +} + +void LowEnergyConnectionServer::GetCodecLocalDelayRange( + ::fuchsia::bluetooth::le::CodecDelayGetCodecLocalDelayRangeRequest + parameters, + GetCodecLocalDelayRangeCallback callback) { + bt_log(INFO, "fidl", "request received to read controller supported delay"); + + if (!parameters.has_logical_transport_type()) { + bt_log(WARN, + "fidl", + "request to read controller delay missing logical_transport_type"); + callback(fpromise::error(ZX_ERR_INVALID_ARGS)); + return; + } + + if (!parameters.has_data_direction()) { + bt_log(WARN, + "fidl", + "request to read controller delay missing data_direction"); + callback(fpromise::error(ZX_ERR_INVALID_ARGS)); + return; + } + + if (!parameters.has_codec_attributes()) { + bt_log(WARN, + "fidl", + "request to read controller delay missing codec_attributes"); + callback(fpromise::error(ZX_ERR_INVALID_ARGS)); + return; + } + + if (!parameters.codec_attributes().has_codec_id()) { + bt_log(WARN, "fidl", "request to read controller delay missing codec_id"); + callback(fpromise::error(ZX_ERR_INVALID_ARGS)); + return; + } + + // Process required parameters + pw::bluetooth::emboss::LogicalTransportType transport_type = + fidl_helpers::LogicalTransportTypeFromFidl( + parameters.logical_transport_type()); + pw::bluetooth::emboss::DataPathDirection direction = + fidl_helpers::DataPathDirectionFromFidl(parameters.data_direction()); + bt::StaticPacket codec_id = + fidl_helpers::CodecIdFromFidl(parameters.codec_attributes().codec_id()); + + // Codec configuration is optional + std::optional> codec_configuration; + if (parameters.codec_attributes().has_codec_configuration()) { + codec_configuration = parameters.codec_attributes().codec_configuration(); + } else { + codec_configuration = std::nullopt; + } + + adapter_->GetSupportedDelayRange( + codec_id, + transport_type, + direction, + codec_configuration, + [callback = std::move(callback)]( + pw::Status status, uint32_t min_delay_us, uint32_t max_delay_us) { + if (!status.ok()) { + bt_log(WARN, "fidl", "failed to get controller supported delay"); + callback(fpromise::error(ZX_ERR_INTERNAL)); + return; + } + bt_log(INFO, + "fidl", + "controller supported delay [%d, %d] microseconds", + min_delay_us, + max_delay_us); + fuchsia::bluetooth::le::CodecDelay_GetCodecLocalDelayRange_Response + response; + zx::duration min_delay = zx::usec(min_delay_us); + zx::duration max_delay = zx::usec(max_delay_us); + response.set_min_controller_delay(min_delay.get()); + response.set_max_controller_delay(max_delay.get()); + callback( + fuchsia::bluetooth::le::CodecDelay_GetCodecLocalDelayRange_Result:: + WithResponse(std::move(response))); + }); +} + +void LowEnergyConnectionServer::ConnectL2cap( + fuchsia::bluetooth::le::ConnectionConnectL2capRequest parameters) { + // Temporarily log and close the binding until implemented. + bt_log(WARN, "fidl", "ConnectL2cap not implemented"); + binding()->Close(ZX_ERR_NOT_SUPPORTED); +} + +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_connection_server_test.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_connection_server_test.cc new file mode 100644 index 0000000000..3c89006892 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_connection_server_test.cc @@ -0,0 +1,385 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_connection_server.h" + +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/adapter_test_fixture.h" + +namespace bthost { +namespace { + +namespace fble = fuchsia::bluetooth::le; +namespace fbg = fuchsia::bluetooth::gatt2; + +const bt::DeviceAddress kTestAddr(bt::DeviceAddress::Type::kLEPublic, + {0x01, 0, 0, 0, 0, 0}); + +// Used for test cases that require all of the test infrastructure, but don't +// want to auto- configure the client and server at startup. +class LowEnergyConnectionServerTest + : public bthost::testing::AdapterTestFixture { + public: + LowEnergyConnectionServerTest() = default; + ~LowEnergyConnectionServerTest() override = default; + + fble::Connection* client() { return client_.get(); } + + void UnbindClient() { client_.Unbind(); } + + bt::PeerId peer_id() const { return peer_id_; } + + bool server_closed_cb_called() const { return server_closed_cb_called_; } + + protected: + void EstablishConnectionAndStartServer() { + // Since LowEnergyConnectionHandle must be created by + // LowEnergyConnectionManager, we discover and connect to a fake peer to get + // a LowEnergyConnectionHandle. + std::unique_ptr fake_peer = + std::make_unique( + kTestAddr, pw_dispatcher(), /*connectable=*/true); + test_device()->AddPeer(std::move(fake_peer)); + + std::optional peer_id; + bt::gap::LowEnergyDiscoverySessionPtr session; + adapter()->le()->StartDiscovery( + /*active=*/true, [&](bt::gap::LowEnergyDiscoverySessionPtr cb_session) { + session = std::move(cb_session); + session->SetResultCallback( + [&](const bt::gap::Peer& peer) { peer_id = peer.identifier(); }); + }); + RunLoopUntilIdle(); + BT_ASSERT(peer_id); + peer_id_ = *peer_id; + + std::optional + conn_result; + adapter()->le()->Connect( + peer_id_, + [&](bt::gap::LowEnergyConnectionManager::ConnectionResult result) { + conn_result = std::move(result); + }, + bt::gap::LowEnergyConnectionOptions()); + RunLoopUntilIdle(); + BT_ASSERT(conn_result); + BT_ASSERT(conn_result->is_ok()); + std::unique_ptr connection = + std::move(*conn_result).value(); + + // Start our FIDL connection server. + fidl::InterfaceHandle handle; + server_ = std::make_unique( + adapter()->AsWeakPtr(), + gatt()->GetWeakPtr(), + std::move(connection), + handle.NewRequest().TakeChannel(), + /*closed_cb=*/[this] { + server_closed_cb_called_ = true; + server_.reset(); + }); + client_ = handle.Bind(); + } + + private: + std::unique_ptr server_; + fble::ConnectionPtr client_; + bool server_closed_cb_called_ = false; + bt::PeerId peer_id_; +}; + +// Tests that want to automatically allocate and start the client and server +// before entering the test body. +class LowEnergyConnectionServerAutoStartTest + : public LowEnergyConnectionServerTest { + public: + LowEnergyConnectionServerAutoStartTest() = default; + ~LowEnergyConnectionServerAutoStartTest() override = default; + + void SetUp() override { + LowEnergyConnectionServerTest::SetUp(); + EstablishConnectionAndStartServer(); + } + + protected: + void RunGetCodecDelayRangeTest( + fuchsia::bluetooth::le::CodecDelayGetCodecLocalDelayRangeRequest&& params, + std::optional err); +}; + +TEST_F(LowEnergyConnectionServerAutoStartTest, RequestGattClientTwice) { + fidl::InterfaceHandle handle_0; + client()->RequestGattClient(handle_0.NewRequest()); + fbg::ClientPtr client_0 = handle_0.Bind(); + std::optional client_epitaph_0; + client_0.set_error_handler( + [&](zx_status_t epitaph) { client_epitaph_0 = epitaph; }); + RunLoopUntilIdle(); + EXPECT_FALSE(client_epitaph_0); + + fidl::InterfaceHandle handle_1; + client()->RequestGattClient(handle_1.NewRequest()); + fbg::ClientPtr client_1 = handle_1.Bind(); + std::optional client_epitaph_1; + client_1.set_error_handler( + [&](zx_status_t epitaph) { client_epitaph_1 = epitaph; }); + RunLoopUntilIdle(); + EXPECT_FALSE(client_epitaph_0); + ASSERT_TRUE(client_epitaph_1); + EXPECT_EQ(client_epitaph_1.value(), ZX_ERR_ALREADY_BOUND); +} + +TEST_F(LowEnergyConnectionServerAutoStartTest, GattClientServerError) { + fidl::InterfaceHandle handle_0; + client()->RequestGattClient(handle_0.NewRequest()); + fbg::ClientPtr client_0 = handle_0.Bind(); + std::optional client_epitaph_0; + client_0.set_error_handler( + [&](zx_status_t epitaph) { client_epitaph_0 = epitaph; }); + RunLoopUntilIdle(); + EXPECT_FALSE(client_epitaph_0); + + // Calling WatchServices twice should cause a server error. + client_0->WatchServices({}, [](auto, auto) {}); + client_0->WatchServices({}, [](auto, auto) {}); + RunLoopUntilIdle(); + EXPECT_TRUE(client_epitaph_0); + + // Requesting a new GATT client should succeed. + fidl::InterfaceHandle handle_1; + client()->RequestGattClient(handle_1.NewRequest()); + fbg::ClientPtr client_1 = handle_1.Bind(); + std::optional client_epitaph_1; + client_1.set_error_handler( + [&](zx_status_t epitaph) { client_epitaph_1 = epitaph; }); + RunLoopUntilIdle(); + EXPECT_FALSE(client_epitaph_1); +} + +TEST_F(LowEnergyConnectionServerAutoStartTest, + RequestGattClientThenUnbindThenRequestAgainShouldSucceed) { + fidl::InterfaceHandle handle_0; + client()->RequestGattClient(handle_0.NewRequest()); + fbg::ClientPtr client_0 = handle_0.Bind(); + std::optional client_epitaph_0; + client_0.set_error_handler( + [&](zx_status_t epitaph) { client_epitaph_0 = epitaph; }); + RunLoopUntilIdle(); + EXPECT_FALSE(client_epitaph_0); + client_0.Unbind(); + RunLoopUntilIdle(); + + // Requesting a new GATT client should succeed. + fidl::InterfaceHandle handle_1; + client()->RequestGattClient(handle_1.NewRequest()); + fbg::ClientPtr client_1 = handle_1.Bind(); + std::optional client_epitaph_1; + client_1.set_error_handler( + [&](zx_status_t epitaph) { client_epitaph_1 = epitaph; }); + RunLoopUntilIdle(); + EXPECT_FALSE(client_epitaph_1); +} + +static ::fuchsia::bluetooth::le::CodecDelayGetCodecLocalDelayRangeRequest +CreateDelayRangeRequestParams(bool has_vendor_config) { + ::fuchsia::bluetooth::le::CodecDelayGetCodecLocalDelayRangeRequest params; + + // params.logical_transport_type + params.set_logical_transport_type( + fuchsia::bluetooth::LogicalTransportType::LE_CIS); + + // params.data_direction + params.set_data_direction(fuchsia::bluetooth::DataDirection::INPUT); + + // params.codec_attributes + if (has_vendor_config) { + uint16_t kCompanyId = 0x1234; + uint16_t kVendorId = 0xfedc; + fuchsia::bluetooth::VendorCodingFormat vendor_coding_format; + vendor_coding_format.set_company_id(kCompanyId); + vendor_coding_format.set_vendor_id(kVendorId); + fuchsia::bluetooth::CodecAttributes codec_attributes; + codec_attributes.mutable_codec_id()->set_vendor_format( + std::move(vendor_coding_format)); + std::vector codec_configuration{0x4f, 0x77, 0x65, 0x6e}; + codec_attributes.set_codec_configuration(codec_configuration); + params.set_codec_attributes(std::move(codec_attributes)); + } else { + fuchsia::bluetooth::CodecAttributes codec_attributes; + codec_attributes.mutable_codec_id()->set_assigned_format( + fuchsia::bluetooth::AssignedCodingFormat::LINEAR_PCM); + params.set_codec_attributes(std::move(codec_attributes)); + } + + return params; +} + +void LowEnergyConnectionServerAutoStartTest::RunGetCodecDelayRangeTest( + ::fuchsia::bluetooth::le::CodecDelayGetCodecLocalDelayRangeRequest&& params, + std::optional err = std::nullopt) { + fuchsia::bluetooth::le::CodecDelay_GetCodecLocalDelayRange_Result result; + LowEnergyConnectionServer::GetCodecLocalDelayRangeCallback callback = + [&](fuchsia::bluetooth::le::CodecDelay_GetCodecLocalDelayRange_Result + cb_result) { result = std::move(cb_result); }; + client()->GetCodecLocalDelayRange(std::move(params), std::move(callback)); + RunLoopUntilIdle(); + + if (err.has_value()) { + ASSERT_TRUE(result.is_err()); + EXPECT_EQ(result.err(), err.value()); + } else { + ASSERT_TRUE(result.is_response()); + auto& response = result.response(); + // These are the default values returned by the FakeController + EXPECT_EQ(response.min_controller_delay(), zx::sec(0).get()); + EXPECT_EQ(response.max_controller_delay(), zx::sec(4).get()); + } +} + +// Invoking GetCodecLocalDelay with a spec-defined coding format +TEST_F(LowEnergyConnectionServerAutoStartTest, + GetCodecLocalDelaySpecCodingFormat) { + ::fuchsia::bluetooth::le::CodecDelayGetCodecLocalDelayRangeRequest params = + CreateDelayRangeRequestParams(/*has_vendor_config=*/false); + RunGetCodecDelayRangeTest(std::move(params)); +} + +// Invoking GetCodecLocalDelay with a vendor-defined coding format +TEST_F(LowEnergyConnectionServerAutoStartTest, + GetCodecLocalDelayVendorCodingFormat) { + ::fuchsia::bluetooth::le::CodecDelayGetCodecLocalDelayRangeRequest params = + CreateDelayRangeRequestParams(/*has_vendor_config=*/true); + RunGetCodecDelayRangeTest(std::move(params)); +} + +// Invoking GetCodecLocalDelay with missing parameters +TEST_F(LowEnergyConnectionServerAutoStartTest, + GetCodecLocalDelayMissingParams) { + // Logical transport type missing + ::fuchsia::bluetooth::le::CodecDelayGetCodecLocalDelayRangeRequest params = + CreateDelayRangeRequestParams(/*has_vendor_config=*/false); + params.clear_logical_transport_type(); + RunGetCodecDelayRangeTest(std::move(params), ZX_ERR_INVALID_ARGS); + + // Data direction is missing + params = CreateDelayRangeRequestParams(/*has_vendor_config=*/false); + params.clear_data_direction(); + RunGetCodecDelayRangeTest(std::move(params), ZX_ERR_INVALID_ARGS); + + // Codec attributes is missing + params = CreateDelayRangeRequestParams(/*has_vendor_config=*/true); + params.clear_codec_attributes(); + RunGetCodecDelayRangeTest(std::move(params), ZX_ERR_INVALID_ARGS); + + // codec_attributes.codec_id is missing + params = CreateDelayRangeRequestParams(/*has_vendor_config=*/true); + params.mutable_codec_attributes()->clear_codec_id(); + RunGetCodecDelayRangeTest(std::move(params), ZX_ERR_INVALID_ARGS); +} + +// Calling GetCodecLocalDelay when the controller doesn't support it +TEST_F(LowEnergyConnectionServerAutoStartTest, + GetCodecLocalDelayCommandNotSupported) { + // Disable the Read Local Supported Controller Delay instruction + bt::testing::FakeController::Settings settings; + pw::bluetooth::emboss::MakeSupportedCommandsView( + settings.supported_commands, sizeof(settings.supported_commands)) + .read_local_supported_controller_delay() + .Write(false); + test_device()->set_settings(settings); + + ::fuchsia::bluetooth::le::CodecDelayGetCodecLocalDelayRangeRequest params = + CreateDelayRangeRequestParams(/*has_vendor_config=*/false); + RunGetCodecDelayRangeTest(std::move(params), ZX_ERR_INTERNAL); +} + +// Class that creates and manages an AcceptCis request and associated objects +class AcceptCisRequest { + public: + AcceptCisRequest(fble::Connection* connection_client, + bt::iso::CigCisIdentifier id) { + fuchsia::bluetooth::le::ConnectionAcceptCisRequest params; + params.set_cig_id(id.cig_id()); + params.set_cis_id(id.cis_id()); + params.set_connection_stream(stream_handle_.NewRequest()); + client_stream_ptr_ = stream_handle_.Bind(); + client_stream_ptr_.set_error_handler( + [this](zx_status_t epitaph) { epitaph_ = epitaph; }); + connection_client->AcceptCis(std::move(params)); + } + std::optional epitaph() { return epitaph_; } + + private: + fidl::InterfaceHandle + stream_handle_; + fuchsia::bluetooth::le::IsochronousStreamPtr client_stream_ptr_; + std::optional epitaph_; +}; + +// Verify that all calls to AcceptCis() with unique CIG/CIS pairs are accepted +// and duplicate calls are rejected and the IsochronousStream handle is closed +// with an INVALID_ARGS epitaph. +TEST_F(LowEnergyConnectionServerTest, MultipleAcceptCisCalls) { + // AcceptCis() should only be called on a connection where we are acting as + // the peripheral. + bt::testing::FakeController::Settings settings; + settings.le_connection_role = + pw::bluetooth::emboss::ConnectionRole::PERIPHERAL; + test_device()->set_settings(settings); + EstablishConnectionAndStartServer(); + + AcceptCisRequest request1(client(), {/*cig_id=*/0x10, /*cis_id=*/0x08}); + AcceptCisRequest request2(client(), {/*cig_id=*/0x11, /*cis_id=*/0x08}); + AcceptCisRequest request3(client(), {/*cig_id=*/0x10, /*cis_id=*/0x07}); + AcceptCisRequest request1_dup(client(), {/*cig_id=*/0x10, /*cis_id=*/0x08}); + RunLoopUntilIdle(); + + // All unique requests are pending + EXPECT_FALSE(request1.epitaph()); + EXPECT_FALSE(request2.epitaph()); + EXPECT_FALSE(request3.epitaph()); + + // Duplicate request is rejected + ASSERT_TRUE(request1_dup.epitaph()); + EXPECT_EQ(*(request1_dup.epitaph()), ZX_ERR_INVALID_ARGS); +} + +// Calling AcceptCis when we are the central should fail with +// ZX_ERR_NOT_SUPPORTED +TEST_F(LowEnergyConnectionServerTest, AcceptCisCalledFromCentral) { + bt::testing::FakeController::Settings settings; + settings.le_connection_role = pw::bluetooth::emboss::ConnectionRole::CENTRAL; + test_device()->set_settings(settings); + EstablishConnectionAndStartServer(); + + AcceptCisRequest request(client(), {/*cig_id=*/0x10, /*cis_id=*/0x08}); + RunLoopUntilIdle(); + ASSERT_TRUE(request.epitaph()); + EXPECT_EQ(*(request.epitaph()), ZX_ERR_NOT_SUPPORTED); +} + +TEST_F(LowEnergyConnectionServerAutoStartTest, ServerClosedOnConnectionClosed) { + adapter()->le()->Disconnect(peer_id()); + RunLoopUntilIdle(); + EXPECT_TRUE(server_closed_cb_called()); +} + +TEST_F(LowEnergyConnectionServerAutoStartTest, + ServerClosedWhenFIDLClientClosesConnection) { + UnbindClient(); + RunLoopUntilIdle(); + EXPECT_TRUE(server_closed_cb_called()); +} + +} // namespace +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_peripheral_server.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_peripheral_server.cc new file mode 100644 index 0000000000..cd937e007a --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_peripheral_server.cc @@ -0,0 +1,565 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_peripheral_server.h" + +#include +#include + +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/helpers.h" +#include "pw_bluetooth_sapphire/internal/host/common/advertising_data.h" +#include "pw_bluetooth_sapphire/internal/host/common/assert.h" +#include "pw_bluetooth_sapphire/internal/host/common/identifier.h" +#include "pw_bluetooth_sapphire/internal/host/common/log.h" +#include "pw_bluetooth_sapphire/internal/host/gap/low_energy_advertising_manager.h" +#include "pw_bluetooth_sapphire/internal/host/gap/low_energy_connection_manager.h" +#include "pw_bluetooth_sapphire/internal/host/gap/peer.h" +#include "pw_bluetooth_sapphire/internal/host/hci-spec/constants.h" +#include "pw_bluetooth_sapphire/internal/host/hci-spec/util.h" +#include "pw_bluetooth_sapphire/internal/host/sm/types.h" + +#define LOG_TAG "fidl" + +using bt::sm::BondableMode; +namespace fble = fuchsia::bluetooth::le; + +namespace bthost { +namespace { + +fble::PeripheralError FidlErrorFromStatus(bt::hci::Result<> status) { + BT_ASSERT_MSG(status.is_error(), + "FidlErrorFromStatus called on success status"); + return status.error_value().Visit( + [](bt::HostError host_error) { + switch (host_error) { + case bt::HostError::kNotSupported: + return fble::PeripheralError::NOT_SUPPORTED; + case bt::HostError::kInvalidParameters: + return fble::PeripheralError::INVALID_PARAMETERS; + case bt::HostError::kAdvertisingDataTooLong: + return fble::PeripheralError::ADVERTISING_DATA_TOO_LONG; + case bt::HostError::kScanResponseTooLong: + return fble::PeripheralError::SCAN_RESPONSE_DATA_TOO_LONG; + case bt::HostError::kCanceled: + return fble::PeripheralError::ABORTED; + default: + break; + } + return fble::PeripheralError::FAILED; + }, + [](auto /*hci_error*/) { return fble::PeripheralError::FAILED; }); +} + +} // namespace + +LowEnergyPeripheralServer::AdvertisementInstance::AdvertisementInstance( + LowEnergyPeripheralServer* peripheral_server, + AdvertisementInstanceId id, + fuchsia::bluetooth::le::AdvertisingParameters parameters, + fidl::InterfaceHandle handle, + AdvertiseCompleteCallback complete_cb) + : peripheral_server_(peripheral_server), + id_(id), + parameters_(std::move(parameters)), + advertise_complete_cb_(std::move(complete_cb)), + weak_self_(this) { + BT_ASSERT(advertise_complete_cb_); + advertised_peripheral_.Bind(std::move(handle)); + advertised_peripheral_.set_error_handler( + [this, peripheral_server, id](zx_status_t /*status*/) { + CloseWith(fpromise::ok()); + peripheral_server->RemoveAdvertisingInstance(id); + }); +} + +LowEnergyPeripheralServer::AdvertisementInstance::~AdvertisementInstance() { + if (advertise_complete_cb_) { + CloseWith(fpromise::error(fble::PeripheralError::ABORTED)); + } +} + +void LowEnergyPeripheralServer::AdvertisementInstance::StartAdvertising() { + auto self = weak_self_.GetWeakPtr(); + auto status_cb = [self](auto adv_instance, bt::hci::Result<> status) { + if (!self.is_alive()) { + bt_log( + DEBUG, LOG_TAG, "advertisement canceled before advertising started"); + // Destroying `adv_instance` will stop advertising. + return; + } + + if (bt_is_error(status, + WARN, + LOG_TAG, + "failed to start advertising (status: %s)", + bt_str(status))) { + self->CloseWith(fpromise::error(FidlErrorFromStatus(status))); + self->peripheral_server_->RemoveAdvertisingInstance(self->id_); + return; + } + + self->Register(std::move(adv_instance)); + }; + + peripheral_server_->StartAdvertisingInternal( + parameters_, std::move(status_cb), self->id_); +} + +void LowEnergyPeripheralServer::ListenL2cap( + fble::ChannelListenerRegistryListenL2capRequest request, + ListenL2capCallback callback) { + // TODO(fxbug.dev/42178956): Implement ListenL2cap. + fble::ChannelListenerRegistry_ListenL2cap_Result result; + callback(std::move(result.set_err(ZX_ERR_NOT_SUPPORTED))); +} + +void LowEnergyPeripheralServer::AdvertisementInstance::Register( + bt::gap::AdvertisementInstance instance) { + BT_ASSERT(!instance_); + instance_ = std::move(instance); +} + +void LowEnergyPeripheralServer::AdvertisementInstance::OnConnected( + bt::gap::AdvertisementId advertisement_id, + bt::gap::Adapter::LowEnergy::ConnectionResult result) { + BT_ASSERT(advertisement_id != bt::gap::kInvalidAdvertisementId); + + // HCI advertising ends when a connection is received (even for error + // results), so clear the stale advertisement handle. + instance_.reset(); + + if (result.is_error()) { + bt_log(INFO, + LOG_TAG, + "incoming connection failed; restarting advertising (adv instance " + "id: %zu, prev adv " + "id: %s)", + id_, + bt_str(advertisement_id)); + StartAdvertising(); + return; + } + + std::unique_ptr conn = + std::move(result).value(); + bt::PeerId peer_id = conn->peer_identifier(); + bt::gap::Peer* peer = + peripheral_server_->adapter()->peer_cache()->FindById(peer_id); + BT_ASSERT(peer); + + bt_log(INFO, + LOG_TAG, + "peripheral received connection to advertisement (peer: %s, adv id: " + "%s, adv " + "instance id: %zu)", + bt_str(peer->identifier()), + bt_str(advertisement_id), + id_); + + fidl::InterfaceHandle conn_handle = + peripheral_server_->CreateConnectionServer(std::move(conn)); + + // Restart advertising after the client acknowledges the connection. + auto self = weak_self_.GetWeakPtr(); + auto on_connected_cb = [self] { + if (self.is_alive()) { + self->StartAdvertising(); + } + }; + advertised_peripheral_->OnConnected(fidl_helpers::PeerToFidlLe(*peer), + std::move(conn_handle), + std::move(on_connected_cb)); +} + +void LowEnergyPeripheralServer::AdvertisementInstance::CloseWith( + fpromise::result result) { + if (advertise_complete_cb_) { + advertised_peripheral_.Unbind(); + advertise_complete_cb_(std::move(result)); + } +} + +LowEnergyPeripheralServer::AdvertisementInstanceDeprecated:: + AdvertisementInstanceDeprecated( + fidl::InterfaceRequest + handle) + : handle_(std::move(handle)) { + BT_DEBUG_ASSERT(handle_); +} + +LowEnergyPeripheralServer::AdvertisementInstanceDeprecated:: + ~AdvertisementInstanceDeprecated() { + handle_closed_wait_.Cancel(); +} + +zx_status_t +LowEnergyPeripheralServer::AdvertisementInstanceDeprecated::Register( + bt::gap::AdvertisementInstance instance) { + BT_DEBUG_ASSERT(!instance_); + + instance_ = std::move(instance); + + handle_closed_wait_.set_object(handle_.channel().get()); + handle_closed_wait_.set_trigger(ZX_CHANNEL_PEER_CLOSED); + handle_closed_wait_.set_handler( + [this](auto*, auto*, zx_status_t status, const auto*) { + // Don't do anything if the wait was explicitly canceled by us. + if (status != ZX_ERR_CANCELED) { + bt_log(TRACE, LOG_TAG, "AdvertisingHandle closed"); + instance_.reset(); + } + }); + + zx_status_t status = + handle_closed_wait_.Begin(async_get_default_dispatcher()); + if (status != ZX_OK) { + bt_log(DEBUG, + LOG_TAG, + "failed to begin wait on AdvertisingHandle: %s", + zx_status_get_string(status)); + } + return status; +} + +LowEnergyPeripheralServer::LowEnergyPeripheralServer( + bt::gap::Adapter::WeakPtr adapter, + bt::gatt::GATT::WeakPtr gatt, + fidl::InterfaceRequest request) + : AdapterServerBase(std::move(adapter), this, std::move(request)), + gatt_(std::move(gatt)), + weak_self_(this) {} + +LowEnergyPeripheralServer::~LowEnergyPeripheralServer() { + BT_ASSERT(adapter()->bredr()); +} + +void LowEnergyPeripheralServer::Advertise( + fble::AdvertisingParameters parameters, + fidl::InterfaceHandle + advertised_peripheral, + AdvertiseCallback callback) { + // Advertise and StartAdvertising may not be used simultaneously. + if (advertisement_deprecated_.has_value()) { + callback(fpromise::error(fble::PeripheralError::FAILED)); + return; + } + + // TODO(fxbug.dev/42156474): As a temporary hack until multiple advertisements + // is supported, don't allow more than one advertisement. The current behavior + // of hci::LegacyLowEnergyAdvertiser is to replace the current advertisement, + // which is not the intended behavior of `Advertise`. NOTE: This is + // insufficient when there are multiple Peripheral clients advertising, but + // that is the status quo with `StartAdvertising` anyway (the last advertiser + // wins). + if (!advertisements_.empty()) { + callback(fpromise::error(fble::PeripheralError::FAILED)); + return; + } + + AdvertisementInstanceId instance_id = next_advertisement_instance_id_++; + auto [iter, inserted] = + advertisements_.try_emplace(instance_id, + this, + instance_id, + std::move(parameters), + std::move(advertised_peripheral), + std::move(callback)); + BT_ASSERT(inserted); + iter->second.StartAdvertising(); +} + +void LowEnergyPeripheralServer::StartAdvertising( + fble::AdvertisingParameters parameters, + ::fidl::InterfaceRequest token, + StartAdvertisingCallback callback) { + fble::Peripheral_StartAdvertising_Result result; + + // Advertise and StartAdvertising may not be used simultaneously. + if (!advertisements_.empty()) { + result.set_err(fble::PeripheralError::INVALID_PARAMETERS); + callback(std::move(result)); + return; + } + + if (!token) { + result.set_err(fble::PeripheralError::INVALID_PARAMETERS); + callback(std::move(result)); + return; + } + + if (advertisement_deprecated_) { + bt_log(DEBUG, LOG_TAG, "reconfigure existing advertising instance"); + advertisement_deprecated_.reset(); + } + + // Create an entry to mark that the request is in progress. + advertisement_deprecated_.emplace(std::move(token)); + + auto self = weak_self_.GetWeakPtr(); + auto status_cb = [self, callback = std::move(callback), func = __FUNCTION__]( + auto instance, bt::hci::Result<> status) { + // Advertising will be stopped when |instance| gets destroyed. + if (!self.is_alive()) { + return; + } + + BT_ASSERT(self->advertisement_deprecated_); + BT_ASSERT(self->advertisement_deprecated_->id() == + bt::gap::kInvalidAdvertisementId); + + fble::Peripheral_StartAdvertising_Result result; + if (status.is_error()) { + bt_log(WARN, + LOG_TAG, + "%s: failed to start advertising (status: %s)", + func, + bt_str(status)); + + result.set_err(FidlErrorFromStatus(status)); + + // The only scenario in which it is valid to leave |advertisement_| intact + // in a failure scenario is if StartAdvertising was called while a + // previous call was in progress. This aborts the prior request causing it + // to end with the "kCanceled" status. This means that another request is + // currently progress. + if (!status.error_value().is(bt::HostError::kCanceled)) { + self->advertisement_deprecated_.reset(); + } + + callback(std::move(result)); + return; + } + + zx_status_t ecode = + self->advertisement_deprecated_->Register(std::move(instance)); + if (ecode != ZX_OK) { + result.set_err(fble::PeripheralError::FAILED); + self->advertisement_deprecated_.reset(); + callback(std::move(result)); + return; + } + + result.set_response({}); + callback(std::move(result)); + }; + + StartAdvertisingInternal(parameters, std::move(status_cb)); +} + +const bt::gap::LowEnergyConnectionHandle* +LowEnergyPeripheralServer::FindConnectionForTesting(bt::PeerId id) const { + auto connections_iter = std::find_if( + connections_.begin(), connections_.end(), [id](const auto& conn) { + return conn.second->conn()->peer_identifier() == id; + }); + if (connections_iter != connections_.end()) { + return connections_iter->second->conn(); + } + return nullptr; +} + +void LowEnergyPeripheralServer::OnConnectedDeprecated( + bt::gap::AdvertisementId advertisement_id, + bt::gap::Adapter::LowEnergy::ConnectionResult result) { + BT_ASSERT(advertisement_id != bt::gap::kInvalidAdvertisementId); + + // Abort connection procedure if advertisement was canceled by the client. + if (!advertisement_deprecated_ || + advertisement_deprecated_->id() != advertisement_id) { + bt_log( + INFO, + LOG_TAG, + "dropping connection to canceled advertisement (advertisement id: %s)", + bt_str(advertisement_id)); + return; + } + + zx::channel local, remote; + zx_status_t status = zx::channel::create(0, &local, &remote); + if (status != ZX_OK) { + bt_log(ERROR, + LOG_TAG, + "failed to create channel for Connection (status: %s)", + zx_status_get_string(status)); + return; + } + + if (result.is_error()) { + bt_log(INFO, + LOG_TAG, + "incoming connection to advertisement failed (advertisement id: %s)", + bt_str(advertisement_id)); + return; + } + + auto conn = std::move(result).value(); + auto peer_id = conn->peer_identifier(); + auto* peer = adapter()->peer_cache()->FindById(peer_id); + BT_ASSERT(peer); + + bt_log(INFO, + LOG_TAG, + "central connected (peer: %s, advertisement id: %s)", + bt_str(peer->identifier()), + bt_str(advertisement_id)); + + fidl::InterfaceHandle conn_handle = + CreateConnectionServer(std::move(conn)); + + binding()->events().OnPeerConnected(fidl_helpers::PeerToFidlLe(*peer), + std::move(conn_handle)); + advertisement_deprecated_.reset(); +} + +fidl::InterfaceHandle +LowEnergyPeripheralServer::CreateConnectionServer( + std::unique_ptr connection) { + zx::channel local, remote; + zx_status_t status = zx::channel::create(0, &local, &remote); + BT_ASSERT(status == ZX_OK); + + auto conn_server_id = next_connection_server_id_++; + auto conn_server = std::make_unique( + adapter(), + gatt_, + std::move(connection), + std::move(local), + [this, conn_server_id] { + bt_log(INFO, LOG_TAG, "connection closed"); + connections_.erase(conn_server_id); + }); + connections_[conn_server_id] = std::move(conn_server); + + return fidl::InterfaceHandle(std::move(remote)); +} + +void LowEnergyPeripheralServer::StartAdvertisingInternal( + fuchsia::bluetooth::le::AdvertisingParameters& parameters, + bt::gap::Adapter::LowEnergy::AdvertisingStatusCallback status_cb, + std::optional advertisement_instance) { + bt::AdvertisingData adv_data; + bool include_tx_power_level = false; + if (parameters.has_data()) { + auto maybe_adv_data = + fidl_helpers::AdvertisingDataFromFidl(parameters.data()); + if (!maybe_adv_data) { + bt_log(WARN, LOG_TAG, "invalid advertising data"); + status_cb({}, ToResult(bt::HostError::kInvalidParameters)); + return; + } + + adv_data = std::move(*maybe_adv_data); + if (parameters.data().has_include_tx_power_level() && + parameters.data().include_tx_power_level()) { + bt_log(TRACE, + LOG_TAG, + "Including TX Power level in advertising data at HCI layer"); + include_tx_power_level = true; + } + } + + bt::AdvertisingData scan_rsp; + if (parameters.has_scan_response()) { + auto maybe_scan_rsp = + fidl_helpers::AdvertisingDataFromFidl(parameters.scan_response()); + if (!maybe_scan_rsp) { + bt_log(WARN, LOG_TAG, "invalid scan response in advertising data"); + status_cb({}, ToResult(bt::HostError::kInvalidParameters)); + return; + } + scan_rsp = std::move(*maybe_scan_rsp); + } + + fble::AdvertisingModeHint mode_hint = fble::AdvertisingModeHint::SLOW; + if (parameters.has_mode_hint()) { + mode_hint = parameters.mode_hint(); + } + bt::gap::AdvertisingInterval interval = + fidl_helpers::AdvertisingIntervalFromFidl(mode_hint); + + std::optional + connectable_params; + + // Per the API contract of `AdvertisingParameters` FIDL, if + // `connection_options` is present or the deprecated `connectable` parameter + // is true, advertisements will be connectable. `connectable_parameter` was + // the predecessor of `connection_options` and + // TODO(fxbug.dev/42121197): will be removed once all consumers of it have + // migrated to `connection_options`. + bool connectable = parameters.has_connection_options() || + (parameters.has_connectable() && parameters.connectable()); + if (connectable) { + connectable_params.emplace(); + + auto self = weak_self_.GetWeakPtr(); + connectable_params->connection_cb = + [self, advertisement_instance]( + bt::gap::AdvertisementId advertisement_id, + bt::gap::Adapter::LowEnergy::ConnectionResult result) { + if (!self.is_alive()) { + return; + } + + // Handle connection for deprecated StartAdvertising method. + if (!advertisement_instance) { + self->OnConnectedDeprecated(advertisement_id, std::move(result)); + return; + } + + auto advertisement_iter = + self->advertisements_.find(*advertisement_instance); + if (advertisement_iter == self->advertisements_.end()) { + if (result.is_ok()) { + bt_log(DEBUG, + LOG_TAG, + "releasing connection handle for canceled advertisement " + "(peer: %s)", + bt_str(result.value()->peer_identifier())); + result.value()->Release(); + } + return; + } + advertisement_iter->second.OnConnected(advertisement_id, + std::move(result)); + }; + + // Per the API contract of the `ConnectionOptions` FIDL, the bondable mode + // of the connection defaults to bondable mode unless the + // `connection_options` table exists and `bondable_mode` is explicitly set + // to false. + connectable_params->bondable_mode = + (!parameters.has_connection_options() || + !parameters.connection_options().has_bondable_mode() || + parameters.connection_options().bondable_mode()) + ? BondableMode::Bondable + : BondableMode::NonBondable; + } + + bool extended_pdu = false; + if (parameters.has_advertising_procedure()) { + extended_pdu = parameters.advertising_procedure().is_extended(); + } + + BT_ASSERT(adapter()->le()); + adapter()->le()->StartAdvertising(std::move(adv_data), + std::move(scan_rsp), + interval, + extended_pdu, + /*anonymous=*/false, + include_tx_power_level, + std::move(connectable_params), + std::move(status_cb)); +} + +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_peripheral_server_test.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_peripheral_server_test.cc new file mode 100644 index 0000000000..2851111724 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_peripheral_server_test.cc @@ -0,0 +1,1093 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_peripheral_server.h" + +#include "fuchsia/bluetooth/cpp/fidl.h" +#include "fuchsia/bluetooth/le/cpp/fidl.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/adapter_test_fixture.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/fake_adapter_test_fixture.h" +#include "pw_bluetooth_sapphire/internal/host/common/advertising_data.h" +#include "pw_bluetooth_sapphire/internal/host/common/byte_buffer.h" +#include "pw_bluetooth_sapphire/internal/host/gap/low_energy_advertising_manager.h" +#include "pw_bluetooth_sapphire/internal/host/gap/low_energy_connection_manager.h" +#include "pw_bluetooth_sapphire/internal/host/testing/fake_peer.h" +#include "pw_bluetooth_sapphire/internal/host/testing/test_helpers.h" + +namespace bthost { +namespace { + +bool IsChannelPeerClosed(const zx::channel& channel) { + zx_signals_t ignored; + return ZX_OK == channel.wait_one(/*signals=*/ZX_CHANNEL_PEER_CLOSED, + /*deadline=*/zx::time(ZX_TIME_INFINITE_PAST), + &ignored); +} + +namespace fble = fuchsia::bluetooth::le; +const bt::DeviceAddress kTestAddr(bt::DeviceAddress::Type::kLEPublic, + {0x01, 0, 0, 0, 0, 0}); +const bt::DeviceAddress kTestAddr2(bt::DeviceAddress::Type::kLEPublic, + {0x02, 0, 0, 0, 0, 0}); + +using bt::testing::FakePeer; +using FidlAdvHandle = fidl::InterfaceHandle; + +class LowEnergyPeripheralServerTestFakeAdapter + : public bt::fidl::testing::FakeAdapterTestFixture { + public: + LowEnergyPeripheralServerTestFakeAdapter() = default; + ~LowEnergyPeripheralServerTestFakeAdapter() override = default; + + void SetUp() override { + bt::fidl::testing::FakeAdapterTestFixture::SetUp(); + + fake_gatt_ = + std::make_unique(pw_dispatcher()); + + // Create a LowEnergyPeripheralServer and bind it to a local client. + fidl::InterfaceHandle handle; + server_ = std::make_unique( + adapter()->AsWeakPtr(), fake_gatt_->GetWeakPtr(), handle.NewRequest()); + peripheral_client_.Bind(std::move(handle)); + } + + void TearDown() override { + RunLoopUntilIdle(); + + peripheral_client_ = nullptr; + server_ = nullptr; + bt::fidl::testing::FakeAdapterTestFixture::TearDown(); + } + + LowEnergyPeripheralServer* server() const { return server_.get(); } + + void SetOnPeerConnectedCallback( + fble::Peripheral::OnPeerConnectedCallback cb) { + peripheral_client_.events().OnPeerConnected = std::move(cb); + } + + private: + std::unique_ptr server_; + fble::PeripheralPtr peripheral_client_; + std::unique_ptr fake_gatt_; + BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE( + LowEnergyPeripheralServerTestFakeAdapter); +}; + +class LowEnergyPeripheralServerTest + : public bthost::testing::AdapterTestFixture { + public: + LowEnergyPeripheralServerTest() = default; + ~LowEnergyPeripheralServerTest() override = default; + + void SetUp() override { + AdapterTestFixture::SetUp(); + + fake_gatt_ = + std::make_unique(pw_dispatcher()); + + // Create a LowEnergyPeripheralServer and bind it to a local client. + fidl::InterfaceHandle handle; + server_ = std::make_unique( + adapter(), fake_gatt_->GetWeakPtr(), handle.NewRequest()); + peripheral_client_.Bind(std::move(handle)); + } + + void TearDown() override { + RunLoopUntilIdle(); + + peripheral_client_ = nullptr; + server_ = nullptr; + AdapterTestFixture::TearDown(); + } + + LowEnergyPeripheralServer* server() const { return server_.get(); } + + void SetOnPeerConnectedCallback( + fble::Peripheral::OnPeerConnectedCallback cb) { + peripheral_client_.events().OnPeerConnected = std::move(cb); + } + + private: + std::unique_ptr server_; + fble::PeripheralPtr peripheral_client_; + std::unique_ptr fake_gatt_; + BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(LowEnergyPeripheralServerTest); +}; + +class BoolParam : public LowEnergyPeripheralServerTest, + public ::testing::WithParamInterface {}; + +class FakeAdvertisedPeripheral : public ServerBase { + public: + struct Connection { + fble::Peer peer; + fble::ConnectionHandle connection; + OnConnectedCallback callback; + }; + + FakeAdvertisedPeripheral( + fidl::InterfaceRequest request) + : ServerBase(this, std::move(request)) {} + + void Unbind() { binding()->Unbind(); } + + void OnConnected(fble::Peer peer, + fidl::InterfaceHandle connection, + OnConnectedCallback callback) override { + connections_.push_back( + {std::move(peer), std::move(connection), std::move(callback)}); + } + + std::optional last_connected_peer() const { + if (connections_.empty()) { + return std::nullopt; + } + return bt::PeerId(connections_.back().peer.id().value); + } + + std::vector& connections() { return connections_; } + + private: + std::vector connections_; +}; + +// Tests that aborting a StartAdvertising command sequence does not cause a +// crash in successive requests. +TEST_F(LowEnergyPeripheralServerTest, + StartAdvertisingWhilePendingDoesNotCrash) { + fble::AdvertisingParameters params1, params2, params3; + FidlAdvHandle token1, token2, token3; + + std::optional> result1, result2, + result3; + server()->StartAdvertising(std::move(params1), + token1.NewRequest(), + [&](auto result) { result1 = std::move(result); }); + server()->StartAdvertising(std::move(params2), + token2.NewRequest(), + [&](auto result) { result2 = std::move(result); }); + server()->StartAdvertising(std::move(params3), + token3.NewRequest(), + [&](auto result) { result3 = std::move(result); }); + RunLoopUntilIdle(); + + ASSERT_TRUE(result1); + ASSERT_TRUE(result2); + ASSERT_TRUE(result3); + EXPECT_TRUE(result1->is_error()); + EXPECT_EQ(fble::PeripheralError::ABORTED, result1->error()); + EXPECT_TRUE(result2->is_error()); + EXPECT_EQ(fble::PeripheralError::ABORTED, result2->error()); + EXPECT_TRUE(result3->is_ok()); +} + +// Same as the test above but tests that an error status leaves the server in +// the expected state. +TEST_F(LowEnergyPeripheralServerTest, + StartAdvertisingWhilePendingDoesNotCrashWithControllerError) { + test_device()->SetDefaultResponseStatus( + bt::hci_spec::kLESetAdvertisingEnable, + pw::bluetooth::emboss::StatusCode::COMMAND_DISALLOWED); + fble::AdvertisingParameters params1, params2, params3, params4; + FidlAdvHandle token1, token2, token3, token4; + + std::optional> result1, result2, + result3, result4; + server()->StartAdvertising(std::move(params1), + token1.NewRequest(), + [&](auto result) { result1 = std::move(result); }); + server()->StartAdvertising(std::move(params2), + token2.NewRequest(), + [&](auto result) { result2 = std::move(result); }); + server()->StartAdvertising(std::move(params3), + token3.NewRequest(), + [&](auto result) { result3 = std::move(result); }); + RunLoopUntilIdle(); + + ASSERT_TRUE(result1); + ASSERT_TRUE(result2); + ASSERT_TRUE(result3); + EXPECT_TRUE(result1->is_error()); + EXPECT_EQ(fble::PeripheralError::ABORTED, result1->error()); + EXPECT_TRUE(result2->is_error()); + EXPECT_EQ(fble::PeripheralError::ABORTED, result2->error()); + EXPECT_TRUE(result3->is_error()); + EXPECT_EQ(fble::PeripheralError::FAILED, result3->error()); + + // The next request should succeed as normal. + test_device()->ClearDefaultResponseStatus( + bt::hci_spec::kLESetAdvertisingEnable); + server()->StartAdvertising(std::move(params4), + token4.NewRequest(), + [&](auto result) { result4 = std::move(result); }); + RunLoopUntilIdle(); + + ASSERT_TRUE(result4); + EXPECT_TRUE(result4->is_ok()); +} + +TEST_F(LowEnergyPeripheralServerTest, + AdvertiseWhilePendingDoesNotCrashWithControllerError) { + test_device()->SetDefaultResponseStatus( + bt::hci_spec::kLESetAdvertisingEnable, + pw::bluetooth::emboss::StatusCode::COMMAND_DISALLOWED); + fble::AdvertisingParameters params1, params2, params3, params4; + + fble::AdvertisedPeripheralHandle adv_peripheral_handle_1; + FakeAdvertisedPeripheral adv_peripheral_server_1( + adv_peripheral_handle_1.NewRequest()); + fble::AdvertisedPeripheralHandle adv_peripheral_handle_2; + FakeAdvertisedPeripheral adv_peripheral_server_2( + adv_peripheral_handle_2.NewRequest()); + fble::AdvertisedPeripheralHandle adv_peripheral_handle_3; + FakeAdvertisedPeripheral adv_peripheral_server_3( + adv_peripheral_handle_3.NewRequest()); + + std::optional> result1, result2, + result3, result4; + server()->Advertise(std::move(params1), + std::move(adv_peripheral_handle_1), + [&](auto result) { result1 = std::move(result); }); + server()->Advertise(std::move(params2), + std::move(adv_peripheral_handle_2), + [&](auto result) { result2 = std::move(result); }); + server()->Advertise(std::move(params3), + std::move(adv_peripheral_handle_3), + [&](auto result) { result3 = std::move(result); }); + RunLoopUntilIdle(); + ASSERT_TRUE(result1); + ASSERT_TRUE(result2); + ASSERT_TRUE(result3); + EXPECT_TRUE(result1->is_error()); + EXPECT_EQ(fble::PeripheralError::FAILED, result1->error()); + EXPECT_TRUE(result2->is_error()); + EXPECT_EQ(fble::PeripheralError::FAILED, result2->error()); + EXPECT_TRUE(result3->is_error()); + EXPECT_EQ(fble::PeripheralError::FAILED, result3->error()); + + // The next request should succeed as normal. + test_device()->ClearDefaultResponseStatus( + bt::hci_spec::kLESetAdvertisingEnable); + + fble::AdvertisedPeripheralHandle adv_peripheral_handle_4; + FakeAdvertisedPeripheral adv_peripheral_server_4( + adv_peripheral_handle_4.NewRequest()); + server()->Advertise(std::move(params4), + std::move(adv_peripheral_handle_4), + [&](auto result) { result4 = std::move(result); }); + RunLoopUntilIdle(); + EXPECT_FALSE(result4); + adv_peripheral_server_4.Unbind(); + RunLoopUntilIdle(); + EXPECT_TRUE(result4); +} + +TEST_F(LowEnergyPeripheralServerTest, + StartAdvertisingNoConnectionRelatedParamsNoConnection) { + fble::Peer peer; + // `conn` is stored so the bondable mode of the connection resulting from + // `OnPeerConnected` can be checked. The connection would otherwise be dropped + // immediately after `ConnectLowEnergy`. + fidl::InterfaceHandle conn; + auto peer_connected_cb = [&](auto cb_peer, auto cb_conn) { + peer = std::move(cb_peer); + conn = std::move(cb_conn); + }; + SetOnPeerConnectedCallback(peer_connected_cb); + + fble::AdvertisingParameters params; + + FidlAdvHandle token; + + std::optional> result; + server()->StartAdvertising( + std::move(params), token.NewRequest(), [&](auto cb_result) { + result = std::move(cb_result); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(result.has_value()); + ASSERT_FALSE(result->is_error()); + + test_device()->AddPeer( + std::make_unique(kTestAddr, pw_dispatcher())); + test_device()->ConnectLowEnergy(kTestAddr); + RunLoopUntilIdle(); + + ASSERT_FALSE(peer.has_id()); + ASSERT_FALSE(conn.is_valid()); +} + +TEST_F(LowEnergyPeripheralServerTest, + AdvertiseNoConnectionRelatedParamsNoConnection) { + fble::AdvertisedPeripheralHandle adv_peripheral_handle; + FakeAdvertisedPeripheral adv_peripheral_server( + adv_peripheral_handle.NewRequest()); + fble::AdvertisingParameters params; + std::optional> result; + server()->Advertise(std::move(params), + std::move(adv_peripheral_handle), + [&](auto cb_result) { result = std::move(cb_result); }); + RunLoopUntilIdle(); + EXPECT_FALSE(result.has_value()); + + test_device()->AddPeer( + std::make_unique(kTestAddr, pw_dispatcher())); + test_device()->ConnectLowEnergy(kTestAddr); + RunLoopUntilIdle(); + EXPECT_FALSE(adv_peripheral_server.last_connected_peer()); + adv_peripheral_server.Unbind(); + RunLoopUntilIdle(); + EXPECT_TRUE(result); +} + +TEST_F(LowEnergyPeripheralServerTest, + StartAdvertisingConnectableParameterTrueConnectsBondable) { + fble::Peer peer; + // `conn` is stored so the bondable mode of the connection resulting from + // `OnPeerConnected` can be checked. The connection would otherwise be dropped + // immediately after `ConnectLowEnergy`. + fidl::InterfaceHandle conn; + auto peer_connected_cb = [&](auto cb_peer, auto cb_conn) { + peer = std::move(cb_peer); + conn = std::move(cb_conn); + }; + SetOnPeerConnectedCallback(peer_connected_cb); + + fble::AdvertisingParameters params; + params.set_connectable(true); + + FidlAdvHandle token; + + std::optional> result; + server()->StartAdvertising( + std::move(params), token.NewRequest(), [&](auto cb_result) { + result = std::move(cb_result); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(result.has_value()); + ASSERT_FALSE(result->is_error()); + + test_device()->AddPeer( + std::make_unique(kTestAddr, pw_dispatcher())); + test_device()->ConnectLowEnergy(kTestAddr); + RunLoopUntilIdle(); + + ASSERT_TRUE(peer.has_id()); + ASSERT_TRUE(conn.is_valid()); + + auto connected_id = bt::PeerId(peer.id().value); + const bt::gap::LowEnergyConnectionHandle* conn_handle = + server()->FindConnectionForTesting(connected_id); + + ASSERT_TRUE(conn_handle); + ASSERT_EQ(conn_handle->bondable_mode(), bt::sm::BondableMode::Bondable); +} + +TEST_F(LowEnergyPeripheralServerTest, + StartAdvertisingEmptyConnectionOptionsConnectsBondable) { + fble::Peer peer; + // `conn` is stored so the bondable mode of the connection resulting from + // `OnPeerConnected` can be checked. The connection would otherwise be dropped + // immediately after `ConnectLowEnergy`. + fidl::InterfaceHandle conn; + auto peer_connected_cb = [&](auto cb_peer, auto cb_conn) { + peer = std::move(cb_peer); + conn = std::move(cb_conn); + }; + SetOnPeerConnectedCallback(peer_connected_cb); + + fble::AdvertisingParameters params; + fble::ConnectionOptions conn_opts; + params.set_connection_options(std::move(conn_opts)); + + FidlAdvHandle token; + + std::optional> result; + server()->StartAdvertising( + std::move(params), token.NewRequest(), [&](auto cb_result) { + result = std::move(cb_result); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(result.has_value()); + ASSERT_FALSE(result->is_error()); + + test_device()->AddPeer( + std::make_unique(kTestAddr, pw_dispatcher())); + test_device()->ConnectLowEnergy(kTestAddr); + RunLoopUntilIdle(); + + ASSERT_TRUE(peer.has_id()); + ASSERT_TRUE(conn.is_valid()); + + auto connected_id = bt::PeerId(peer.id().value); + const bt::gap::LowEnergyConnectionHandle* conn_handle = + server()->FindConnectionForTesting(connected_id); + + ASSERT_TRUE(conn_handle); + ASSERT_EQ(conn_handle->bondable_mode(), bt::sm::BondableMode::Bondable); +} + +TEST_F(LowEnergyPeripheralServerTest, + AdvertiseEmptyConnectionOptionsConnectsBondable) { + fble::AdvertisedPeripheralHandle adv_peripheral_handle; + FakeAdvertisedPeripheral adv_peripheral_server( + adv_peripheral_handle.NewRequest()); + + fble::AdvertisingParameters params; + fble::ConnectionOptions conn_opts; + params.set_connection_options(std::move(conn_opts)); + + std::optional> result; + server()->Advertise(std::move(params), + std::move(adv_peripheral_handle), + [&](auto cb_result) { result = std::move(cb_result); }); + RunLoopUntilIdle(); + EXPECT_FALSE(result.has_value()); + + test_device()->AddPeer( + std::make_unique(kTestAddr, pw_dispatcher())); + test_device()->ConnectLowEnergy(kTestAddr); + RunLoopUntilIdle(); + auto connected_id = adv_peripheral_server.last_connected_peer(); + ASSERT_TRUE(connected_id); + + const bt::gap::LowEnergyConnectionHandle* conn_handle = + server()->FindConnectionForTesting(*connected_id); + ASSERT_TRUE(conn_handle); + ASSERT_EQ(conn_handle->bondable_mode(), bt::sm::BondableMode::Bondable); + + adv_peripheral_server.Unbind(); + RunLoopUntilIdle(); + EXPECT_TRUE(result.has_value()); +} + +TEST_P(BoolParam, AdvertiseBondableOrNonBondableConnectsBondableOrNonBondable) { + const bool bondable = GetParam(); + + fble::AdvertisedPeripheralHandle adv_peripheral_handle; + FakeAdvertisedPeripheral adv_peripheral_server( + adv_peripheral_handle.NewRequest()); + + fble::AdvertisingParameters params; + fble::ConnectionOptions conn_opts; + conn_opts.set_bondable_mode(bondable); + params.set_connection_options(std::move(conn_opts)); + + std::optional> result; + server()->Advertise(std::move(params), + std::move(adv_peripheral_handle), + [&](auto cb_result) { result = std::move(cb_result); }); + RunLoopUntilIdle(); + EXPECT_FALSE(result.has_value()); + + test_device()->AddPeer( + std::make_unique(kTestAddr, pw_dispatcher())); + test_device()->ConnectLowEnergy(kTestAddr); + RunLoopUntilIdle(); + auto connected_id = adv_peripheral_server.last_connected_peer(); + ASSERT_TRUE(connected_id); + + const bt::gap::LowEnergyConnectionHandle* conn_handle = + server()->FindConnectionForTesting(*connected_id); + ASSERT_TRUE(conn_handle); + EXPECT_EQ(conn_handle->bondable_mode(), + bondable ? bt::sm::BondableMode::Bondable + : bt::sm::BondableMode::NonBondable); + + adv_peripheral_server.Unbind(); + RunLoopUntilIdle(); +} + +TEST_P(BoolParam, + StartAdvertisingBondableOrNonBondableConnectsBondableOrNonBondable) { + const bool bondable = GetParam(); + + fble::Peer peer; + // `conn` is stored so the bondable mode of the connection resulting from + // `OnPeerConnected` can be checked. The connection would otherwise be dropped + // immediately after `ConnectLowEnergy`. + fidl::InterfaceHandle conn; + auto peer_connected_cb = [&](auto cb_peer, auto cb_conn) { + peer = std::move(cb_peer); + conn = std::move(cb_conn); + }; + SetOnPeerConnectedCallback(peer_connected_cb); + + fble::AdvertisingParameters params; + fble::ConnectionOptions conn_opts; + conn_opts.set_bondable_mode(bondable); + params.set_connection_options(std::move(conn_opts)); + + FidlAdvHandle token; + + std::optional> result; + server()->StartAdvertising( + std::move(params), token.NewRequest(), [&](auto cb_result) { + result = std::move(cb_result); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(result); + ASSERT_FALSE(result->is_error()); + + test_device()->AddPeer( + std::make_unique(kTestAddr, pw_dispatcher())); + test_device()->ConnectLowEnergy(kTestAddr); + RunLoopUntilIdle(); + + ASSERT_TRUE(peer.has_id()); + ASSERT_TRUE(conn.is_valid()); + + auto connected_id = bt::PeerId(peer.id().value); + const bt::gap::LowEnergyConnectionHandle* conn_handle = + server()->FindConnectionForTesting(connected_id); + + ASSERT_TRUE(conn_handle); + EXPECT_EQ(conn_handle->bondable_mode(), + bondable ? bt::sm::BondableMode::Bondable + : bt::sm::BondableMode::NonBondable); +} + +TEST_F(LowEnergyPeripheralServerTest, + RestartStartAdvertisingDuringInboundConnKeepsNewAdvAlive) { + fble::Peer peer; + // `conn` is stored so that the connection is not dropped immediately after + // connection. + fidl::InterfaceHandle conn; + auto peer_connected_cb = [&](auto cb_peer, auto cb_conn) { + peer = std::move(cb_peer); + conn = std::move(cb_conn); + }; + SetOnPeerConnectedCallback(peer_connected_cb); + + FidlAdvHandle first_token, second_token; + + fble::AdvertisingParameters params; + params.set_connectable(true); + std::optional> result; + server()->StartAdvertising( + std::move(params), first_token.NewRequest(), [&](auto cb_result) { + result = std::move(cb_result); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(result.has_value()); + EXPECT_TRUE(result->is_ok()); + + fit::closure complete_interrogation; + // Hang interrogation so we can control when the inbound connection procedure + // completes. + test_device()->pause_responses_for_opcode( + bt::hci_spec::kReadRemoteVersionInfo, [&](fit::closure trigger) { + complete_interrogation = std::move(trigger); + }); + + test_device()->AddPeer( + std::make_unique(kTestAddr, pw_dispatcher())); + test_device()->ConnectLowEnergy(kTestAddr); + RunLoopUntilIdle(); + + EXPECT_FALSE(peer.has_id()); + EXPECT_FALSE(conn.is_valid()); + // test_device()->ConnectLowEnergy caused interrogation as part of the inbound + // GAP connection process, so this closure should be filled in. + ASSERT_TRUE(complete_interrogation); + + // Hang the SetAdvertisingParameters HCI command so we can invoke the + // advertising status callback after connection completion. + fit::closure complete_start_advertising; + test_device()->pause_responses_for_opcode( + bt::hci_spec::kLESetAdvertisingParameters, [&](fit::closure trigger) { + complete_start_advertising = std::move(trigger); + }); + + // Restart advertising during inbound connection, simulating the race seen in + // https://fxbug.dev/42152329. + result = std::nullopt; + server()->StartAdvertising( + fble::AdvertisingParameters{}, + second_token.NewRequest(), + [&](auto cb_result) { result = std::move(cb_result); }); + RunLoopUntilIdle(); + ASSERT_TRUE(complete_start_advertising); + // Advertising shouldn't complete until we trigger the above closure + EXPECT_FALSE(result.has_value()); + // The first AdvertisingHandle should be closed, as we have started a second + // advertisement. + EXPECT_TRUE(IsChannelPeerClosed(first_token.channel())); + + // Allow interrogation to complete, enabling the connection process to + // proceed. + complete_interrogation(); + RunLoopUntilIdle(); + // Connection should have been dropped after completing because first + // advertisement was canceled. + EXPECT_FALSE(peer.has_id()); + EXPECT_FALSE(conn.is_valid()); + + // Allow second StartAdvertising to complete. + complete_start_advertising(); + RunLoopUntilIdle(); + ASSERT_TRUE(result.has_value()); + EXPECT_TRUE(result->is_ok()); + // The second advertising handle should still be active. + EXPECT_FALSE(IsChannelPeerClosed(second_token.channel())); +} + +// Ensures that a connection to a canceled advertisement received after the +// advertisement is canceled doesn't end or get sent to a new +// AdvertisedPeripheral. +TEST_F(LowEnergyPeripheralServerTest, + RestartAdvertiseDuringInboundConnKeepsNewAdvAlive) { + fble::AdvertisedPeripheralHandle adv_peripheral_handle_0; + FakeAdvertisedPeripheral adv_peripheral_server_0( + adv_peripheral_handle_0.NewRequest()); + + fble::AdvertisingParameters params; + params.set_connectable(true); + std::optional> result; + server()->Advertise(std::move(params), + std::move(adv_peripheral_handle_0), + [&](auto cb_result) { result = std::move(cb_result); }); + RunLoopUntilIdle(); + EXPECT_FALSE(result.has_value()); + + fit::closure complete_interrogation; + // Hang interrogation so we can control when the inbound connection procedure + // completes. + test_device()->pause_responses_for_opcode( + bt::hci_spec::kReadRemoteVersionInfo, [&](fit::closure trigger) { + complete_interrogation = std::move(trigger); + }); + + test_device()->AddPeer( + std::make_unique(kTestAddr, pw_dispatcher())); + test_device()->ConnectLowEnergy(kTestAddr); + RunLoopUntilIdle(); + EXPECT_FALSE(adv_peripheral_server_0.last_connected_peer()); + // test_device()->ConnectLowEnergy caused interrogation as part of the inbound + // GAP connection process, so this closure should be filled in. + ASSERT_TRUE(complete_interrogation); + + // Cancel the first advertisement. + adv_peripheral_server_0.Unbind(); + RunLoopUntilIdle(); + ASSERT_TRUE(result.has_value()); + EXPECT_TRUE(result->is_ok()); + + // Hang the SetAdvertisingParameters HCI command so we can invoke the + // advertising status callback of the second advertising request after + // connection completion. + fit::closure complete_start_advertising; + test_device()->pause_responses_for_opcode( + bt::hci_spec::kLESetAdvertisingParameters, [&](fit::closure trigger) { + complete_start_advertising = std::move(trigger); + }); + + // Restart advertising during inbound connection, simulating the race seen in + // https://fxbug.dev/42152329. + fble::AdvertisedPeripheralHandle adv_peripheral_handle_1; + FakeAdvertisedPeripheral adv_peripheral_server_1( + adv_peripheral_handle_1.NewRequest()); + bool server_1_closed = false; + adv_peripheral_server_1.set_error_handler( + [&](auto) { server_1_closed = true; }); + result = std::nullopt; + server()->Advertise(fble::AdvertisingParameters{}, + std::move(adv_peripheral_handle_1), + [&](auto cb_result) { result = std::move(cb_result); }); + RunLoopUntilIdle(); + ASSERT_TRUE(complete_start_advertising); + EXPECT_FALSE(result.has_value()); + + // Allow interrogation to complete, enabling the connection process to + // proceed. + complete_interrogation(); + RunLoopUntilIdle(); + // The connection should have been dropped. + EXPECT_FALSE(adv_peripheral_server_1.last_connected_peer()); + EXPECT_FALSE(adv_peripheral_server_0.last_connected_peer()); + + // Allow second Advertise to complete. + complete_start_advertising(); + RunLoopUntilIdle(); + EXPECT_FALSE(result.has_value()); + EXPECT_FALSE(server_1_closed); + EXPECT_FALSE(adv_peripheral_server_1.last_connected_peer()); + + adv_peripheral_server_1.Unbind(); + RunLoopUntilIdle(); + EXPECT_TRUE(result.has_value()); +} + +TEST_F(LowEnergyPeripheralServerTestFakeAdapter, + StartAdvertisingWithIncludeTxPowerSetToTrue) { + fble::AdvertisingParameters params; + fble::AdvertisingData adv_data; + adv_data.set_include_tx_power_level(true); + params.set_data(std::move(adv_data)); + + FidlAdvHandle token; + + server()->StartAdvertising( + std::move(params), token.NewRequest(), [&](auto) {}); + RunLoopUntilIdle(); + ASSERT_EQ(adapter()->fake_le()->registered_advertisements().size(), 1u); + EXPECT_TRUE(adapter() + ->fake_le() + ->registered_advertisements() + .begin() + ->second.include_tx_power_level); +} + +TEST_F(LowEnergyPeripheralServerTestFakeAdapter, + AdvertiseWithIncludeTxPowerSetToTrue) { + fble::AdvertisingParameters params; + fble::AdvertisingData adv_data; + adv_data.set_include_tx_power_level(true); + params.set_data(std::move(adv_data)); + + fble::AdvertisedPeripheralHandle adv_peripheral_handle; + FakeAdvertisedPeripheral adv_peripheral_server( + adv_peripheral_handle.NewRequest()); + + std::optional> result; + server()->Advertise(std::move(params), + std::move(adv_peripheral_handle), + [&](auto cb_result) { result = std::move(cb_result); }); + RunLoopUntilIdle(); + ASSERT_EQ(adapter()->fake_le()->registered_advertisements().size(), 1u); + EXPECT_TRUE(adapter() + ->fake_le() + ->registered_advertisements() + .begin() + ->second.include_tx_power_level); + + adv_peripheral_server.Unbind(); + RunLoopUntilIdle(); +} + +TEST_F(LowEnergyPeripheralServerTestFakeAdapter, AdvertiseInvalidAdvData) { + fble::AdvertisingData adv_data; + adv_data.set_name(std::string(bt::kMaxNameLength + 1, '*')); + fble::AdvertisingParameters params; + params.set_data(std::move(adv_data)); + + fidl::InterfaceHandle + advertised_peripheral_client; + fidl::InterfaceRequest + advertised_peripheral_server = advertised_peripheral_client.NewRequest(); + + std::optional> adv_result; + server()->Advertise(std::move(params), + std::move(advertised_peripheral_client), + [&](auto result) { adv_result = std::move(result); }); + RunLoopUntilIdle(); + EXPECT_EQ(adapter()->fake_le()->registered_advertisements().size(), 0u); + ASSERT_TRUE(adv_result); + EXPECT_TRUE(adv_result.value().is_error()); + EXPECT_EQ(adv_result->error(), fble::PeripheralError::INVALID_PARAMETERS); +} + +TEST_F(LowEnergyPeripheralServerTestFakeAdapter, + AdvertiseInvalidScanResponseData) { + fble::AdvertisingData adv_data; + adv_data.set_name(std::string(bt::kMaxNameLength + 1, '*')); + fble::AdvertisingParameters params; + params.set_scan_response(std::move(adv_data)); + + fidl::InterfaceHandle + advertised_peripheral_client; + fidl::InterfaceRequest + advertised_peripheral_server = advertised_peripheral_client.NewRequest(); + + std::optional> adv_result; + server()->Advertise(std::move(params), + std::move(advertised_peripheral_client), + [&](auto result) { adv_result = std::move(result); }); + RunLoopUntilIdle(); + EXPECT_EQ(adapter()->fake_le()->registered_advertisements().size(), 0u); + ASSERT_TRUE(adv_result); + EXPECT_TRUE(adv_result.value().is_error()); + EXPECT_EQ(adv_result->error(), fble::PeripheralError::INVALID_PARAMETERS); +} + +TEST_F(LowEnergyPeripheralServerTest, AdvertiseAndReceiveTwoConnections) { + fble::AdvertisedPeripheralHandle adv_peripheral_handle; + FakeAdvertisedPeripheral adv_peripheral_server( + adv_peripheral_handle.NewRequest()); + + fble::AdvertisingParameters params; + fble::ConnectionOptions conn_opts; + params.set_connection_options(std::move(conn_opts)); + + std::optional> adv_result; + server()->Advertise( + std::move(params), std::move(adv_peripheral_handle), [&](auto cb_result) { + adv_result = std::move(cb_result); + }); + RunLoopUntilIdle(); + EXPECT_FALSE(adv_result.has_value()); + + test_device()->AddPeer( + std::make_unique(kTestAddr, pw_dispatcher())); + test_device()->ConnectLowEnergy(kTestAddr); + RunLoopUntilIdle(); + ASSERT_TRUE(adv_peripheral_server.last_connected_peer()); + + // Sending response to first connection should restart advertising. + adv_peripheral_server.connections()[0].callback(); + RunLoopUntilIdle(); + + test_device()->AddPeer( + std::make_unique(kTestAddr2, pw_dispatcher())); + test_device()->ConnectLowEnergy(kTestAddr2); + RunLoopUntilIdle(); + ASSERT_EQ(adv_peripheral_server.connections().size(), 2u); + + adv_peripheral_server.Unbind(); + RunLoopUntilIdle(); + ASSERT_TRUE(adv_result.has_value()); + EXPECT_TRUE(adv_result->is_ok()); +} + +TEST_F(LowEnergyPeripheralServerTest, + AdvertiseCanceledBeforeAdvertisingStarts) { + fit::closure send_adv_enable_response; + test_device()->pause_responses_for_opcode( + bt::hci_spec::kLESetAdvertisingEnable, [&](fit::closure send_rsp) { + send_adv_enable_response = std::move(send_rsp); + }); + + fble::AdvertisedPeripheralHandle adv_peripheral_handle; + FakeAdvertisedPeripheral adv_peripheral_server( + adv_peripheral_handle.NewRequest()); + + fble::AdvertisingParameters params; + std::optional> adv_result; + server()->Advertise( + std::move(params), std::move(adv_peripheral_handle), [&](auto cb_result) { + adv_result = std::move(cb_result); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(send_adv_enable_response); + + adv_peripheral_server.Unbind(); + RunLoopUntilIdle(); + send_adv_enable_response(); + RunLoopUntilIdle(); + ASSERT_TRUE(adv_result.has_value()); + EXPECT_TRUE(adv_result->is_ok()); +} + +TEST_P(BoolParam, AdvertiseTwiceCausesSecondToFail) { + fble::AdvertisedPeripheralHandle adv_peripheral_handle_0; + FakeAdvertisedPeripheral adv_peripheral_server_0( + adv_peripheral_handle_0.NewRequest()); + bool adv_peripheral_server_0_closed = false; + adv_peripheral_server_0.set_error_handler( + [&](auto) { adv_peripheral_server_0_closed = true; }); + + fble::AdvertisingParameters params_0; + fble::ConnectionOptions conn_opts; + params_0.set_connection_options(std::move(conn_opts)); + + std::optional> adv_result_0; + server()->Advertise( + std::move(params_0), + std::move(adv_peripheral_handle_0), + [&](auto cb_result) { adv_result_0 = std::move(cb_result); }); + + // Test both with and without running the loop between Advertise requests. + if (GetParam()) { + RunLoopUntilIdle(); + EXPECT_FALSE(adv_result_0.has_value()); + EXPECT_FALSE(adv_peripheral_server_0_closed); + } + + fble::AdvertisedPeripheralHandle adv_peripheral_handle_1; + FakeAdvertisedPeripheral adv_peripheral_server_1( + adv_peripheral_handle_1.NewRequest()); + bool adv_peripheral_server_1_closed = false; + adv_peripheral_server_1.set_error_handler( + [&](auto) { adv_peripheral_server_1_closed = true; }); + fble::AdvertisingParameters params_1; + std::optional> adv_result_1; + server()->Advertise( + std::move(params_1), + std::move(adv_peripheral_handle_1), + [&](auto cb_result) { adv_result_1 = std::move(cb_result); }); + RunLoopUntilIdle(); + EXPECT_FALSE(adv_result_0.has_value()); + EXPECT_FALSE(adv_peripheral_server_0_closed); + ASSERT_TRUE(adv_result_1.has_value()); + ASSERT_TRUE(adv_result_1->is_error()); + EXPECT_EQ(adv_result_1->error(), fble::PeripheralError::FAILED); + EXPECT_TRUE(adv_peripheral_server_1_closed); + + // Server 0 should still receive connections. + test_device()->AddPeer( + std::make_unique(kTestAddr, pw_dispatcher())); + test_device()->ConnectLowEnergy(kTestAddr); + RunLoopUntilIdle(); + EXPECT_TRUE(adv_peripheral_server_0.last_connected_peer()); + + adv_peripheral_server_0.Unbind(); + RunLoopUntilIdle(); + ASSERT_TRUE(adv_result_0.has_value()); + EXPECT_TRUE(adv_result_0->is_ok()); +} + +TEST_F(LowEnergyPeripheralServerTest, + CallAdvertiseTwiceSequentiallyBothSucceed) { + fble::AdvertisedPeripheralHandle adv_peripheral_handle_0; + FakeAdvertisedPeripheral adv_peripheral_server_0( + adv_peripheral_handle_0.NewRequest()); + fble::AdvertisingParameters params_0; + std::optional> adv_result_0; + server()->Advertise( + std::move(params_0), + std::move(adv_peripheral_handle_0), + [&](auto cb_result) { adv_result_0 = std::move(cb_result); }); + RunLoopUntilIdle(); + EXPECT_FALSE(adv_result_0.has_value()); + + adv_peripheral_server_0.Unbind(); + RunLoopUntilIdle(); + ASSERT_TRUE(adv_result_0.has_value()); + EXPECT_TRUE(adv_result_0->is_ok()); + + fble::AdvertisedPeripheralHandle adv_peripheral_handle_1; + FakeAdvertisedPeripheral adv_peripheral_server_1( + adv_peripheral_handle_1.NewRequest()); + + fble::AdvertisingParameters params_1; + fble::ConnectionOptions conn_opts; + params_1.set_connection_options(std::move(conn_opts)); + + std::optional> adv_result_1; + server()->Advertise( + std::move(params_1), + std::move(adv_peripheral_handle_1), + [&](auto cb_result) { adv_result_1 = std::move(cb_result); }); + RunLoopUntilIdle(); + EXPECT_FALSE(adv_result_1.has_value()); + + // Server 1 should receive connections. + test_device()->AddPeer( + std::make_unique(kTestAddr, pw_dispatcher())); + test_device()->ConnectLowEnergy(kTestAddr); + RunLoopUntilIdle(); + EXPECT_TRUE(adv_peripheral_server_1.last_connected_peer()); + + adv_peripheral_server_1.Unbind(); + RunLoopUntilIdle(); + ASSERT_TRUE(adv_result_1.has_value()); + EXPECT_TRUE(adv_result_1->is_ok()); +} + +TEST_F(LowEnergyPeripheralServerTest, PeerDisconnectClosesConnection) { + fble::AdvertisedPeripheralHandle adv_peripheral_handle; + FakeAdvertisedPeripheral adv_peripheral_server( + adv_peripheral_handle.NewRequest()); + + fble::AdvertisingParameters params; + fble::ConnectionOptions conn_opts; + params.set_connection_options(std::move(conn_opts)); + + std::optional> adv_result; + server()->Advertise( + std::move(params), std::move(adv_peripheral_handle), [&](auto cb_result) { + adv_result = std::move(cb_result); + }); + RunLoopUntilIdle(); + EXPECT_FALSE(adv_result.has_value()); + + test_device()->AddPeer( + std::make_unique(kTestAddr, pw_dispatcher())); + test_device()->ConnectLowEnergy(kTestAddr); + RunLoopUntilIdle(); + EXPECT_TRUE(adv_peripheral_server.last_connected_peer()); + fidl::InterfacePtr connection = + adv_peripheral_server.connections()[0].connection.Bind(); + bool connection_closed = false; + connection.set_error_handler([&](auto) { connection_closed = true; }); + EXPECT_FALSE(connection_closed); + RunLoopUntilIdle(); + + adv_peripheral_server.Unbind(); + RunLoopUntilIdle(); + ASSERT_TRUE(adv_result.has_value()); + EXPECT_TRUE(adv_result->is_ok()); + EXPECT_FALSE(connection_closed); + + test_device()->Disconnect(kTestAddr); + RunLoopUntilIdle(); + EXPECT_TRUE(connection_closed); +} + +TEST_F(LowEnergyPeripheralServerTest, + IncomingConnectionFailureContinuesAdvertising) { + fble::AdvertisedPeripheralHandle adv_peripheral_handle; + FakeAdvertisedPeripheral adv_peripheral_server( + adv_peripheral_handle.NewRequest()); + + fble::AdvertisingParameters params; + fble::ConnectionOptions conn_opts; + params.set_connection_options(std::move(conn_opts)); + + std::optional> adv_result; + server()->Advertise( + std::move(params), std::move(adv_peripheral_handle), [&](auto cb_result) { + adv_result = std::move(cb_result); + }); + RunLoopUntilIdle(); + EXPECT_FALSE(adv_result.has_value()); + + // Cause peer interrogation to fail. This will result in a connection error + // status to be received. Advertising should be immediately resumed, allowing + // future connections. + test_device()->SetDefaultCommandStatus( + bt::hci_spec::kReadRemoteVersionInfo, + pw::bluetooth::emboss::StatusCode::UNSUPPORTED_REMOTE_FEATURE); + + test_device()->AddPeer( + std::make_unique(kTestAddr, pw_dispatcher())); + test_device()->ConnectLowEnergy(kTestAddr); + RunLoopUntilIdle(); + EXPECT_FALSE(adv_peripheral_server.last_connected_peer()); + EXPECT_FALSE(adv_result.has_value()); + + // Allow next interrogation to succeed. + test_device()->ClearDefaultCommandStatus( + bt::hci_spec::kReadRemoteVersionInfo); + + test_device()->AddPeer( + std::make_unique(kTestAddr, pw_dispatcher())); + test_device()->ConnectLowEnergy(kTestAddr); + RunLoopUntilIdle(); + EXPECT_TRUE(adv_peripheral_server.last_connected_peer()); + EXPECT_FALSE(adv_result.has_value()); + + adv_peripheral_server.Unbind(); + RunLoopUntilIdle(); + EXPECT_TRUE(adv_result.has_value()); +} + +INSTANTIATE_TEST_SUITE_P(LowEnergyPeripheralServerTest, + BoolParam, + ::testing::Bool()); + +} // namespace +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/measure_tape/BUILD.bazel b/pw_bluetooth_sapphire/fuchsia/host/fidl/measure_tape/BUILD.bazel new file mode 100644 index 0000000000..9a9bc66666 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/measure_tape/BUILD.bazel @@ -0,0 +1,42 @@ +# Copyright 2024 The Pigweed Authors +# +# 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. + +package(default_visibility = ["//visibility:public"]) + +cc_library( + name = "peer_hlcpp", + srcs = [ + "hlcpp_measure_tape_for_peer.cc", + ], + hdrs = [ + "hlcpp_measure_tape_for_peer.h", + ], + deps = [ + "@fuchsia_sdk//fidl/fuchsia.bluetooth:fuchsia.bluetooth_hlcpp", + "@fuchsia_sdk//fidl/fuchsia.bluetooth.le:fuchsia.bluetooth.le_hlcpp", + ], +) + +cc_library( + name = "read_by_type_result_hlcpp", + srcs = [ + "hlcpp_measure_tape_for_read_by_type_result.cc", + ], + hdrs = [ + "hlcpp_measure_tape_for_read_by_type_result.h", + ], + deps = [ + "@fuchsia_sdk//fidl/fuchsia.bluetooth.gatt2:fuchsia.bluetooth.gatt2_hlcpp", + ], +) diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/measure_tape/hlcpp_measure_tape_for_peer.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/measure_tape/hlcpp_measure_tape_for_peer.cc new file mode 100644 index 0000000000..11b4071264 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/measure_tape/hlcpp_measure_tape_for_peer.cc @@ -0,0 +1,232 @@ +// Copyright 2024 The Pigweed Authors +// +// 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. + +// Code generated by "measure-tape"; DO NOT EDIT. +// +// See tools/fidl/measure-tape/README.md + +// clang-format off +#include "hlcpp_measure_tape_for_peer.h" + +#include +#include +#include + + +namespace measure_tape { +namespace fuchsia { +namespace bluetooth { +namespace le { + +namespace { + +class MeasuringTape { + public: + MeasuringTape() = default; + + void Measure(const ::fuchsia::bluetooth::le::AdvertisingData& value) { + num_bytes_ += 16; + MeasureOutOfLine(value); + } + + void MeasureOutOfLine(const ::fuchsia::bluetooth::le::AdvertisingData& value) { + int32_t max_ordinal = 0; + if (value.has_name()) { + num_bytes_ += 16; + num_bytes_ += FIDL_ALIGN(value.name().length()); + max_ordinal = 1; + } + if (value.has_appearance()) { + max_ordinal = 2; + } + if (value.has_tx_power_level()) { + max_ordinal = 3; + } + if (value.has_service_uuids()) { + num_bytes_ += 16; + num_bytes_ += FIDL_ALIGN(value.service_uuids().size() * 16); + max_ordinal = 4; + } + if (value.has_service_data()) { + num_bytes_ += 16; + for (const auto& service_data_elem : value.service_data()) { + Measure(service_data_elem); + } + max_ordinal = 5; + } + if (value.has_manufacturer_data()) { + num_bytes_ += 16; + for (const auto& manufacturer_data_elem : value.manufacturer_data()) { + Measure(manufacturer_data_elem); + } + max_ordinal = 6; + } + if (value.has_uris()) { + num_bytes_ += 16; + for (const auto& uris_elem : value.uris()) { + num_bytes_ += 16; + num_bytes_ += FIDL_ALIGN(uris_elem.length()); + } + max_ordinal = 7; + } + if (value.has_include_tx_power_level()) { + max_ordinal = 8; + } + num_bytes_ += 8 * max_ordinal; + } + + void Measure(const ::fuchsia::bluetooth::le::ManufacturerData& value) { + num_bytes_ += FIDL_ALIGN(24); + MeasureOutOfLine(value); + } + + void MeasureOutOfLine(const ::fuchsia::bluetooth::le::ManufacturerData& value) { + num_bytes_ += FIDL_ALIGN(value.data.size() * 1); + } + + void Measure(const ::fuchsia::bluetooth::le::Peer& value) { + num_bytes_ += 16; + MeasureOutOfLine(value); + } + + void MeasureOutOfLine(const ::fuchsia::bluetooth::le::Peer& value) { + int32_t max_ordinal = 0; + if (value.has_id()) { + Measure(value.id()); + max_ordinal = 1; + } + if (value.has_connectable()) { + max_ordinal = 2; + } + if (value.has_rssi()) { + max_ordinal = 3; + } + if (value.has_advertising_data()) { + Measure(value.advertising_data()); + max_ordinal = 4; + } + if (value.has_name()) { + num_bytes_ += 16; + num_bytes_ += FIDL_ALIGN(value.name().length()); + max_ordinal = 5; + } + if (value.has_data()) { + Measure(value.data()); + max_ordinal = 6; + } + if (value.has_bonded()) { + max_ordinal = 7; + } + if (value.has_last_updated()) { + num_bytes_ += 8; + max_ordinal = 8; + } + num_bytes_ += 8 * max_ordinal; + } + + void Measure(const ::fuchsia::bluetooth::le::ScanData& value) { + num_bytes_ += 16; + MeasureOutOfLine(value); + } + + void MeasureOutOfLine(const ::fuchsia::bluetooth::le::ScanData& value) { + int32_t max_ordinal = 0; + if (value.has_tx_power()) { + max_ordinal = 1; + } + if (value.has_appearance()) { + max_ordinal = 2; + } + if (value.has_service_uuids()) { + num_bytes_ += 16; + num_bytes_ += FIDL_ALIGN(value.service_uuids().size() * 16); + max_ordinal = 3; + } + if (value.has_service_data()) { + num_bytes_ += 16; + for (const auto& service_data_elem : value.service_data()) { + Measure(service_data_elem); + } + max_ordinal = 4; + } + if (value.has_manufacturer_data()) { + num_bytes_ += 16; + for (const auto& manufacturer_data_elem : value.manufacturer_data()) { + Measure(manufacturer_data_elem); + } + max_ordinal = 5; + } + if (value.has_uris()) { + num_bytes_ += 16; + for (const auto& uris_elem : value.uris()) { + num_bytes_ += 16; + num_bytes_ += FIDL_ALIGN(uris_elem.length()); + } + max_ordinal = 6; + } + if (value.has_timestamp()) { + num_bytes_ += 8; + max_ordinal = 7; + } + num_bytes_ += 8 * max_ordinal; + } + + void Measure(const ::fuchsia::bluetooth::le::ServiceData& value) { + num_bytes_ += FIDL_ALIGN(32); + MeasureOutOfLine(value); + } + + void MeasureOutOfLine(const ::fuchsia::bluetooth::le::ServiceData& value) { + num_bytes_ += FIDL_ALIGN(value.data.size() * 1); + } + + void Measure(const ::fuchsia::bluetooth::PeerId& value) { + num_bytes_ += FIDL_ALIGN(8); + } + + void Measure(const ::fuchsia::bluetooth::Uuid& value) { + num_bytes_ += FIDL_ALIGN(16); + } + + Size Done() { + if (maxed_out_) { + return Size(ZX_CHANNEL_MAX_MSG_BYTES, ZX_CHANNEL_MAX_MSG_HANDLES); + } + return Size(num_bytes_, num_handles_); + } + +private: + void MaxOut() { maxed_out_ = true; } + + bool maxed_out_ = false; + int64_t num_bytes_ = 0; + int64_t num_handles_ = 0; +}; + +} // namespace + + +Size Measure(const ::fuchsia::bluetooth::le::Peer& value) { + MeasuringTape tape; + tape.Measure(value); + return tape.Done(); +} + + + +} // le +} // bluetooth +} // fuchsia +} // measure_tape + diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/measure_tape/hlcpp_measure_tape_for_peer.h b/pw_bluetooth_sapphire/fuchsia/host/fidl/measure_tape/hlcpp_measure_tape_for_peer.h new file mode 100644 index 0000000000..8ce3f46f7c --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/measure_tape/hlcpp_measure_tape_for_peer.h @@ -0,0 +1,51 @@ +// Copyright 2024 The Pigweed Authors +// +// 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. + +// Code generated by "measure-tape"; DO NOT EDIT. +// +// See tools/fidl/measure-tape/README.md + +// clang-format off +#pragma once + +#include + + +namespace measure_tape { +namespace fuchsia { +namespace bluetooth { +namespace le { + +struct Size { + explicit Size(int64_t num_bytes, int64_t num_handles) + : num_bytes(num_bytes), num_handles(num_handles) {} + + const int64_t num_bytes; + const int64_t num_handles; +}; + + +// Helper function to measure ::fuchsia::bluetooth::le::Peer. +// +// In most cases, the size returned is a precise size. Otherwise, the size +// returned is a safe upper-bound. +Size Measure(const ::fuchsia::bluetooth::le::Peer& value); + + + +} // le +} // bluetooth +} // fuchsia +} // measure_tape + diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/measure_tape/hlcpp_measure_tape_for_read_by_type_result.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/measure_tape/hlcpp_measure_tape_for_read_by_type_result.cc new file mode 100644 index 0000000000..1e2d15ad7e --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/measure_tape/hlcpp_measure_tape_for_read_by_type_result.cc @@ -0,0 +1,114 @@ +// Copyright 2024 The Pigweed Authors +// +// 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. + +// Code generated by "measure-tape"; DO NOT EDIT. +// +// See tools/fidl/measure-tape/README.md + +// clang-format off +#include "hlcpp_measure_tape_for_read_by_type_result.h" + +#include +#include + + +namespace measure_tape { +namespace fuchsia { +namespace bluetooth { +namespace gatt2 { + +namespace { + +class MeasuringTape { + public: + MeasuringTape() = default; + + void Measure(const ::fuchsia::bluetooth::gatt2::Handle& value) { + num_bytes_ += FIDL_ALIGN(8); + } + + void Measure(const ::fuchsia::bluetooth::gatt2::ReadByTypeResult& value) { + num_bytes_ += 16; + MeasureOutOfLine(value); + } + + void MeasureOutOfLine(const ::fuchsia::bluetooth::gatt2::ReadByTypeResult& value) { + int32_t max_ordinal = 0; + if (value.has_handle()) { + Measure(value.handle()); + max_ordinal = 1; + } + if (value.has_value()) { + Measure(value.value()); + max_ordinal = 2; + } + if (value.has_error()) { + max_ordinal = 3; + } + num_bytes_ += 8 * max_ordinal; + } + + void Measure(const ::fuchsia::bluetooth::gatt2::ReadValue& value) { + num_bytes_ += 16; + MeasureOutOfLine(value); + } + + void MeasureOutOfLine(const ::fuchsia::bluetooth::gatt2::ReadValue& value) { + int32_t max_ordinal = 0; + if (value.has_handle()) { + Measure(value.handle()); + max_ordinal = 1; + } + if (value.has_value()) { + num_bytes_ += 16; + num_bytes_ += FIDL_ALIGN(value.value().size() * 1); + max_ordinal = 2; + } + if (value.has_maybe_truncated()) { + max_ordinal = 3; + } + num_bytes_ += 8 * max_ordinal; + } + + Size Done() { + if (maxed_out_) { + return Size(ZX_CHANNEL_MAX_MSG_BYTES, ZX_CHANNEL_MAX_MSG_HANDLES); + } + return Size(num_bytes_, num_handles_); + } + +private: + void MaxOut() { maxed_out_ = true; } + + bool maxed_out_ = false; + int64_t num_bytes_ = 0; + int64_t num_handles_ = 0; +}; + +} // namespace + + +Size Measure(const ::fuchsia::bluetooth::gatt2::ReadByTypeResult& value) { + MeasuringTape tape; + tape.Measure(value); + return tape.Done(); +} + + + +} // gatt2 +} // bluetooth +} // fuchsia +} // measure_tape + diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/measure_tape/hlcpp_measure_tape_for_read_by_type_result.h b/pw_bluetooth_sapphire/fuchsia/host/fidl/measure_tape/hlcpp_measure_tape_for_read_by_type_result.h new file mode 100644 index 0000000000..258623aa1e --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/measure_tape/hlcpp_measure_tape_for_read_by_type_result.h @@ -0,0 +1,51 @@ +// Copyright 2024 The Pigweed Authors +// +// 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. + +// Code generated by "measure-tape"; DO NOT EDIT. +// +// See tools/fidl/measure-tape/README.md + +// clang-format off +#pragma once + +#include + + +namespace measure_tape { +namespace fuchsia { +namespace bluetooth { +namespace gatt2 { + +struct Size { + explicit Size(int64_t num_bytes, int64_t num_handles) + : num_bytes(num_bytes), num_handles(num_handles) {} + + const int64_t num_bytes; + const int64_t num_handles; +}; + + +// Helper function to measure ::fuchsia::bluetooth::gatt2::ReadByTypeResult. +// +// In most cases, the size returned is a precise size. Otherwise, the size +// returned is a safe upper-bound. +Size Measure(const ::fuchsia::bluetooth::gatt2::ReadByTypeResult& value); + + + +} // gatt2 +} // bluetooth +} // fuchsia +} // measure_tape + diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/meta/host_server_watch_peers_fuzzer.cml b/pw_bluetooth_sapphire/fuchsia/host/fidl/meta/host_server_watch_peers_fuzzer.cml new file mode 100644 index 0000000000..9b282c8eb0 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/meta/host_server_watch_peers_fuzzer.cml @@ -0,0 +1,19 @@ +// Copyright 2024 The Pigweed Authors +// +// 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: [ "//src/sys/fuzzing/libfuzzer/default.shard.cml" ], + program: { + args: [ "test/host_server_watch_peers_fuzzer" ], + }, +} diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/profile_server.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/profile_server.cc new file mode 100644 index 0000000000..893a906113 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/profile_server.cc @@ -0,0 +1,1335 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/fidl/profile_server.h" + +#include +#include +#include + +#include +#include + +#include "fuchsia/bluetooth/bredr/cpp/fidl.h" +#include "lib/fidl/cpp/binding.h" +#include "lib/fidl/cpp/interface_ptr.h" +#include "lib/fpromise/result.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/helpers.h" +#include "pw_bluetooth_sapphire/internal/host/common/host_error.h" +#include "pw_bluetooth_sapphire/internal/host/common/log.h" +#include "pw_bluetooth_sapphire/internal/host/common/uuid.h" +#include "pw_bluetooth_sapphire/internal/host/common/weak_self.h" +#include "pw_intrusive_ptr/intrusive_ptr.h" +#include "zircon/errors.h" + +namespace fidlbredr = fuchsia::bluetooth::bredr; +namespace fbt = fuchsia::bluetooth; +namespace android_emb = pw::bluetooth::vendor::android_hci; +using fidlbredr::DataElement; +using fidlbredr::Profile; +using pw::bluetooth::AclPriority; +using FeaturesBits = pw::bluetooth::Controller::FeaturesBits; + +namespace bthost { + +namespace { + +bt::l2cap::ChannelParameters FidlToChannelParameters( + const fbt::ChannelParameters& fidl) { + bt::l2cap::ChannelParameters params; + if (fidl.has_channel_mode()) { + switch (fidl.channel_mode()) { + case fbt::ChannelMode::BASIC: + params.mode = bt::l2cap::RetransmissionAndFlowControlMode::kBasic; + break; + case fbt::ChannelMode::ENHANCED_RETRANSMISSION: + params.mode = bt::l2cap::RetransmissionAndFlowControlMode:: + kEnhancedRetransmission; + break; + default: + BT_PANIC("FIDL channel parameter contains invalid mode"); + } + } + if (fidl.has_max_rx_packet_size()) { + params.max_rx_sdu_size = fidl.max_rx_packet_size(); + } + if (fidl.has_flush_timeout()) { + params.flush_timeout = std::chrono::nanoseconds(fidl.flush_timeout()); + } + return params; +} + +fbt::ChannelMode ChannelModeToFidl(const bt::l2cap::AnyChannelMode& mode) { + if (auto* flow_control_mode = + std::get_if(&mode)) { + switch (*flow_control_mode) { + case bt::l2cap::RetransmissionAndFlowControlMode::kBasic: + return fbt::ChannelMode::BASIC; + break; + case bt::l2cap::RetransmissionAndFlowControlMode::kEnhancedRetransmission: + return fbt::ChannelMode::ENHANCED_RETRANSMISSION; + break; + default: + // Intentionally unhandled, fall through to PANIC. + break; + } + } + BT_PANIC("Could not convert channel parameter mode to unsupported FIDL mode"); +} + +fbt::ChannelParameters ChannelInfoToFidlChannelParameters( + const bt::l2cap::ChannelInfo& info) { + fbt::ChannelParameters params; + params.set_channel_mode(ChannelModeToFidl(info.mode)); + params.set_max_rx_packet_size(info.max_rx_sdu_size); + if (info.flush_timeout) { + params.set_flush_timeout(info.flush_timeout->count()); + } + return params; +} + +// NOLINTNEXTLINE(misc-no-recursion) +fidlbredr::DataElementPtr DataElementToFidl(const bt::sdp::DataElement* in) { + auto elem = std::make_unique(); + bt_log(TRACE, "fidl", "DataElementToFidl: %s", in->ToString().c_str()); + BT_DEBUG_ASSERT(in); + switch (in->type()) { + case bt::sdp::DataElement::Type::kUnsignedInt: { + switch (in->size()) { + case bt::sdp::DataElement::Size::kOneByte: + elem->set_uint8(*in->Get()); + break; + case bt::sdp::DataElement::Size::kTwoBytes: + elem->set_uint16(*in->Get()); + break; + case bt::sdp::DataElement::Size::kFourBytes: + elem->set_uint32(*in->Get()); + break; + case bt::sdp::DataElement::Size::kEightBytes: + elem->set_uint64(*in->Get()); + break; + default: + bt_log(INFO, "fidl", "no 128-bit integer support in FIDL yet"); + return nullptr; + } + return elem; + } + case bt::sdp::DataElement::Type::kSignedInt: { + switch (in->size()) { + case bt::sdp::DataElement::Size::kOneByte: + elem->set_int8(*in->Get()); + break; + case bt::sdp::DataElement::Size::kTwoBytes: + elem->set_int16(*in->Get()); + break; + case bt::sdp::DataElement::Size::kFourBytes: + elem->set_int32(*in->Get()); + break; + case bt::sdp::DataElement::Size::kEightBytes: + elem->set_int64(*in->Get()); + break; + default: + bt_log(INFO, "fidl", "no 128-bit integer support in FIDL yet"); + return nullptr; + } + return elem; + } + case bt::sdp::DataElement::Type::kUuid: { + auto uuid = in->Get(); + BT_DEBUG_ASSERT(uuid); + elem->set_uuid(fidl_helpers::UuidToFidl(*uuid)); + return elem; + } + case bt::sdp::DataElement::Type::kString: { + auto bytes = in->Get(); + BT_DEBUG_ASSERT(bytes); + std::vector data(bytes->cbegin(), bytes->cend()); + elem->set_str(data); + return elem; + } + case bt::sdp::DataElement::Type::kBoolean: { + elem->set_b(*in->Get()); + return elem; + } + case bt::sdp::DataElement::Type::kSequence: { + std::vector elems; + const bt::sdp::DataElement* it; + for (size_t idx = 0; (it = in->At(idx)); ++idx) { + elems.emplace_back(DataElementToFidl(it)); + } + elem->set_sequence(std::move(elems)); + return elem; + } + case bt::sdp::DataElement::Type::kAlternative: { + std::vector elems; + const bt::sdp::DataElement* it; + for (size_t idx = 0; (it = in->At(idx)); ++idx) { + elems.emplace_back(DataElementToFidl(it)); + } + elem->set_alternatives(std::move(elems)); + return elem; + } + case bt::sdp::DataElement::Type::kUrl: { + elem->set_url(*in->GetUrl()); + return elem; + } + case bt::sdp::DataElement::Type::kNull: { + bt_log(INFO, "fidl", "no support for null DataElement types in FIDL"); + return nullptr; + } + } +} + +fidlbredr::ProtocolDescriptorPtr DataElementToProtocolDescriptor( + const bt::sdp::DataElement* in) { + auto desc = std::make_unique(); + if (in->type() != bt::sdp::DataElement::Type::kSequence) { + bt_log(DEBUG, + "fidl", + "DataElement type is not kSequence (in: %s)", + bt_str(*in)); + return nullptr; + } + const auto protocol_uuid = in->At(0)->Get(); + if (!protocol_uuid) { + bt_log(DEBUG, + "fidl", + "first DataElement in sequence is not type kUUID (in: %s)", + bt_str(*in)); + return nullptr; + } + desc->set_protocol( + static_cast(*protocol_uuid->As16Bit())); + const bt::sdp::DataElement* it; + std::vector params; + for (size_t idx = 1; (it = in->At(idx)); ++idx) { + params.push_back(std::move(*DataElementToFidl(it))); + } + desc->set_params(std::move(params)); + + return desc; +} + +AclPriority FidlToAclPriority(fidlbredr::A2dpDirectionPriority in) { + switch (in) { + case fidlbredr::A2dpDirectionPriority::SOURCE: + return AclPriority::kSource; + case fidlbredr::A2dpDirectionPriority::SINK: + return AclPriority::kSink; + default: + return AclPriority::kNormal; + } +} + +} // namespace + +ProfileServer::ProfileServer(bt::gap::Adapter::WeakPtr adapter, + fidl::InterfaceRequest request) + : ServerBase(this, std::move(request)), + advertised_total_(0), + searches_total_(0), + adapter_(std::move(adapter)), + weak_self_(this) {} + +ProfileServer::~ProfileServer() { + sco_connection_servers_.clear(); + + if (adapter().is_alive()) { + // Unregister anything that we have registered. + for (const auto& it : current_advertised_) { + adapter()->bredr()->UnregisterService(it.second.registration_handle); + } + for (const auto& it : searches_) { + adapter()->bredr()->RemoveServiceSearch(it.second.search_id); + } + } +} + +void ProfileServer::L2capParametersExt::RequestParameters( + fuchsia::bluetooth::ChannelParameters requested, + RequestParametersCallback callback) { + if (requested.has_flush_timeout()) { + channel_->SetBrEdrAutomaticFlushTimeout( + std::chrono::nanoseconds(requested.flush_timeout()), + [chan = channel_, cb = std::move(callback)](auto result) { + if (result.is_ok()) { + bt_log(DEBUG, + "fidl", + "L2capParametersExt::RequestParameters: setting flush " + "timeout succeeded"); + } else { + bt_log(INFO, + "fidl", + "L2capParametersExt::RequestParameters: setting flush " + "timeout failed"); + } + // Return the current parameters even if the request failed. + // TODO(fxbug.dev/42152567): set current security requirements in + // returned channel parameters + cb(fidlbredr::L2capParametersExt_RequestParameters_Result:: + WithResponse( + fidlbredr::L2capParametersExt_RequestParameters_Response( + ChannelInfoToFidlChannelParameters(chan->info())))); + }); + return; + } + + // No other channel parameters are supported, so just return the current + // parameters. + // TODO(fxbug.dev/42152567): set current security requirements in returned + // channel parameters + callback(fidlbredr::L2capParametersExt_RequestParameters_Result::WithResponse( + fidlbredr::L2capParametersExt_RequestParameters_Response( + ChannelInfoToFidlChannelParameters(channel_->info())))); +} + +void ProfileServer::L2capParametersExt::handle_unknown_method( + uint64_t ordinal, bool method_has_response) { + bt_log(WARN, "fidl", "L2capParametersExt: unknown method received"); +} + +void ProfileServer::AudioOffloadExt::GetSupportedFeatures( + GetSupportedFeaturesCallback callback) { + fidlbredr::AudioOffloadExt_GetSupportedFeatures_Response response; + std::vector* mutable_audio_offload_features = + response.mutable_audio_offload_features(); + const bt::gap::AdapterState& adapter_state = adapter_->state(); + + if (!adapter_state.IsControllerFeatureSupported( + FeaturesBits::kAndroidVendorExtensions)) { + callback( + fidlbredr::AudioOffloadExt_GetSupportedFeatures_Result::WithResponse( + std::move(response))); + return; + } + + const uint32_t a2dp_offload_capabilities = + adapter_state.android_vendor_capabilities + ->a2dp_source_offload_capability_mask(); + const uint32_t sbc_capability = + static_cast(android_emb::A2dpCodecType::SBC); + const uint32_t aac_capability = + static_cast(android_emb::A2dpCodecType::AAC); + + if (a2dp_offload_capabilities & sbc_capability) { + fidlbredr::AudioSbcSupport audio_sbc_support; + mutable_audio_offload_features->push_back( + fidlbredr::AudioOffloadFeatures::WithSbc(std::move(audio_sbc_support))); + } + if (a2dp_offload_capabilities & aac_capability) { + fidlbredr::AudioAacSupport audio_aac_support; + mutable_audio_offload_features->push_back( + fidlbredr::AudioOffloadFeatures::WithAac(std::move(audio_aac_support))); + } + + callback(fidlbredr::AudioOffloadExt_GetSupportedFeatures_Result::WithResponse( + std::move(response))); +} + +void ProfileServer::AudioOffloadExt::StartAudioOffload( + fidlbredr::AudioOffloadConfiguration audio_offload_configuration, + fidl::InterfaceRequest controller) { + auto audio_offload_controller_server = + std::make_unique(std::move(controller), channel_); + WeakPtr server_ptr = + audio_offload_controller_server->GetWeakPtr(); + + std::unique_ptr config = + AudioOffloadConfigFromFidl(audio_offload_configuration); + if (!config) { + bt_log(ERROR, "fidl", "%s: invalid config received", __FUNCTION__); + server_ptr->Close(/*epitaph_value=*/ZX_ERR_NOT_SUPPORTED); + return; + } + + auto error_handler = [this, server_ptr](zx_status_t status) { + if (!server_ptr.is_alive()) { + bt_log(ERROR, "fidl", "audio offload controller server was destroyed"); + return; + } + + bt_log(DEBUG, + "fidl", + "audio offload controller server closed (reason: %s)", + zx_status_get_string(status)); + if (!profile_server_.audio_offload_controller_server_) { + bt_log(WARN, + "fidl", + "could not find controller server in audio offload controller " + "error callback"); + } + + bt::hci::ResultCallback<> stop_cb = + [server_ptr]( + fit::result> result) { + if (result.is_error()) { + bt_log(ERROR, + "fidl", + "stopping audio offload failed in error handler: %s", + bt_str(result)); + server_ptr->Close(/*epitaph_value=*/ZX_ERR_UNAVAILABLE); + return; + } + bt_log(ERROR, + "fidl", + "stopping audio offload complete: %s", + bt_str(result)); + }; + channel_->StopA2dpOffload(std::move(stop_cb)); + }; + audio_offload_controller_server->set_error_handler(error_handler); + profile_server_.audio_offload_controller_server_ = + std::move(audio_offload_controller_server); + + auto callback = + [this, server_ptr]( + fit::result> result) { + if (!server_ptr.is_alive()) { + bt_log( + ERROR, "fidl", "audio offload controller server was destroyed"); + return; + } + if (result.is_error()) { + bt_log(ERROR, "fidl", "StartAudioOffload failed: %s", bt_str(result)); + + auto host_error = result.error_value().host_error(); + if (host_error == bt::HostError::kInProgress) { + server_ptr->Close(/*epitaph_value=*/ZX_ERR_ALREADY_BOUND); + } else if (host_error == bt::HostError::kFailed) { + server_ptr->Close(/*epitaph_value=*/ZX_ERR_INTERNAL); + } else { + server_ptr->Close(/*epitaph_value=*/ZX_ERR_UNAVAILABLE); + } + profile_server_.audio_offload_controller_server_ = nullptr; + return; + } + // Send OnStarted event to tell Rust Profiles that we've finished + // offloading + server_ptr->SendOnStartedEvent(); + }; + channel_->StartA2dpOffload(*config, std::move(callback)); +} + +void ProfileServer::AudioOffloadController::Stop( + AudioOffloadController::StopCallback callback) { + if (!channel_.is_alive()) { + bt_log(ERROR, "fidl", "Audio offload controller server was destroyed"); + return; + } + + channel_->StopA2dpOffload( + [stop_callback = std::move(callback), + this](fit::result> result) { + if (result.is_error()) { + bt_log( + ERROR, + "fidl", + "Stop a2dp offload failed with error %s. Closing with " + "ZX_ERR_UNAVAILABLE", + bt::HostErrorToString(result.error_value().host_error()).c_str()); + Close(/*epitaph_value=*/ZX_ERR_UNAVAILABLE); + return; + } + stop_callback( + fidlbredr::AudioOffloadController_Stop_Result::WithResponse({})); + }); +} + +std::unique_ptr +ProfileServer::AudioOffloadExt::AudioOffloadConfigFromFidl( + fidlbredr::AudioOffloadConfiguration& audio_offload_configuration) { + auto codec = + fidl_helpers::FidlToCodecType(audio_offload_configuration.codec()); + if (!codec.has_value()) { + bt_log(WARN, "fidl", "%s: invalid codec", __FUNCTION__); + return nullptr; + } + + std::unique_ptr config = + std::make_unique(); + + std::optional sampling_frequency = + fidl_helpers::FidlToSamplingFrequency( + audio_offload_configuration.sampling_frequency()); + if (!sampling_frequency.has_value()) { + bt_log(WARN, "fidl", "Invalid sampling frequency"); + return nullptr; + } + + std::optional audio_bits_per_sample = + fidl_helpers::FidlToBitsPerSample( + audio_offload_configuration.bits_per_sample()); + if (!audio_bits_per_sample.has_value()) { + bt_log(WARN, "fidl", "Invalid audio bits per sample"); + return nullptr; + } + + std::optional audio_channel_mode = + fidl_helpers::FidlToChannelMode( + audio_offload_configuration.channel_mode()); + if (!audio_channel_mode.has_value()) { + bt_log(WARN, "fidl", "Invalid channel mode"); + return nullptr; + } + + config->codec = codec.value(); + config->max_latency = audio_offload_configuration.max_latency(); + config->scms_t_enable = fidl_helpers::FidlToScmsTEnable( + audio_offload_configuration.scms_t_enable()); + config->sampling_frequency = sampling_frequency.value(); + config->bits_per_sample = audio_bits_per_sample.value(); + config->channel_mode = audio_channel_mode.value(); + config->encoded_audio_bit_rate = + audio_offload_configuration.encoded_bit_rate(); + + if (audio_offload_configuration.encoder_settings().is_sbc()) { + if (audio_offload_configuration.sampling_frequency() == + fuchsia::bluetooth::bredr::AudioSamplingFrequency::HZ_88200 || + audio_offload_configuration.sampling_frequency() == + fuchsia::bluetooth::bredr::AudioSamplingFrequency::HZ_96000) { + bt_log(WARN, + "fidl", + "%s: sbc encoder cannot use sampling frequency %hhu", + __FUNCTION__, + static_cast( + audio_offload_configuration.sampling_frequency())); + return nullptr; + } + + config->sbc_configuration = fidl_helpers::FidlToEncoderSettingsSbc( + audio_offload_configuration.encoder_settings(), + audio_offload_configuration.sampling_frequency(), + audio_offload_configuration.channel_mode()); + } else if (audio_offload_configuration.encoder_settings().is_aac()) { + config->aac_configuration = fidl_helpers::FidlToEncoderSettingsAac( + audio_offload_configuration.encoder_settings(), + audio_offload_configuration.sampling_frequency(), + audio_offload_configuration.channel_mode()); + } + + return config; +} + +void ProfileServer::AudioOffloadExt::handle_unknown_method( + uint64_t ordinal, bool method_has_response) { + bt_log(WARN, "fidl", "AudioOffloadExt: unknown method received"); +} + +void ProfileServer::AudioOffloadController::handle_unknown_method( + uint64_t ordinal, bool method_has_response) { + bt_log(WARN, "fidl", "AudioOffloadController: unknown method received"); +} + +ProfileServer::ScoConnectionServer::ScoConnectionServer( + fidl::InterfaceRequest request, + ProfileServer* profile_server) + : ServerBase(this, std::move(request)), + profile_server_(profile_server), + weak_self_(this) { + binding()->set_error_handler([this](zx_status_t) { Close(ZX_ERR_CANCELED); }); +} + +ProfileServer::ScoConnectionServer::~ScoConnectionServer() { + if (connection_.is_alive()) { + connection_->Deactivate(); + } +} + +void ProfileServer::ScoConnectionServer::Activate() { + auto rx_callback = [this] { TryRead(); }; + auto closed_cb = [this] { Close(ZX_ERR_PEER_CLOSED); }; + bool activated = + connection_->Activate(std::move(rx_callback), std::move(closed_cb)); + if (!activated) { + OnError(fidlbredr::ScoErrorCode::FAILURE); + } +} + +void ProfileServer::ScoConnectionServer::OnError( + ::fuchsia::bluetooth::bredr::ScoErrorCode error) { + OnConnectionComplete( + ::fuchsia::bluetooth::bredr::ScoConnectionOnConnectionCompleteRequest:: + WithError(std::move(error))); // NOLINT(performance-move-const-arg) + Close(ZX_ERR_PEER_CLOSED); +} + +void ProfileServer::ScoConnectionServer::Read(ReadCallback callback) { + if (!connection_.is_alive()) { + Close(ZX_ERR_IO_REFUSED); + return; + } + + if (connection_->parameters().view().input_data_path().Read() != + pw::bluetooth::emboss::ScoDataPath::HCI) { + bt_log(WARN, "fidl", "%s called for an offloaded SCO connection", __func__); + Close(ZX_ERR_IO_NOT_PRESENT); + return; + } + + if (read_cb_) { + bt_log(WARN, + "fidl", + "%s called when a read callback was already present", + __func__); + Close(ZX_ERR_BAD_STATE); + return; + } + read_cb_ = std::move(callback); + TryRead(); +} + +void ProfileServer::ScoConnectionServer::Write( + fidlbredr::ScoConnectionWriteRequest request, WriteCallback callback) { + if (!connection_.is_alive()) { + Close(ZX_ERR_IO_REFUSED); + return; + } + + if (connection_->parameters().view().output_data_path().Read() != + pw::bluetooth::emboss::ScoDataPath::HCI) { + bt_log(WARN, "fidl", "%s called for a non-HCI SCO connection", __func__); + Close(ZX_ERR_IO_NOT_PRESENT); + return; + } + + if (!request.has_data()) { + Close(ZX_ERR_INVALID_ARGS); + return; + } + std::vector data = std::move(*request.mutable_data()); + + auto buffer = std::make_unique(data.size()); + buffer->Write(data.data(), data.size()); + if (!connection_->Send(std::move(buffer))) { + bt_log(WARN, "fidl", "%s: failed to send SCO packet", __func__); + Close(ZX_ERR_IO); + return; + } + callback(fidlbredr::ScoConnection_Write_Result::WithResponse( + fidlbredr::ScoConnection_Write_Response())); +} + +void ProfileServer::ScoConnectionServer::handle_unknown_method( + uint64_t ordinal, bool method_has_response) { + bt_log(WARN, + "fidl", + "ScoConnectionServer received unknown method with ordinal %lu", + ordinal); +} + +void ProfileServer::ScoConnectionServer::TryRead() { + if (!read_cb_) { + return; + } + std::unique_ptr packet = connection_->Read(); + if (!packet) { + return; + } + std::vector payload; + fidlbredr::RxPacketStatus status = + fidl_helpers::ScoPacketStatusToFidl(packet->packet_status_flag()); + if (packet->packet_status_flag() != + bt::hci_spec::SynchronousDataPacketStatusFlag::kNoDataReceived) { + payload = packet->view().payload_data().ToVector(); + } + fidlbredr::ScoConnection_Read_Response response; + response.set_data(std::move(payload)); + response.set_status_flag(status); + read_cb_( + fidlbredr::ScoConnection_Read_Result::WithResponse(std::move(response))); +} + +void ProfileServer::ScoConnectionServer::Close(zx_status_t epitaph) { + if (connection_.is_alive()) { + connection_->Deactivate(); + } + binding()->Close(epitaph); + profile_server_->sco_connection_servers_.erase(this); +} + +void ProfileServer::Advertise( + fuchsia::bluetooth::bredr::ProfileAdvertiseRequest request, + AdvertiseCallback callback) { + if (!request.has_services() || !request.has_receiver()) { + callback(fidlbredr::Profile_Advertise_Result::WithErr( + fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS)); + return; + } + if (!request.has_parameters()) { + request.set_parameters(fbt::ChannelParameters()); + } + std::vector registering; + + for (auto& definition : request.services()) { + auto rec = fidl_helpers::ServiceDefinitionToServiceRecord(definition); + // Drop the receiver on error. + if (rec.is_error()) { + bt_log(WARN, + "fidl", + "%s: Failed to create service record from service defintion", + __FUNCTION__); + callback( + fpromise::error(fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS)); + return; + } + registering.emplace_back(std::move(rec.value())); + } + + BT_ASSERT(adapter().is_alive()); + BT_ASSERT(adapter()->bredr()); + + uint64_t next = advertised_total_ + 1; + + auto registration_handle = adapter()->bredr()->RegisterService( + std::move(registering), + FidlToChannelParameters(request.parameters()), + [this, next](auto channel, const auto& protocol_list) { + OnChannelConnected(next, std::move(channel), std::move(protocol_list)); + }); + + if (!registration_handle) { + bt_log(WARN, "fidl", "%s: Failed to register service", __FUNCTION__); + callback(fpromise::error(fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS)); + return; + }; + + const auto registered_records = + adapter()->bredr()->GetRegisteredServices(registration_handle); + std::vector + registered_definitions; + for (auto& record : registered_records) { + auto def = fidl_helpers::ServiceRecordToServiceDefinition(record); + // Shouldn't fail in practice; the records are all well-formed and validated + // earlier in this function. + if (def.is_error()) { + bt_log(WARN, + "fidl", + "Failed to construct service definition from record: %lu", + def.error()); + continue; + } + registered_definitions.emplace_back(std::move(def.value())); + } + + fidlbredr::ConnectionReceiverPtr receiver = + request.mutable_receiver()->Bind(); + // Monitor events on the `ConnectionReceiver`. Remove the service if the FIDL + // client revokes the service registration. + receiver.events().OnRevoke = [this, ad_id = next]() { + bt_log(DEBUG, + "fidl", + "Connection receiver revoked. Ending service advertisement %lu", + ad_id); + OnConnectionReceiverClosed(ad_id); + }; + // Errors on the `ConnectionReceiver` will result in service unregistration. + receiver.set_error_handler([this, ad_id = next](zx_status_t status) { + bt_log(DEBUG, + "fidl", + "Connection receiver closed with error: %s. Ending service " + "advertisement %lu", + zx_status_get_string(status), + ad_id); + OnConnectionReceiverClosed(ad_id); + }); + + current_advertised_.try_emplace( + next, std::move(receiver), registration_handle); + advertised_total_ = next; + fuchsia::bluetooth::bredr::Profile_Advertise_Response result; + result.set_services(std::move(registered_definitions)); + callback(fuchsia::bluetooth::bredr::Profile_Advertise_Result::WithResponse( + std::move(result))); +} + +void ProfileServer::Search( + ::fuchsia::bluetooth::bredr::ProfileSearchRequest request) { + if (!request.has_results() || !request.has_service_uuid()) { + bt_log(WARN, "fidl", "%s: missing parameter", __FUNCTION__); + return; + } + + bt::UUID search_uuid(static_cast(request.service_uuid())); + std::unordered_set attributes; + if (request.has_attr_ids() && !request.attr_ids().empty()) { + attributes.insert(request.attr_ids().begin(), request.attr_ids().end()); + // Always request the ProfileDescriptor for the event + attributes.insert(bt::sdp::kBluetoothProfileDescriptorList); + } + + BT_DEBUG_ASSERT(adapter().is_alive()); + + auto next = searches_total_ + 1; + + auto search_id = adapter()->bredr()->AddServiceSearch( + search_uuid, + std::move(attributes), + [this, next](auto id, const auto& attrs) { + OnServiceFound(next, id, attrs); + }); + + if (!search_id) { + return; + } + + auto results_ptr = request.mutable_results()->Bind(); + results_ptr.set_error_handler( + [this, next](zx_status_t status) { OnSearchResultError(next, status); }); + + searches_.try_emplace(next, std::move(results_ptr), search_id); + searches_total_ = next; +} + +void ProfileServer::Connect(fuchsia::bluetooth::PeerId peer_id, + fidlbredr::ConnectParameters connection, + ConnectCallback callback) { + bt::PeerId id{peer_id.value}; + + // Anything other than L2CAP is not supported by this server. + if (!connection.is_l2cap()) { + bt_log( + WARN, + "fidl", + "%s: non-l2cap connections are not supported (is_rfcomm: %d, peer: %s)", + __FUNCTION__, + connection.is_rfcomm(), + bt_str(id)); + callback(fpromise::error(fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS)); + return; + } + + // The L2CAP parameters must include a PSM. ChannelParameters are optional. + auto l2cap_params = std::move(connection.l2cap()); + if (!l2cap_params.has_psm()) { + bt_log(WARN, + "fidl", + "%s: missing l2cap psm (peer: %s)", + __FUNCTION__, + bt_str(id)); + callback(fpromise::error(fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS)); + return; + } + uint16_t psm = l2cap_params.psm(); + + fbt::ChannelParameters parameters = + std::move(*l2cap_params.mutable_parameters()); + + auto connected_cb = [self = weak_self_.GetWeakPtr(), + cb = callback.share(), + id](bt::l2cap::Channel::WeakPtr chan) { + if (!chan.is_alive()) { + bt_log(INFO, + "fidl", + "Connect: Channel socket is empty, returning failed. (peer: %s)", + bt_str(id)); + cb(fpromise::error(fuchsia::bluetooth::ErrorCode::FAILED)); + return; + } + + if (!self.is_alive()) { + cb(fpromise::error(fuchsia::bluetooth::ErrorCode::FAILED)); + return; + } + + std::optional fidl_chan = + self->ChannelToFidl(std::move(chan)); + if (!fidl_chan) { + cb(fpromise::error(fuchsia::bluetooth::ErrorCode::FAILED)); + return; + } + + cb(fpromise::ok(std::move(fidl_chan.value()))); + }; + BT_DEBUG_ASSERT(adapter().is_alive()); + + adapter()->bredr()->OpenL2capChannel( + id, + psm, + fidl_helpers::FidlToBrEdrSecurityRequirements(parameters), + FidlToChannelParameters(parameters), + std::move(connected_cb)); +} + +void ProfileServer::ConnectSco( + ::fuchsia::bluetooth::bredr::ProfileConnectScoRequest request) { + if (!request.has_connection()) { + bt_log(WARN, "fidl", "%s missing connection", __FUNCTION__); + return; + } + std::unique_ptr connection = + std::make_unique( + std::move(*request.mutable_connection()), this); + ScoConnectionServer* connection_raw = connection.get(); + WeakPtr connection_weak = connection->GetWeakPtr(); + + if (!request.has_peer_id() || !request.has_initiator() || + !request.has_params() || request.params().empty()) { + connection->OnError(fidlbredr::ScoErrorCode::INVALID_ARGUMENTS); + return; + } + bt::PeerId peer_id(request.peer_id().value); + + if (request.initiator() && request.params().size() != 1u) { + bt_log(WARN, + "fidl", + "%s: too many parameters in initiator request (peer: %s)", + __FUNCTION__, + bt_str(peer_id)); + connection->OnError(fidlbredr::ScoErrorCode::INVALID_ARGUMENTS); + return; + } + + auto params_result = + fidl_helpers::FidlToScoParametersVector(request.params()); + if (params_result.is_error()) { + bt_log(WARN, + "fidl", + "%s: invalid parameters (peer: %s)", + __FUNCTION__, + bt_str(peer_id)); + connection->OnError(fidlbredr::ScoErrorCode::INVALID_ARGUMENTS); + return; + } + connection->set_parameters(std::move(*request.mutable_params())); + auto params = params_result.value(); + + sco_connection_servers_.emplace(connection_raw, std::move(connection)); + + if (request.initiator()) { + auto callback = [self = weak_self_.GetWeakPtr(), connection_weak]( + bt::sco::ScoConnectionManager::OpenConnectionResult + result) mutable { + // The connection may complete after this server is destroyed. + if (!self.is_alive()) { + // Prevent leaking connections. + if (result.is_ok()) { + result.value()->Deactivate(); + } + return; + } + if (result.is_error()) { + self->OnScoConnectionResult(connection_weak, result.take_error()); + return; + } + self->OnScoConnectionResult( + connection_weak, + fit::ok(std::make_pair(std::move(result.value()), + /*parameter index=*/0u))); + }; + // If the BR/EDR connection doesn't exist, no handle will be returned and + // the callback will be synchronously called with an error. + std::optional handle = + adapter()->bredr()->OpenScoConnection( + peer_id, params.front(), std::move(callback)); + if (handle && connection_weak.is_alive()) { + connection_weak->set_request_handle(std::move(*handle)); + } + return; + } + + auto callback = [self = weak_self_.GetWeakPtr(), connection_weak]( + bt::sco::ScoConnectionManager::AcceptConnectionResult + result) mutable { + // The connection may complete after this server is destroyed. + if (!self.is_alive()) { + // Prevent leaking connections. + if (result.is_ok()) { + result.value().first->Deactivate(); + } + return; + } + + self->OnScoConnectionResult(connection_weak, std::move(result)); + }; + // If the BR/EDR connection doesn't exist, no handle will be returned and the + // callback will be synchronously called with an error. + std::optional handle = + adapter()->bredr()->AcceptScoConnection( + peer_id, params, std::move(callback)); + if (handle && connection_weak.is_alive()) { + connection_weak->set_request_handle(std::move(*handle)); + } +} + +void ProfileServer::handle_unknown_method(uint64_t ordinal, + bool method_has_response) { + bt_log(WARN, "fidl", "ProfileServer: unknown method received"); +} + +void ProfileServer::OnChannelConnected( + uint64_t ad_id, + bt::l2cap::Channel::WeakPtr channel, + const bt::sdp::DataElement& protocol_list) { + auto it = current_advertised_.find(ad_id); + if (it == current_advertised_.end()) { + // The receiver has disappeared, do nothing. + return; + } + + BT_DEBUG_ASSERT(adapter().is_alive()); + auto handle = channel->link_handle(); + auto id = adapter()->bredr()->GetPeerId(handle); + + // The protocol that is connected should be L2CAP, because that is the only + // thing that we can connect. We can't say anything about what the higher + // level protocols will be. + auto prot_seq = protocol_list.At(0); + BT_ASSERT(prot_seq); + + fidlbredr::ProtocolDescriptorPtr desc = + DataElementToProtocolDescriptor(prot_seq); + BT_ASSERT(desc); + + fuchsia::bluetooth::PeerId peer_id{id.value()}; + + std::vector list; + list.emplace_back(std::move(*desc)); + + std::optional fidl_chan = + ChannelToFidl(std::move(channel)); + if (!fidl_chan) { + bt_log(INFO, "fidl", "ChannelToFidl failed. Ignoring channel."); + return; + } + + it->second.receiver->Connected( + peer_id, std::move(fidl_chan.value()), std::move(list)); +} + +void ProfileServer::OnConnectionReceiverClosed(uint64_t ad_id) { + auto it = current_advertised_.find(ad_id); + if (it == current_advertised_.end() || !adapter().is_alive()) { + return; + } + + adapter()->bredr()->UnregisterService(it->second.registration_handle); + + current_advertised_.erase(it); +} + +void ProfileServer::OnSearchResultError(uint64_t search_id, + zx_status_t status) { + bt_log(DEBUG, + "fidl", + "Search result closed, ending search %lu reason %s", + search_id, + zx_status_get_string(status)); + + auto it = searches_.find(search_id); + + if (it == searches_.end() || !adapter().is_alive()) { + return; + } + + adapter()->bredr()->RemoveServiceSearch(it->second.search_id); + + searches_.erase(it); +} + +void ProfileServer::OnServiceFound( + uint64_t search_id, + bt::PeerId peer_id, + const std::map& attributes) { + auto search_it = searches_.find(search_id); + if (search_it == searches_.end()) { + // Search was de-registered. + return; + } + + // Convert ProfileDescriptor Attribute + auto it = attributes.find(bt::sdp::kProtocolDescriptorList); + + fidl::VectorPtr descriptor_list; + + if (it != attributes.end()) { + std::vector list; + size_t idx = 0; + auto* sdp_list_element = it->second.At(idx); + while (sdp_list_element != nullptr) { + fidlbredr::ProtocolDescriptorPtr desc = + DataElementToProtocolDescriptor(sdp_list_element); + if (!desc) { + break; + } + list.push_back(std::move(*desc)); + sdp_list_element = it->second.At(++idx); + } + descriptor_list = std::move(list); + } + + // Add the rest of the attributes + std::vector fidl_attrs; + + for (const auto& it : attributes) { + auto attr = std::make_unique(); + attr->set_id(it.first); + attr->set_element(std::move(*DataElementToFidl(&it.second))); + fidl_attrs.emplace_back(std::move(*attr)); + } + + fuchsia::bluetooth::PeerId fidl_peer_id{peer_id.value()}; + + search_it->second.results->ServiceFound(fidl_peer_id, + std::move(descriptor_list), + std::move(fidl_attrs), + [](auto) {}); +} + +void ProfileServer::OnScoConnectionResult( + WeakPtr& server, + bt::sco::ScoConnectionManager::AcceptConnectionResult result) { + if (result.is_error()) { + if (!server.is_alive()) { + return; + } + + bt_log(INFO, + "fidl", + "%s: SCO connection failed (status: %s)", + __FUNCTION__, + bt::HostErrorToString(result.error_value()).c_str()); + + fidlbredr::ScoErrorCode fidl_error = fidlbredr::ScoErrorCode::FAILURE; + if (result.error_value() == bt::HostError::kCanceled) { + fidl_error = fidlbredr::ScoErrorCode::CANCELLED; + } + if (result.error_value() == bt::HostError::kParametersRejected) { + fidl_error = fidlbredr::ScoErrorCode::PARAMETERS_REJECTED; + } + server->OnError(fidl_error); + return; + } + + bt::sco::ScoConnection::WeakPtr connection = std::move(result.value().first); + const uint16_t max_tx_data_size = connection->max_tx_sdu_size(); + + if (!server.is_alive()) { + connection->Deactivate(); + return; + } + server->set_connection(std::move(connection)); + + server->Activate(); + if (!server.is_alive()) { + return; + } + + size_t parameter_index = result.value().second; + BT_ASSERT_MSG(parameter_index < server->parameters().size(), + "parameter_index (%zu) >= request->parameters.size() (%zu)", + parameter_index, + server->parameters().size()); + fidlbredr::ScoConnectionParameters parameters = + fidl::Clone(server->parameters()[parameter_index]); + parameters.set_max_tx_data_size(max_tx_data_size); + server->OnConnectedParams(std::move(parameters)); +} + +void ProfileServer::OnAudioDirectionExtError(AudioDirectionExt* ext_server, + zx_status_t status) { + bt_log(DEBUG, + "fidl", + "audio direction ext server closed (reason: %s)", + zx_status_get_string(status)); + auto handle = audio_direction_ext_servers_.extract(ext_server->unique_id()); + if (handle.empty()) { + bt_log(WARN, + "fidl", + "could not find ext server in audio direction ext error callback"); + } +} + +fidl::InterfaceHandle +ProfileServer::BindAudioDirectionExtServer( + bt::l2cap::Channel::WeakPtr channel) { + fidl::InterfaceHandle client; + + bt::l2cap::Channel::UniqueId unique_id = channel->unique_id(); + + auto audio_direction_ext_server = std::make_unique( + client.NewRequest(), std::move(channel)); + AudioDirectionExt* server_ptr = audio_direction_ext_server.get(); + + audio_direction_ext_server->set_error_handler( + [this, server_ptr](zx_status_t status) { + OnAudioDirectionExtError(server_ptr, status); + }); + + audio_direction_ext_servers_[unique_id] = + std::move(audio_direction_ext_server); + + return client; +} + +void ProfileServer::OnL2capParametersExtError(L2capParametersExt* ext_server, + zx_status_t status) { + bt_log(DEBUG, + "fidl", + "fidl parameters ext server closed (reason: %s)", + zx_status_get_string(status)); + auto handle = l2cap_parameters_ext_servers_.extract(ext_server->unique_id()); + if (handle.empty()) { + bt_log(WARN, + "fidl", + "could not find ext server in l2cap parameters ext error callback"); + } +} + +fidl::InterfaceHandle +ProfileServer::BindL2capParametersExtServer( + bt::l2cap::Channel::WeakPtr channel) { + fidl::InterfaceHandle client; + + bt::l2cap::Channel::UniqueId unique_id = channel->unique_id(); + + auto l2cap_parameters_ext_server = std::make_unique( + client.NewRequest(), std::move(channel)); + L2capParametersExt* server_ptr = l2cap_parameters_ext_server.get(); + + l2cap_parameters_ext_server->set_error_handler( + [this, server_ptr](zx_status_t status) { + OnL2capParametersExtError(server_ptr, status); + }); + + l2cap_parameters_ext_servers_[unique_id] = + std::move(l2cap_parameters_ext_server); + return client; +} + +void ProfileServer::OnAudioOffloadExtError(AudioOffloadExt* ext_server, + zx_status_t status) { + bt_log(DEBUG, + "fidl", + "audio offload ext server closed (reason: %s)", + zx_status_get_string(status)); + auto handle = audio_offload_ext_servers_.extract(ext_server->unique_id()); + if (handle.empty()) { + bt_log(WARN, + "fidl", + "could not find ext server in audio offload ext error callback"); + } +} + +fidl::InterfaceHandle +ProfileServer::BindAudioOffloadExtServer(bt::l2cap::Channel::WeakPtr channel) { + fidl::InterfaceHandle client; + + bt::l2cap::Channel::UniqueId unique_id = channel->unique_id(); + + std::unique_ptr + audio_offload_ext_server = std::make_unique( + *this, client.NewRequest(), std::move(channel), adapter_); + AudioOffloadExt* server_ptr = audio_offload_ext_server.get(); + + audio_offload_ext_server->set_error_handler( + [this, server_ptr](zx_status_t status) { + OnAudioOffloadExtError(server_ptr, status); + }); + + audio_offload_ext_servers_[unique_id] = std::move(audio_offload_ext_server); + + return client; +} + +std::optional> +ProfileServer::BindBrEdrConnectionServer( + bt::l2cap::Channel::WeakPtr channel, + fit::callback closed_callback) { + fidl::InterfaceHandle client; + + bt::l2cap::Channel::UniqueId unique_id = channel->unique_id(); + + std::unique_ptr connection_server = + BrEdrConnectionServer::Create( + client.NewRequest(), std::move(channel), std::move(closed_callback)); + if (!connection_server) { + return std::nullopt; + } + + bredr_connection_servers_[unique_id] = std::move(connection_server); + return client; +} + +std::optional ProfileServer::ChannelToFidl( + bt::l2cap::Channel::WeakPtr channel) { + BT_ASSERT(channel.is_alive()); + fidlbredr::Channel fidl_chan; + fidl_chan.set_channel_mode(ChannelModeToFidl(channel->mode())); + fidl_chan.set_max_tx_sdu_size(channel->max_tx_sdu_size()); + if (channel->info().flush_timeout) { + fidl_chan.set_flush_timeout(channel->info().flush_timeout->count()); + } + + auto closed_cb = [this, unique_id = channel->unique_id()]() { + bt_log(DEBUG, + "fidl", + "Channel closed_cb called, destroying servers (unique_id: %d)", + unique_id); + bredr_connection_servers_.erase(unique_id); + l2cap_parameters_ext_servers_.erase(unique_id); + audio_direction_ext_servers_.erase(unique_id); + audio_offload_ext_servers_.erase(unique_id); + audio_offload_controller_server_ = nullptr; + }; + if (use_sockets_) { + auto sock = l2cap_socket_factory_.MakeSocketForChannel( + channel, std::move(closed_cb)); + fidl_chan.set_socket(std::move(sock)); + } else { + std::optional> + connection = BindBrEdrConnectionServer(channel, std::move(closed_cb)); + if (!connection) { + return std::nullopt; + } + fidl_chan.set_connection(std::move(connection.value())); + } + + if (adapter()->state().IsControllerFeatureSupported( + FeaturesBits::kSetAclPriorityCommand)) { + fidl_chan.set_ext_direction(BindAudioDirectionExtServer(channel)); + } + + if (adapter()->state().IsControllerFeatureSupported( + FeaturesBits::kAndroidVendorExtensions) && + adapter() + ->state() + .android_vendor_capabilities->a2dp_source_offload_capability_mask()) { + fidl_chan.set_ext_audio_offload(BindAudioOffloadExtServer(channel)); + } + + fidl_chan.set_ext_l2cap(BindL2capParametersExtServer(channel)); + + return fidl_chan; +} + +void ProfileServer::AudioDirectionExt::SetPriority( + fuchsia::bluetooth::bredr::A2dpDirectionPriority priority, + SetPriorityCallback callback) { + channel_->RequestAclPriority( + FidlToAclPriority(priority), [cb = std::move(callback)](auto result) { + if (result.is_ok()) { + cb(fpromise::ok()); + return; + } + bt_log(DEBUG, "fidl", "ACL priority request failed"); + cb(fpromise::error(fuchsia::bluetooth::ErrorCode::FAILED)); + }); +} + +void ProfileServer::AudioDirectionExt::handle_unknown_method( + uint64_t ordinal, bool method_has_response) { + bt_log(WARN, "fidl", "AudioDirectionExt: unknown method received"); +} + +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/profile_server_test.cc b/pw_bluetooth_sapphire/fuchsia/host/fidl/profile_server_test.cc new file mode 100644 index 0000000000..dcf0409818 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/profile_server_test.cc @@ -0,0 +1,3751 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/fidl/profile_server.h" + +#include +#include +#include + +#include + +#include "fuchsia/bluetooth/bredr/cpp/fidl.h" +#include "fuchsia/bluetooth/cpp/fidl.h" +#include "lib/fidl/cpp/vector.h" +#include "lib/zx/socket.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/adapter_test_fixture.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/fake_adapter_test_fixture.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/helpers.h" +#include "pw_bluetooth_sapphire/internal/host/common/host_error.h" +#include "pw_bluetooth_sapphire/internal/host/gap/fake_pairing_delegate.h" +#include "pw_bluetooth_sapphire/internal/host/l2cap/fake_channel.h" +#include "pw_bluetooth_sapphire/internal/host/l2cap/fake_l2cap.h" +#include "pw_bluetooth_sapphire/internal/host/sdp/data_element.h" +#include "pw_bluetooth_sapphire/internal/host/sdp/sdp.h" +#include "pw_bluetooth_sapphire/internal/host/testing/fake_peer.h" +#include "pw_bluetooth_sapphire/internal/host/testing/test_helpers.h" +#include "pw_bluetooth_sapphire/internal/host/testing/test_packets.h" +#include "pw_unit_test/framework.h" + +namespace bthost { +namespace { + +namespace fbt = fuchsia::bluetooth; +namespace fidlbredr = fuchsia::bluetooth::bredr; +namespace android_emb = pw::bluetooth::vendor::android_hci; + +using bt::l2cap::testing::FakeChannel; +using pw::bluetooth::AclPriority; +using FeaturesBits = pw::bluetooth::Controller::FeaturesBits; + +void NopAdvertiseCallback(fidlbredr::Profile_Advertise_Result) {} + +const bt::DeviceAddress kTestDevAddr(bt::DeviceAddress::Type::kBREDR, {1}); +constexpr bt::l2cap::Psm kPsm = bt::l2cap::kAVDTP; + +constexpr uint16_t kSynchronousDataPacketLength = 64; +constexpr uint8_t kTotalNumSynchronousDataPackets = 1; + +fidlbredr::ScoConnectionParameters CreateScoConnectionParameters( + fidlbredr::HfpParameterSet param_set = fidlbredr::HfpParameterSet::T2) { + fidlbredr::ScoConnectionParameters params; + params.set_parameter_set(param_set); + params.set_air_coding_format(fbt::AssignedCodingFormat::MSBC); + params.set_air_frame_size(8u); + params.set_io_bandwidth(32000); + params.set_io_coding_format(fbt::AssignedCodingFormat::LINEAR_PCM); + params.set_io_frame_size(16u); + params.set_io_pcm_data_format( + fuchsia::hardware::audio::SampleFormat::PCM_SIGNED); + params.set_io_pcm_sample_payload_msb_position(3u); + params.set_path(fidlbredr::DataPath::OFFLOAD); + return params; +} + +fidlbredr::ServiceDefinition MakeFIDLServiceDefinition() { + fidlbredr::ServiceDefinition def; + def.mutable_service_class_uuids()->emplace_back( + fidl_helpers::UuidToFidl(bt::sdp::profile::kAudioSink)); + + fidlbredr::ProtocolDescriptor l2cap_proto; + l2cap_proto.set_protocol(fidlbredr::ProtocolIdentifier::L2CAP); + fidlbredr::DataElement l2cap_data_el; + l2cap_data_el.set_uint16(fidlbredr::PSM_AVDTP); + std::vector l2cap_params; + l2cap_params.emplace_back(std::move(l2cap_data_el)); + l2cap_proto.set_params(std::move(l2cap_params)); + + def.mutable_protocol_descriptor_list()->emplace_back(std::move(l2cap_proto)); + + fidlbredr::ProtocolDescriptor avdtp_proto; + avdtp_proto.set_protocol(fidlbredr::ProtocolIdentifier::AVDTP); + fidlbredr::DataElement avdtp_data_el; + avdtp_data_el.set_uint16(0x0103); // Version 1.3 + std::vector avdtp_params; + avdtp_params.emplace_back(std::move(avdtp_data_el)); + avdtp_proto.set_params(std::move(avdtp_params)); + + def.mutable_protocol_descriptor_list()->emplace_back(std::move(avdtp_proto)); + + fidlbredr::ProfileDescriptor prof_desc; + prof_desc.set_profile_id( + fidlbredr::ServiceClassProfileIdentifier::ADVANCED_AUDIO_DISTRIBUTION); + prof_desc.set_major_version(1); + prof_desc.set_minor_version(3); + def.mutable_profile_descriptors()->emplace_back(std::move(prof_desc)); + + // Additional attributes are also OK. + fidlbredr::Attribute addl_attr; + addl_attr.set_id(0x000A); // Documentation URL ID + fidlbredr::DataElement doc_url_el; + doc_url_el.set_url("fuchsia.dev"); + addl_attr.set_element(std::move(doc_url_el)); + def.mutable_additional_attributes()->emplace_back(std::move(addl_attr)); + + return def; +} + +fidlbredr::ServiceDefinition MakeMapMceServiceDefinition() { + // MAP MCE service definition requires RFCOMM and OBEX. + fidlbredr::ServiceDefinition def; + def.mutable_service_class_uuids()->emplace_back( + fidl_helpers::UuidToFidl(bt::sdp::profile::kMessageNotificationServer)); + + // [[L2CAP], [RFCOMM, Channel#], [OBEX]] + fidlbredr::ProtocolDescriptor l2cap_proto; + l2cap_proto.set_protocol(fidlbredr::ProtocolIdentifier::L2CAP); + std::vector l2cap_params; + l2cap_proto.set_params(std::move(l2cap_params)); + def.mutable_protocol_descriptor_list()->emplace_back(std::move(l2cap_proto)); + fidlbredr::ProtocolDescriptor rfcomm_proto; + rfcomm_proto.set_protocol(fidlbredr::ProtocolIdentifier::RFCOMM); + fidlbredr::DataElement rfcomm_data_el; + rfcomm_data_el.set_uint8(5); // Random RFCOMM channel + std::vector rfcomm_params; + rfcomm_params.emplace_back(std::move(rfcomm_data_el)); + rfcomm_proto.set_params(std::move(rfcomm_params)); + def.mutable_protocol_descriptor_list()->emplace_back(std::move(rfcomm_proto)); + fidlbredr::ProtocolDescriptor obex_proto; + obex_proto.set_protocol(fidlbredr::ProtocolIdentifier::OBEX); + std::vector obex_params; + obex_proto.set_params(std::move(obex_params)); + def.mutable_protocol_descriptor_list()->emplace_back(std::move(obex_proto)); + + // Additional protocols. NOTE: This is fictional and not part of a real MCE + // definition. + std::vector additional_proto; + fidlbredr::ProtocolDescriptor additional_l2cap_proto; + additional_l2cap_proto.set_protocol(fidlbredr::ProtocolIdentifier::L2CAP); + fidlbredr::DataElement additional_l2cap_data_el; + additional_l2cap_data_el.set_uint16(fidlbredr::PSM_DYNAMIC); + std::vector additional_l2cap_params; + additional_l2cap_params.emplace_back(std::move(additional_l2cap_data_el)); + additional_l2cap_proto.set_params(std::move(additional_l2cap_params)); + additional_proto.emplace_back(std::move(additional_l2cap_proto)); + fidlbredr::ProtocolDescriptor additional_obex_proto; + additional_obex_proto.set_protocol(fidlbredr::ProtocolIdentifier::OBEX); + std::vector additional_obex_params; + additional_obex_proto.set_params(std::move(additional_obex_params)); + additional_proto.emplace_back(std::move(additional_obex_proto)); + def.mutable_additional_protocol_descriptor_lists()->emplace_back( + std::move(additional_proto)); + + fidlbredr::Information info; + info.set_language("en"); + info.set_name("foo_test"); + def.mutable_information()->emplace_back(std::move(info)); + + fidlbredr::ProfileDescriptor prof_desc; + prof_desc.set_profile_id( + fidlbredr::ServiceClassProfileIdentifier::MESSAGE_ACCESS_PROFILE); + prof_desc.set_major_version(1); + prof_desc.set_minor_version(4); + def.mutable_profile_descriptors()->emplace_back(std::move(prof_desc)); + + // Additional attributes - one requests a dynamic PSM. + fidlbredr::Attribute goep_attr; + goep_attr.set_id(0x200); // GoepL2capPsm + fidlbredr::DataElement goep_el; + goep_el.set_uint16(fidlbredr::PSM_DYNAMIC); + goep_attr.set_element(std::move(goep_el)); + def.mutable_additional_attributes()->emplace_back(std::move(goep_attr)); + + fidlbredr::Attribute addl_attr; + addl_attr.set_id(0x317); // MAP supported features + fidlbredr::DataElement addl_el; + addl_el.set_uint32(1); // Random features + addl_attr.set_element(std::move(addl_el)); + def.mutable_additional_attributes()->emplace_back(std::move(addl_attr)); + + return def; +} + +// Returns a basic protocol list element with a protocol descriptor list that +// only contains an L2CAP descriptor. +bt::sdp::DataElement MakeL2capProtocolListElement() { + bt::sdp::DataElement l2cap_uuid_el; + l2cap_uuid_el.Set(bt::UUID(bt::sdp::protocol::kL2CAP)); + std::vector l2cap_descriptor_list; + l2cap_descriptor_list.emplace_back(std::move(l2cap_uuid_el)); + std::vector protocols; + protocols.emplace_back(std::move(l2cap_descriptor_list)); + bt::sdp::DataElement protocol_list_el; + protocol_list_el.Set(std::move(protocols)); + return protocol_list_el; +} + +using TestingBase = bthost::testing::AdapterTestFixture; +class ProfileServerTest : public TestingBase { + public: + ProfileServerTest() = default; + ~ProfileServerTest() override = default; + + protected: + void SetUp(FeaturesBits features) { + bt::testing::FakeController::Settings settings; + settings.ApplyDualModeDefaults(); + settings.synchronous_data_packet_length = kSynchronousDataPacketLength; + settings.total_num_synchronous_data_packets = + kTotalNumSynchronousDataPackets; + TestingBase::SetUp(settings, features); + + fidlbredr::ProfileHandle profile_handle; + client_.Bind(std::move(profile_handle)); + server_ = std::make_unique(adapter()->AsWeakPtr(), + client_.NewRequest(dispatcher())); + } + void SetUp() override { SetUp(FeaturesBits{0}); } + + void TearDown() override { + RunLoopUntilIdle(); + client_ = nullptr; + server_ = nullptr; + TestingBase::TearDown(); + } + + ProfileServer* server() const { return server_.get(); } + + fidlbredr::ProfilePtr& client() { return client_; } + + bt::gap::PeerCache* peer_cache() const { return adapter()->peer_cache(); } + + private: + std::unique_ptr server_; + fidlbredr::ProfilePtr client_; + + BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(ProfileServerTest); +}; + +class FakeConnectionReceiver + : public fidlbredr::testing::ConnectionReceiver_TestBase { + public: + FakeConnectionReceiver(fidl::InterfaceRequest request, + async_dispatcher_t* dispatcher) + : binding_(this, std::move(request), dispatcher), + connected_count_(0), + closed_(false) { + binding_.set_error_handler([&](zx_status_t /*status*/) { closed_ = true; }); + } + + void Connected(fuchsia::bluetooth::PeerId peer_id, + fidlbredr::Channel channel, + std::vector protocol) override { + peer_id_ = peer_id; + channel_ = std::move(channel); + protocol_ = std::move(protocol); + connected_count_++; + } + + void Revoke() { binding_.events().OnRevoke(); } + + size_t connected_count() const { return connected_count_; } + const std::optional& peer_id() const { + return peer_id_; + } + const std::optional& channel() const { return channel_; } + const std::optional>& protocol() + const { + return protocol_; + } + bool closed() { return closed_; } + + std::optional bind_ext_direction() { + if (!channel().has_value()) { + return std::nullopt; + } + fidlbredr::AudioDirectionExtPtr client = + channel_.value().mutable_ext_direction()->Bind(); + return client; + } + + fidlbredr::Channel take_channel() { + fidlbredr::Channel channel = std::move(channel_.value()); + channel_.reset(); + return channel; + } + + private: + fidl::Binding binding_; + size_t connected_count_; + std::optional peer_id_; + std::optional channel_; + std::optional> protocol_; + bool closed_; + + void NotImplemented_(const std::string& name) override { + FAIL() << name << " is not implemented"; + } +}; + +class FakeSearchResults : public fidlbredr::testing::SearchResults_TestBase { + public: + FakeSearchResults(fidl::InterfaceRequest request, + async_dispatcher_t* dispatcher) + : binding_(this, std::move(request), dispatcher), + service_found_count_(0) { + binding_.set_error_handler([this](zx_status_t) { closed_ = true; }); + } + + void ServiceFound(fuchsia::bluetooth::PeerId peer_id, + fidl::VectorPtr protocol, + std::vector attributes, + ServiceFoundCallback callback) override { + peer_id_ = peer_id; + attributes_ = std::move(attributes); + callback(fidlbredr::SearchResults_ServiceFound_Result::WithResponse( + fidlbredr::SearchResults_ServiceFound_Response())); + service_found_count_++; + } + + bool closed() const { return closed_; } + size_t service_found_count() const { return service_found_count_; } + const std::optional& peer_id() const { + return peer_id_; + } + const std::optional>& attributes() const { + return attributes_; + } + + private: + bool closed_ = false; + fidl::Binding binding_; + std::optional peer_id_; + std::optional> attributes_; + size_t service_found_count_; + + void NotImplemented_(const std::string& name) override { + FAIL() << name << " is not implemented"; + } +}; + +TEST_F(ProfileServerTest, ErrorOnInvalidDefinition) { + fidlbredr::ConnectionReceiverHandle receiver_handle; + fidl::InterfaceRequest request = + receiver_handle.NewRequest(); + + std::vector services; + fidlbredr::ServiceDefinition def; + // Empty service definition is not allowed - it must contain at least a + // service UUID. + + services.emplace_back(std::move(def)); + + size_t cb_count = 0; + auto cb = [&](fidlbredr::Profile_Advertise_Result result) { + cb_count++; + EXPECT_TRUE(result.is_err()); + EXPECT_EQ(result.err(), fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + }; + + fidlbredr::ProfileAdvertiseRequest adv_request; + adv_request.set_services(std::move(services)); + adv_request.set_receiver(std::move(receiver_handle)); + client()->Advertise(std::move(adv_request), std::move(cb)); + + RunLoopUntilIdle(); + + ASSERT_EQ(cb_count, 1u); + // Server should close because it's an invalid definition. + zx_signals_t signals; + request.channel().wait_one(ZX_CHANNEL_PEER_CLOSED, zx::time(0), &signals); + EXPECT_TRUE(signals & ZX_CHANNEL_PEER_CLOSED); +} + +TEST_F(ProfileServerTest, ErrorOnMultipleAdvertiseRequests) { + fidlbredr::ConnectionReceiverHandle receiver_handle1; + fidl::InterfaceRequest request1 = + receiver_handle1.NewRequest(); + + std::vector services1; + services1.emplace_back(MakeFIDLServiceDefinition()); + + size_t cb1_count = 0; + auto cb1 = [&](fidlbredr::Profile_Advertise_Result result) { + cb1_count++; + EXPECT_TRUE(result.is_response()); + }; + + fidlbredr::ProfileAdvertiseRequest adv_request1; + adv_request1.set_services(std::move(services1)); + adv_request1.set_receiver(std::move(receiver_handle1)); + client()->Advertise(std::move(adv_request1), std::move(cb1)); + + RunLoopUntilIdle(); + + // First callback should be invoked with success since the advertisement is + // valid. + ASSERT_EQ(cb1_count, 1u); + + fidlbredr::ConnectionReceiverHandle receiver_handle2; + fidl::InterfaceRequest request2 = + receiver_handle2.NewRequest(); + + std::vector services2; + services2.emplace_back(MakeFIDLServiceDefinition()); + + // Second callback should error because the second advertisement is requesting + // a taken PSM. + size_t cb2_count = 0; + auto cb2 = [&](fidlbredr::Profile_Advertise_Result response) { + cb2_count++; + EXPECT_TRUE(response.is_err()); + EXPECT_EQ(response.err(), fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + }; + + fidlbredr::ProfileAdvertiseRequest adv_request2; + adv_request2.set_services(std::move(services2)); + adv_request2.set_receiver(std::move(receiver_handle2)); + client()->Advertise(std::move(adv_request2), std::move(cb2)); + + RunLoopUntilIdle(); + + ASSERT_EQ(cb1_count, 1u); + ASSERT_EQ(cb2_count, 1u); + + // Second channel should close. + zx_signals_t signals; + request2.channel().wait_one(ZX_CHANNEL_PEER_CLOSED, zx::time(0), &signals); + EXPECT_TRUE(signals & ZX_CHANNEL_PEER_CLOSED); +} + +TEST_F(ProfileServerTest, ErrorOnInvalidConnectParametersNoPsm) { + // Random peer, since we don't expect the connection. + fuchsia::bluetooth::PeerId peer_id{123}; + + // No PSM provided - this is invalid. + fidlbredr::L2capParameters l2cap_params; + fidlbredr::ConnectParameters conn_params; + l2cap_params.set_parameters(fbt::ChannelParameters()); + conn_params.set_l2cap(std::move(l2cap_params)); + + // Expect an error result. + auto sock_cb = [](fidlbredr::Profile_Connect_Result result) { + EXPECT_TRUE(result.is_err()); + EXPECT_EQ(result.err(), fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + }; + + client()->Connect(peer_id, std::move(conn_params), std::move(sock_cb)); + RunLoopUntilIdle(); +} + +TEST_F(ProfileServerTest, ErrorOnInvalidConnectParametersRfcomm) { + // Random peer, since we don't expect the connection. + fuchsia::bluetooth::PeerId peer_id{123}; + + // RFCOMM Parameters are provided - this is not supported. + fidlbredr::RfcommParameters rfcomm_params; + fidlbredr::ConnectParameters conn_params; + conn_params.set_rfcomm(std::move(rfcomm_params)); + + // Expect an error result. + auto sock_cb = [](fidlbredr::Profile_Connect_Result result) { + EXPECT_TRUE(result.is_err()); + EXPECT_EQ(result.err(), fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + }; + + client()->Connect(peer_id, std::move(conn_params), std::move(sock_cb)); + RunLoopUntilIdle(); +} + +TEST_F(ProfileServerTest, DynamicPsmAdvertisementIsUpdated) { + fidlbredr::ConnectionReceiverHandle receiver_handle; + fidl::InterfaceRequest request = + receiver_handle.NewRequest(); + + std::vector services; + services.emplace_back(MakeMapMceServiceDefinition()); + + size_t cb_count = 0; + auto cb = [&](fidlbredr::Profile_Advertise_Result result) { + cb_count++; + EXPECT_TRUE(result.is_response()); + EXPECT_EQ(result.response().services().size(), 1u); + const auto registered_def = + std::move(result.response().mutable_services()->front()); + const auto original_def = MakeMapMceServiceDefinition(); + // The UUIDs, primary protocol list, & profile descriptors should be + // unchanged. + ASSERT_TRUE(::fidl::Equals(registered_def.service_class_uuids(), + original_def.service_class_uuids())); + ASSERT_TRUE(::fidl::Equals(registered_def.protocol_descriptor_list(), + original_def.protocol_descriptor_list())); + ASSERT_TRUE(::fidl::Equals(registered_def.profile_descriptors(), + original_def.profile_descriptors())); + // The additional protocol list should be updated with a randomly assigned + // dynamic PSM. + EXPECT_EQ(registered_def.additional_protocol_descriptor_lists().size(), 1u); + EXPECT_NE(registered_def.additional_protocol_descriptor_lists() + .front() + .front() + .params() + .front() + .uint16(), + fidlbredr::PSM_DYNAMIC); + // TODO(b/327758656): Verify information and additional attributes once + // implemented. + }; + + fidlbredr::ProfileAdvertiseRequest adv_request; + adv_request.set_services(std::move(services)); + adv_request.set_receiver(std::move(receiver_handle)); + client()->Advertise(std::move(adv_request), std::move(cb)); + + RunLoopUntilIdle(); + EXPECT_EQ(cb_count, 1u); +} + +TEST_F(ProfileServerTest, RevokeConnectionReceiverUnregistersAdvertisement) { + fidlbredr::ConnectionReceiverHandle receiver_handle; + FakeConnectionReceiver connect_receiver(receiver_handle.NewRequest(), + dispatcher()); + + std::vector services; + services.emplace_back(MakeFIDLServiceDefinition()); + + size_t cb_count = 0; + auto cb = [&](fidlbredr::Profile_Advertise_Result result) { + cb_count++; + EXPECT_TRUE(result.is_response()); + }; + + fidlbredr::ProfileAdvertiseRequest adv_request; + adv_request.set_services(std::move(services)); + adv_request.set_receiver(std::move(receiver_handle)); + client()->Advertise(std::move(adv_request), std::move(cb)); + RunLoopUntilIdle(); + + // Advertisement should be registered. The callback should be invoked with the + // advertised set of services, and the `ConnectionReceiver` should still be + // open. + ASSERT_EQ(cb_count, 1u); + ASSERT_FALSE(connect_receiver.closed()); + + // Server end of `ConnectionReceiver` revokes the advertisement. + connect_receiver.Revoke(); + RunLoopUntilIdle(); + + // Profile server should drop the advertisement - the `connect_receiver` + // should be closed. + ASSERT_TRUE(connect_receiver.closed()); +} + +class ProfileServerTestConnectedPeer : public ProfileServerTest { + public: + ProfileServerTestConnectedPeer() = default; + ~ProfileServerTestConnectedPeer() override = default; + + protected: + void SetUp(FeaturesBits features) { + ProfileServerTest::SetUp(features); + peer_ = peer_cache()->NewPeer(kTestDevAddr, /*connectable=*/true); + std::unique_ptr fake_peer = + std::make_unique(kTestDevAddr, pw_dispatcher()); + test_device()->AddPeer(std::move(fake_peer)); + + std::optional> status; + auto connect_cb = [this, &status](auto cb_status, auto cb_conn_ref) { + ASSERT_TRUE(cb_conn_ref); + status = cb_status; + connection_ = std::move(cb_conn_ref); + }; + + EXPECT_TRUE(adapter()->bredr()->Connect(peer_->identifier(), connect_cb)); + EXPECT_EQ(bt::gap::Peer::ConnectionState::kInitializing, + peer_->bredr()->connection_state()); + + RunLoopUntilIdle(); + ASSERT_TRUE(status.has_value()); + EXPECT_EQ(fit::ok(), status.value()); + ASSERT_TRUE(connection_); + EXPECT_EQ(peer_->identifier(), connection_->peer_id()); + EXPECT_NE(bt::gap::Peer::ConnectionState::kNotConnected, + peer_->bredr()->connection_state()); + } + + void SetUp() override { SetUp(FeaturesBits::kHciSco); } + + void TearDown() override { + connection_ = nullptr; + peer_ = nullptr; + ProfileServerTest::TearDown(); + } + + bt::gap::BrEdrConnection* connection() const { return connection_; } + + bt::gap::Peer* peer() const { return peer_; } + + private: + bt::gap::BrEdrConnection* connection_; + bt::gap::Peer* peer_; + + BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(ProfileServerTestConnectedPeer); +}; + +class ProfileServerTestScoConnected : public ProfileServerTestConnectedPeer { + public: + void SetUp() override { + fidlbredr::ScoConnectionParameters params = + CreateScoConnectionParameters(fidlbredr::HfpParameterSet::D0); + params.set_path(fidlbredr::DataPath::HOST); + SetUp(std::move(params)); + } + + void SetUp(fidlbredr::ScoConnectionParameters conn_params) { + ProfileServerTestConnectedPeer::SetUp(FeaturesBits::kHciSco); + + test_device()->set_configure_sco_cb( + [](auto, auto, auto, auto cb) { cb(PW_STATUS_OK); }); + test_device()->set_reset_sco_cb([](auto cb) { cb(PW_STATUS_OK); }); + + std::vector sco_params_list; + sco_params_list.emplace_back(std::move(conn_params)); + + fidlbredr::ProfileConnectScoRequest request; + request.set_peer_id( + fuchsia::bluetooth::PeerId{peer()->identifier().value()}); + request.set_initiator(false); + request.set_params(std::move(sco_params_list)); + fidlbredr::ScoConnectionHandle connection_handle; + request.set_connection(connection_handle.NewRequest()); + + sco_connection_ = connection_handle.Bind(); + sco_connection_.set_error_handler([this](zx_status_t status) { + sco_connection_ = nullptr; + sco_conn_error_ = status; + }); + + std::optional + connection_complete; + sco_connection_.events().OnConnectionComplete = + [&](fidlbredr::ScoConnectionOnConnectionCompleteRequest request) { + connection_complete = std::move(request); + }; + + client()->ConnectSco(std::move(request)); + RunLoopUntilIdle(); + test_device()->SendConnectionRequest(peer()->address(), + pw::bluetooth::emboss::LinkType::SCO); + RunLoopUntilIdle(); + ASSERT_TRUE(connection_complete.has_value()); + ASSERT_TRUE(connection_complete->is_connected_params()); + + // OnConnectionComplete should never be called again. + sco_connection_.events().OnConnectionComplete = + [](fidlbredr::ScoConnectionOnConnectionCompleteRequest request) { + FAIL(); + }; + + // Find the link handle used for the SCO connection. + bt::testing::FakePeer* fake_peer = + test_device()->FindPeer(peer()->address()); + ASSERT_TRUE(fake_peer); + // There are 2 connections: BR/EDR, SCO + ASSERT_EQ(fake_peer->logical_links().size(), 2u); + bt::testing::FakePeer::HandleSet links = fake_peer->logical_links(); + // The link that is not the BR/EDR connection link must be the SCO link. + links.erase(connection()->link().handle()); + sco_conn_handle_ = *links.begin(); + } + + void TearDown() override { ProfileServerTestConnectedPeer::TearDown(); } + + fidlbredr::ScoConnectionPtr& sco_connection() { return sco_connection_; } + + std::optional sco_conn_error() const { return sco_conn_error_; } + + bt::hci_spec::ConnectionHandle sco_handle() const { return sco_conn_handle_; } + + private: + fidlbredr::ScoConnectionPtr sco_connection_; + bt::hci_spec::ConnectionHandle sco_conn_handle_; + std::optional sco_conn_error_; +}; + +class ProfileServerTestOffloadedScoConnected + : public ProfileServerTestScoConnected { + public: + void SetUp() override { + fidlbredr::ScoConnectionParameters params = + CreateScoConnectionParameters(fidlbredr::HfpParameterSet::D0); + params.set_path(fidlbredr::DataPath::OFFLOAD); + ProfileServerTestScoConnected::SetUp(std::move(params)); + } +}; + +TEST_F(ProfileServerTestConnectedPeer, ConnectL2capChannelParametersUseSocket) { + std::unique_ptr pairing_delegate = + std::make_unique( + bt::sm::IOCapability::kDisplayYesNo); + adapter()->SetPairingDelegate(pairing_delegate->GetWeakPtr()); + // Approve pairing requests. + pairing_delegate->SetConfirmPairingCallback( + [](bt::PeerId, auto confirm_cb) { confirm_cb(true); }); + pairing_delegate->SetCompletePairingCallback( + [&](bt::PeerId, bt::sm::Result<> status) { + EXPECT_EQ(fit::ok(), status); + }); + + bt::l2cap::ChannelParameters expected_params; + expected_params.mode = + bt::l2cap::RetransmissionAndFlowControlMode::kEnhancedRetransmission; + expected_params.max_rx_sdu_size = bt::l2cap::kMinACLMTU; + l2cap()->ExpectOutboundL2capChannel( + connection()->link().handle(), kPsm, 0x40, 0x41, expected_params); + + // Expect a non-empty channel result. + std::optional channel; + auto chan_cb = [&channel](fidlbredr::Profile_Connect_Result result) { + EXPECT_TRUE(result.is_response()); + channel = std::move(result.response().channel); + }; + // Initiates pairing + + fuchsia::bluetooth::PeerId peer_id{peer()->identifier().value()}; + + // Set L2CAP channel parameters + fbt::ChannelParameters chan_params; + fidlbredr::L2capParameters l2cap_params; + fidlbredr::ConnectParameters conn_params; + chan_params.set_channel_mode(fbt::ChannelMode::ENHANCED_RETRANSMISSION); + chan_params.set_max_rx_packet_size(bt::l2cap::kMinACLMTU); + l2cap_params.set_psm(kPsm); + l2cap_params.set_parameters(std::move(chan_params)); + conn_params.set_l2cap(std::move(l2cap_params)); + + client()->Connect(peer_id, std::move(conn_params), std::move(chan_cb)); + RunLoopUntilIdle(); + + ASSERT_TRUE(channel.has_value()); + EXPECT_TRUE(channel->has_socket()); + EXPECT_FALSE(channel->IsEmpty()); + EXPECT_EQ(channel->channel_mode(), chan_params.channel_mode()); + // FakeL2cap returns channels with max tx sdu size of kDefaultMTU. + EXPECT_EQ(channel->max_tx_sdu_size(), bt::l2cap::kDefaultMTU); + EXPECT_FALSE(channel->has_ext_direction()); + EXPECT_FALSE(channel->has_flush_timeout()); +} + +TEST_F(ProfileServerTestConnectedPeer, + ConnectL2capChannelParametersUseConnection) { + server()->set_use_sockets(false); + + std::unique_ptr pairing_delegate = + std::make_unique( + bt::sm::IOCapability::kDisplayYesNo); + adapter()->SetPairingDelegate(pairing_delegate->GetWeakPtr()); + // Approve pairing requests. + pairing_delegate->SetConfirmPairingCallback( + [](bt::PeerId, auto confirm_cb) { confirm_cb(true); }); + pairing_delegate->SetCompletePairingCallback( + [&](bt::PeerId, bt::sm::Result<> status) { + EXPECT_EQ(fit::ok(), status); + }); + + bt::l2cap::ChannelParameters expected_params; + expected_params.mode = + bt::l2cap::RetransmissionAndFlowControlMode::kEnhancedRetransmission; + expected_params.max_rx_sdu_size = bt::l2cap::kMinACLMTU; + l2cap()->ExpectOutboundL2capChannel( + connection()->link().handle(), kPsm, 0x40, 0x41, expected_params); + + // Expect a non-empty channel result. + std::optional channel; + auto chan_cb = [&channel](fidlbredr::Profile_Connect_Result result) { + EXPECT_TRUE(result.is_response()); + channel = std::move(result.response().channel); + }; + // Initiates pairing + + fuchsia::bluetooth::PeerId peer_id{peer()->identifier().value()}; + + // Set L2CAP channel parameters + fbt::ChannelParameters chan_params; + fidlbredr::L2capParameters l2cap_params; + fidlbredr::ConnectParameters conn_params; + chan_params.set_channel_mode(fbt::ChannelMode::ENHANCED_RETRANSMISSION); + chan_params.set_max_rx_packet_size(bt::l2cap::kMinACLMTU); + l2cap_params.set_psm(kPsm); + l2cap_params.set_parameters(std::move(chan_params)); + conn_params.set_l2cap(std::move(l2cap_params)); + + client()->Connect(peer_id, std::move(conn_params), std::move(chan_cb)); + RunLoopUntilIdle(); + + ASSERT_TRUE(channel.has_value()); + EXPECT_TRUE(channel->has_connection()); + EXPECT_FALSE(channel->IsEmpty()); + EXPECT_EQ(channel->channel_mode(), chan_params.channel_mode()); + // FakeL2cap returns channels with max tx sdu size of kDefaultMTU. + EXPECT_EQ(channel->max_tx_sdu_size(), bt::l2cap::kDefaultMTU); + EXPECT_FALSE(channel->has_ext_direction()); + EXPECT_FALSE(channel->has_flush_timeout()); +} + +TEST_F(ProfileServerTestConnectedPeer, + ConnectWithAuthenticationRequiredButLinkKeyNotAuthenticatedFails) { + std::unique_ptr pairing_delegate = + std::make_unique( + bt::sm::IOCapability::kNoInputNoOutput); + adapter()->SetPairingDelegate(pairing_delegate->GetWeakPtr()); + pairing_delegate->SetCompletePairingCallback( + [&](bt::PeerId, bt::sm::Result<> status) { + EXPECT_EQ(fit::ok(), status); + }); + + fbt::SecurityRequirements security; + security.set_authentication_required(true); + + // Set L2CAP channel parameters + fbt::ChannelParameters chan_params; + fidlbredr::L2capParameters l2cap_params; + fidlbredr::ConnectParameters conn_params; + chan_params.set_security_requirements(std::move(security)); + l2cap_params.set_psm(kPsm); + l2cap_params.set_parameters(std::move(chan_params)); + conn_params.set_l2cap(std::move(l2cap_params)); + + size_t sock_cb_count = 0; + auto sock_cb = [&](fidlbredr::Profile_Connect_Result result) { + sock_cb_count++; + ASSERT_TRUE(result.is_err()); + EXPECT_EQ(fuchsia::bluetooth::ErrorCode::FAILED, result.err()); + }; + + fuchsia::bluetooth::PeerId peer_id{peer()->identifier().value()}; + + // Initiates pairing. + // FakeController will create an unauthenticated key. + client()->Connect(peer_id, std::move(conn_params), std::move(sock_cb)); + RunLoopUntilIdle(); + + EXPECT_EQ(1u, sock_cb_count); +} + +// Tests receiving an empty Channel results in an error propagated through the +// callback. +TEST_F(ProfileServerTestConnectedPeer, ConnectEmptyChannelResponse) { + std::unique_ptr pairing_delegate = + std::make_unique( + bt::sm::IOCapability::kDisplayYesNo); + adapter()->SetPairingDelegate(pairing_delegate->GetWeakPtr()); + // Approve pairing requests. + pairing_delegate->SetConfirmPairingCallback( + [](bt::PeerId, auto confirm_cb) { confirm_cb(true); }); + pairing_delegate->SetCompletePairingCallback( + [&](bt::PeerId, bt::sm::Result<> status) { + EXPECT_EQ(fit::ok(), status); + }); + + // Make the l2cap channel creation fail. + l2cap()->set_simulate_open_channel_failure(true); + + bt::l2cap::ChannelParameters expected_params; + expected_params.mode = + bt::l2cap::RetransmissionAndFlowControlMode::kEnhancedRetransmission; + expected_params.max_rx_sdu_size = bt::l2cap::kMinACLMTU; + l2cap()->ExpectOutboundL2capChannel( + connection()->link().handle(), kPsm, 0x40, 0x41, expected_params); + + fbt::ChannelParameters chan_params; + chan_params.set_channel_mode(fbt::ChannelMode::ENHANCED_RETRANSMISSION); + chan_params.set_max_rx_packet_size(bt::l2cap::kMinACLMTU); + auto sock_cb = [](fidlbredr::Profile_Connect_Result result) { + EXPECT_TRUE(result.is_err()); + EXPECT_EQ(fuchsia::bluetooth::ErrorCode::FAILED, result.err()); + }; + // Initiates pairing + + fuchsia::bluetooth::PeerId peer_id{peer()->identifier().value()}; + + // Set L2CAP channel parameters + fidlbredr::L2capParameters l2cap_params; + fidlbredr::ConnectParameters conn_params; + l2cap_params.set_psm(kPsm); + l2cap_params.set_parameters(std::move(chan_params)); + conn_params.set_l2cap(std::move(l2cap_params)); + + client()->Connect(peer_id, std::move(conn_params), std::move(sock_cb)); + RunLoopUntilIdle(); +} + +TEST_F( + ProfileServerTestConnectedPeer, + AdvertiseChannelParametersReceivedInOnChannelConnectedCallbackUseSocket) { + constexpr uint16_t kTxMtu = bt::l2cap::kMinACLMTU; + + std::unique_ptr pairing_delegate = + std::make_unique( + bt::sm::IOCapability::kDisplayYesNo); + adapter()->SetPairingDelegate(pairing_delegate->GetWeakPtr()); + + fidlbredr::ConnectionReceiverHandle connect_receiver_handle; + FakeConnectionReceiver connect_receiver(connect_receiver_handle.NewRequest(), + dispatcher()); + + std::vector services; + services.emplace_back(MakeFIDLServiceDefinition()); + fbt::ChannelParameters chan_params; + chan_params.set_channel_mode(fbt::ChannelMode::ENHANCED_RETRANSMISSION); + + fidlbredr::ProfileAdvertiseRequest adv_request; + adv_request.set_services(std::move(services)); + adv_request.set_parameters(std::move(chan_params)); + adv_request.set_receiver(std::move(connect_receiver_handle)); + client()->Advertise(std::move(adv_request), NopAdvertiseCallback); + RunLoopUntilIdle(); + + ASSERT_EQ(connect_receiver.connected_count(), 0u); + EXPECT_TRUE(l2cap()->TriggerInboundL2capChannel( + connection()->link().handle(), kPsm, 0x40, 0x41, kTxMtu)); + RunLoopUntilIdle(); + + ASSERT_EQ(connect_receiver.connected_count(), 1u); + ASSERT_EQ(connect_receiver.peer_id().value().value, + peer()->identifier().value()); + ASSERT_TRUE(connect_receiver.channel().value().has_socket()); + EXPECT_EQ(connect_receiver.channel().value().channel_mode(), + fbt::ChannelMode::ENHANCED_RETRANSMISSION); + EXPECT_EQ(connect_receiver.channel().value().max_tx_sdu_size(), kTxMtu); + EXPECT_FALSE(connect_receiver.channel().value().has_ext_direction()); + EXPECT_FALSE(connect_receiver.channel().value().has_flush_timeout()); +} + +TEST_F( + ProfileServerTestConnectedPeer, + AdvertiseChannelParametersReceivedInOnChannelConnectedCallbackUseConnection) { + server()->set_use_sockets(false); + + constexpr uint16_t kTxMtu = bt::l2cap::kMinACLMTU; + + std::unique_ptr pairing_delegate = + std::make_unique( + bt::sm::IOCapability::kDisplayYesNo); + adapter()->SetPairingDelegate(pairing_delegate->GetWeakPtr()); + + fidlbredr::ConnectionReceiverHandle connect_receiver_handle; + FakeConnectionReceiver connect_receiver(connect_receiver_handle.NewRequest(), + dispatcher()); + + std::vector services; + services.emplace_back(MakeFIDLServiceDefinition()); + fbt::ChannelParameters chan_params; + chan_params.set_channel_mode(fbt::ChannelMode::ENHANCED_RETRANSMISSION); + + fidlbredr::ProfileAdvertiseRequest adv_request; + adv_request.set_services(std::move(services)); + adv_request.set_parameters(std::move(chan_params)); + adv_request.set_receiver(std::move(connect_receiver_handle)); + client()->Advertise(std::move(adv_request), NopAdvertiseCallback); + RunLoopUntilIdle(); + + ASSERT_EQ(connect_receiver.connected_count(), 0u); + EXPECT_TRUE(l2cap()->TriggerInboundL2capChannel( + connection()->link().handle(), kPsm, 0x40, 0x41, kTxMtu)); + RunLoopUntilIdle(); + + ASSERT_EQ(connect_receiver.connected_count(), 1u); + ASSERT_EQ(connect_receiver.peer_id().value().value, + peer()->identifier().value()); + ASSERT_TRUE(connect_receiver.channel().value().has_connection()); + EXPECT_EQ(connect_receiver.channel().value().channel_mode(), + fbt::ChannelMode::ENHANCED_RETRANSMISSION); + EXPECT_EQ(connect_receiver.channel().value().max_tx_sdu_size(), kTxMtu); + EXPECT_FALSE(connect_receiver.channel().value().has_ext_direction()); + EXPECT_FALSE(connect_receiver.channel().value().has_flush_timeout()); +} + +class AclPrioritySupportedTest : public ProfileServerTestConnectedPeer { + public: + void SetUp() override { + ProfileServerTestConnectedPeer::SetUp(FeaturesBits::kSetAclPriorityCommand); + } +}; + +class PriorityTest : public AclPrioritySupportedTest, + public ::testing::WithParamInterface< + std::pair> {}; + +TEST_P(PriorityTest, OutboundConnectAndSetPriority) { + const fidlbredr::A2dpDirectionPriority kPriority = GetParam().first; + const bool kExpectSuccess = GetParam().second; + + std::unique_ptr pairing_delegate = + std::make_unique( + bt::sm::IOCapability::kDisplayYesNo); + adapter()->SetPairingDelegate(pairing_delegate->GetWeakPtr()); + // Approve pairing requests. + pairing_delegate->SetConfirmPairingCallback( + [](bt::PeerId, auto confirm_cb) { confirm_cb(true); }); + pairing_delegate->SetCompletePairingCallback( + [&](bt::PeerId, bt::sm::Result<> status) { + EXPECT_EQ(fit::ok(), status); + }); + + l2cap()->ExpectOutboundL2capChannel(connection()->link().handle(), + kPsm, + 0x40, + 0x41, + bt::l2cap::ChannelParameters()); + + FakeChannel::WeakPtr fake_channel; + l2cap()->set_channel_callback( + [&](FakeChannel::WeakPtr chan) { fake_channel = std::move(chan); }); + + // Expect a non-empty channel result. + std::optional channel; + auto chan_cb = [&channel](fidlbredr::Profile_Connect_Result result) { + ASSERT_TRUE(result.is_response()); + channel = std::move(result.response().channel); + }; + + fuchsia::bluetooth::PeerId peer_id{peer()->identifier().value()}; + + // Set L2CAP channel parameters + fidlbredr::L2capParameters l2cap_params; + fidlbredr::ConnectParameters conn_params; + l2cap_params.set_psm(kPsm); + conn_params.set_l2cap(std::move(l2cap_params)); + + // Initiates pairing + client()->Connect(peer_id, std::move(conn_params), std::move(chan_cb)); + RunLoopUntilIdle(); + ASSERT_TRUE(fake_channel.is_alive()); + ASSERT_TRUE(channel.has_value()); + ASSERT_TRUE(channel->has_ext_direction()); + fidlbredr::AudioDirectionExtPtr client = + channel->mutable_ext_direction()->Bind(); + + size_t priority_cb_count = 0; + fake_channel->set_acl_priority_fails(!kExpectSuccess); + client->SetPriority( + kPriority, [&](fidlbredr::AudioDirectionExt_SetPriority_Result result) { + EXPECT_EQ(result.is_response(), kExpectSuccess); + priority_cb_count++; + }); + + RunLoopUntilIdle(); + EXPECT_EQ(priority_cb_count, 1u); + client = nullptr; + RunLoopUntilIdle(); + + if (kExpectSuccess) { + switch (kPriority) { + case fidlbredr::A2dpDirectionPriority::SOURCE: + EXPECT_EQ(fake_channel->requested_acl_priority(), AclPriority::kSource); + break; + case fidlbredr::A2dpDirectionPriority::SINK: + EXPECT_EQ(fake_channel->requested_acl_priority(), AclPriority::kSink); + break; + default: + EXPECT_EQ(fake_channel->requested_acl_priority(), AclPriority::kNormal); + } + } else { + EXPECT_EQ(fake_channel->requested_acl_priority(), AclPriority::kNormal); + } +} + +const std::array, 4> + kPriorityParams = {{{fidlbredr::A2dpDirectionPriority::SOURCE, false}, + {fidlbredr::A2dpDirectionPriority::SOURCE, true}, + {fidlbredr::A2dpDirectionPriority::SINK, true}, + {fidlbredr::A2dpDirectionPriority::NORMAL, true}}}; +INSTANTIATE_TEST_SUITE_P(ProfileServerTestConnectedPeer, + PriorityTest, + ::testing::ValuesIn(kPriorityParams)); + +TEST_F(AclPrioritySupportedTest, InboundConnectAndSetPriority) { + constexpr uint16_t kTxMtu = bt::l2cap::kMinACLMTU; + + std::unique_ptr pairing_delegate = + std::make_unique( + bt::sm::IOCapability::kDisplayYesNo); + adapter()->SetPairingDelegate(pairing_delegate->GetWeakPtr()); + + FakeChannel::WeakPtr fake_channel; + l2cap()->set_channel_callback( + [&](FakeChannel::WeakPtr chan) { fake_channel = std::move(chan); }); + + fidlbredr::ConnectionReceiverHandle connect_receiver_handle; + FakeConnectionReceiver connect_receiver(connect_receiver_handle.NewRequest(), + dispatcher()); + + std::vector services; + services.emplace_back(MakeFIDLServiceDefinition()); + fidlbredr::ProfileAdvertiseRequest adv_request; + adv_request.set_services(std::move(services)); + adv_request.set_receiver(std::move(connect_receiver_handle)); + client()->Advertise(std::move(adv_request), NopAdvertiseCallback); + RunLoopUntilIdle(); + + ASSERT_EQ(connect_receiver.connected_count(), 0u); + EXPECT_TRUE(l2cap()->TriggerInboundL2capChannel( + connection()->link().handle(), kPsm, 0x40, 0x41, kTxMtu)); + + RunLoopUntilIdle(); + ASSERT_EQ(connect_receiver.connected_count(), 1u); + ASSERT_TRUE(connect_receiver.channel().has_value()); + ASSERT_TRUE(connect_receiver.channel().value().has_ext_direction()); + // Taking value() is safe because of the has_ext_direction() check. + fidlbredr::AudioDirectionExtPtr client = + connect_receiver.bind_ext_direction().value(); + + size_t priority_cb_count = 0; + client->SetPriority( + fidlbredr::A2dpDirectionPriority::SINK, + [&](fidlbredr::AudioDirectionExt_SetPriority_Result result) { + EXPECT_TRUE(result.is_response()); + priority_cb_count++; + }); + + RunLoopUntilIdle(); + EXPECT_EQ(priority_cb_count, 1u); + ASSERT_TRUE(fake_channel.is_alive()); + EXPECT_EQ(fake_channel->requested_acl_priority(), AclPriority::kSink); +} + +// Verifies that a socket channel relay is correctly set up such that bytes +// written to the socket are sent to the channel. +TEST_F(ProfileServerTestConnectedPeer, ConnectReturnsValidSocket) { + std::unique_ptr pairing_delegate = + std::make_unique( + bt::sm::IOCapability::kDisplayYesNo); + adapter()->SetPairingDelegate(pairing_delegate->GetWeakPtr()); + // Approve pairing requests. + pairing_delegate->SetConfirmPairingCallback( + [](bt::PeerId, auto confirm_cb) { confirm_cb(true); }); + pairing_delegate->SetCompletePairingCallback( + [&](bt::PeerId, bt::sm::Result<> status) { + EXPECT_EQ(fit::ok(), status); + }); + + bt::l2cap::ChannelParameters expected_params; + l2cap()->ExpectOutboundL2capChannel( + connection()->link().handle(), kPsm, 0x40, 0x41, expected_params); + + std::optional fake_chan; + l2cap()->set_channel_callback( + [&fake_chan](FakeChannel::WeakPtr chan) { fake_chan = std::move(chan); }); + + // Expect a non-empty channel result. + std::optional channel; + auto result_cb = [&channel](fidlbredr::Profile_Connect_Result result) { + EXPECT_TRUE(result.is_response()); + channel = std::move(result.response().channel); + }; + + fuchsia::bluetooth::PeerId peer_id{peer()->identifier().value()}; + + // Set L2CAP channel parameters + fidlbredr::L2capParameters l2cap_params; + fidlbredr::ConnectParameters conn_params; + l2cap_params.set_psm(kPsm); + l2cap_params.set_parameters(fbt::ChannelParameters()); + conn_params.set_l2cap(std::move(l2cap_params)); + + // Initiates pairing + client()->Connect(peer_id, std::move(conn_params), std::move(result_cb)); + RunLoopUntilIdle(); + + ASSERT_TRUE(channel.has_value()); + ASSERT_TRUE(channel->has_socket()); + auto& socket = channel->socket(); + + ASSERT_TRUE(fake_chan.has_value()); + FakeChannel::WeakPtr fake_chan_ptr = fake_chan.value(); + size_t send_count = 0; + fake_chan_ptr->SetSendCallback([&send_count](auto buffer) { send_count++; }, + pw_dispatcher()); + + const char write_data[2] = "a"; + size_t bytes_written = 0; + auto status = + socket.write(0, write_data, sizeof(write_data) - 1, &bytes_written); + EXPECT_EQ(ZX_OK, status); + EXPECT_EQ(1u, bytes_written); + RunLoopUntilIdle(); + EXPECT_EQ(1u, send_count); +} + +// Verifies that a BrEdrConnectionServer is correctly set up such that bytes +// written to the Connection are sent to the channel. +TEST_F(ProfileServerTestConnectedPeer, ConnectReturnsValidConnection) { + server()->set_use_sockets(false); + + std::unique_ptr pairing_delegate = + std::make_unique( + bt::sm::IOCapability::kDisplayYesNo); + adapter()->SetPairingDelegate(pairing_delegate->GetWeakPtr()); + // Approve pairing requests. + pairing_delegate->SetConfirmPairingCallback( + [](bt::PeerId, auto confirm_cb) { confirm_cb(true); }); + pairing_delegate->SetCompletePairingCallback( + [&](bt::PeerId, bt::sm::Result<> status) { + EXPECT_EQ(fit::ok(), status); + }); + + bt::l2cap::ChannelParameters expected_params; + l2cap()->ExpectOutboundL2capChannel( + connection()->link().handle(), kPsm, 0x40, 0x41, expected_params); + + std::optional fake_chan; + l2cap()->set_channel_callback( + [&fake_chan](FakeChannel::WeakPtr chan) { fake_chan = std::move(chan); }); + + // Expect a non-empty channel result. + std::optional channel; + auto result_cb = [&channel](fidlbredr::Profile_Connect_Result result) { + EXPECT_TRUE(result.is_response()); + channel = std::move(result.response().channel); + }; + + fuchsia::bluetooth::PeerId peer_id{peer()->identifier().value()}; + + // Set L2CAP channel parameters + fidlbredr::L2capParameters l2cap_params; + fidlbredr::ConnectParameters conn_params; + l2cap_params.set_psm(kPsm); + l2cap_params.set_parameters(fbt::ChannelParameters()); + conn_params.set_l2cap(std::move(l2cap_params)); + + // Initiates pairing + client()->Connect(peer_id, std::move(conn_params), std::move(result_cb)); + RunLoopUntilIdle(); + + ASSERT_TRUE(channel.has_value()); + ASSERT_TRUE(channel->has_connection()); + + ASSERT_TRUE(fake_chan.has_value()); + ASSERT_TRUE(fake_chan.value()->activated()); + FakeChannel::WeakPtr fake_chan_ptr = fake_chan.value(); + int send_count = 0; + fake_chan_ptr->SetSendCallback([&send_count](auto buffer) { send_count++; }, + pw_dispatcher()); + + fidl::InterfacePtr conn = channel->mutable_connection()->Bind(); + int send_cb_count = 0; + std::vector packets{fbt::Packet{std::vector{0x02}}}; + conn->Send(std::move(packets), [&](fbt::Channel_Send_Result result) { + EXPECT_TRUE(result.is_response()); + send_cb_count++; + }); + RunLoopUntilIdle(); + EXPECT_EQ(1, send_count); + EXPECT_EQ(1, send_cb_count); +} + +TEST_F(ProfileServerTestConnectedPeer, + ConnectFailsDueToChannelActivationFailure) { + server()->set_use_sockets(false); + + std::unique_ptr pairing_delegate = + std::make_unique( + bt::sm::IOCapability::kDisplayYesNo); + adapter()->SetPairingDelegate(pairing_delegate->GetWeakPtr()); + // Approve pairing requests. + pairing_delegate->SetConfirmPairingCallback( + [](bt::PeerId, auto confirm_cb) { confirm_cb(true); }); + pairing_delegate->SetCompletePairingCallback( + [&](bt::PeerId, bt::sm::Result<> status) { + EXPECT_EQ(fit::ok(), status); + }); + + bt::l2cap::ChannelParameters expected_params; + l2cap()->ExpectOutboundL2capChannel( + connection()->link().handle(), kPsm, 0x40, 0x41, expected_params); + + std::optional fake_chan; + l2cap()->set_channel_callback([&fake_chan](FakeChannel::WeakPtr chan) { + chan->set_activate_fails(true); + fake_chan = std::move(chan); + }); + + int connect_cb_count = 0; + auto connect_cb = [&](fidlbredr::Profile_Connect_Result result) { + EXPECT_TRUE(result.is_err()); + connect_cb_count++; + }; + + fuchsia::bluetooth::PeerId peer_id{peer()->identifier().value()}; + + fidlbredr::L2capParameters l2cap_params; + fidlbredr::ConnectParameters conn_params; + l2cap_params.set_psm(kPsm); + l2cap_params.set_parameters(fbt::ChannelParameters()); + conn_params.set_l2cap(std::move(l2cap_params)); + + client()->Connect(peer_id, std::move(conn_params), std::move(connect_cb)); + RunLoopUntilIdle(); + EXPECT_EQ(connect_cb_count, 1); + EXPECT_FALSE(fake_chan.value()->activated()); +} + +// Verifies that a socket channel relay is correctly set up such that bytes +// written to the socket are sent to the channel. +TEST_F(ProfileServerTestConnectedPeer, ConnectionReceiverReturnsValidSocket) { + std::unique_ptr pairing_delegate = + std::make_unique( + bt::sm::IOCapability::kDisplayYesNo); + adapter()->SetPairingDelegate(pairing_delegate->GetWeakPtr()); + + fidlbredr::ConnectionReceiverHandle connect_receiver_handle; + FakeConnectionReceiver connect_receiver(connect_receiver_handle.NewRequest(), + dispatcher()); + + std::optional fake_chan; + l2cap()->set_channel_callback( + [&fake_chan](FakeChannel::WeakPtr chan) { fake_chan = std::move(chan); }); + + std::vector services; + services.emplace_back(MakeFIDLServiceDefinition()); + + fidlbredr::ProfileAdvertiseRequest adv_request; + adv_request.set_services(std::move(services)); + adv_request.set_receiver(std::move(connect_receiver_handle)); + client()->Advertise(std::move(adv_request), NopAdvertiseCallback); + RunLoopUntilIdle(); + + ASSERT_EQ(connect_receiver.connected_count(), 0u); + EXPECT_TRUE(l2cap()->TriggerInboundL2capChannel( + connection()->link().handle(), kPsm, 0x40, 0x41)); + RunLoopUntilIdle(); + + ASSERT_EQ(connect_receiver.connected_count(), 1u); + ASSERT_EQ(connect_receiver.peer_id().value().value, + peer()->identifier().value()); + ASSERT_TRUE(connect_receiver.channel().has_value()); + ASSERT_TRUE(connect_receiver.channel().value().has_socket()); + // Taking channel is safe because of the previous checks. + fidlbredr::Channel channel = connect_receiver.take_channel(); + + ASSERT_TRUE(fake_chan.has_value()); + FakeChannel::WeakPtr fake_chan_ptr = fake_chan.value(); + size_t send_count = 0; + fake_chan_ptr->SetSendCallback([&send_count](auto buffer) { send_count++; }, + pw_dispatcher()); + + const char write_data[2] = "a"; + size_t bytes_written = 0; + int status = channel.socket().write( + 0, write_data, sizeof(write_data) - 1, &bytes_written); + EXPECT_EQ(ZX_OK, status); + EXPECT_EQ(1u, bytes_written); + RunLoopUntilIdle(); + EXPECT_EQ(1u, send_count); +} + +// Verifies that a BrEdrConnectionServer is correctly set up such that bytes +// written to the Connection are sent to the channel. +TEST_F(ProfileServerTestConnectedPeer, + ConnectionReceiverReturnsValidConnection) { + server()->set_use_sockets(false); + + std::unique_ptr pairing_delegate = + std::make_unique( + bt::sm::IOCapability::kDisplayYesNo); + adapter()->SetPairingDelegate(pairing_delegate->GetWeakPtr()); + + fidlbredr::ConnectionReceiverHandle connect_receiver_handle; + FakeConnectionReceiver connect_receiver(connect_receiver_handle.NewRequest(), + dispatcher()); + + std::optional fake_chan; + l2cap()->set_channel_callback( + [&fake_chan](FakeChannel::WeakPtr chan) { fake_chan = std::move(chan); }); + + std::vector services; + services.emplace_back(MakeFIDLServiceDefinition()); + + fidlbredr::ProfileAdvertiseRequest adv_request; + adv_request.set_services(std::move(services)); + adv_request.set_receiver(std::move(connect_receiver_handle)); + client()->Advertise(std::move(adv_request), NopAdvertiseCallback); + RunLoopUntilIdle(); + + ASSERT_EQ(connect_receiver.connected_count(), 0u); + EXPECT_TRUE(l2cap()->TriggerInboundL2capChannel( + connection()->link().handle(), kPsm, 0x40, 0x41)); + RunLoopUntilIdle(); + + ASSERT_EQ(connect_receiver.connected_count(), 1u); + ASSERT_EQ(connect_receiver.peer_id().value().value, + peer()->identifier().value()); + ASSERT_TRUE(connect_receiver.channel().has_value()); + ASSERT_TRUE(connect_receiver.channel().value().has_connection()); + // Taking channel is safe because of the previous checks. + fidlbredr::Channel channel = connect_receiver.take_channel(); + + ASSERT_TRUE(fake_chan.has_value()); + FakeChannel::WeakPtr fake_chan_ptr = fake_chan.value(); + int send_count = 0; + fake_chan_ptr->SetSendCallback([&send_count](auto buffer) { send_count++; }, + pw_dispatcher()); + + fidl::InterfacePtr conn = channel.mutable_connection()->Bind(); + int send_cb_count = 0; + std::vector packets{fbt::Packet{std::vector{0x02}}}; + conn->Send(std::move(packets), [&](fbt::Channel_Send_Result result) { + EXPECT_TRUE(result.is_response()); + send_cb_count++; + }); + RunLoopUntilIdle(); + EXPECT_EQ(1, send_count); + EXPECT_EQ(1, send_cb_count); +} + +TEST_F(ProfileServerTest, ConnectScoWithInvalidParameters) { + std::vector bad_sco_params; + bad_sco_params.emplace_back(); + fidlbredr::ProfileConnectScoRequest request; + request.set_peer_id(fuchsia::bluetooth::PeerId{1}); + request.set_initiator(true); + request.set_params(std::move(bad_sco_params)); + fidlbredr::ScoConnectionHandle connection_handle; + request.set_connection(connection_handle.NewRequest()); + + fidlbredr::ScoConnectionPtr sco_connection = connection_handle.Bind(); + std::optional sco_connection_status; + sco_connection.set_error_handler( + [&](zx_status_t status) { sco_connection_status = status; }); + std::optional + connection_complete; + sco_connection.events().OnConnectionComplete = + [&](fidlbredr::ScoConnectionOnConnectionCompleteRequest request) { + connection_complete = std::move(request); + }; + + client()->ConnectSco(std::move(request)); + RunLoopUntilIdle(); + ASSERT_TRUE(connection_complete); + ASSERT_TRUE(connection_complete->is_error()); + EXPECT_EQ(connection_complete->error(), + fidlbredr::ScoErrorCode::INVALID_ARGUMENTS); + EXPECT_FALSE(sco_connection.is_bound()); +} + +TEST_F(ProfileServerTest, ConnectScoWithMissingPeerId) { + fidlbredr::ScoConnectionParameters sco_params = + CreateScoConnectionParameters(); + EXPECT_TRUE(fidl_helpers::FidlToScoParameters(sco_params).is_ok()); + std::vector sco_params_list; + sco_params_list.emplace_back(std::move(sco_params)); + fidlbredr::ProfileConnectScoRequest request; + request.set_initiator(true); + request.set_params(std::move(sco_params_list)); + fidlbredr::ScoConnectionHandle connection_handle; + request.set_connection(connection_handle.NewRequest()); + + fidlbredr::ScoConnectionPtr sco_connection = connection_handle.Bind(); + std::optional sco_connection_status; + sco_connection.set_error_handler( + [&](zx_status_t status) { sco_connection_status = status; }); + std::optional + connection_complete; + sco_connection.events().OnConnectionComplete = + [&](fidlbredr::ScoConnectionOnConnectionCompleteRequest request) { + connection_complete = std::move(request); + }; + + client()->ConnectSco(std::move(request)); + RunLoopUntilIdle(); + ASSERT_TRUE(connection_complete); + ASSERT_TRUE(connection_complete->is_error()); + EXPECT_EQ(connection_complete->error(), + fidlbredr::ScoErrorCode::INVALID_ARGUMENTS); + EXPECT_FALSE(sco_connection.is_bound()); +} + +TEST_F(ProfileServerTest, ConnectScoWithMissingConnectionDoesNotCrash) { + fidlbredr::ScoConnectionParameters sco_params = + CreateScoConnectionParameters(); + EXPECT_TRUE(fidl_helpers::FidlToScoParameters(sco_params).is_ok()); + std::vector sco_params_list; + sco_params_list.emplace_back(std::move(sco_params)); + fidlbredr::ProfileConnectScoRequest request; + request.set_peer_id(fuchsia::bluetooth::PeerId{1}); + request.set_initiator(true); + request.set_params(std::move(sco_params_list)); + client()->ConnectSco(std::move(request)); + RunLoopUntilIdle(); +} + +TEST_F(ProfileServerTest, ConnectScoWithEmptyParameters) { + fidlbredr::ScoConnectionHandle connection_handle; + fidlbredr::ProfileConnectScoRequest request; + request.set_peer_id(fuchsia::bluetooth::PeerId{1}); + request.set_initiator(true); + request.set_params({}); + request.set_connection(connection_handle.NewRequest()); + + fidlbredr::ScoConnectionPtr sco_connection = connection_handle.Bind(); + std::optional sco_connection_status; + sco_connection.set_error_handler( + [&](zx_status_t status) { sco_connection_status = status; }); + std::optional + connection_complete; + sco_connection.events().OnConnectionComplete = + [&](fidlbredr::ScoConnectionOnConnectionCompleteRequest request) { + connection_complete = std::move(request); + }; + + client()->ConnectSco(std::move(request)); + RunLoopUntilIdle(); + ASSERT_TRUE(connection_complete); + ASSERT_TRUE(connection_complete->is_error()); + EXPECT_EQ(connection_complete->error(), + fidlbredr::ScoErrorCode::INVALID_ARGUMENTS); + EXPECT_FALSE(sco_connection.is_bound()); + ASSERT_TRUE(sco_connection_status); + EXPECT_EQ(sco_connection_status.value(), ZX_ERR_PEER_CLOSED); +} + +TEST_F(ProfileServerTest, ConnectScoInitiatorWithTooManyParameters) { + std::vector sco_params_list; + sco_params_list.emplace_back(CreateScoConnectionParameters()); + sco_params_list.emplace_back(CreateScoConnectionParameters()); + + fidlbredr::ScoConnectionHandle connection_handle; + fidlbredr::ProfileConnectScoRequest request; + request.set_peer_id(fuchsia::bluetooth::PeerId{1}); + request.set_initiator(true); + request.set_params(std::move(sco_params_list)); + request.set_connection(connection_handle.NewRequest()); + + fidlbredr::ScoConnectionPtr sco_connection = connection_handle.Bind(); + std::optional sco_connection_status; + sco_connection.set_error_handler( + [&](zx_status_t status) { sco_connection_status = status; }); + std::optional + connection_complete; + sco_connection.events().OnConnectionComplete = + [&](fidlbredr::ScoConnectionOnConnectionCompleteRequest request) { + connection_complete = std::move(request); + }; + + client()->ConnectSco(std::move(request)); + RunLoopUntilIdle(); + ASSERT_TRUE(connection_complete); + ASSERT_TRUE(connection_complete->is_error()); + EXPECT_EQ(connection_complete->error(), + fidlbredr::ScoErrorCode::INVALID_ARGUMENTS); + EXPECT_FALSE(sco_connection.is_bound()); + ASSERT_TRUE(sco_connection_status); + EXPECT_EQ(sco_connection_status.value(), ZX_ERR_PEER_CLOSED); +} + +TEST_F(ProfileServerTest, ConnectScoWithUnconnectedPeerReturnsError) { + fidlbredr::ScoConnectionParameters sco_params = + CreateScoConnectionParameters(); + EXPECT_TRUE(fidl_helpers::FidlToScoParameters(sco_params).is_ok()); + std::vector sco_params_list; + sco_params_list.emplace_back(std::move(sco_params)); + + fidlbredr::ScoConnectionHandle connection_handle; + fidlbredr::ProfileConnectScoRequest request; + request.set_peer_id(fuchsia::bluetooth::PeerId{1}); + request.set_initiator(true); + request.set_params(std::move(sco_params_list)); + request.set_connection(connection_handle.NewRequest()); + + fidlbredr::ScoConnectionPtr sco_connection = connection_handle.Bind(); + std::optional sco_connection_status; + sco_connection.set_error_handler( + [&](zx_status_t status) { sco_connection_status = status; }); + std::optional + connection_complete; + sco_connection.events().OnConnectionComplete = + [&](fidlbredr::ScoConnectionOnConnectionCompleteRequest request) { + connection_complete = std::move(request); + }; + + client()->ConnectSco(std::move(request)); + RunLoopUntilIdle(); + ASSERT_TRUE(connection_complete); + ASSERT_TRUE(connection_complete->is_error()); + EXPECT_EQ(connection_complete->error(), fidlbredr::ScoErrorCode::FAILURE); + EXPECT_FALSE(sco_connection.is_bound()); + ASSERT_TRUE(sco_connection_status); + EXPECT_EQ(sco_connection_status.value(), ZX_ERR_PEER_CLOSED); +} + +TEST_F(ProfileServerTestConnectedPeer, ConnectScoInitiatorSuccess) { + fidlbredr::ScoConnectionParameters sco_params = + CreateScoConnectionParameters(fidlbredr::HfpParameterSet::T1); + EXPECT_TRUE(fidl_helpers::FidlToScoParameters(sco_params).is_ok()); + std::vector sco_params_list; + sco_params_list.emplace_back(std::move(sco_params)); + + fidlbredr::ScoConnectionHandle connection_handle; + fidlbredr::ProfileConnectScoRequest request; + request.set_peer_id(fuchsia::bluetooth::PeerId{peer()->identifier().value()}); + request.set_initiator(true); + request.set_params(std::move(sco_params_list)); + request.set_connection(connection_handle.NewRequest()); + + fidlbredr::ScoConnectionPtr sco_connection = connection_handle.Bind(); + std::optional sco_connection_status; + sco_connection.set_error_handler( + [&](zx_status_t status) { sco_connection_status = status; }); + std::optional + connection_complete; + sco_connection.events().OnConnectionComplete = + [&](fidlbredr::ScoConnectionOnConnectionCompleteRequest request) { + connection_complete = std::move(request); + }; + + client()->ConnectSco(std::move(request)); + RunLoopUntilIdle(); + ASSERT_TRUE(connection_complete); + ASSERT_TRUE(connection_complete->is_connected_params()); + EXPECT_TRUE(sco_connection.is_bound()); + ASSERT_TRUE(connection_complete->connected_params().has_parameter_set()); + EXPECT_EQ(connection_complete->connected_params().parameter_set(), + fidlbredr::HfpParameterSet::T1); + ASSERT_TRUE(connection_complete->connected_params().has_max_tx_data_size()); + EXPECT_EQ(connection_complete->connected_params().max_tx_data_size(), + kSynchronousDataPacketLength); +} + +TEST_F(ProfileServerTestConnectedPeer, ConnectScoResponderSuccess) { + // Use 2 parameter sets to test that the profile server returns the second set + // when a SCO connection request is received (T2 is ESCO only and D0 is SCO + // only, so D0 will be used to accept the connection). + std::vector sco_params_list; + sco_params_list.emplace_back( + CreateScoConnectionParameters(fidlbredr::HfpParameterSet::T2)); + sco_params_list.emplace_back( + CreateScoConnectionParameters(fidlbredr::HfpParameterSet::D0)); + + fidlbredr::ScoConnectionHandle connection_handle; + fidlbredr::ProfileConnectScoRequest request; + request.set_peer_id(fuchsia::bluetooth::PeerId{peer()->identifier().value()}); + request.set_initiator(false); + request.set_params(std::move(sco_params_list)); + request.set_connection(connection_handle.NewRequest()); + + fidlbredr::ScoConnectionPtr sco_connection = connection_handle.Bind(); + std::optional sco_connection_status; + sco_connection.set_error_handler( + [&](zx_status_t status) { sco_connection_status = status; }); + std::optional + connection_complete; + sco_connection.events().OnConnectionComplete = + [&](fidlbredr::ScoConnectionOnConnectionCompleteRequest request) { + connection_complete = std::move(request); + }; + + client()->ConnectSco(std::move(request)); + RunLoopUntilIdle(); + // Receive a SCO connection request. The D0 parameters will be used to accept + // the request. + test_device()->SendConnectionRequest(peer()->address(), + pw::bluetooth::emboss::LinkType::SCO); + RunLoopUntilIdle(); + ASSERT_TRUE(connection_complete); + ASSERT_TRUE(connection_complete->is_connected_params()); + EXPECT_TRUE(sco_connection.is_bound()); + ASSERT_TRUE(connection_complete->connected_params().has_parameter_set()); + EXPECT_EQ(connection_complete->connected_params().parameter_set(), + fidlbredr::HfpParameterSet::D0); +} + +TEST_F(ProfileServerTestConnectedPeer, + ScoConnectionReadBeforeConnectionComplete) { + std::vector sco_params_list; + sco_params_list.emplace_back( + CreateScoConnectionParameters(fidlbredr::HfpParameterSet::D0)); + + fidlbredr::ScoConnectionHandle connection_handle; + fidlbredr::ProfileConnectScoRequest request; + request.set_peer_id(fuchsia::bluetooth::PeerId{peer()->identifier().value()}); + request.set_initiator(false); + request.set_params(std::move(sco_params_list)); + request.set_connection(connection_handle.NewRequest()); + + fidlbredr::ScoConnectionPtr sco_connection = connection_handle.Bind(); + std::optional sco_connection_status; + sco_connection.set_error_handler( + [&](zx_status_t status) { sco_connection_status = status; }); + std::optional + connection_complete; + sco_connection.events().OnConnectionComplete = + [&](fidlbredr::ScoConnectionOnConnectionCompleteRequest request) { + connection_complete = std::move(request); + }; + + client()->ConnectSco(std::move(request)); + RunLoopUntilIdle(); + ASSERT_FALSE(connection_complete); + + sco_connection->Read( + [](fidlbredr::ScoConnection_Read_Result result) { FAIL(); }); + RunLoopUntilIdle(); + ASSERT_TRUE(sco_connection_status); + EXPECT_EQ(sco_connection_status.value(), ZX_ERR_IO_REFUSED); +} + +TEST_F(ProfileServerTestConnectedPeer, + ScoConnectionWriteBeforeConnectionComplete) { + std::vector sco_params_list; + sco_params_list.emplace_back( + CreateScoConnectionParameters(fidlbredr::HfpParameterSet::D0)); + + fidlbredr::ScoConnectionHandle connection_handle; + fidlbredr::ProfileConnectScoRequest request; + request.set_peer_id(fuchsia::bluetooth::PeerId{peer()->identifier().value()}); + request.set_initiator(false); + request.set_params(std::move(sco_params_list)); + request.set_connection(connection_handle.NewRequest()); + + fidlbredr::ScoConnectionPtr sco_connection = connection_handle.Bind(); + std::optional sco_connection_status; + sco_connection.set_error_handler( + [&](zx_status_t status) { sco_connection_status = status; }); + std::optional + connection_complete; + sco_connection.events().OnConnectionComplete = + [&](fidlbredr::ScoConnectionOnConnectionCompleteRequest request) { + connection_complete = std::move(request); + }; + + client()->ConnectSco(std::move(request)); + RunLoopUntilIdle(); + ASSERT_FALSE(connection_complete); + + ::fuchsia::bluetooth::bredr::ScoConnectionWriteRequest write; + write.set_data({0x00}); + sco_connection->Write( + std::move(write), + [](::fuchsia::bluetooth::bredr::ScoConnection_Write_Result result) { + FAIL(); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(sco_connection_status); + EXPECT_EQ(sco_connection_status.value(), ZX_ERR_IO_REFUSED); +} + +TEST_F(ProfileServerTestConnectedPeer, + ConnectScoResponderUnconnectedPeerReturnsError) { + std::vector sco_params_list; + sco_params_list.emplace_back(CreateScoConnectionParameters()); + + fidlbredr::ScoConnectionHandle connection_handle; + fidlbredr::ProfileConnectScoRequest request; + request.set_peer_id(fuchsia::bluetooth::PeerId{1}); + request.set_initiator(false); + request.set_params(std::move(sco_params_list)); + request.set_connection(connection_handle.NewRequest()); + + fidlbredr::ScoConnectionPtr sco_connection = connection_handle.Bind(); + std::optional sco_connection_status; + sco_connection.set_error_handler( + [&](zx_status_t status) { sco_connection_status = status; }); + std::optional + connection_complete; + sco_connection.events().OnConnectionComplete = + [&](fidlbredr::ScoConnectionOnConnectionCompleteRequest request) { + connection_complete = std::move(request); + }; + + client()->ConnectSco(std::move(request)); + RunLoopUntilIdle(); + ASSERT_TRUE(connection_complete); + ASSERT_TRUE(connection_complete->is_error()); + EXPECT_EQ(connection_complete->error(), fidlbredr::ScoErrorCode::FAILURE); + EXPECT_FALSE(sco_connection.is_bound()); + ASSERT_TRUE(sco_connection_status); + EXPECT_EQ(sco_connection_status.value(), ZX_ERR_PEER_CLOSED); +} + +TEST_F(ProfileServerTestConnectedPeer, ConnectScoInitiatorAndCloseProtocol) { + fidlbredr::ScoConnectionParameters sco_params = + CreateScoConnectionParameters(); + EXPECT_TRUE(fidl_helpers::FidlToScoParameters(sco_params).is_ok()); + std::vector sco_params_list; + sco_params_list.emplace_back(std::move(sco_params)); + + fidlbredr::ProfileConnectScoRequest request; + request.set_peer_id(fuchsia::bluetooth::PeerId{peer()->identifier().value()}); + request.set_initiator(true); + request.set_params(std::move(sco_params_list)); + fidlbredr::ScoConnectionHandle connection_handle; + request.set_connection(connection_handle.NewRequest()); + + fidlbredr::ScoConnectionPtr sco_connection = connection_handle.Bind(); + std::optional sco_connection_status; + sco_connection.set_error_handler( + [&](zx_status_t status) { sco_connection_status = status; }); + std::optional + connection_complete; + sco_connection.events().OnConnectionComplete = + [&](fidlbredr::ScoConnectionOnConnectionCompleteRequest request) { + connection_complete = std::move(request); + }; + + client()->ConnectSco(std::move(request)); + sco_connection.Unbind(); + RunLoopUntilIdle(); + ASSERT_FALSE(connection_complete); +} + +// Verifies that the profile server gracefully ignores connection results after +// the receiver has closed. +TEST_F(ProfileServerTestConnectedPeer, + ConnectScoInitiatorAndCloseReceiverBeforeCompleteEvent) { + fidlbredr::ScoConnectionParameters sco_params = + CreateScoConnectionParameters(); + EXPECT_TRUE(fidl_helpers::FidlToScoParameters(sco_params).is_ok()); + std::vector sco_params_list; + sco_params_list.emplace_back(std::move(sco_params)); + + test_device()->SetDefaultCommandStatus( + bt::hci_spec::kEnhancedSetupSynchronousConnection, + pw::bluetooth::emboss::StatusCode::SUCCESS); + fidlbredr::ProfileConnectScoRequest request; + request.set_peer_id(fuchsia::bluetooth::PeerId{peer()->identifier().value()}); + request.set_initiator(true); + request.set_params(std::move(sco_params_list)); + fidlbredr::ScoConnectionHandle connection_handle; + request.set_connection(connection_handle.NewRequest()); + + fidlbredr::ScoConnectionPtr sco_connection = connection_handle.Bind(); + std::optional sco_connection_status; + sco_connection.set_error_handler( + [&](zx_status_t status) { sco_connection_status = status; }); + std::optional + connection_complete; + sco_connection.events().OnConnectionComplete = + [&](fidlbredr::ScoConnectionOnConnectionCompleteRequest request) { + connection_complete = std::move(request); + }; + + client()->ConnectSco(std::move(request)); + sco_connection.Unbind(); + RunLoopUntilIdle(); + ASSERT_FALSE(connection_complete); + test_device()->SendCommandChannelPacket( + bt::testing::SynchronousConnectionCompletePacket( + 0x00, + peer()->address(), + bt::hci_spec::LinkType::kSCO, + pw::bluetooth::emboss::StatusCode::CONNECTION_TIMEOUT)); + RunLoopUntilIdle(); + ASSERT_FALSE(connection_complete); +} + +class ProfileServerTestFakeAdapter + : public bt::fidl::testing::FakeAdapterTestFixture { + public: + ProfileServerTestFakeAdapter() = default; + ~ProfileServerTestFakeAdapter() override = default; + + void SetUp() override { + FakeAdapterTestFixture::SetUp(); + + fidlbredr::ProfileHandle profile_handle; + client_.Bind(std::move(profile_handle)); + server_ = std::make_unique(adapter()->AsWeakPtr(), + client_.NewRequest(dispatcher())); + } + + void TearDown() override { FakeAdapterTestFixture::TearDown(); } + + fidlbredr::ProfilePtr& client() { return client_; } + + private: + std::unique_ptr server_; + fidlbredr::ProfilePtr client_; + BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(ProfileServerTestFakeAdapter); +}; + +TEST_F(ProfileServerTestFakeAdapter, + ConnectChannelParametersContainsFlushTimeout) { + const bt::PeerId kPeerId; + const fuchsia::bluetooth::PeerId kFidlPeerId{kPeerId.value()}; + const pw::chrono::SystemClock::duration kFlushTimeout( + std::chrono::milliseconds(100)); + + FakeChannel::WeakPtr last_channel; + adapter()->fake_bredr()->set_l2cap_channel_callback( + [&](FakeChannel::WeakPtr chan) { last_channel = std::move(chan); }); + + // Set L2CAP channel parameters + fbt::ChannelParameters chan_params; + fidlbredr::L2capParameters l2cap_params; + fidlbredr::ConnectParameters conn_params; + chan_params.set_flush_timeout(kFlushTimeout.count()); + l2cap_params.set_psm(fidlbredr::PSM_AVDTP); + l2cap_params.set_parameters(std::move(chan_params)); + conn_params.set_l2cap(std::move(l2cap_params)); + + std::optional response_channel; + client()->Connect(kFidlPeerId, + std::move(conn_params), + [&](fidlbredr::Profile_Connect_Result result) { + ASSERT_TRUE(result.is_response()); + response_channel = std::move(result.response().channel); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(last_channel.is_alive()); + EXPECT_EQ(last_channel->info().flush_timeout, std::optional(kFlushTimeout)); + ASSERT_TRUE(response_channel.has_value()); + ASSERT_TRUE(response_channel->has_flush_timeout()); + ASSERT_EQ(response_channel->flush_timeout(), kFlushTimeout.count()); +} + +TEST_F(ProfileServerTestFakeAdapter, + AdvertiseChannelParametersContainsFlushTimeout) { + const pw::chrono::SystemClock::duration kFlushTimeout( + std::chrono::milliseconds(100)); + const bt::hci_spec::ConnectionHandle kHandle(1); + + std::vector services; + services.emplace_back(MakeFIDLServiceDefinition()); + fbt::ChannelParameters chan_params; + chan_params.set_flush_timeout(kFlushTimeout.count()); + + fidlbredr::ConnectionReceiverHandle connect_receiver_handle; + FakeConnectionReceiver connect_receiver(connect_receiver_handle.NewRequest(), + dispatcher()); + + fidlbredr::ProfileAdvertiseRequest adv_request; + adv_request.set_services(std::move(services)); + adv_request.set_parameters(std::move(chan_params)); + adv_request.set_receiver(std::move(connect_receiver_handle)); + client()->Advertise(std::move(adv_request), NopAdvertiseCallback); + RunLoopUntilIdle(); + + ASSERT_EQ(adapter()->fake_bredr()->registered_services().size(), 1u); + auto service_iter = adapter()->fake_bredr()->registered_services().begin(); + EXPECT_EQ(service_iter->second.channel_params.flush_timeout, + std::optional(kFlushTimeout)); + + bt::l2cap::ChannelInfo chan_info = + bt::l2cap::ChannelInfo::MakeBasicMode(bt::l2cap::kDefaultMTU, + bt::l2cap::kDefaultMTU, + bt::l2cap::kAVDTP, + kFlushTimeout); + auto channel = + std::make_unique(bt::l2cap::kFirstDynamicChannelId, + bt::l2cap::kFirstDynamicChannelId, + kHandle, + bt::LinkType::kACL, + chan_info); + service_iter->second.connect_callback(channel->GetWeakPtr(), + MakeL2capProtocolListElement()); + RunLoopUntilIdle(); + ASSERT_TRUE(connect_receiver.channel().has_value()); + fidlbredr::Channel fidl_channel = connect_receiver.take_channel(); + ASSERT_TRUE(fidl_channel.has_flush_timeout()); + EXPECT_EQ(fidl_channel.flush_timeout(), kFlushTimeout.count()); + + channel->Close(); + RunLoopUntilIdle(); +} + +TEST_F(ProfileServerTestFakeAdapter, ClientClosesAdvertisement) { + fidlbredr::ConnectionReceiverHandle receiver_handle; + fidl::InterfaceRequest request = + receiver_handle.NewRequest(); + + std::vector services; + services.emplace_back(MakeFIDLServiceDefinition()); + + size_t cb_count = 0; + auto cb = [&](fidlbredr::Profile_Advertise_Result result) { + cb_count++; + EXPECT_TRUE(result.is_response()); + }; + + fidlbredr::ProfileAdvertiseRequest adv_request; + adv_request.set_services(std::move(services)); + adv_request.set_receiver(std::move(receiver_handle)); + client()->Advertise(std::move(adv_request), std::move(cb)); + RunLoopUntilIdle(); + ASSERT_EQ(cb_count, 1u); + ASSERT_EQ(adapter()->fake_bredr()->registered_services().size(), 1u); + + // Client closes Advertisement by dropping the `ConnectionReceiver`. This is + // OK, and the profile server should handle this by unregistering the + // advertisement. + request = receiver_handle.NewRequest(); + RunLoopUntilIdle(); + ASSERT_EQ(adapter()->fake_bredr()->registered_services().size(), 0u); +} + +TEST_F(ProfileServerTestFakeAdapter, AdvertiseWithMissingFields) { + size_t cb_ok_count = 0; + auto adv_ok_cb = [&](fidlbredr::Profile_Advertise_Result result) { + cb_ok_count++; + EXPECT_TRUE(result.is_response()); + }; + size_t cb_err_count = 0; + auto adv_err_cb = [&](fidlbredr::Profile_Advertise_Result result) { + cb_err_count++; + ASSERT_TRUE(result.is_err()); + EXPECT_EQ(result.err(), fuchsia::bluetooth::ErrorCode::INVALID_ARGUMENTS); + }; + + fidlbredr::ProfileAdvertiseRequest adv_request_missing_receiver; + std::vector services1; + services1.emplace_back(MakeFIDLServiceDefinition()); + adv_request_missing_receiver.set_services(std::move(services1)); + adv_request_missing_receiver.set_parameters( + ::fuchsia::bluetooth::ChannelParameters()); + client()->Advertise(std::move(adv_request_missing_receiver), adv_err_cb); + RunLoopUntilIdle(); + ASSERT_EQ(cb_err_count, 1u); + ASSERT_EQ(adapter()->fake_bredr()->registered_services().size(), 0u); + + fidlbredr::ConnectionReceiverHandle connect_receiver_handle1; + FakeConnectionReceiver connect_receiver1( + connect_receiver_handle1.NewRequest(), dispatcher()); + + fidlbredr::ProfileAdvertiseRequest adv_request_missing_services; + adv_request_missing_services.set_receiver( + std::move(connect_receiver_handle1)); + adv_request_missing_services.set_parameters( + ::fuchsia::bluetooth::ChannelParameters()); + client()->Advertise(std::move(adv_request_missing_services), adv_err_cb); + RunLoopUntilIdle(); + ASSERT_EQ(cb_err_count, 2u); + ASSERT_EQ(adapter()->fake_bredr()->registered_services().size(), 0u); + + // Missing parameters is allowed. + fidlbredr::ProfileAdvertiseRequest adv_request_missing_parameters; + std::vector services2; + services2.emplace_back(MakeFIDLServiceDefinition()); + adv_request_missing_parameters.set_services(std::move(services2)); + fidlbredr::ConnectionReceiverHandle connect_receiver_handle2; + FakeConnectionReceiver connect_receiver2( + connect_receiver_handle2.NewRequest(), dispatcher()); + adv_request_missing_parameters.set_receiver( + std::move(connect_receiver_handle2)); + client()->Advertise(std::move(adv_request_missing_parameters), adv_ok_cb); + RunLoopUntilIdle(); + ASSERT_EQ(cb_ok_count, 1u); + ASSERT_EQ(adapter()->fake_bredr()->registered_services().size(), 1u); +} + +TEST_F(ProfileServerTestFakeAdapter, + L2capParametersExtRequestParametersSucceeds) { + const bt::PeerId kPeerId; + const fuchsia::bluetooth::PeerId kFidlPeerId{kPeerId.value()}; + const zx::duration kFlushTimeout(zx::msec(100)); + const uint16_t kMaxRxSduSize(200); + + FakeChannel::WeakPtr last_channel; + adapter()->fake_bredr()->set_l2cap_channel_callback( + [&](FakeChannel::WeakPtr chan) { last_channel = chan; }); + + // Set L2CAP channel parameters + fbt::ChannelParameters chan_params; + fidlbredr::L2capParameters l2cap_params; + fidlbredr::ConnectParameters conn_params; + chan_params.set_channel_mode(fbt::ChannelMode::BASIC); + chan_params.set_max_rx_packet_size(kMaxRxSduSize); + l2cap_params.set_psm(fidlbredr::PSM_AVDTP); + l2cap_params.set_parameters(std::move(chan_params)); + conn_params.set_l2cap(std::move(l2cap_params)); + + std::optional response_channel; + client()->Connect(kFidlPeerId, + std::move(conn_params), + [&](fidlbredr::Profile_Connect_Result result) { + ASSERT_TRUE(result.is_response()); + response_channel = std::move(result.response().channel); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(last_channel.is_alive()); + EXPECT_FALSE(last_channel->info().flush_timeout.has_value()); + ASSERT_TRUE(response_channel.has_value()); + ASSERT_FALSE(response_channel->has_flush_timeout()); + ASSERT_TRUE(response_channel->has_ext_l2cap()); + + fbt::ChannelParameters request_chan_params; + request_chan_params.set_flush_timeout(kFlushTimeout.get()); + + std::optional result_chan_params; + fidlbredr::L2capParametersExtPtr l2cap_client = + response_channel->mutable_ext_l2cap()->Bind(); + l2cap_client->RequestParameters( + std::move(request_chan_params), + [&](fidlbredr::L2capParametersExt_RequestParameters_Result new_params) { + result_chan_params = new_params.response().ResultValue_(); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(result_chan_params.has_value()); + ASSERT_TRUE(result_chan_params->has_channel_mode()); + ASSERT_TRUE(result_chan_params->has_max_rx_packet_size()); + // TODO(fxbug.dev/42152567): set current security requirements in returned + // channel parameters + ASSERT_FALSE(result_chan_params->has_security_requirements()); + ASSERT_TRUE(result_chan_params->has_flush_timeout()); + EXPECT_EQ(result_chan_params->channel_mode(), fbt::ChannelMode::BASIC); + EXPECT_EQ(result_chan_params->max_rx_packet_size(), kMaxRxSduSize); + EXPECT_EQ(result_chan_params->flush_timeout(), kFlushTimeout.get()); + l2cap_client.Unbind(); + RunLoopUntilIdle(); +} + +TEST_F(ProfileServerTestFakeAdapter, L2capParametersExtRequestParametersFails) { + const bt::PeerId kPeerId; + const fuchsia::bluetooth::PeerId kFidlPeerId{kPeerId.value()}; + const zx::duration kFlushTimeout(zx::msec(100)); + + FakeChannel::WeakPtr last_channel; + adapter()->fake_bredr()->set_l2cap_channel_callback( + [&](FakeChannel::WeakPtr chan) { last_channel = chan; }); + + // Set L2CAP channel parameters + fidlbredr::L2capParameters l2cap_params; + fidlbredr::ConnectParameters conn_params; + l2cap_params.set_psm(fidlbredr::PSM_AVDTP); + conn_params.set_l2cap(std::move(l2cap_params)); + + std::optional response_channel; + client()->Connect(kFidlPeerId, + std::move(conn_params), + [&](fidlbredr::Profile_Connect_Result result) { + ASSERT_TRUE(result.is_response()); + response_channel = std::move(result.response().channel); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(last_channel.is_alive()); + EXPECT_FALSE(last_channel->info().flush_timeout.has_value()); + ASSERT_TRUE(response_channel.has_value()); + ASSERT_FALSE(response_channel->has_flush_timeout()); + ASSERT_TRUE(response_channel->has_ext_l2cap()); + + last_channel->set_flush_timeout_succeeds(false); + + fbt::ChannelParameters request_chan_params; + request_chan_params.set_flush_timeout(kFlushTimeout.get()); + std::optional result_chan_params; + fidlbredr::L2capParametersExtPtr l2cap_client = + response_channel->mutable_ext_l2cap()->Bind(); + l2cap_client->RequestParameters( + std::move(request_chan_params), + [&](fidlbredr::L2capParametersExt_RequestParameters_Result new_params) { + result_chan_params = new_params.response().ResultValue_(); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(result_chan_params.has_value()); + EXPECT_FALSE(result_chan_params->has_flush_timeout()); + l2cap_client.Unbind(); + RunLoopUntilIdle(); +} + +TEST_F(ProfileServerTestFakeAdapter, + L2capParametersExtRequestParametersClosedOnChannelClosed) { + const bt::PeerId kPeerId; + const fuchsia::bluetooth::PeerId kFidlPeerId{kPeerId.value()}; + + FakeChannel::WeakPtr last_channel; + adapter()->fake_bredr()->set_l2cap_channel_callback( + [&](FakeChannel::WeakPtr chan) { last_channel = chan; }); + + // Set L2CAP channel parameters + fidlbredr::L2capParameters l2cap_params; + fidlbredr::ConnectParameters conn_params; + l2cap_params.set_psm(fidlbredr::PSM_AVDTP); + conn_params.set_l2cap(std::move(l2cap_params)); + + std::optional response_channel; + client()->Connect(kFidlPeerId, + std::move(conn_params), + [&](fidlbredr::Profile_Connect_Result result) { + ASSERT_TRUE(result.is_response()); + response_channel = std::move(result.response().channel); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(last_channel.is_alive()); + ASSERT_TRUE(response_channel.has_value()); + + fidlbredr::L2capParametersExtPtr l2cap_client = + response_channel->mutable_ext_l2cap()->Bind(); + bool l2cap_client_closed = false; + l2cap_client.set_error_handler( + [&](zx_status_t /*status*/) { l2cap_client_closed = true; }); + + // Closing the channel should close l2cap_client (after running the loop). + last_channel->Close(); + // Destroy the channel (like the real LogicalLink would) to verify that + // ProfileServer doesn't try to use channel pointers. + EXPECT_TRUE(adapter()->fake_bredr()->DestroyChannel(last_channel->id())); + + // Any request for the closed channel should be ignored. + fbt::ChannelParameters request_chan_params; + std::optional result_chan_params; + l2cap_client->RequestParameters( + std::move(request_chan_params), + [&](fidlbredr::L2capParametersExt_RequestParameters_Result new_params) { + result_chan_params = new_params.response().ResultValue_(); + }); + RunLoopUntilIdle(); + EXPECT_TRUE(l2cap_client_closed); + EXPECT_FALSE(result_chan_params.has_value()); + l2cap_client.Unbind(); + RunLoopUntilIdle(); +} + +TEST_F(ProfileServerTestFakeAdapter, + AudioDirectionExtRequestParametersClosedOnChannelClosed) { + const bt::PeerId kPeerId; + const fuchsia::bluetooth::PeerId kFidlPeerId{kPeerId.value()}; + + FakeChannel::WeakPtr last_channel; + adapter()->fake_bredr()->set_l2cap_channel_callback( + [&](FakeChannel::WeakPtr chan) { last_channel = chan; }); + + // Set L2CAP channel parameters + fidlbredr::L2capParameters l2cap_params; + fidlbredr::ConnectParameters conn_params; + l2cap_params.set_psm(fidlbredr::PSM_AVDTP); + conn_params.set_l2cap(std::move(l2cap_params)); + + std::optional response_channel; + client()->Connect(kFidlPeerId, + std::move(conn_params), + [&](fidlbredr::Profile_Connect_Result result) { + ASSERT_TRUE(result.is_response()); + response_channel = std::move(result.response().channel); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(last_channel.is_alive()); + ASSERT_TRUE(response_channel.has_value()); + + fidlbredr::AudioDirectionExtPtr audio_client = + response_channel->mutable_ext_direction()->Bind(); + bool audio_client_closed = false; + audio_client.set_error_handler( + [&](zx_status_t /*status*/) { audio_client_closed = true; }); + + // Closing the channel should close audio_client (after running the loop). + last_channel->Close(); + // Destroy the channel (like the real LogicalLink would) to verify that + // ProfileServer doesn't try to use channel pointers. + EXPECT_TRUE(adapter()->fake_bredr()->DestroyChannel(last_channel->id())); + + // Any request for the closed channel should be ignored. + size_t priority_cb_count = 0; + audio_client->SetPriority( + fidlbredr::A2dpDirectionPriority::NORMAL, + [&](fidlbredr::AudioDirectionExt_SetPriority_Result result) { + priority_cb_count++; + }); + + RunLoopUntilIdle(); + EXPECT_TRUE(audio_client_closed); + EXPECT_EQ(priority_cb_count, 0u); + audio_client.Unbind(); + RunLoopUntilIdle(); +} + +TEST_F(ProfileServerTestFakeAdapter, + AudioOffloadExtRequestParametersClosedOnChannelClosed) { + const bt::PeerId kPeerId; + const fuchsia::bluetooth::PeerId kFidlPeerId{kPeerId.value()}; + + FakeChannel::WeakPtr last_channel; + adapter()->fake_bredr()->set_l2cap_channel_callback( + [&](FakeChannel::WeakPtr chan) { last_channel = std::move(chan); }); + + // Support Android Vendor Extensions to enable Audio Offload Extension + adapter()->mutable_state().controller_features |= + FeaturesBits::kAndroidVendorExtensions; + bt::StaticPacket< + android_emb::LEGetVendorCapabilitiesCommandCompleteEventWriter> + params; + params.SetToZeros(); + params.view().status().Write(pw::bluetooth::emboss::StatusCode::SUCCESS); + params.view().version_supported().major_number().Write(0); + params.view().version_supported().minor_number().Write(98); + params.view().a2dp_source_offload_capability_mask().aac().Write(true); + adapter()->mutable_state().android_vendor_capabilities = + bt::gap::AndroidVendorCapabilities::New(params.view()); + + // Set L2CAP channel parameters + fidlbredr::L2capParameters l2cap_params; + fidlbredr::ConnectParameters conn_params; + l2cap_params.set_psm(fidlbredr::PSM_AVDTP); + conn_params.set_l2cap(std::move(l2cap_params)); + + std::optional response_channel; + client()->Connect(kFidlPeerId, + std::move(conn_params), + [&](fidlbredr::Profile_Connect_Result result) { + ASSERT_TRUE(result.is_response()); + response_channel = std::move(result.response().channel); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(last_channel.is_alive()); + ASSERT_TRUE(response_channel.has_value()); + ASSERT_TRUE(response_channel->has_ext_audio_offload()); + + fidlbredr::AudioOffloadExtPtr audio_client = + response_channel->mutable_ext_audio_offload()->Bind(); + bool audio_client_closed = false; + audio_client.set_error_handler( + [&](zx_status_t /*status*/) { audio_client_closed = true; }); + + // Closing the channel should close |audio_client| (after running the loop). + last_channel->Close(); + // Destroy the channel (like the real LogicalLink would) to verify that + // ProfileServer doesn't try to use channel pointers. + EXPECT_TRUE(adapter()->fake_bredr()->DestroyChannel(last_channel->id())); + + // Any request for the closed channel should be ignored. + std::optional + result_features; + audio_client->GetSupportedFeatures( + [&result_features]( + fidlbredr::AudioOffloadExt_GetSupportedFeatures_Result features) { + result_features = std::move(features.response()); + }); + + RunLoopUntilIdle(); + EXPECT_TRUE(audio_client_closed); + EXPECT_FALSE(result_features.has_value()); + audio_client.Unbind(); + RunLoopUntilIdle(); +} + +class ProfileServerInvalidSamplingFrequencyTest + : public ProfileServerTestFakeAdapter, + public ::testing::WithParamInterface { +}; + +const std::vector + kInvalidSamplingFrequencies = { + fidlbredr::AudioSamplingFrequency::HZ_88200, + fidlbredr::AudioSamplingFrequency::HZ_96000, +}; + +INSTANTIATE_TEST_SUITE_P(ProfileServerTestFakeAdapter, + ProfileServerInvalidSamplingFrequencyTest, + ::testing::ValuesIn(kInvalidSamplingFrequencies)); + +TEST_P(ProfileServerInvalidSamplingFrequencyTest, SbcInvalidSamplingFrequency) { + // enable a2dp offloading + adapter()->mutable_state().controller_features |= + FeaturesBits::kAndroidVendorExtensions; + + // enable offloaded sbc encoding + bt::StaticPacket< + android_emb::LEGetVendorCapabilitiesCommandCompleteEventWriter> + params; + params.view().status().Write(pw::bluetooth::emboss::StatusCode::SUCCESS); + params.view().version_supported().major_number().Write(0); + params.view().version_supported().minor_number().Write(98); + params.view().a2dp_source_offload_capability_mask().sbc().Write(true); + adapter()->mutable_state().android_vendor_capabilities = + bt::gap::AndroidVendorCapabilities::New(params.view()); + + // set up a fake channel and connection + FakeChannel::WeakPtr fake_channel; + adapter()->fake_bredr()->set_l2cap_channel_callback( + [&](auto chan) { fake_channel = std::move(chan); }); + + const bt::PeerId peer_id(1); + const fuchsia::bluetooth::PeerId fidl_peer_id{peer_id.value()}; + + fidlbredr::L2capParameters l2cap_params; + l2cap_params.set_psm(fidlbredr::PSM_AVDTP); + + fbt::ChannelParameters chan_params; + l2cap_params.set_parameters(std::move(chan_params)); + + fidlbredr::ConnectParameters conn_params; + conn_params.set_l2cap(std::move(l2cap_params)); + + std::optional response_channel; + client()->Connect( + fidl_peer_id, + std::move(conn_params), + [&response_channel](fidlbredr::Profile_Connect_Result result) { + ASSERT_TRUE(result.is_response()); + response_channel = std::move(result.response().channel); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(response_channel.has_value()); + ASSERT_TRUE(response_channel->has_ext_audio_offload()); + + // set up the bad configuration + std::unique_ptr codec = + fidlbredr::AudioOffloadFeatures::New(); + std::unique_ptr codec_value = + fidlbredr::AudioSbcSupport::New(); + codec->set_sbc(std::move(*codec_value)); + + std::unique_ptr encoder_settings = + std::make_unique(); + std::unique_ptr encoder_settings_value = + fuchsia::media::SbcEncoderSettings::New(); + encoder_settings->set_sbc(*encoder_settings_value); + + std::unique_ptr config = + std::make_unique(); + config->set_codec(std::move(*codec)); + config->set_max_latency(10); + config->set_scms_t_enable(true); + config->set_sampling_frequency(GetParam()); + config->set_bits_per_sample(fidlbredr::AudioBitsPerSample::BPS_16); + config->set_channel_mode(fidlbredr::AudioChannelMode::MONO); + config->set_encoded_bit_rate(10); + config->set_encoder_settings(std::move(*encoder_settings)); + + // attempt to start the audio offload + fidl::InterfaceHandle controller_handle; + fidl::InterfaceRequest controller_request = + controller_handle.NewRequest(); + fidl::InterfacePtr audio_offload_ext_client = + response_channel->mutable_ext_audio_offload()->Bind(); + audio_offload_ext_client->StartAudioOffload(std::move(*config), + std::move(controller_request)); + + fidl::InterfacePtr + audio_offload_controller_client; + audio_offload_controller_client.Bind(std::move(controller_handle)); + + std::optional audio_offload_controller_epitaph; + audio_offload_controller_client.set_error_handler( + [&](zx_status_t status) { audio_offload_controller_epitaph = status; }); + + RunLoopUntilIdle(); + + // Verify that |audio_offload_controller_client| was closed with + // |ZX_ERR_INTERNAL| epitaph + ASSERT_TRUE(audio_offload_controller_epitaph.has_value()); + EXPECT_EQ(audio_offload_controller_epitaph.value(), ZX_ERR_NOT_SUPPORTED); +} + +class AndroidSupportedFeaturesTest + : public ProfileServerTestFakeAdapter, + public ::testing::WithParamInterface< + std::pair> {}; + +TEST_P(AndroidSupportedFeaturesTest, AudioOffloadExtGetSupportedFeatures) { + const bool android_vendor_ext_support = GetParam().first; + const uint32_t a2dp_offload_capabilities = GetParam().second; + + if (android_vendor_ext_support) { + adapter()->mutable_state().controller_features |= + FeaturesBits::kAndroidVendorExtensions; + + bt::StaticPacket< + android_emb::LEGetVendorCapabilitiesCommandCompleteEventWriter> + params; + params.SetToZeros(); + params.view().status().Write(pw::bluetooth::emboss::StatusCode::SUCCESS); + params.view().version_supported().major_number().Write(0); + params.view().version_supported().minor_number().Write(98); + params.view() + .a2dp_source_offload_capability_mask() + .BackingStorage() + .UncheckedWriteUInt(a2dp_offload_capabilities); + adapter()->mutable_state().android_vendor_capabilities = + bt::gap::AndroidVendorCapabilities::New(params.view()); + } + + const bt::PeerId peer_id(1); + const fuchsia::bluetooth::PeerId fidl_peer_id{peer_id.value()}; + + // Set L2CAP channel parameters + fidlbredr::L2capParameters l2cap_params; + fidlbredr::ConnectParameters conn_params; + l2cap_params.set_psm(fidlbredr::PSM_AVDTP); + l2cap_params.set_parameters(fbt::ChannelParameters()); + conn_params.set_l2cap(std::move(l2cap_params)); + + std::optional response_channel; + client()->Connect( + fidl_peer_id, + std::move(conn_params), + [&response_channel](fidlbredr::Profile_Connect_Result result) { + ASSERT_TRUE(result.is_response()); + response_channel = std::move(result.response().channel); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(response_channel.has_value()); + if (!android_vendor_ext_support || !a2dp_offload_capabilities) { + EXPECT_FALSE(response_channel->has_ext_audio_offload()); + return; + } + ASSERT_TRUE(response_channel->has_ext_audio_offload()); + + std::optional + result_features; + fidlbredr::AudioOffloadExtPtr audio_offload_ext_client = + response_channel->mutable_ext_audio_offload()->Bind(); + audio_offload_ext_client->GetSupportedFeatures( + [&result_features]( + fidlbredr::AudioOffloadExt_GetSupportedFeatures_Result features) { + result_features = std::move(features.response()); + }); + RunLoopUntilIdle(); + + EXPECT_TRUE(result_features->has_audio_offload_features()); + const std::vector& audio_offload_features = + result_features->audio_offload_features(); + const uint32_t audio_offload_features_size = + std::bitset::digits>( + a2dp_offload_capabilities) + .count(); + EXPECT_EQ(audio_offload_features_size, audio_offload_features.size()); + + uint32_t capabilities = 0; + const uint32_t sbc_capability = + static_cast(android_emb::A2dpCodecType::SBC); + const uint32_t aac_capability = + static_cast(android_emb::A2dpCodecType::AAC); + for (const fidlbredr::AudioOffloadFeatures& feature : + audio_offload_features) { + if (feature.is_sbc()) { + capabilities |= sbc_capability; + } + if (feature.is_aac()) { + capabilities |= aac_capability; + } + } + EXPECT_EQ(capabilities, a2dp_offload_capabilities); +} + +TEST_P(AndroidSupportedFeaturesTest, AudioOffloadExtStartAudioOffloadSuccess) { + const bool android_vendor_ext_support = GetParam().first; + const uint32_t a2dp_offload_capabilities = GetParam().second; + + if (android_vendor_ext_support) { + adapter()->mutable_state().controller_features |= + FeaturesBits::kAndroidVendorExtensions; + + bt::StaticPacket< + android_emb::LEGetVendorCapabilitiesCommandCompleteEventWriter> + params; + params.SetToZeros(); + params.view().status().Write(pw::bluetooth::emboss::StatusCode::SUCCESS); + params.view().version_supported().major_number().Write(0); + params.view().version_supported().minor_number().Write(98); + params.view() + .a2dp_source_offload_capability_mask() + .BackingStorage() + .UncheckedWriteUInt(a2dp_offload_capabilities); + adapter()->mutable_state().android_vendor_capabilities = + bt::gap::AndroidVendorCapabilities::New(params.view()); + } + + const bt::PeerId peer_id(1); + const fuchsia::bluetooth::PeerId fidl_peer_id{peer_id.value()}; + + // Set L2CAP channel parameters + fidlbredr::L2capParameters l2cap_params; + fidlbredr::ConnectParameters conn_params; + l2cap_params.set_psm(fidlbredr::PSM_AVDTP); + l2cap_params.set_parameters(fbt::ChannelParameters()); + conn_params.set_l2cap(std::move(l2cap_params)); + + std::optional response_channel; + client()->Connect( + fidl_peer_id, + std::move(conn_params), + [&response_channel](fidlbredr::Profile_Connect_Result result) { + ASSERT_TRUE(result.is_response()); + response_channel = std::move(result.response().channel); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(response_channel.has_value()); + if (!android_vendor_ext_support || !a2dp_offload_capabilities) { + EXPECT_FALSE(response_channel->has_ext_audio_offload()); + return; + } + ASSERT_TRUE(response_channel->has_ext_audio_offload()); + + // Set Audio Offload Configuration Values + std::unique_ptr codec = + fidlbredr::AudioOffloadFeatures::New(); + std::unique_ptr codec_value = + fidlbredr::AudioSbcSupport::New(); + codec->set_sbc(std::move(*codec_value)); + + std::unique_ptr encoder_settings = + std::make_unique(); + std::unique_ptr encoder_settings_value = + fuchsia::media::SbcEncoderSettings::New(); + encoder_settings->set_sbc(*encoder_settings_value); + + std::unique_ptr config = + std::make_unique(); + config->set_codec(std::move(*codec)); + config->set_max_latency(10); + config->set_scms_t_enable(true); + config->set_sampling_frequency(fidlbredr::AudioSamplingFrequency::HZ_44100); + config->set_bits_per_sample(fidlbredr::AudioBitsPerSample::BPS_16); + config->set_channel_mode(fidlbredr::AudioChannelMode::MONO); + config->set_encoded_bit_rate(10); + config->set_encoder_settings(std::move(*encoder_settings)); + + fidlbredr::AudioOffloadExtPtr audio_offload_ext_client = + response_channel->mutable_ext_audio_offload()->Bind(); + fidlbredr::AudioOffloadControllerHandle controller_handle; + fidl::InterfaceRequest controller_request = + controller_handle.NewRequest(); + audio_offload_ext_client->StartAudioOffload(std::move(*config), + std::move(controller_request)); + + fidlbredr::AudioOffloadControllerPtr audio_offload_controller_client; + audio_offload_controller_client.Bind(std::move(controller_handle)); + + std::optional audio_offload_controller_epitaph; + audio_offload_controller_client.set_error_handler( + [&](zx_status_t status) { audio_offload_controller_epitaph = status; }); + + size_t on_started_count = 0; + audio_offload_controller_client.events().OnStarted = [&]() { + on_started_count++; + }; + + RunLoopUntilIdle(); + + // Verify that OnStarted event was sent successfully + EXPECT_EQ(on_started_count, 1u); + + // Verify that |audio_offload_controller_client| was not closed with an + // epitaph + ASSERT_FALSE(audio_offload_controller_epitaph.has_value()); +} + +TEST_P(AndroidSupportedFeaturesTest, AudioOffloadExtStartAudioOffloadFail) { + FakeChannel::WeakPtr fake_channel; + adapter()->fake_bredr()->set_l2cap_channel_callback( + [&](FakeChannel::WeakPtr chan) { fake_channel = std::move(chan); }); + + const bool android_vendor_ext_support = GetParam().first; + const uint32_t a2dp_offload_capabilities = GetParam().second; + + if (android_vendor_ext_support) { + adapter()->mutable_state().controller_features |= + FeaturesBits::kAndroidVendorExtensions; + + bt::StaticPacket< + android_emb::LEGetVendorCapabilitiesCommandCompleteEventWriter> + params; + params.SetToZeros(); + params.view().status().Write(pw::bluetooth::emboss::StatusCode::SUCCESS); + params.view().version_supported().major_number().Write(0); + params.view().version_supported().minor_number().Write(98); + params.view() + .a2dp_source_offload_capability_mask() + .BackingStorage() + .UncheckedWriteUInt(a2dp_offload_capabilities); + adapter()->mutable_state().android_vendor_capabilities = + bt::gap::AndroidVendorCapabilities::New(params.view()); + } + + const bt::PeerId peer_id(1); + const fuchsia::bluetooth::PeerId fidl_peer_id{peer_id.value()}; + + // Set L2CAP channel parameters + fidlbredr::L2capParameters l2cap_params; + fidlbredr::ConnectParameters conn_params; + l2cap_params.set_psm(fidlbredr::PSM_AVDTP); + l2cap_params.set_parameters(fbt::ChannelParameters()); + conn_params.set_l2cap(std::move(l2cap_params)); + + std::optional response_channel; + client()->Connect( + fidl_peer_id, + std::move(conn_params), + [&response_channel](fidlbredr::Profile_Connect_Result result) { + ASSERT_TRUE(result.is_response()); + response_channel = std::move(result.response().channel); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(response_channel.has_value()); + if (!android_vendor_ext_support || !a2dp_offload_capabilities) { + EXPECT_FALSE(response_channel->has_ext_audio_offload()); + return; + } + ASSERT_TRUE(response_channel->has_ext_audio_offload()); + + // Make A2DP offloading fail, resulting in |ZX_ERR_INTERNAL| epitaph + ASSERT_TRUE(fake_channel.is_alive()); + fake_channel->set_a2dp_offload_fails(bt::HostError::kFailed); + + // Set Audio Offload Configuration Values + std::unique_ptr codec = + fidlbredr::AudioOffloadFeatures::New(); + std::unique_ptr codec_value = + fidlbredr::AudioSbcSupport::New(); + codec->set_sbc(std::move(*codec_value)); + + std::unique_ptr encoder_settings = + std::make_unique(); + std::unique_ptr encoder_settings_value = + fuchsia::media::SbcEncoderSettings::New(); + encoder_settings->set_sbc(*encoder_settings_value); + + std::unique_ptr config = + std::make_unique(); + config->set_codec(std::move(*codec)); + config->set_max_latency(10); + config->set_scms_t_enable(true); + config->set_sampling_frequency(fidlbredr::AudioSamplingFrequency::HZ_44100); + config->set_bits_per_sample(fidlbredr::AudioBitsPerSample::BPS_16); + config->set_channel_mode(fidlbredr::AudioChannelMode::MONO); + config->set_encoded_bit_rate(10); + config->set_encoder_settings(std::move(*encoder_settings)); + + fidlbredr::AudioOffloadExtPtr audio_offload_ext_client = + response_channel->mutable_ext_audio_offload()->Bind(); + fidlbredr::AudioOffloadControllerHandle controller_handle; + fidl::InterfaceRequest controller_request = + controller_handle.NewRequest(); + audio_offload_ext_client->StartAudioOffload(std::move(*config), + std::move(controller_request)); + + fidlbredr::AudioOffloadControllerPtr audio_offload_controller_client; + audio_offload_controller_client.Bind(std::move(controller_handle)); + + std::optional audio_offload_controller_epitaph; + audio_offload_controller_client.set_error_handler( + [&](zx_status_t status) { audio_offload_controller_epitaph = status; }); + + size_t cb_count = 0; + audio_offload_controller_client.events().OnStarted = [&]() { cb_count++; }; + + RunLoopUntilIdle(); + EXPECT_EQ(cb_count, 0u); + + // Verify that |audio_offload_controller_client| was closed with + // |ZX_ERR_INTERNAL| epitaph + ASSERT_TRUE(audio_offload_controller_epitaph.has_value()); + EXPECT_EQ(audio_offload_controller_epitaph.value(), ZX_ERR_INTERNAL); + + bt::l2cap::A2dpOffloadStatus a2dp_offload_status = + fake_channel->a2dp_offload_status(); + EXPECT_EQ(bt::l2cap::A2dpOffloadStatus::kStopped, a2dp_offload_status); +} + +TEST_P(AndroidSupportedFeaturesTest, + AudioOffloadExtStartAudioOffloadInProgress) { + FakeChannel::WeakPtr fake_channel; + adapter()->fake_bredr()->set_l2cap_channel_callback( + [&](FakeChannel::WeakPtr chan) { fake_channel = std::move(chan); }); + + const bool android_vendor_ext_support = GetParam().first; + const uint32_t a2dp_offload_capabilities = GetParam().second; + + if (android_vendor_ext_support) { + adapter()->mutable_state().controller_features |= + FeaturesBits::kAndroidVendorExtensions; + + bt::StaticPacket< + android_emb::LEGetVendorCapabilitiesCommandCompleteEventWriter> + params; + params.SetToZeros(); + params.view().status().Write(pw::bluetooth::emboss::StatusCode::SUCCESS); + params.view().version_supported().major_number().Write(0); + params.view().version_supported().minor_number().Write(98); + params.view() + .a2dp_source_offload_capability_mask() + .BackingStorage() + .UncheckedWriteUInt(a2dp_offload_capabilities); + adapter()->mutable_state().android_vendor_capabilities = + bt::gap::AndroidVendorCapabilities::New(params.view()); + } + + const bt::PeerId peer_id(1); + const fuchsia::bluetooth::PeerId fidl_peer_id{peer_id.value()}; + + // Set L2CAP channel parameters + fidlbredr::L2capParameters l2cap_params; + fidlbredr::ConnectParameters conn_params; + l2cap_params.set_psm(fidlbredr::PSM_AVDTP); + l2cap_params.set_parameters(fbt::ChannelParameters()); + conn_params.set_l2cap(std::move(l2cap_params)); + + std::optional response_channel; + client()->Connect( + fidl_peer_id, + std::move(conn_params), + [&response_channel](fidlbredr::Profile_Connect_Result result) { + ASSERT_TRUE(result.is_response()); + response_channel = std::move(result.response().channel); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(response_channel.has_value()); + if (!android_vendor_ext_support || !a2dp_offload_capabilities) { + EXPECT_FALSE(response_channel->has_ext_audio_offload()); + return; + } + ASSERT_TRUE(response_channel->has_ext_audio_offload()); + + // Make A2DP offloading fail, resulting in |ZX_ERR_ALREADY_BOUND| epitaph + ASSERT_TRUE(fake_channel.is_alive()); + fake_channel->set_a2dp_offload_fails(bt::HostError::kInProgress); + + // Set Audio Offload Configuration Values + std::unique_ptr codec = + fidlbredr::AudioOffloadFeatures::New(); + std::unique_ptr codec_value = + fidlbredr::AudioSbcSupport::New(); + codec->set_sbc(std::move(*codec_value)); + + std::unique_ptr encoder_settings = + std::make_unique(); + std::unique_ptr encoder_settings_value = + fuchsia::media::SbcEncoderSettings::New(); + encoder_settings->set_sbc(*encoder_settings_value); + + std::unique_ptr config = + std::make_unique(); + config->set_codec(std::move(*codec)); + config->set_max_latency(10); + config->set_scms_t_enable(true); + config->set_sampling_frequency(fidlbredr::AudioSamplingFrequency::HZ_44100); + config->set_bits_per_sample(fidlbredr::AudioBitsPerSample::BPS_16); + config->set_channel_mode(fidlbredr::AudioChannelMode::MONO); + config->set_encoded_bit_rate(10); + config->set_encoder_settings(std::move(*encoder_settings)); + + fidlbredr::AudioOffloadExtPtr audio_offload_ext_client = + response_channel->mutable_ext_audio_offload()->Bind(); + fidlbredr::AudioOffloadControllerHandle controller_handle; + fidl::InterfaceRequest controller_request = + controller_handle.NewRequest(); + audio_offload_ext_client->StartAudioOffload(std::move(*config), + std::move(controller_request)); + + fidlbredr::AudioOffloadControllerPtr audio_offload_controller_client; + audio_offload_controller_client.Bind(std::move(controller_handle)); + + std::optional audio_offload_controller_epitaph; + audio_offload_controller_client.set_error_handler( + [&](zx_status_t status) { audio_offload_controller_epitaph = status; }); + + size_t cb_count = 0; + audio_offload_controller_client.events().OnStarted = [&]() { cb_count++; }; + + RunLoopUntilIdle(); + EXPECT_EQ(cb_count, 0u); + + // Verify that |audio_offload_controller_client| was closed with + // |ZX_ERR_ALREADY_BOUND| epitaph + ASSERT_TRUE(audio_offload_controller_epitaph.has_value()); + EXPECT_EQ(audio_offload_controller_epitaph.value(), ZX_ERR_ALREADY_BOUND); +} + +TEST_P(AndroidSupportedFeaturesTest, + AudioOffloadExtStartAudioOffloadControllerError) { + FakeChannel::WeakPtr fake_channel; + adapter()->fake_bredr()->set_l2cap_channel_callback( + [&](FakeChannel::WeakPtr chan) { fake_channel = std::move(chan); }); + + const bool android_vendor_ext_support = GetParam().first; + const uint32_t a2dp_offload_capabilities = GetParam().second; + + if (android_vendor_ext_support) { + adapter()->mutable_state().controller_features |= + FeaturesBits::kAndroidVendorExtensions; + + bt::StaticPacket< + android_emb::LEGetVendorCapabilitiesCommandCompleteEventWriter> + params; + params.SetToZeros(); + params.view().status().Write(pw::bluetooth::emboss::StatusCode::SUCCESS); + params.view().version_supported().major_number().Write(0); + params.view().version_supported().minor_number().Write(98); + params.view() + .a2dp_source_offload_capability_mask() + .BackingStorage() + .UncheckedWriteUInt(a2dp_offload_capabilities); + adapter()->mutable_state().android_vendor_capabilities = + bt::gap::AndroidVendorCapabilities::New(params.view()); + } + + const bt::PeerId peer_id(1); + const fuchsia::bluetooth::PeerId fidl_peer_id{peer_id.value()}; + + // Set L2CAP channel parameters + fidlbredr::L2capParameters l2cap_params; + fidlbredr::ConnectParameters conn_params; + l2cap_params.set_psm(fidlbredr::PSM_AVDTP); + l2cap_params.set_parameters(fbt::ChannelParameters()); + conn_params.set_l2cap(std::move(l2cap_params)); + + std::optional response_channel; + client()->Connect( + fidl_peer_id, + std::move(conn_params), + [&response_channel](fidlbredr::Profile_Connect_Result result) { + ASSERT_TRUE(result.is_response()); + response_channel = std::move(result.response().channel); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(response_channel.has_value()); + if (!android_vendor_ext_support || !a2dp_offload_capabilities) { + EXPECT_FALSE(response_channel->has_ext_audio_offload()); + return; + } + ASSERT_TRUE(response_channel->has_ext_audio_offload()); + + // Set Audio Offload Configuration Values + std::unique_ptr codec = + fidlbredr::AudioOffloadFeatures::New(); + std::unique_ptr codec_value = + fidlbredr::AudioSbcSupport::New(); + codec->set_sbc(std::move(*codec_value)); + + std::unique_ptr encoder_settings = + std::make_unique(); + std::unique_ptr encoder_settings_value = + fuchsia::media::SbcEncoderSettings::New(); + encoder_settings->set_sbc(*encoder_settings_value); + + std::unique_ptr config = + std::make_unique(); + config->set_codec(std::move(*codec)); + config->set_max_latency(10); + config->set_scms_t_enable(true); + config->set_sampling_frequency(fidlbredr::AudioSamplingFrequency::HZ_44100); + config->set_bits_per_sample(fidlbredr::AudioBitsPerSample::BPS_16); + config->set_channel_mode(fidlbredr::AudioChannelMode::MONO); + config->set_encoded_bit_rate(10); + config->set_encoder_settings(std::move(*encoder_settings)); + + fidlbredr::AudioOffloadExtPtr audio_offload_ext_client = + response_channel->mutable_ext_audio_offload()->Bind(); + fidlbredr::AudioOffloadControllerHandle controller_handle; + fidl::InterfaceRequest controller_request = + controller_handle.NewRequest(); + audio_offload_ext_client->StartAudioOffload(std::move(*config), + std::move(controller_request)); + + fidlbredr::AudioOffloadControllerPtr audio_offload_controller_client; + audio_offload_controller_client.Bind(std::move(controller_handle)); + + std::optional audio_offload_controller_epitaph; + audio_offload_controller_client.set_error_handler( + [&](zx_status_t status) { audio_offload_controller_epitaph = status; }); + + size_t cb_count = 0; + audio_offload_controller_client.events().OnStarted = [&]() { cb_count++; }; + + // Close client end of protocol to trigger audio offload error handler + audio_offload_controller_client.Unbind(); + + RunLoopUntilIdle(); + EXPECT_EQ(cb_count, 0u); + ASSERT_FALSE(audio_offload_controller_epitaph.has_value()); +} + +TEST_P(AndroidSupportedFeaturesTest, AudioOffloadControllerStopSuccess) { + const bool android_vendor_ext_support = GetParam().first; + const uint32_t a2dp_offload_capabilities = GetParam().second; + + if (android_vendor_ext_support) { + adapter()->mutable_state().controller_features |= + FeaturesBits::kAndroidVendorExtensions; + + bt::StaticPacket< + android_emb::LEGetVendorCapabilitiesCommandCompleteEventWriter> + params; + params.SetToZeros(); + params.view().status().Write(pw::bluetooth::emboss::StatusCode::SUCCESS); + params.view().version_supported().major_number().Write(0); + params.view().version_supported().minor_number().Write(98); + params.view() + .a2dp_source_offload_capability_mask() + .BackingStorage() + .UncheckedWriteUInt(a2dp_offload_capabilities); + adapter()->mutable_state().android_vendor_capabilities = + bt::gap::AndroidVendorCapabilities::New(params.view()); + } + + const bt::PeerId peer_id(1); + const fuchsia::bluetooth::PeerId fidl_peer_id{peer_id.value()}; + + // Set L2CAP channel parameters + fidlbredr::L2capParameters l2cap_params; + fidlbredr::ConnectParameters conn_params; + l2cap_params.set_psm(fidlbredr::PSM_AVDTP); + l2cap_params.set_parameters(fbt::ChannelParameters()); + conn_params.set_l2cap(std::move(l2cap_params)); + + std::optional response_channel; + client()->Connect( + fidl_peer_id, + std::move(conn_params), + [&response_channel](fidlbredr::Profile_Connect_Result result) { + ASSERT_TRUE(result.is_response()); + response_channel = std::move(result.response().channel); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(response_channel.has_value()); + if (!android_vendor_ext_support || !a2dp_offload_capabilities) { + EXPECT_FALSE(response_channel->has_ext_audio_offload()); + return; + } + ASSERT_TRUE(response_channel->has_ext_audio_offload()); + + // Set Audio Offload Configuration Values + std::unique_ptr codec = + fidlbredr::AudioOffloadFeatures::New(); + std::unique_ptr codec_value = + fidlbredr::AudioSbcSupport::New(); + codec->set_sbc(std::move(*codec_value)); + + std::unique_ptr encoder_settings = + std::make_unique(); + std::unique_ptr encoder_settings_value = + fuchsia::media::SbcEncoderSettings::New(); + encoder_settings->set_sbc(*encoder_settings_value); + + std::unique_ptr config = + std::make_unique(); + config->set_codec(std::move(*codec)); + config->set_max_latency(10); + config->set_scms_t_enable(true); + config->set_sampling_frequency(fidlbredr::AudioSamplingFrequency::HZ_44100); + config->set_bits_per_sample(fidlbredr::AudioBitsPerSample::BPS_16); + config->set_channel_mode(fidlbredr::AudioChannelMode::MONO); + config->set_encoded_bit_rate(10); + config->set_encoder_settings(std::move(*encoder_settings)); + + fidlbredr::AudioOffloadExtPtr audio_offload_ext_client = + response_channel->mutable_ext_audio_offload()->Bind(); + fidlbredr::AudioOffloadControllerHandle controller_handle; + fidl::InterfaceRequest controller_request = + controller_handle.NewRequest(); + audio_offload_ext_client->StartAudioOffload(std::move(*config), + std::move(controller_request)); + + fidlbredr::AudioOffloadControllerPtr audio_offload_controller_client; + audio_offload_controller_client.Bind(std::move(controller_handle)); + + std::optional audio_offload_controller_epitaph; + audio_offload_controller_client.set_error_handler( + [&](zx_status_t status) { audio_offload_controller_epitaph = status; }); + + size_t on_started_count = 0; + audio_offload_controller_client.events().OnStarted = [&]() { + on_started_count++; + }; + + RunLoopUntilIdle(); + + // Verify that OnStarted event was sent successfully + EXPECT_EQ(on_started_count, 1u); + + // Verify that |audio_offload_controller_client| was not closed with an + // epitaph + ASSERT_FALSE(audio_offload_controller_epitaph.has_value()); + + bool stop_callback_called = false; + auto callback = [&stop_callback_called]( + fidlbredr::AudioOffloadController_Stop_Result result) { + stop_callback_called = true; + }; + + audio_offload_controller_client->Stop(std::move(callback)); + + RunLoopUntilIdle(); + + // Verify that audio offload was stopped successfully + ASSERT_TRUE(stop_callback_called); +} + +TEST_P(AndroidSupportedFeaturesTest, AudioOffloadControllerStopFail) { + FakeChannel::WeakPtr fake_channel; + adapter()->fake_bredr()->set_l2cap_channel_callback( + [&](FakeChannel::WeakPtr chan) { fake_channel = std::move(chan); }); + + const bool android_vendor_ext_support = GetParam().first; + const uint32_t a2dp_offload_capabilities = GetParam().second; + + if (android_vendor_ext_support) { + adapter()->mutable_state().controller_features |= + FeaturesBits::kAndroidVendorExtensions; + + bt::StaticPacket< + android_emb::LEGetVendorCapabilitiesCommandCompleteEventWriter> + params; + params.SetToZeros(); + params.view().status().Write(pw::bluetooth::emboss::StatusCode::SUCCESS); + params.view().version_supported().major_number().Write(0); + params.view().version_supported().minor_number().Write(98); + params.view() + .a2dp_source_offload_capability_mask() + .BackingStorage() + .UncheckedWriteUInt(a2dp_offload_capabilities); + adapter()->mutable_state().android_vendor_capabilities = + bt::gap::AndroidVendorCapabilities::New(params.view()); + } + + const bt::PeerId peer_id(1); + const fuchsia::bluetooth::PeerId fidl_peer_id{peer_id.value()}; + + // Set L2CAP channel parameters + fidlbredr::L2capParameters l2cap_params; + fidlbredr::ConnectParameters conn_params; + l2cap_params.set_psm(fidlbredr::PSM_AVDTP); + l2cap_params.set_parameters(fbt::ChannelParameters()); + conn_params.set_l2cap(std::move(l2cap_params)); + + std::optional response_channel; + client()->Connect( + fidl_peer_id, + std::move(conn_params), + [&response_channel](fidlbredr::Profile_Connect_Result result) { + ASSERT_TRUE(result.is_response()); + response_channel = std::move(result.response().channel); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(response_channel.has_value()); + if (!android_vendor_ext_support || !a2dp_offload_capabilities) { + EXPECT_FALSE(response_channel->has_ext_audio_offload()); + return; + } + ASSERT_TRUE(response_channel->has_ext_audio_offload()); + + // Set Audio Offload Configuration Values + std::unique_ptr codec = + fidlbredr::AudioOffloadFeatures::New(); + std::unique_ptr codec_value = + fidlbredr::AudioSbcSupport::New(); + codec->set_sbc(std::move(*codec_value)); + + std::unique_ptr encoder_settings = + std::make_unique(); + std::unique_ptr encoder_settings_value = + fuchsia::media::SbcEncoderSettings::New(); + encoder_settings->set_sbc(*encoder_settings_value); + + std::unique_ptr config = + std::make_unique(); + config->set_codec(std::move(*codec)); + config->set_max_latency(10); + config->set_scms_t_enable(true); + config->set_sampling_frequency(fidlbredr::AudioSamplingFrequency::HZ_44100); + config->set_bits_per_sample(fidlbredr::AudioBitsPerSample::BPS_16); + config->set_channel_mode(fidlbredr::AudioChannelMode::MONO); + config->set_encoded_bit_rate(10); + config->set_encoder_settings(std::move(*encoder_settings)); + + fidlbredr::AudioOffloadExtPtr audio_offload_ext_client = + response_channel->mutable_ext_audio_offload()->Bind(); + fidlbredr::AudioOffloadControllerHandle controller_handle; + fidl::InterfaceRequest controller_request = + controller_handle.NewRequest(); + audio_offload_ext_client->StartAudioOffload(std::move(*config), + std::move(controller_request)); + + fidlbredr::AudioOffloadControllerPtr audio_offload_controller_client; + audio_offload_controller_client.Bind(std::move(controller_handle)); + + std::optional audio_offload_controller_epitaph; + audio_offload_controller_client.set_error_handler( + [&](zx_status_t status) { audio_offload_controller_epitaph = status; }); + + size_t on_started_count = 0; + audio_offload_controller_client.events().OnStarted = [&]() { + on_started_count++; + }; + + RunLoopUntilIdle(); + + // Verify that OnStarted event was sent successfully + EXPECT_EQ(on_started_count, 1u); + + // Verify that |audio_offload_controller_client| was not closed with an + // epitaph + ASSERT_FALSE(audio_offload_controller_epitaph.has_value()); + + // Make A2DP offloading fail, resulting in |ZX_ERR_UNAVAILABLE| epitaph + ASSERT_TRUE(fake_channel.is_alive()); + fake_channel->set_a2dp_offload_fails(bt::HostError::kInProgress); + + size_t cb_count = 0; + audio_offload_controller_client->Stop( + [&cb_count](fidlbredr::AudioOffloadController_Stop_Result result) { + cb_count++; + }); + + RunLoopUntilIdle(); + + EXPECT_EQ(cb_count, 0u); + + // Verify that |audio_offload_controller_client| was closed with + // |ZX_ERR_UNAVAILABLE| epitaph + ASSERT_TRUE(audio_offload_controller_epitaph.has_value()); + EXPECT_EQ(audio_offload_controller_epitaph.value(), ZX_ERR_UNAVAILABLE); +} + +TEST_P(AndroidSupportedFeaturesTest, + AudioOffloadControllerStopAfterAlreadyStopped) { + const bool android_vendor_ext_support = GetParam().first; + const uint32_t a2dp_offload_capabilities = GetParam().second; + + if (android_vendor_ext_support) { + adapter()->mutable_state().controller_features |= + FeaturesBits::kAndroidVendorExtensions; + + bt::StaticPacket< + android_emb::LEGetVendorCapabilitiesCommandCompleteEventWriter> + params; + params.SetToZeros(); + params.view().status().Write(pw::bluetooth::emboss::StatusCode::SUCCESS); + params.view().version_supported().major_number().Write(0); + params.view().version_supported().minor_number().Write(98); + params.view() + .a2dp_source_offload_capability_mask() + .BackingStorage() + .UncheckedWriteUInt(a2dp_offload_capabilities); + adapter()->mutable_state().android_vendor_capabilities = + bt::gap::AndroidVendorCapabilities::New(params.view()); + } + + const bt::PeerId peer_id(1); + const fuchsia::bluetooth::PeerId fidl_peer_id{peer_id.value()}; + + // Set L2CAP channel parameters + fidlbredr::L2capParameters l2cap_params; + fidlbredr::ConnectParameters conn_params; + l2cap_params.set_psm(fidlbredr::PSM_AVDTP); + l2cap_params.set_parameters(fbt::ChannelParameters()); + conn_params.set_l2cap(std::move(l2cap_params)); + + std::optional response_channel; + client()->Connect( + fidl_peer_id, + std::move(conn_params), + [&response_channel](fidlbredr::Profile_Connect_Result result) { + ASSERT_TRUE(result.is_response()); + response_channel = std::move(result.response().channel); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(response_channel.has_value()); + if (!android_vendor_ext_support || !a2dp_offload_capabilities) { + EXPECT_FALSE(response_channel->has_ext_audio_offload()); + return; + } + ASSERT_TRUE(response_channel->has_ext_audio_offload()); + + // Set Audio Offload Configuration Values + std::unique_ptr codec = + fidlbredr::AudioOffloadFeatures::New(); + std::unique_ptr codec_value = + fidlbredr::AudioSbcSupport::New(); + codec->set_sbc(std::move(*codec_value)); + + std::unique_ptr encoder_settings = + std::make_unique(); + std::unique_ptr encoder_settings_value = + fuchsia::media::SbcEncoderSettings::New(); + encoder_settings->set_sbc(*encoder_settings_value); + + std::unique_ptr config = + std::make_unique(); + config->set_codec(std::move(*codec)); + config->set_max_latency(10); + config->set_scms_t_enable(true); + config->set_sampling_frequency(fidlbredr::AudioSamplingFrequency::HZ_44100); + config->set_bits_per_sample(fidlbredr::AudioBitsPerSample::BPS_16); + config->set_channel_mode(fidlbredr::AudioChannelMode::MONO); + config->set_encoded_bit_rate(10); + config->set_encoder_settings(std::move(*encoder_settings)); + + fidlbredr::AudioOffloadExtPtr audio_offload_ext_client = + response_channel->mutable_ext_audio_offload()->Bind(); + fidlbredr::AudioOffloadControllerHandle controller_handle; + fidl::InterfaceRequest controller_request = + controller_handle.NewRequest(); + audio_offload_ext_client->StartAudioOffload(std::move(*config), + std::move(controller_request)); + + fidlbredr::AudioOffloadControllerPtr audio_offload_controller_client; + audio_offload_controller_client.Bind(std::move(controller_handle)); + + std::optional audio_offload_controller_epitaph; + audio_offload_controller_client.set_error_handler( + [&](zx_status_t status) { audio_offload_controller_epitaph = status; }); + + size_t on_started_count = 0; + audio_offload_controller_client.events().OnStarted = [&]() { + on_started_count++; + }; + + RunLoopUntilIdle(); + + // Verify that OnStarted event was sent successfully + EXPECT_EQ(on_started_count, 1u); + + // Verify that |audio_offload_controller_client| was not closed with an + // epitaph + ASSERT_FALSE(audio_offload_controller_epitaph.has_value()); + + bool stop_callback_called = false; + auto callback = [&stop_callback_called]( + fidlbredr::AudioOffloadController_Stop_Result result) { + stop_callback_called = true; + }; + + audio_offload_controller_client->Stop(std::move(callback)); + + RunLoopUntilIdle(); + + // Verify that audio offload stopped successfully + ASSERT_TRUE(stop_callback_called); + + size_t cb_count = 0; + audio_offload_controller_client->Stop( + [&cb_count](fidlbredr::AudioOffloadController_Stop_Result result) { + cb_count++; + }); + + RunLoopUntilIdle(); + + // Verify that stopping audio offload has no effect when it's already stopped + EXPECT_EQ(cb_count, 1u); +} + +TEST_P(AndroidSupportedFeaturesTest, + AudioOffloadControllerUnbindStopsAudioOffload) { + FakeChannel::WeakPtr fake_channel; + adapter()->fake_bredr()->set_l2cap_channel_callback( + [&](FakeChannel::WeakPtr chan) { fake_channel = std::move(chan); }); + + const bool android_vendor_ext_support = GetParam().first; + const uint32_t a2dp_offload_capabilities = GetParam().second; + + if (android_vendor_ext_support) { + adapter()->mutable_state().controller_features |= + FeaturesBits::kAndroidVendorExtensions; + + bt::StaticPacket< + android_emb::LEGetVendorCapabilitiesCommandCompleteEventWriter> + params; + params.SetToZeros(); + params.view().status().Write(pw::bluetooth::emboss::StatusCode::SUCCESS); + params.view().version_supported().major_number().Write(0); + params.view().version_supported().minor_number().Write(98); + params.view() + .a2dp_source_offload_capability_mask() + .BackingStorage() + .UncheckedWriteUInt(a2dp_offload_capabilities); + adapter()->mutable_state().android_vendor_capabilities = + bt::gap::AndroidVendorCapabilities::New(params.view()); + } + + const bt::PeerId peer_id(1); + const fuchsia::bluetooth::PeerId fidl_peer_id{peer_id.value()}; + + // Set L2CAP channel parameters + fidlbredr::L2capParameters l2cap_params; + fidlbredr::ConnectParameters conn_params; + l2cap_params.set_psm(fidlbredr::PSM_AVDTP); + l2cap_params.set_parameters(fbt::ChannelParameters()); + conn_params.set_l2cap(std::move(l2cap_params)); + + std::optional response_channel; + client()->Connect( + fidl_peer_id, + std::move(conn_params), + [&response_channel](fidlbredr::Profile_Connect_Result result) { + ASSERT_TRUE(result.is_response()); + response_channel = std::move(result.response().channel); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(response_channel.has_value()); + if (!android_vendor_ext_support || !a2dp_offload_capabilities) { + EXPECT_FALSE(response_channel->has_ext_audio_offload()); + return; + } + ASSERT_TRUE(response_channel->has_ext_audio_offload()); + + // Set Audio Offload Configuration Values + std::unique_ptr codec = + fidlbredr::AudioOffloadFeatures::New(); + std::unique_ptr codec_value = + fidlbredr::AudioSbcSupport::New(); + codec->set_sbc(std::move(*codec_value)); + + std::unique_ptr encoder_settings = + std::make_unique(); + std::unique_ptr encoder_settings_value = + fuchsia::media::SbcEncoderSettings::New(); + encoder_settings->set_sbc(*encoder_settings_value); + + std::unique_ptr config = + std::make_unique(); + config->set_codec(std::move(*codec)); + config->set_max_latency(10); + config->set_scms_t_enable(true); + config->set_sampling_frequency(fidlbredr::AudioSamplingFrequency::HZ_44100); + config->set_bits_per_sample(fidlbredr::AudioBitsPerSample::BPS_16); + config->set_channel_mode(fidlbredr::AudioChannelMode::MONO); + config->set_encoded_bit_rate(10); + config->set_encoder_settings(std::move(*encoder_settings)); + + fidlbredr::AudioOffloadExtPtr audio_offload_ext_client = + response_channel->mutable_ext_audio_offload()->Bind(); + fidlbredr::AudioOffloadControllerHandle controller_handle; + fidl::InterfaceRequest controller_request = + controller_handle.NewRequest(); + audio_offload_ext_client->StartAudioOffload(std::move(*config), + std::move(controller_request)); + + fidlbredr::AudioOffloadControllerPtr audio_offload_controller_client; + audio_offload_controller_client.Bind(std::move(controller_handle)); + + std::optional audio_offload_controller_epitaph; + audio_offload_controller_client.set_error_handler( + [&](zx_status_t status) { audio_offload_controller_epitaph = status; }); + + size_t on_started_count = 0; + audio_offload_controller_client.events().OnStarted = [&]() { + on_started_count++; + }; + + RunLoopUntilIdle(); + + // Verify that OnStarted event was sent successfully + EXPECT_EQ(on_started_count, 1u); + + // Verify that |audio_offload_controller_client| was not closed with an + // epitaph + ASSERT_FALSE(audio_offload_controller_epitaph.has_value()); + + bt::l2cap::A2dpOffloadStatus a2dp_offload_status = + fake_channel->a2dp_offload_status(); + + // Verify that |a2dp_offload_status| is set to started + ASSERT_EQ(bt::l2cap::A2dpOffloadStatus::kStarted, a2dp_offload_status); + + audio_offload_controller_client.Unbind(); + + RunLoopUntilIdle(); + + // Verify that client is unbound from fidl channel + ASSERT_FALSE(audio_offload_controller_client.is_bound()); + + a2dp_offload_status = fake_channel->a2dp_offload_status(); + + // Verify that |a2dp_offload_status| is set to stopped + ASSERT_EQ(bt::l2cap::A2dpOffloadStatus::kStopped, a2dp_offload_status); +} + +const std::vector> kVendorCapabilitiesParams = { + {{true, static_cast(android_emb::A2dpCodecType::SBC)}, + {true, static_cast(android_emb::A2dpCodecType::AAC)}, + {true, + static_cast(android_emb::A2dpCodecType::SBC) | + static_cast(android_emb::A2dpCodecType::AAC)}, + {true, 0}, + {false, 0}}}; +INSTANTIATE_TEST_SUITE_P(ProfileServerTestFakeAdapter, + AndroidSupportedFeaturesTest, + ::testing::ValuesIn(kVendorCapabilitiesParams)); + +TEST_F(ProfileServerTestFakeAdapter, ServiceFoundRelayedToFidlClient) { + fidlbredr::SearchResultsHandle search_results_handle; + FakeSearchResults search_results(search_results_handle.NewRequest(), + dispatcher()); + + fidlbredr::ServiceClassProfileIdentifier search_uuid = + fidlbredr::ServiceClassProfileIdentifier::AUDIO_SINK; + + EXPECT_EQ(adapter()->fake_bredr()->registered_searches().size(), 0u); + EXPECT_EQ(search_results.service_found_count(), 0u); + + // FIDL client registers a service search. + fidlbredr::ProfileSearchRequest request; + request.set_service_uuid(search_uuid); + request.set_attr_ids({}); + request.set_results(std::move(search_results_handle)); + client()->Search(std::move(request)); + RunLoopUntilIdle(); + + EXPECT_EQ(adapter()->fake_bredr()->registered_searches().size(), 1u); + + // Trigger a match on the service search with some data. Should be received by + // the FIDL client. + bt::PeerId peer_id = bt::PeerId{10}; + bt::UUID uuid(static_cast(search_uuid)); + + bt::sdp::AttributeId attr_id = 50; // Random Attribute ID + bt::sdp::DataElement elem = bt::sdp::DataElement(); + elem.SetUrl("https://foobar.dev"); // Random URL + auto attributes = std::map(); + attributes.emplace(attr_id, std::move(elem)); + adapter()->fake_bredr()->TriggerServiceFound( + peer_id, uuid, std::move(attributes)); + + RunLoopUntilIdle(); + + EXPECT_EQ(search_results.service_found_count(), 1u); + EXPECT_EQ(search_results.peer_id().value().value, peer_id.value()); + EXPECT_EQ(search_results.attributes().value().size(), 1u); + EXPECT_EQ(search_results.attributes().value()[0].id(), attr_id); + EXPECT_EQ(search_results.attributes().value()[0].element().url(), + std::string("https://foobar.dev")); +} + +TEST_F(ProfileServerTestFakeAdapter, SearchWithMissingServiceUuidFails) { + fidlbredr::SearchResultsHandle search_results_handle; + FakeSearchResults search_results(search_results_handle.NewRequest(), + dispatcher()); + + // service_uuid is not set + fidlbredr::ProfileSearchRequest request; + request.set_results(std::move(search_results_handle)); + client()->Search(std::move(request)); + RunLoopUntilIdle(); + EXPECT_EQ(adapter()->fake_bredr()->registered_searches().size(), 0u); + EXPECT_TRUE(search_results.closed()); +} + +TEST_F(ProfileServerTestFakeAdapter, SearchWithMissingResultsClientFails) { + // results is not set + fidlbredr::ProfileSearchRequest request; + request.set_service_uuid( + fidlbredr::ServiceClassProfileIdentifier::AUDIO_SINK); + client()->Search(std::move(request)); + RunLoopUntilIdle(); + EXPECT_EQ(adapter()->fake_bredr()->registered_searches().size(), 0u); +} + +TEST_F(ProfileServerTestFakeAdapter, SearchWithMissingAttrIdsSucceeds) { + fidlbredr::SearchResultsHandle search_results_handle; + FakeSearchResults search_results(search_results_handle.NewRequest(), + dispatcher()); + + fidlbredr::ProfileSearchRequest request; + request.set_service_uuid( + fidlbredr::ServiceClassProfileIdentifier::AUDIO_SINK); + request.set_results(std::move(search_results_handle)); + client()->Search(std::move(request)); + RunLoopUntilIdle(); + EXPECT_EQ(adapter()->fake_bredr()->registered_searches().size(), 1u); +} + +TEST_F(ProfileServerTestScoConnected, ScoConnectionRead2Packets) { + // Queue a read request before the packet is received. + std::optional packet_status; + std::optional> packet; + sco_connection()->Read( + [&](::fuchsia::bluetooth::bredr::ScoConnection_Read_Result result) { + ASSERT_TRUE(result.is_response()); + packet_status = result.response().status_flag(); + packet = std::move(*result.response().mutable_data()); + }); + RunLoopUntilIdle(); + EXPECT_FALSE(packet_status); + EXPECT_FALSE(packet); + + bt::StaticByteBuffer packet_buffer_0( + bt::LowerBits(sco_handle()), + bt::UpperBits(sco_handle()) | + 0x30, // handle + packet status flag: kDataPartiallyLost + 0x01, // payload length + 0x00 // payload + ); + bt::BufferView packet_buffer_0_payload = + packet_buffer_0.view(sizeof(bt::hci_spec::SynchronousDataHeader)); + test_device()->SendScoDataChannelPacket(packet_buffer_0); + RunLoopUntilIdle(); + ASSERT_TRUE(packet_status); + EXPECT_EQ(packet_status.value(), + fidlbredr::RxPacketStatus::DATA_PARTIALLY_LOST); + ASSERT_TRUE(packet); + EXPECT_THAT(packet.value(), + ::testing::ElementsAreArray(packet_buffer_0_payload)); + packet_status.reset(); + packet.reset(); + + // Receive a second packet. This time, receive the packet before Read() is + // called. + bt::StaticByteBuffer packet_buffer_1( + bt::LowerBits(sco_handle()), + bt::UpperBits( + sco_handle()), // handle + packet status flag: kCorrectlyReceived + 0x01, // payload length + 0x01 // payload + ); + bt::BufferView packet_buffer_1_payload = + packet_buffer_1.view(sizeof(bt::hci_spec::SynchronousDataHeader)); + test_device()->SendScoDataChannelPacket(packet_buffer_1); + RunLoopUntilIdle(); + + sco_connection()->Read( + [&](::fuchsia::bluetooth::bredr::ScoConnection_Read_Result result) { + ASSERT_TRUE(result.is_response()); + packet_status = result.response().status_flag(); + packet = std::move(*result.response().mutable_data()); + }); + RunLoopUntilIdle(); + ASSERT_TRUE(packet_status); + EXPECT_EQ(packet_status.value(), + fidlbredr::RxPacketStatus::CORRECTLY_RECEIVED_DATA); + ASSERT_TRUE(packet); + EXPECT_THAT(packet.value(), + ::testing::ElementsAreArray(packet_buffer_1_payload)); +} + +TEST_F(ProfileServerTestScoConnected, + ScoConnectionReadWhileReadPendingClosesConnection) { + std::optional packet_status_0; + std::optional> packet_0; + sco_connection()->Read( + [&](::fuchsia::bluetooth::bredr::ScoConnection_Read_Result result) { + ASSERT_TRUE(result.is_response()); + packet_status_0 = result.response().status_flag(); + packet_0 = std::move(*result.response().mutable_data()); + }); + + RunLoopUntilIdle(); + EXPECT_FALSE(packet_status_0); + EXPECT_FALSE(packet_0); + + std::optional packet_status_1; + std::optional> packet_1; + sco_connection()->Read( + [&](::fuchsia::bluetooth::bredr::ScoConnection_Read_Result result) { + ASSERT_TRUE(result.is_response()); + packet_status_1 = result.response().status_flag(); + packet_1 = std::move(*result.response().mutable_data()); + }); + + RunLoopUntilIdle(); + EXPECT_FALSE(packet_status_0); + EXPECT_FALSE(packet_0); + EXPECT_FALSE(packet_status_1); + EXPECT_FALSE(packet_1); + EXPECT_FALSE(sco_connection()); + ASSERT_TRUE(sco_conn_error()); + EXPECT_EQ(sco_conn_error().value(), ZX_ERR_BAD_STATE); +} + +TEST_F(ProfileServerTestOffloadedScoConnected, ScoConnectionReadFails) { + sco_connection()->Read( + [&](::fuchsia::bluetooth::bredr::ScoConnection_Read_Result result) { + FAIL(); + }); + RunLoopUntilIdle(); + EXPECT_FALSE(sco_connection()); + ASSERT_TRUE(sco_conn_error()); + EXPECT_EQ(sco_conn_error().value(), ZX_ERR_IO_NOT_PRESENT); +} + +TEST_F(ProfileServerTestScoConnected, ScoConnectionWriteTwice) { + bt::StaticByteBuffer payload_0(0x00); + bt::DynamicByteBuffer packet_buffer_0 = bt::testing::ScoDataPacket( + sco_handle(), + bt::hci_spec::SynchronousDataPacketStatusFlag::kCorrectlyReceived, + payload_0.view()); + + bt::StaticByteBuffer payload_1(0x01); + bt::DynamicByteBuffer packet_buffer_1 = bt::testing::ScoDataPacket( + sco_handle(), + bt::hci_spec::SynchronousDataPacketStatusFlag::kCorrectlyReceived, + payload_1.view()); + + int sco_cb_count = 0; + test_device()->SetScoDataCallback([&](const bt::ByteBuffer& buffer) { + if (sco_cb_count == 0) { + EXPECT_THAT(buffer, ::testing::ElementsAreArray(packet_buffer_0)); + } else if (sco_cb_count == 1) { + EXPECT_THAT(buffer, ::testing::ElementsAreArray(packet_buffer_1)); + } else { + ADD_FAILURE() << "Unexpected packet sent"; + } + sco_cb_count++; + }); + int write_cb_0_count = 0; + fidlbredr::ScoConnectionWriteRequest request_0; + request_0.set_data(payload_0.ToVector()); + sco_connection()->Write(std::move(request_0), + [&](fidlbredr::ScoConnection_Write_Result result) { + ASSERT_TRUE(result.is_response()); + write_cb_0_count++; + }); + RunLoopUntilIdle(); + EXPECT_EQ(sco_cb_count, 1); + EXPECT_EQ(write_cb_0_count, 1); + + int write_cb_1_count = 0; + fidlbredr::ScoConnectionWriteRequest request_1; + request_1.set_data(payload_1.ToVector()); + sco_connection()->Write(std::move(request_1), + [&](fidlbredr::ScoConnection_Write_Result result) { + ASSERT_TRUE(result.is_response()); + write_cb_1_count++; + }); + RunLoopUntilIdle(); + EXPECT_EQ(sco_cb_count, 2); + EXPECT_EQ(write_cb_1_count, 1); + + test_device()->ClearScoDataCallback(); +} + +TEST_F(ProfileServerTestScoConnected, ScoConnectionWriteMissingDataField) { + int write_cb_count = 0; + // The `data` field is not set. + fidlbredr::ScoConnectionWriteRequest request; + sco_connection()->Write( + std::move(request), + [&](fidlbredr::ScoConnection_Write_Result result) { write_cb_count++; }); + RunLoopUntilIdle(); + EXPECT_EQ(write_cb_count, 0); + EXPECT_FALSE(sco_connection()); + ASSERT_TRUE(sco_conn_error()); + EXPECT_EQ(sco_conn_error().value(), ZX_ERR_INVALID_ARGS); +} + +TEST_F(ProfileServerTestOffloadedScoConnected, ScoConnectionWriteFails) { + int write_cb_count = 0; + fidlbredr::ScoConnectionWriteRequest request; + request.set_data({0x00}); + sco_connection()->Write( + std::move(request), + [&](fidlbredr::ScoConnection_Write_Result result) { write_cb_count++; }); + RunLoopUntilIdle(); + EXPECT_EQ(write_cb_count, 0); + EXPECT_FALSE(sco_connection()); + ASSERT_TRUE(sco_conn_error()); + EXPECT_EQ(sco_conn_error().value(), ZX_ERR_IO_NOT_PRESENT); +} + +} // namespace +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/adapter_test_fixture.h b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/adapter_test_fixture.h new file mode 100644 index 0000000000..42fabf7311 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/adapter_test_fixture.h @@ -0,0 +1,68 @@ +// Copyright 2024 The Pigweed Authors +// +// 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. + +#pragma once + +#include + +#include "pw_bluetooth_sapphire/internal/host/common/macros.h" +#include "pw_bluetooth_sapphire/internal/host/gap/adapter.h" +#include "pw_bluetooth_sapphire/internal/host/gatt/fake_layer.h" +#include "pw_bluetooth_sapphire/internal/host/l2cap/fake_l2cap.h" +#include "pw_bluetooth_sapphire/internal/host/testing/controller_test.h" +#include "pw_bluetooth_sapphire/internal/host/testing/fake_controller.h" +#include "pw_bluetooth_sapphire/internal/host/testing/loop_fixture.h" + +namespace bthost::testing { + +// This test fixture provides an instance of the Bluetooth stack with mock data +// plane (L2CAP) and GATT test doubles. The fixture is backed by a +// FakeController and an event loop which can be used to test interactions with +// the Bluetooth controller. +class AdapterTestFixture + : public bt::testing::TestLoopFixture, + public bt::testing::ControllerTest { + public: + AdapterTestFixture() + : bt::testing::ControllerTest( + pw_dispatcher_), + pw_dispatcher_(dispatcher()) {} + ~AdapterTestFixture() override = default; + + pw::async::Dispatcher& pw_dispatcher() { return pw_dispatcher_; } + + protected: + void SetUp() override; + void SetUp(bt::testing::FakeController::Settings settings, + pw::bluetooth::Controller::FeaturesBits features = + pw::bluetooth::Controller::FeaturesBits{0}); + void TearDown() override; + + bt::gap::Adapter::WeakPtr adapter() const { return adapter_->AsWeakPtr(); } + bt::gatt::testing::FakeLayer* gatt() const { return gatt_.get(); } + std::unique_ptr take_gatt() { + return std::move(gatt_); + } + bt::l2cap::testing::FakeL2cap* l2cap() const { return l2cap_; } + + private: + pw::async_fuchsia::FuchsiaDispatcher pw_dispatcher_; + std::unique_ptr adapter_; + bt::l2cap::testing::FakeL2cap* l2cap_; + std::unique_ptr gatt_; + + BT_DISALLOW_COPY_ASSIGN_AND_MOVE(AdapterTestFixture); +}; + +} // namespace bthost::testing diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/bredr_connection_server.h b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/bredr_connection_server.h new file mode 100644 index 0000000000..5965d58ce8 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/bredr_connection_server.h @@ -0,0 +1,102 @@ +// Copyright 2024 The Pigweed Authors +// +// 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. + +#pragma once + +#include + +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/server_base.h" +#include "pw_bluetooth_sapphire/internal/host/common/byte_buffer.h" +#include "pw_bluetooth_sapphire/internal/host/common/weak_self.h" + +namespace bthost { + +// BrEdrConnectionServer relays packets and disconnections between the +// Connection FIDL protocol and a corresponding L2CAP channel. +class BrEdrConnectionServer : public ServerBase { + public: + // The number of inbound packets to queue in this class. + static constexpr size_t kDefaultReceiveQueueLimit = 20; + + // `channel` is the Channel that this Connection corresponds to. + // BrEdrConnection server will activate and manage the lifetime of this + // chanel. `closed_callback` will be called when either the Connection + // protocol or the L2CAP channel closes. Returns nullptr on failure (failure + // to activate the Channel). + static std::unique_ptr Create( + fidl::InterfaceRequest request, + bt::l2cap::Channel::WeakPtr channel, + fit::callback closed_callback); + + ~BrEdrConnectionServer() override; + + private: + enum class State { + kActivating, // Default state. + kActivated, + kDeactivating, + kDeactivated, + }; + + BrEdrConnectionServer( + fidl::InterfaceRequest request, + bt::l2cap::Channel::WeakPtr channel, + fit::callback closed_callback); + + // fuchsia::bluetooth::Channel overrides: + void Send(std::vector<::fuchsia::bluetooth::Packet> packets, + SendCallback callback) override; + void Receive(ReceiveCallback callback) override; + void WatchChannelParameters(WatchChannelParametersCallback callback) override; + void handle_unknown_method(uint64_t ordinal, + bool method_has_response) override; + + bool Activate(); + void Deactivate(); + + void OnChannelDataReceived(bt::ByteBufferPtr rx_data); + void OnChannelClosed(); + void OnProtocolClosed(); + void DeactivateAndRequestDestruction(); + void ServiceReceiveQueue(); + + bt::l2cap::Channel::WeakPtr channel_; + + // The maximum number of inbound packets to queue when the FIDL protocol is + // full. + const size_t receive_queue_max_frames_ = kDefaultReceiveQueueLimit; + + // We use a std::deque here to minimize the number dynamic memory allocations + // (cf. std::list, which would require allocation on each SDU). This comes, + // however, at the cost of higher memory usage when the number of SDUs is + // small. (libc++ uses a minimum of 4KB per deque.) + std::deque receive_queue_; + + // Client callback called when either FIDL protocol closes or L2CAP channel + // closes. + fit::callback closed_cb_; + + // Callback for pending Channel::Receive() call. + ReceiveCallback receive_cb_ = nullptr; + + // Pending callback for a WatchChannelParameters call. + std::optional + pending_watch_channel_parameters_; + + State state_ = State::kActivating; + + WeakSelf weak_self_; // Keep last. +}; + +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/fake_adapter_test_fixture.h b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/fake_adapter_test_fixture.h new file mode 100644 index 0000000000..c906bc2b57 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/fake_adapter_test_fixture.h @@ -0,0 +1,48 @@ +// Copyright 2024 The Pigweed Authors +// +// 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. + +#pragma once + +#include "pw_async_fuchsia/dispatcher.h" +#include "pw_bluetooth_sapphire/internal/host/common/macros.h" +#include "pw_bluetooth_sapphire/internal/host/gap/fake_adapter.h" +#include "pw_bluetooth_sapphire/internal/host/testing/loop_fixture.h" +#include "pw_unit_test/framework.h" + +namespace bt::fidl::testing { + +class FakeAdapterTestFixture : public bt::testing::TestLoopFixture { + public: + FakeAdapterTestFixture() = default; + ~FakeAdapterTestFixture() override = default; + + void SetUp() override { + adapter_ = std::make_unique(pw_dispatcher()); + } + + void TearDown() override { adapter_ = nullptr; } + + pw::async::Dispatcher& pw_dispatcher() { return dispatcher_; } + + protected: + bt::gap::testing::FakeAdapter* adapter() const { return adapter_.get(); } + + private: + std::unique_ptr adapter_; + pw::async_fuchsia::FuchsiaDispatcher dispatcher_{dispatcher()}; + + BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(FakeAdapterTestFixture); +}; + +} // namespace bt::fidl::testing diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/fake_gatt_fixture.h b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/fake_gatt_fixture.h new file mode 100644 index 0000000000..2f128a9aa8 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/fake_gatt_fixture.h @@ -0,0 +1,62 @@ +// Copyright 2024 The Pigweed Authors +// +// 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. + +#pragma once + +#include + +#include "pw_bluetooth_sapphire/internal/host/common/macros.h" +#include "pw_bluetooth_sapphire/internal/host/gatt/fake_layer.h" +#include "pw_bluetooth_sapphire/internal/host/testing/loop_fixture.h" + +namespace bt::fidl::testing { + +// Provides a common GTest harness base class for clients of the GATT layer and +// emulation of ATT behavior. +class FakeGattFixture : public bt::testing::TestLoopFixture { + public: + FakeGattFixture(); + ~FakeGattFixture() override = default; + + void TearDown() override; + + protected: + const bt::gatt::GATT::WeakPtr& gatt() const { + BT_ASSERT_MSG(weak_gatt_.is_alive(), + "fake GATT layer accessed after it was destroyed!"); + return weak_gatt_; + } + + const bt::gatt::testing::FakeLayer::WeakPtr& fake_gatt() const { + BT_ASSERT_MSG(weak_fake_layer_.is_alive(), + "fake GATT layer accessed after it was destroyed!"); + return weak_fake_layer_; + } + + std::unique_ptr TakeGatt() { + return std::move(gatt_); + } + + private: + // Store both an owning and a weak pointer to allow test code to acquire + // ownership of the layer object for dependency injection. + std::unique_ptr gatt_; + const bt::gatt::GATT::WeakPtr weak_gatt_; + const bt::gatt::testing::FakeLayer::WeakPtr weak_fake_layer_; + pw::async_fuchsia::FuchsiaDispatcher pw_dispatcher_{dispatcher()}; + + BT_DISALLOW_COPY_ASSIGN_AND_MOVE(FakeGattFixture); +}; + +} // namespace bt::fidl::testing diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/fake_hci_transport_server.h b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/fake_hci_transport_server.h new file mode 100644 index 0000000000..2d5ff0b1fd --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/fake_hci_transport_server.h @@ -0,0 +1,146 @@ +// Copyright 2024 The Pigweed Authors +// +// 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. + +#pragma once + +#include +#include + +#include "pw_bluetooth_sapphire/internal/host/common/byte_buffer.h" +#include "pw_bluetooth_sapphire/internal/host/iso/iso_common.h" + +namespace bt::fidl::testing { + +class FakeHciTransportServer final + : public ::fidl::Server { + public: + FakeHciTransportServer( + ::fidl::ServerEnd server_end, + async_dispatcher_t* dispatcher); + + void Unbind() { + binding_.Unbind(); + bound_ = false; + } + + bool bound() const { return bound_; } + + zx_status_t SendEvent(const BufferView& event); + zx_status_t SendAcl(const BufferView& buffer); + zx_status_t SendSco(const BufferView& buffer); + zx_status_t SendIso(const BufferView& buffer); + + // Returns true if the SCO server was successfully unbound. + bool UnbindSco(); + + size_t acks_received() const { return ack_receive_count_; } + size_t sco_acks_received() const { return sco_ack_receive_count_; } + + const std::vector& commands_received() const { + return commands_received_; + } + const std::vector& acl_packets_received() const { + return acl_packets_received_; + } + const std::vector& sco_packets_received() const { + return sco_packets_received_; + } + const std::vector& iso_packets_received() const { + return iso_packets_received_; + } + + // Use custom |ConfigureScoTestCallback| to manually verify configuration + // fields from tests + using ConfigureScoTestCallback = + fit::function; + void set_check_configure_sco(ConfigureScoTestCallback callback) { + check_configure_sco_ = std::move(callback); + } + + // Uee custom |ResetScoTestCallback| to manually perform reset actions from + // tests + using ResetScoTestCallback = fit::function; + void set_reset_sco_callback(ResetScoTestCallback callback) { + reset_sco_cb_ = std::move(callback); + } + + private: + class ScoConnectionServer + : public ::fidl::Server { + public: + ScoConnectionServer( + ::fidl::ServerEnd server_end, + async_dispatcher_t* dispatcher, + FakeHciTransportServer* hci_server); + + zx_status_t Send(const BufferView& buffer); + + void Unbind(); + + private: + // Server overrides: + void Send(SendRequest& request, SendCompleter::Sync& completer) override; + void AckReceive(AckReceiveCompleter::Sync& completer) override; + void Stop(StopCompleter::Sync& completer) override; + void handle_unknown_method( + ::fidl::UnknownMethodMetadata + metadata, + ::fidl::UnknownMethodCompleter::Sync& completer) override; + + void OnUnbound(::fidl::UnbindInfo info, + ::fidl::ServerEnd + server_end); + + FakeHciTransportServer* hci_server_; + ::fidl::ServerBindingRef + binding_; + }; + + // Server overrides: + void Send(SendRequest& request, SendCompleter::Sync& completer) override; + void AckReceive(AckReceiveCompleter::Sync& completer) override; + void ConfigureSco(ConfigureScoRequest& request, + ConfigureScoCompleter::Sync& completer) override; + void handle_unknown_method( + ::fidl::UnknownMethodMetadata + metadata, + ::fidl::UnknownMethodCompleter::Sync& completer) override; + + void OnUnbound( + ::fidl::UnbindInfo info, + ::fidl::ServerEnd server_end); + + std::vector commands_received_; + + std::vector acl_packets_received_; + + std::vector sco_packets_received_; + ConfigureScoTestCallback check_configure_sco_; + ResetScoTestCallback reset_sco_cb_; + + std::vector iso_packets_received_; + + std::optional sco_server_; + + size_t ack_receive_count_ = 0u; + size_t sco_ack_receive_count_ = 0u; + + async_dispatcher_t* dispatcher_; + bool bound_ = true; + ::fidl::ServerBindingRef binding_; +}; + +} // namespace bt::fidl::testing diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/fake_vendor_server.h b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/fake_vendor_server.h new file mode 100644 index 0000000000..985962443d --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/fake_vendor_server.h @@ -0,0 +1,111 @@ +// Copyright 2024 The Pigweed Authors +// +// 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. + +#pragma once + +#include + +#include + +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/fake_hci_transport_server.h" + +namespace bt::fidl::testing { + +class FakeVendorServer final + : public ::fidl::Server { + public: + FakeVendorServer( + ::fidl::ServerEnd server_end, + async_dispatcher_t* dispatcher) + : binding_(::fidl::BindServer(dispatcher, std::move(server_end), this)), + dispatcher_(dispatcher) {} + + void Unbind() { binding_.Unbind(); } + + fidl::testing::FakeHciTransportServer* hci_server() { + return &fake_hci_server_.value(); + } + + void set_open_hci_error(bool val) { open_hci_error_ = val; } + + private: + void GetFeatures(GetFeaturesCompleter::Sync& completer) override { + fuchsia_hardware_bluetooth::VendorFeatures features; + features.acl_priority_command(true); + completer.Reply(features); + } + + void EncodeCommand(EncodeCommandRequest& request, + EncodeCommandCompleter::Sync& completer) override { + BT_ASSERT(request.set_acl_priority()->priority().has_value()); + BT_ASSERT(request.set_acl_priority()->direction().has_value()); + std::vector tmp{static_cast( + WhichSetAclPriority(request.set_acl_priority()->priority().value(), + request.set_acl_priority()->direction().value()))}; + completer.Reply(fit::success(tmp)); + } + + // Not supported + void OpenHci(OpenHciCompleter::Sync& completer) override { + BT_PANIC("OpenHci not supported"); + } + + void OpenSnoop(OpenSnoopCompleter::Sync& completer) override { + BT_PANIC("OpenSnoop not supported"); + } + + void OpenHciTransport(OpenHciTransportCompleter::Sync& completer) override { + if (open_hci_error_) { + completer.Reply(fit::error(ZX_ERR_INTERNAL)); + return; + } + + auto [hci_client_end, hci_server_end] = + ::fidl::Endpoints::Create(); + + fake_hci_server_.emplace(std::move(hci_server_end), dispatcher_); + completer.Reply(fit::success(std::move(hci_client_end))); + } + + void handle_unknown_method( + ::fidl::UnknownMethodMetadata + metadata, + ::fidl::UnknownMethodCompleter::Sync& completer) override { + // Not implemented + } + + uint8_t WhichSetAclPriority( + fuchsia_hardware_bluetooth::VendorAclPriority priority, + fuchsia_hardware_bluetooth::VendorAclDirection direction) { + if (priority == fuchsia_hardware_bluetooth::VendorAclPriority::kHigh) { + if (direction == + fuchsia_hardware_bluetooth::VendorAclDirection::kSource) { + return static_cast(pw::bluetooth::AclPriority::kSource); + } + return static_cast(pw::bluetooth::AclPriority::kSink); + } + return static_cast(pw::bluetooth::AclPriority::kNormal); + } + + // Flag for testing. |OpenHci()| returns an error when set to true + bool open_hci_error_ = false; + + std::optional fake_hci_server_; + + ::fidl::ServerBindingRef binding_; + + async_dispatcher_t* dispatcher_; +}; + +} // namespace bt::fidl::testing diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_client_server.h b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_client_server.h new file mode 100644 index 0000000000..8c32592eff --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_client_server.h @@ -0,0 +1,98 @@ +// Copyright 2024 The Pigweed Authors +// +// 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. + +#pragma once + +#include +#include + +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_remote_service_server.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/server_base.h" +#include "pw_bluetooth_sapphire/internal/host/gatt/gatt.h" + +namespace bthost { +class Gatt2ClientServer + : public GattServerBase { + public: + // |error_cb| will be called if the FIDL client closed the protocol or an + // error occurs and this server should be destroyed. + Gatt2ClientServer( + bt::gatt::PeerId peer_id, + bt::gatt::GATT::WeakPtr weak_gatt, + fidl::InterfaceRequest request, + fit::callback error_cb); + ~Gatt2ClientServer() override; + + private: + using WatchServicesCallbackOnce = + fit::callback, + std::vector<::fuchsia::bluetooth::gatt2::Handle>)>; + using WatchServicesRequest = WatchServicesCallbackOnce; + + using ServiceMap = + std::unordered_map; + + struct WatchServicesResult { + std::unordered_set removed; + ServiceMap updated; + }; + + void OnWatchServicesResult(const std::vector& removed, + const bt::gatt::ServiceList& added, + const bt::gatt::ServiceList& modified); + + void TrySendNextWatchServicesResult(); + + // fuchsia::bluetooth::gatt2::Client overrides: + void WatchServices(std::vector<::fuchsia::bluetooth::Uuid> fidl_uuids, + WatchServicesCallback callback) override; + void ConnectToService( + fuchsia::bluetooth::gatt2::ServiceHandle handle, + fidl::InterfaceRequest request) + override; + + // The ID of the peer that this client is attached to. + bt::gatt::PeerId peer_id_; + + // Callback provided by this server's owner that handles fatal errors (by + // closing this server). + fit::callback server_error_cb_; + + // If a service's handle maps to a null value, a connection request to that + // service is in progress. + // TODO(fxbug.dev/42165614): Once FindService() returns the service directly, + // don't use null values. + std::unordered_map> + services_; + + // False initially, and set to true after GATT::ListServices() completes. + // Set to false again if WatchServices() is called with a new UUID list. + bool list_services_complete_ = false; + + // UUIDs of the previous WatchServices() call, if any. + std::unordered_set prev_watch_services_uuids_; + std::optional watch_services_request_; + + // Between clients calls to WatchServices, service watcher results are + // accumulated here. + std::optional next_watch_services_result_; + + bt::gatt::GATT::RemoteServiceWatcherId service_watcher_id_; + + // Must be the last member of this class. + WeakSelf weak_self_; + + BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(Gatt2ClientServer); +}; +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_remote_service_server.h b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_remote_service_server.h new file mode 100644 index 0000000000..493e97194d --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_remote_service_server.h @@ -0,0 +1,112 @@ +// Copyright 2024 The Pigweed Authors +// +// 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. + +#pragma once + +#include + +#include "lib/fidl/cpp/binding.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/server_base.h" +#include "pw_bluetooth_sapphire/internal/host/common/macros.h" +#include "pw_bluetooth_sapphire/internal/host/gatt/gatt.h" + +namespace bthost { + +class Gatt2RemoteServiceServer + : public GattServerBase { + public: + // The maximum number of pending notification values per + // CharacteristicNotifier (for flow control). If exceeded, the notifier + // protocol is closed. + static const size_t kMaxPendingNotifierValues = 20; + + Gatt2RemoteServiceServer( + bt::gatt::RemoteService::WeakPtr service, + bt::gatt::GATT::WeakPtr gatt, + bt::PeerId peer_id, + fidl::InterfaceRequest request); + ~Gatt2RemoteServiceServer() override; + + void Close(zx_status_t status); + + private: + using NotifierId = uint64_t; + + struct CharacteristicNotifier { + bt::gatt::IdType handler_id; + bt::gatt::CharacteristicHandle characteristic_handle; + fidl::InterfacePtr + notifier; + // For flow control, values are only sent when the client responds to the + // previous value with an acknowledgement. This variable stores the queued + // values. + std::queue queued_values; + // `last_value_ack` defaults to true so that the first notification queued + // up is sent to the FIDL client immediately. + bool last_value_ack = true; + }; + + // fuchsia::bluetooth::gatt2::RemoteService overrides: + void DiscoverCharacteristics( + DiscoverCharacteristicsCallback callback) override; + + void ReadByType(::fuchsia::bluetooth::Uuid uuid, + ReadByTypeCallback callback) override; + + void ReadCharacteristic(::fuchsia::bluetooth::gatt2::Handle handle, + ::fuchsia::bluetooth::gatt2::ReadOptions options, + ReadCharacteristicCallback callback) override; + + void WriteCharacteristic(::fuchsia::bluetooth::gatt2::Handle handle, + ::std::vector value, + ::fuchsia::bluetooth::gatt2::WriteOptions options, + WriteCharacteristicCallback callback) override; + + void ReadDescriptor(::fuchsia::bluetooth::gatt2::Handle handle, + ::fuchsia::bluetooth::gatt2::ReadOptions options, + ReadDescriptorCallback callback) override; + + void WriteDescriptor(::fuchsia::bluetooth::gatt2::Handle handle, + ::std::vector value, + ::fuchsia::bluetooth::gatt2::WriteOptions options, + WriteDescriptorCallback callback) override; + + void RegisterCharacteristicNotifier( + ::fuchsia::bluetooth::gatt2::Handle handle, + ::fidl::InterfaceHandle< + ::fuchsia::bluetooth::gatt2::CharacteristicNotifier> notifier, + RegisterCharacteristicNotifierCallback callback) override; + + // Send the next notifier value in the queue if the client acknowledged the + // previous value. + void MaybeNotifyNextValue(NotifierId notifier_id); + + void OnCharacteristicNotifierError(NotifierId notifier_id, + bt::gatt::CharacteristicHandle char_handle, + bt::gatt::IdType handler_id); + + // The remote GATT service that backs this service. + bt::gatt::RemoteService::WeakPtr service_; + + NotifierId next_notifier_id_ = 0u; + std::unordered_map + characteristic_notifiers_; + + // The peer that is serving this service. + bt::PeerId peer_id_; + + WeakSelf weak_self_; +}; + +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_server_ids.h b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_server_ids.h new file mode 100644 index 0000000000..a636932800 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_server_ids.h @@ -0,0 +1,51 @@ +// Copyright 2024 The Pigweed Authors +// +// 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. + +#pragma once + +#include "pw_bluetooth_sapphire/internal/host/common/identifier.h" + +namespace bthost { +// Separate types to help prevent mixing up the two types of service IDs used in +// gatt2/Server. +class ClientServiceId : public bt::Identifier { + public: + constexpr explicit ClientServiceId(uint64_t value) + : Identifier(value) {} + constexpr ClientServiceId() : ClientServiceId(0u) {} +}; + +class InternalServiceId : public bt::Identifier { + public: + constexpr explicit InternalServiceId(uint64_t value) + : Identifier(value) {} + constexpr InternalServiceId() : InternalServiceId(0u) {} +}; +} // namespace bthost + +namespace std { +template <> +struct hash { + size_t operator()(const bthost::ClientServiceId& id) const { + return std::hash()(id.value()); + } +}; + +template <> +struct hash { + size_t operator()(const bthost::InternalServiceId& id) const { + return std::hash()(id.value()); + } +}; +} // namespace std diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_server_server.h b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_server_server.h new file mode 100644 index 0000000000..67e81ff6b5 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_server_server.h @@ -0,0 +1,128 @@ +// Copyright 2024 The Pigweed Authors +// +// 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. + +#pragma once + +#include + +#include "lib/zx/eventpair.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_server_ids.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/server_base.h" +#include "pw_bluetooth_sapphire/internal/host/common/weak_self.h" +#include "pw_bluetooth_sapphire/internal/host/gatt/gatt.h" + +namespace bthost { + +// Implements the gatt2::Server FIDL interface. +// TODO(fxbug.dev/42054947): Support sending gatt2::LocalService::PeerUpdate. +// TODO(fxbug.dev/42147529): Support GATT service includes. +// TODO(fxbug.dev/42180948): Support OnSuppressDiscovery +class Gatt2ServerServer + : public GattServerBase { + public: + // Arbitrary; we only refresh credits when the peer starts to get low. + // The current implementation does not support a value of 0. + static const uint8_t REFRESH_CREDITS_AT = 3; + + // |gatt| - The GATT instance underlying this Server. + // |request| - The FIDL request. + Gatt2ServerServer( + bt::gatt::GATT::WeakPtr gatt, + fidl::InterfaceRequest request); + + ~Gatt2ServerServer() override; + + private: + struct Service { + // The LocalService FIDL proxy + fidl::InterfacePtr local_svc_ptr; + + // The credits available for this LocalService + int16_t credits = fuchsia::bluetooth::gatt2::INITIAL_VALUE_CHANGED_CREDITS; + }; + + // ::fuchsia::bluetooth::gatt2::Server overrides: + void PublishService( + fuchsia::bluetooth::gatt2::ServiceInfo info, + fidl::InterfaceHandle service, + PublishServiceCallback callback) override; + + // Removes the service with the given |id| if it is known, usually as a result + // of FIDL connection errors (such as handle closure). + void RemoveService(InternalServiceId id); + + // Handles the ::fuchsia::bluetooth:gatt2::Server OnSuppressDiscovery event. + void OnSuppressDiscovery(InternalServiceId service_id); + + // If the update has the required fields and there are credits available, + // subtracts a credit from the service and returns true. Otherwise, returns + // false. + bool ValidateValueChangedEvent( + InternalServiceId service_id, + const fuchsia::bluetooth::gatt2::ValueChangedParameters& update, + const char* update_type); + + // Handles the ::fuchsia::bluetooth:gatt2::Server OnNotifyValue event. + void OnNotifyValue(InternalServiceId service_id, + fuchsia::bluetooth::gatt2::ValueChangedParameters update); + + // Handles the ::fuchsia::bluetooth:gatt2::Server OnSuppressDiscovery event. + void OnIndicateValue(InternalServiceId service_id, + fuchsia::bluetooth::gatt2::ValueChangedParameters update, + zx::eventpair confirmation); + + // Called when a remote device issues a read request to one of our services. + void OnReadRequest(bt::PeerId peer_id, + bt::gatt::IdType service_id, + bt::gatt::IdType id, + uint16_t offset, + bt::gatt::ReadResponder responder); + + // Called when a remote device issues a write request to one of our services. + void OnWriteRequest(bt::PeerId peer_id, + bt::gatt::IdType service_id, + bt::gatt::IdType id, + uint16_t offset, + const bt::ByteBuffer& value, + bt::gatt::WriteResponder responder); + + // Called when a remote device has configured notifications or indications on + // a local characteristic. + void OnClientCharacteristicConfiguration(bt::gatt::IdType service_id, + bt::gatt::IdType chrc_id, + bt::PeerId peer_id, + bool notify, + bool indicate); + + // Subtract one credit from the client, potentially refreshing the credits to + // the client. + static void SubtractCredit(Service& svc); + + // The mapping between internal service identifiers and FIDL Service + // implementations. + std::unordered_map services_; + + // Mapping between client-provided Service IDs and internally-generated IDs. + // TODO(fxbug.dev/42147529): This will be necessary for supporting service + // includes. + std::unordered_map service_id_mapping_; + + // Keep this as the last member to make sure that all weak pointers are + // invalidated before other members get destroyed. + WeakSelf weak_self_; + + BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(Gatt2ServerServer); +}; + +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_client_server.h b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_client_server.h new file mode 100644 index 0000000000..303abcfde0 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_client_server.h @@ -0,0 +1,58 @@ +// Copyright 2024 The Pigweed Authors +// +// 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. + +#pragma once + +#include + +#include "lib/fidl/cpp/binding.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_remote_service_server.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/server_base.h" +#include "pw_bluetooth_sapphire/internal/host/common/macros.h" + +namespace bthost { + +// Implements the gatt::Client FIDL interface. +class GattClientServer + : public GattServerBase { + public: + GattClientServer( + bt::gatt::PeerId peer_id, + bt::gatt::GATT::WeakPtr gatt, + fidl::InterfaceRequest request); + ~GattClientServer() override = default; + + private: + // bluetooth::gatt::Client overrides: + void ListServices(::fidl::VectorPtr<::std::string> uuids, + ListServicesCallback callback) override; + void ConnectToService( + uint64_t id, + ::fidl::InterfaceRequest request) + override; + + // The ID of the peer that this client is attached to. + bt::gatt::PeerId peer_id_; + + // Remote GATT services that were connected through this client. The value can + // be null while a ConnectToService request is in progress. + std::unordered_map> + connected_services_; + + WeakSelf weak_self_; + + BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(GattClientServer); +}; + +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_remote_service_server.h b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_remote_service_server.h new file mode 100644 index 0000000000..9aaef8d829 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_remote_service_server.h @@ -0,0 +1,92 @@ +// Copyright 2024 The Pigweed Authors +// +// 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. + +#pragma once + +#include + +#include + +#include "lib/fidl/cpp/binding.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/server_base.h" +#include "pw_bluetooth_sapphire/internal/host/common/macros.h" +#include "pw_bluetooth_sapphire/internal/host/common/weak_self.h" +#include "pw_bluetooth_sapphire/internal/host/gatt/gatt.h" + +namespace bthost { + +// Implements the gatt::RemoteService FIDL interface. +class GattRemoteServiceServer + : public GattServerBase { + public: + GattRemoteServiceServer( + bt::gatt::RemoteService::WeakPtr service, + bt::gatt::GATT::WeakPtr gatt, + bt::PeerId peer_id, + fidl::InterfaceRequest request); + ~GattRemoteServiceServer() override; + + private: + // fuchsia::bluetooth::gatt::RemoteService overrides: + void DiscoverCharacteristics( + DiscoverCharacteristicsCallback callback) override; + void ReadCharacteristic(uint64_t id, + ReadCharacteristicCallback callback) override; + void ReadLongCharacteristic(uint64_t id, + uint16_t offset, + uint16_t max_bytes, + ReadLongCharacteristicCallback callback) override; + void WriteCharacteristic(uint64_t id, + ::std::vector value, + WriteCharacteristicCallback callback) override; + void WriteLongCharacteristic( + uint64_t id, + uint16_t offset, + ::std::vector value, + fuchsia::bluetooth::gatt::WriteOptions write_options, + WriteCharacteristicCallback callback) override; + void WriteCharacteristicWithoutResponse( + uint64_t id, ::std::vector value) override; + void ReadDescriptor(uint64_t id, ReadDescriptorCallback callback) override; + void ReadLongDescriptor(uint64_t id, + uint16_t offset, + uint16_t max_bytes, + ReadLongDescriptorCallback callback) override; + void WriteDescriptor(uint64_t _id, + ::std::vector value, + WriteDescriptorCallback callback) override; + void WriteLongDescriptor(uint64_t _id, + uint16_t offset, + ::std::vector value, + WriteDescriptorCallback callback) override; + void ReadByType(fuchsia::bluetooth::Uuid uuid, + ReadByTypeCallback callback) override; + void NotifyCharacteristic(uint64_t id, + bool enable, + NotifyCharacteristicCallback callback) override; + + // The remote GATT service that backs this service. + bt::gatt::RemoteService::WeakPtr service_; + + const bt::PeerId peer_id_; + + using HandlerId = bt::gatt::IdType; + std::unordered_map + notify_handlers_; + + WeakSelf weak_self_; + BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(GattRemoteServiceServer); +}; + +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_server_server.h b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_server_server.h new file mode 100644 index 0000000000..d65e7926b4 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_server_server.h @@ -0,0 +1,87 @@ +// Copyright 2024 The Pigweed Authors +// +// 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. + +#pragma once + +#include + +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/server_base.h" +#include "pw_bluetooth_sapphire/internal/host/common/macros.h" +#include "pw_bluetooth_sapphire/internal/host/common/weak_self.h" +#include "pw_bluetooth_sapphire/internal/host/gatt/local_service_manager.h" +#include "pw_bluetooth_sapphire/internal/host/gatt/types.h" + +namespace bthost { + +// Implements the gatt::Server FIDL interface. +class GattServerServer + : public GattServerBase { + public: + // |adapter_manager| is used to lazily request a handle to the corresponding + // adapter. It MUST out-live this GattServerServer instance. + GattServerServer( + bt::gatt::GATT::WeakPtr gatt, + fidl::InterfaceRequest request); + + ~GattServerServer() override; + + // Removes the service with the given |id| if it is known. + // This can be called as a result of FIDL connection errors (such as handle + // closure) or as a result of gatt.Service.RemoveService(). + void RemoveService(uint64_t id); + + private: + class LocalServiceImpl; + + // ::fuchsia::bluetooth::gatt::Server overrides: + void PublishService( + fuchsia::bluetooth::gatt::ServiceInfo service_info, + fidl::InterfaceHandle + delegate, + fidl::InterfaceRequest + service_iface, + PublishServiceCallback callback) override; + + // Called when a remote device issues a read request to one of our services. + void OnReadRequest(bt::gatt::IdType service_id, + bt::gatt::IdType id, + uint16_t offset, + bt::gatt::ReadResponder responder); + + // Called when a remote device issues a write request to one of our services. + void OnWriteRequest(bt::gatt::IdType service_id, + bt::gatt::IdType id, + uint16_t offset, + const bt::ByteBuffer& value, + bt::gatt::WriteResponder responder); + + // Called when a remote device has configured notifications or indications on + // a local characteristic. + void OnCharacteristicConfig(bt::gatt::IdType service_id, + bt::gatt::IdType chrc_id, + bt::gatt::PeerId peer_id, + bool notify, + bool indicate); + + // The mapping between service identifiers and FIDL Service implementations. + std::unordered_map> services_; + + // Keep this as the last member to make sure that all weak pointers are + // invalidated before other members get destroyed. + WeakSelf weak_self_; + + BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(GattServerServer); +}; + +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/helpers.h b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/helpers.h new file mode 100644 index 0000000000..e4dbaf9716 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/helpers.h @@ -0,0 +1,339 @@ +// Copyright 2024 The Pigweed Authors +// +// 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. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "pw_bluetooth_sapphire/internal/host/common/advertising_data.h" +#include "pw_bluetooth_sapphire/internal/host/common/byte_buffer.h" +#include "pw_bluetooth_sapphire/internal/host/common/error.h" +#include "pw_bluetooth_sapphire/internal/host/common/identifier.h" +#include "pw_bluetooth_sapphire/internal/host/common/uuid.h" +#include "pw_bluetooth_sapphire/internal/host/gap/adapter.h" +#include "pw_bluetooth_sapphire/internal/host/gap/gap.h" +#include "pw_bluetooth_sapphire/internal/host/gap/low_energy_advertising_manager.h" +#include "pw_bluetooth_sapphire/internal/host/gap/peer.h" +#include "pw_bluetooth_sapphire/internal/host/gap/types.h" +#include "pw_bluetooth_sapphire/internal/host/gatt/types.h" +#include "pw_bluetooth_sapphire/internal/host/iso/iso_common.h" + +// Helpers for implementing the Bluetooth FIDL interfaces. + +namespace bt::gap { + +class DiscoveryFilter; + +} // namespace bt::gap + +namespace bthost::fidl_helpers { + +namespace android_emb = pw::bluetooth::vendor::android_hci; + +// TODO(fxbug.dev/42171179): Temporary logic for converting between the stack +// identifier type (integer) and FIDL identifier type (string). Remove these +// once all FIDL interfaces have been converted to use integer IDs. +std::optional PeerIdFromString(const std::string& id); + +// Functions for generating a FIDL bluetooth::Status + +fuchsia::bluetooth::ErrorCode HostErrorToFidlDeprecated( + bt::HostError host_error); + +fuchsia::bluetooth::Status NewFidlError( + fuchsia::bluetooth::ErrorCode error_code, const std::string& description); + +template +fuchsia::bluetooth::Status ResultToFidlDeprecated( + const fit::result>& result, + std::string msg = "") { + fuchsia::bluetooth::Status fidl_status; + if (result.is_ok()) { + return fidl_status; + } + + auto error = std::make_unique(); + error->description = msg.empty() ? bt_str(result) : std::move(msg); + if (result.is_error()) { + result.error_value().Visit( + [&error](bt::HostError c) { + error->error_code = HostErrorToFidlDeprecated(c); + }, + [&](ProtocolErrorCode c) { + if constexpr (bt::Error< + ProtocolErrorCode>::may_hold_protocol_error()) { + error->error_code = fuchsia::bluetooth::ErrorCode::PROTOCOL_ERROR; + error->protocol_error_code = static_cast(c); + } else { + BT_PANIC("Protocol branch visited by bt::Error"); + } + }); + } + + fidl_status.error = std::move(error); + return fidl_status; +} + +// Convert a bt::HostError to fuchsia.bluetooth.sys.Error. This function does +// only deals with bt::HostError types and does not support Bluetooth +// protocol-specific errors; to represent such errors use protocol-specific FIDL +// error types. +fuchsia::bluetooth::sys::Error HostErrorToFidl(bt::HostError error); + +// Convert a bt::Error to fuchsia.bluetooth.sys.Error. This function does only +// deals with bt::HostError codes and does not support Bluetooth +// protocol-specific errors; to represent such errors use protocol-specific FIDL +// error types. +template +fuchsia::bluetooth::sys::Error HostErrorToFidl( + const bt::Error& error) { + if (!error.is_host_error()) { + return fuchsia::bluetooth::sys::Error::FAILED; + } + return HostErrorToFidl(error.host_error()); +} + +// Convert any bt::Status to a fpromise::result that uses the +// fuchsia.bluetooth.sys library error codes. +template +fpromise::result ResultToFidl( + const fit::result>& status) { + if (status.is_ok()) { + return fpromise::ok(); + } else { + return fpromise::error(HostErrorToFidl(std::move(status).error_value())); + } +} + +// Convert a bt::att::Error to fuchsia.bluetooth.gatt.Error. +fuchsia::bluetooth::gatt::Error GattErrorToFidl(const bt::att::Error& error); + +// Convert a bt::att::Error to fuchsia.bluetooth.gatt2.Error. +fuchsia::bluetooth::gatt2::Error AttErrorToGattFidlError( + const bt::att::Error& error); + +// Convert a fuchsia::bluetooth::Uuid to bt::UUID for old HLCPP FIDL bindings +bt::UUID UuidFromFidl(const fuchsia::bluetooth::Uuid& input); +// Convert a bt::UUID to fuchsia::bluetooth::Uuid for old HLCPP FIDL bindings +fuchsia::bluetooth::Uuid UuidToFidl(const bt::UUID& uuid); + +// Convert a fuchsia_bluetooth::Uuid to bt::UUID for new C++ FIDL bindings +bt::UUID NewUuidFromFidl(const fuchsia_bluetooth::Uuid& input); + +// Functions that convert FIDL types to library objects. +bt::sm::IOCapability IoCapabilityFromFidl( + const fuchsia::bluetooth::sys::InputCapability, + const fuchsia::bluetooth::sys::OutputCapability); +std::optional BrEdrSecurityModeFromFidl( + const fuchsia::bluetooth::sys::BrEdrSecurityMode mode); +bt::gap::LESecurityMode LeSecurityModeFromFidl( + const fuchsia::bluetooth::sys::LeSecurityMode mode); +std::optional SecurityLevelFromFidl( + const fuchsia::bluetooth::sys::PairingSecurityLevel level); + +// fuchsia.bluetooth.sys library helpers. +fuchsia::bluetooth::sys::TechnologyType TechnologyTypeToFidl( + bt::gap::TechnologyType type); +fuchsia::bluetooth::sys::HostInfo HostInfoToFidl( + const bt::gap::Adapter& adapter); +fuchsia::bluetooth::sys::Peer PeerToFidl(const bt::gap::Peer& peer); + +// Functions to convert bonding data structures from FIDL. +std::optional AddressFromFidlBondingData( + const fuchsia::bluetooth::sys::BondingData& data); +bt::sm::PairingData LePairingDataFromFidl( + bt::DeviceAddress peer_address, + const fuchsia::bluetooth::sys::LeBondData& data); +std::optional BredrKeyFromFidl( + const fuchsia::bluetooth::sys::BredrBondData& data); +std::vector BredrServicesFromFidl( + const fuchsia::bluetooth::sys::BredrBondData& data); + +// Function to construct a bonding data structure for a peer. +fuchsia::bluetooth::sys::BondingData PeerToFidlBondingData( + const bt::gap::Adapter& adapter, const bt::gap::Peer& peer); + +// Functions to construct FIDL LE library objects from library objects. Returns +// nullptr if the peer is not LE or if the peer's advertising data failed to +// parse. +fuchsia::bluetooth::le::RemoteDevicePtr NewLERemoteDevice( + const bt::gap::Peer& peer); + +// Validates the contents of a ScanFilter. +bool IsScanFilterValid(const fuchsia::bluetooth::le::ScanFilter& fidl_filter); + +// Populates a library DiscoveryFilter based on a FIDL ScanFilter. Returns false +// if |fidl_filter| contains any malformed data and leaves |out_filter| +// unmodified. +bool PopulateDiscoveryFilter( + const fuchsia::bluetooth::le::ScanFilter& fidl_filter, + bt::gap::DiscoveryFilter* out_filter); +bt::gap::DiscoveryFilter DiscoveryFilterFromFidl( + const fuchsia::bluetooth::le::Filter& fidl_filter); + +// Converts the given |mode_hint| to a stack interval value. +bt::gap::AdvertisingInterval AdvertisingIntervalFromFidl( + fuchsia::bluetooth::le::AdvertisingModeHint mode_hint); + +std::optional AdvertisingDataFromFidl( + const fuchsia::bluetooth::le::AdvertisingData& input); +fuchsia::bluetooth::le::AdvertisingData AdvertisingDataToFidl( + const bt::AdvertisingData& input); +fuchsia::bluetooth::le::AdvertisingDataDeprecated +AdvertisingDataToFidlDeprecated(const bt::AdvertisingData& input); +fuchsia::bluetooth::le::ScanData AdvertisingDataToFidlScanData( + const bt::AdvertisingData& input, + pw::chrono::SystemClock::time_point timestamp); + +// Constructs a fuchsia.bluetooth.le Peer type from the stack representation. +fuchsia::bluetooth::le::Peer PeerToFidlLe(const bt::gap::Peer& peer); + +// Functions that convert FIDL GATT types to library objects. +bt::gatt::ReliableMode ReliableModeFromFidl( + const fuchsia::bluetooth::gatt::WriteOptions& write_options); +// TODO(fxbug.dev/42141942): The 64 bit `fidl_gatt_id` can overflow the 16 bits +// of a bt:att::Handle that underlies Characteristic/DescriptorHandles when +// directly casted. Fix this. +bt::gatt::CharacteristicHandle CharacteristicHandleFromFidl( + uint64_t fidl_gatt_id); +bt::gatt::DescriptorHandle DescriptorHandleFromFidl(uint64_t fidl_gatt_id); + +// Constructs a sdp::ServiceRecord from a FIDL ServiceDefinition |definition| +fpromise::result +ServiceDefinitionToServiceRecord( + const fuchsia_bluetooth_bredr::ServiceDefinition& definition); + +// Constructs a sdp::ServiceRecord from a FIDL ServiceDefinition |definition| +fpromise::result +ServiceDefinitionToServiceRecord( + const fuchsia::bluetooth::bredr::ServiceDefinition& definition); + +// Constructs a FIDL ServiceDefinition from a sdp::ServiceRecord +fpromise::result +ServiceRecordToServiceDefinition(const bt::sdp::ServiceRecord& record); + +bt::gap::BrEdrSecurityRequirements FidlToBrEdrSecurityRequirements( + const fuchsia::bluetooth::ChannelParameters& fidl); + +fpromise::result> +FidlToScoParameters( + const fuchsia::bluetooth::bredr::ScoConnectionParameters& params); +fpromise::result>> +FidlToScoParametersVector( + const std::vector& + params); + +// Returns true if |handle| is within the valid handle range. +bool IsFidlGattHandleValid(fuchsia::bluetooth::gatt2::Handle handle); +bool IsFidlGattServiceHandleValid( + fuchsia::bluetooth::gatt2::ServiceHandle handle); + +fuchsia::bluetooth::bredr::RxPacketStatus ScoPacketStatusToFidl( + bt::hci_spec::SynchronousDataPacketStatusFlag status); + +bt::att::ErrorCode Gatt2ErrorCodeFromFidl( + fuchsia::bluetooth::gatt2::Error error_code); + +bt::att::AccessRequirements Gatt2AccessRequirementsFromFidl( + const fuchsia::bluetooth::gatt2::SecurityRequirements& reqs); + +void FillInAttributePermissionsDefaults( + fuchsia::bluetooth::gatt2::AttributePermissions& reqs); + +// Returns the bt-host representation of the FIDL descriptor, or nullptr if the +// conversion fails. +std::unique_ptr Gatt2DescriptorFromFidl( + const fuchsia::bluetooth::gatt2::Descriptor& fidl_desc); + +// Returns the bt-host representation of the FIDL characteristc, or nullptr if +// the conversion fails. +std::unique_ptr Gatt2CharacteristicFromFidl( + const fuchsia::bluetooth::gatt2::Characteristic& fidl_chrc); + +std::optional FidlToCodecType( + const fuchsia::bluetooth::bredr::AudioOffloadFeatures& codec); + +bt::StaticPacket FidlToScmsTEnable( + bool scms_t_enable); + +std::optional FidlToSamplingFrequency( + fuchsia::bluetooth::bredr::AudioSamplingFrequency sampling_frequency); + +std::optional FidlToBitsPerSample( + fuchsia::bluetooth::bredr::AudioBitsPerSample bits_per_sample); + +std::optional FidlToChannelMode( + fuchsia::bluetooth::bredr::AudioChannelMode channel_mode); + +bt::StaticPacket +FidlToEncoderSettingsSbc( + const fuchsia::bluetooth::bredr::AudioEncoderSettings& encoder_settings, + fuchsia::bluetooth::bredr::AudioSamplingFrequency sampling_frequency, + fuchsia::bluetooth::bredr::AudioChannelMode channel_mode); + +bt::StaticPacket +FidlToEncoderSettingsAac( + const fuchsia::bluetooth::bredr::AudioEncoderSettings& encoder_settings, + fuchsia::bluetooth::bredr::AudioSamplingFrequency sampling_frequency, + fuchsia::bluetooth::bredr::AudioChannelMode channel_mode); + +// For old HLCPP FIDL bindings +std::optional FidlToDataElement( + const fuchsia::bluetooth::bredr::DataElement& fidl); +// For new C++ FIDL bindings +std::optional NewFidlToDataElement( + const fuchsia_bluetooth_bredr::DataElement& fidl); + +const char* DataPathDirectionToString( + pw::bluetooth::emboss::DataPathDirection direction); + +pw::bluetooth::emboss::DataPathDirection DataPathDirectionFromFidl( + const fuchsia::bluetooth::DataDirection& fidl_direction); + +pw::bluetooth::emboss::CodingFormat CodingFormatFromFidl( + const fuchsia::bluetooth::AssignedCodingFormat& fidl_format); + +bt::StaticPacket CodecIdFromFidl( + const fuchsia::bluetooth::CodecId& fidl_codec_id); + +pw::bluetooth::emboss::LogicalTransportType LogicalTransportTypeFromFidl( + const fuchsia::bluetooth::LogicalTransportType& fidl_transport_type); + +pw::bluetooth::emboss::StatusCode FidlHciErrorToStatusCode( + fuchsia_hardware_bluetooth::HciError code); + +fuchsia::bluetooth::le::CisEstablishedParameters CisEstablishedParametersToFidl( + const bt::iso::CisEstablishedParameters& params_in); + +} // namespace bthost::fidl_helpers + +// fidl::TypeConverter specializations for ByteBuffer and friends. +template <> +struct fidl::TypeConverter, bt::ByteBuffer> { + static std::vector Convert(const bt::ByteBuffer& from); +}; diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/host_server.h b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/host_server.h new file mode 100644 index 0000000000..b2cde6592c --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/host_server.h @@ -0,0 +1,306 @@ +// Copyright 2024 The Pigweed Authors +// +// 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. + +#pragma once + +#include +#include + +#include +#include +#include + +#include "fuchsia/bluetooth/cpp/fidl.h" +#include "fuchsia/bluetooth/sys/cpp/fidl.h" +#include "lib/fidl/cpp/interface_request.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/server_base.h" +#include "pw_bluetooth_sapphire/fuchsia/lib/fidl/hanging_getter.h" +#include "pw_bluetooth_sapphire/internal/host/common/identifier.h" +#include "pw_bluetooth_sapphire/internal/host/common/macros.h" +#include "pw_bluetooth_sapphire/internal/host/gap/adapter.h" +#include "pw_bluetooth_sapphire/internal/host/gap/bredr_connection_manager.h" +#include "pw_bluetooth_sapphire/internal/host/gap/bredr_discovery_manager.h" +#include "pw_bluetooth_sapphire/internal/host/gap/low_energy_discovery_manager.h" +#include "pw_bluetooth_sapphire/internal/host/gap/pairing_delegate.h" +#include "pw_bluetooth_sapphire/internal/host/sm/types.h" + +namespace bthost { + +// Implements the Host FIDL interface. Owns all FIDL connections that have been +// opened through it. +class HostServer : public AdapterServerBase, + public bt::gap::PairingDelegate { + public: + HostServer(zx::channel channel, + const bt::gap::Adapter::WeakPtr& adapter, + bt::gatt::GATT::WeakPtr gatt); + ~HostServer() override; + + // ::fuchsia::bluetooth::host::Host overrides: + void RequestProtocol( + ::fuchsia::bluetooth::host::ProtocolRequest request) override; + void WatchState(WatchStateCallback callback) override; + void SetLocalData(::fuchsia::bluetooth::sys::HostData host_data) override; + void SetPeerWatcher( + ::fidl::InterfaceRequest<::fuchsia::bluetooth::host::PeerWatcher> + peer_watcher) override; + void SetLocalName(::std::string local_name, + SetLocalNameCallback callback) override; + void SetDeviceClass(fuchsia::bluetooth::DeviceClass device_class, + SetDeviceClassCallback callback) override; + + void StartDiscovery( + ::fuchsia::bluetooth::host::HostStartDiscoveryRequest request) override; + void SetConnectable(bool connectable, + SetConnectableCallback callback) override; + void SetDiscoverable(bool discoverable, + SetDiscoverableCallback callback) override; + void EnableBackgroundScan(bool enabled) override; + void EnablePrivacy(bool enabled) override; + void SetBrEdrSecurityMode( + ::fuchsia::bluetooth::sys::BrEdrSecurityMode mode) override; + void SetLeSecurityMode( + ::fuchsia::bluetooth::sys::LeSecurityMode mode) override; + void SetPairingDelegate( + ::fuchsia::bluetooth::sys::InputCapability input, + ::fuchsia::bluetooth::sys::OutputCapability output, + ::fidl::InterfaceHandle<::fuchsia::bluetooth::sys::PairingDelegate> + delegate) override; + void Connect(::fuchsia::bluetooth::PeerId id, + ConnectCallback callback) override; + void Disconnect(::fuchsia::bluetooth::PeerId id, + DisconnectCallback callback) override; + void Pair(::fuchsia::bluetooth::PeerId id, + ::fuchsia::bluetooth::sys::PairingOptions options, + PairCallback callback) override; + void Forget(::fuchsia::bluetooth::PeerId id, + ForgetCallback callback) override; + void Shutdown() override; + void SetBondingDelegate( + ::fidl::InterfaceRequest<::fuchsia::bluetooth::host::BondingDelegate> + request) override; + void handle_unknown_method(uint64_t ordinal, + bool method_has_response) override; + + private: + class DiscoverySessionServer + : public ServerBase<::fuchsia::bluetooth::host::DiscoverySession> { + public: + explicit DiscoverySessionServer( + fidl::InterfaceRequest<::fuchsia::bluetooth::host::DiscoverySession> + request, + HostServer* host); + + void Close(zx_status_t epitaph) { binding()->Close(epitaph); } + + // ::fuchsia::bluetooth::host::Discovery overrides: + void Stop() override; + + private: + void handle_unknown_method(uint64_t ordinal, + bool method_has_response) override; + + HostServer* host_; + }; + + class PeerWatcherServer + : public ServerBase<::fuchsia::bluetooth::host::PeerWatcher> { + public: + PeerWatcherServer(::fidl::InterfaceRequest< + ::fuchsia::bluetooth::host::PeerWatcher> request, + bt::gap::PeerCache* peer_cache, + HostServer* host); + ~PeerWatcherServer() override; + + // Called by |adapter()->peer_cache()| when a peer is updated. + void OnPeerUpdated(const bt::gap::Peer& peer); + + // Called by |adapter()->peer_cache()| when a peer is removed. + void OnPeerRemoved(bt::PeerId identifier); + + void MaybeCallCallback(); + + private: + using Updated = std::vector; + using Removed = std::vector; + + // PeerWatcher overrides: + void GetNext(::fuchsia::bluetooth::host::PeerWatcher::GetNextCallback + callback) override; + void handle_unknown_method(uint64_t ordinal, + bool method_has_response) override; + + std::unordered_set updated_; + std::unordered_set removed_; + + bt::gap::PeerCache* peer_cache_; + // Id of the PeerCache::add_peer_updated_callback callback. Used to remove + // the callback when this server is closed. + bt::gap::PeerCache::CallbackId peer_updated_callback_id_; + + ::fuchsia::bluetooth::host::PeerWatcher::GetNextCallback callback_ = + nullptr; + + HostServer* host_; + + // Keep this as the last member to make sure that all weak pointers are + // invalidated before other members get destroyed. + WeakSelf weak_self_; + }; + + class BondingDelegateServer + : public ServerBase<::fuchsia::bluetooth::host::BondingDelegate> { + public: + explicit BondingDelegateServer( + ::fidl::InterfaceRequest<::fuchsia::bluetooth::host::BondingDelegate> + request, + HostServer* host); + + void OnNewBondingData(const bt::gap::Peer& peer); + + private: + // BondingDelegate overrides: + void RestoreBonds( + ::std::vector<::fuchsia::bluetooth::sys::BondingData> bonds, + RestoreBondsCallback callback) override; + void WatchBonds(WatchBondsCallback callback) override; + void handle_unknown_method(uint64_t ordinal, + bool method_has_response) override; + + void MaybeNotifyWatchBonds(); + + HostServer* host_; + // Queued bond updates that will be sent on the next call to WatchBonds. + std::queue<::fuchsia::bluetooth::sys::BondingData> updated_; + fit::callback + watch_bonds_cb_; + }; + + // bt::gap::PairingDelegate overrides: + bt::sm::IOCapability io_capability() const override; + void CompletePairing(bt::PeerId id, bt::sm::Result<> status) override; + void ConfirmPairing(bt::PeerId id, ConfirmCallback confirm) override; + void DisplayPasskey(bt::PeerId id, + uint32_t passkey, + DisplayMethod method, + ConfirmCallback confirm) override; + void RequestPasskey(bt::PeerId id, PasskeyResponseCallback respond) override; + + // Common code used for showing a user intent (except passkey request). + void DisplayPairingRequest(bt::PeerId id, + std::optional passkey, + fuchsia::bluetooth::sys::PairingMethod method, + ConfirmCallback confirm); + + // Called by |adapter()->peer_cache()| when a peer is bonded. + void OnPeerBonded(const bt::gap::Peer& peer); + + void ConnectLowEnergy(bt::PeerId id, ConnectCallback callback); + void ConnectBrEdr(bt::PeerId peer_id, ConnectCallback callback); + + void PairLowEnergy(bt::PeerId id, + ::fuchsia::bluetooth::sys::PairingOptions options, + PairCallback callback); + void PairBrEdr(bt::PeerId id, PairCallback callback); + // Called when a connection is established to a peer, either when initiated + // by a user via a client of Host.fidl, or automatically by the GAP adapter + void RegisterLowEnergyConnection( + std::unique_ptr conn_ref, + bool auto_connect); + + // Called when |server| receives a channel connection error. + void OnConnectionError(Server* server); + + // Helper to start LE Discovery (called by StartDiscovery) + void StartLEDiscovery(); + + void StopDiscovery(zx_status_t epitaph, bool notify_info_change = true); + + void OnDiscoverySessionServerClose(DiscoverySessionServer* server); + + // Resets the I/O capability of this server to no I/O and tells the GAP layer + // to reject incoming pairing requests. + void ResetPairingDelegate(); + + // Resolves any HostInfo watcher with the current adapter state. + void NotifyInfoChange(); + + void RestoreBonds( + ::std::vector<::fuchsia::bluetooth::sys::BondingData> bonds, + ::fuchsia::bluetooth::host::BondingDelegate::RestoreBondsCallback + callback); + + // Helper for binding a fidl::InterfaceRequest to a FIDL server of type + // ServerType. + template + void BindServer(Args... args) { + auto server = std::make_unique(std::move(args)...); + Server* s = server.get(); + server->set_error_handler( + [this, s](zx_status_t status) { this->OnConnectionError(s); }); + servers_[server.get()] = std::move(server); + } + + fuchsia::bluetooth::sys::PairingDelegatePtr pairing_delegate_; + + // We hold a weak pointer to GATT for dispatching GATT FIDL requests. + bt::gatt::GATT::WeakPtr gatt_; + + std::unordered_map> + discovery_session_servers_; + std::unique_ptr le_discovery_session_; + std::unique_ptr bredr_discovery_session_; + + bool requesting_background_scan_; + std::unique_ptr le_background_scan_; + + bool requesting_discoverable_; + std::unique_ptr + bredr_discoverable_session_; + + bt::sm::IOCapability io_capability_; + + // All active FIDL interface servers. + // NOTE: Each key is a raw pointer that is owned by the corresponding value. + // This allows us to create a set of managed objects that can be looked up via + // raw pointer. + std::unordered_map> servers_; + + // All LE connections that were either initiated by this HostServer or + // auto-connected by the system. + // TODO(armansito): Consider storing auto-connected references separately from + // directly connected references. + std::unordered_map> + le_connections_; + + // Used to drive the WatchState() method. + bt_lib_fidl::HangingGetter info_getter_; + + std::optional peer_watcher_server_; + + std::optional bonding_delegate_server_; + + // Keep this as the last member to make sure that all weak pointers are + // invalidated before other members get destroyed. + WeakSelf weak_self_; + + WeakSelf weak_pairing_; + + BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(HostServer); +}; + +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/iso_stream_server.h b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/iso_stream_server.h new file mode 100644 index 0000000000..2f4a4e5979 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/iso_stream_server.h @@ -0,0 +1,62 @@ +// Copyright 2024 The Pigweed Authors +// +// 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. + +#pragma once + +#include +#include + +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/server_base.h" +#include "pw_bluetooth_sapphire/internal/host/common/weak_self.h" +#include "pw_bluetooth_sapphire/internal/host/iso/iso_common.h" + +namespace bthost { +class IsoStreamServer + : public ServerBase { + public: + explicit IsoStreamServer( + fidl::InterfaceRequest request, + fit::callback on_closed_cb); + + void OnStreamEstablished( + bt::iso::IsoStream::WeakPtr stream_ptr, + const bt::iso::CisEstablishedParameters& connection_params); + + void OnStreamEstablishmentFailed(pw::bluetooth::emboss::StatusCode status); + + void OnClosed(); + + void Close(zx_status_t epitaph); + + using WeakPtr = WeakSelf::WeakPtr; + WeakPtr GetWeakPtr() { return weak_self_.GetWeakPtr(); } + + private: + // fuchsia::bluetooth::le::IsochronousStream overrides: + void SetupDataPath( + fuchsia::bluetooth::le::IsochronousStreamSetupDataPathRequest parameters, + SetupDataPathCallback callback) override; + void Read(ReadCallback callback) override; + void handle_unknown_method(uint64_t ordinal, bool has_response) override; + + fit::callback on_closed_cb_; + + std::optional iso_stream_; + + WeakSelf weak_self_; + + BT_DISALLOW_COPY_ASSIGN_AND_MOVE(IsoStreamServer); +}; + +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_central_server.h b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_central_server.h new file mode 100644 index 0000000000..d21544f33f --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_central_server.h @@ -0,0 +1,201 @@ +// Copyright 2024 The Pigweed Authors +// +// 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. + +#pragma once + +#include + +#include +#include + +#include "lib/fidl/cpp/binding.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/gatt_client_server.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_connection_server.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/server_base.h" +#include "pw_bluetooth_sapphire/internal/host/common/macros.h" +#include "pw_bluetooth_sapphire/internal/host/gap/low_energy_connection_manager.h" +#include "pw_bluetooth_sapphire/internal/host/gap/low_energy_discovery_manager.h" +#include "pw_bluetooth_sapphire/internal/host/gap/peer_cache.h" + +namespace bthost { + +// Implements the low_energy::Central FIDL interface. +class LowEnergyCentralServer + : public AdapterServerBase { + public: + // The maximum number of peers that will be queued for a + // ScanResultWatcher.Watch call. This hard limit prevents unbounded memory + // usage for unresponsive clients. The value is mostly arbitrary, as queued + // `PeerId`s are small and peak memory usage, occurring when creating a vector + // of FIDL `le.Peer`s, is limited by the size of the FIDL channel. + constexpr static const size_t kMaxPendingScanResultWatcherPeers = 100; + + LowEnergyCentralServer( + bt::gap::Adapter::WeakPtr adapter, + ::fidl::InterfaceRequest request, + bt::gatt::GATT::WeakPtr gatt); + ~LowEnergyCentralServer() override; + + // Returns the connection pointer in the connections_deprecated_ map, if it + // exists. The pointer will be nullptr if a request is pending. Should only be + // used for testing. + std::optional FindConnectionForTesting( + bt::PeerId identifier); + + private: + class ScanResultWatcherServer + : public ServerBase { + public: + using WatchCallbackOnce = + fit::callback)>; + + // `error_cb` will be called when the client closes the protocol. + ScanResultWatcherServer( + bt::gap::Adapter::WeakPtr adapter, + fidl::InterfaceRequest + watcher, + fit::callback error_cb); + ~ScanResultWatcherServer() override = default; + + // Closes the protocol and sends `epitaph` as the epitaph. Idempotent. + void Close(zx_status_t epitaph); + + // Queue `peers` to be sent in response to `Watch()`. + void AddPeers(std::unordered_set peers); + + // fuchsia::bluetooth::le::ScanResultWatcher overrides: + void Watch(WatchCallback callback) override; + + private: + // If the client has a pending `Watch()`, send the maximum number of peers + // that will fit in the channel. + void MaybeSendPeers(); + + bt::gap::Adapter::WeakPtr adapter_; + std::unordered_set updated_peers_; + WatchCallbackOnce watch_callback_ = nullptr; + fit::callback error_callback_; + }; + + // ScanInstance represents a call to `Scan` that has not stopped yet. + class ScanInstance { + public: + using ScanCompleteCallback = fit::callback; + + ScanInstance( + bt::gap::Adapter::WeakPtr adapter, + LowEnergyCentralServer* central_server, + std::vector filters, + fidl::InterfaceRequest + watcher, + ScanCallback cb); + ~ScanInstance(); + // Closes the ScanResultWatcher protocol with the epitaph `status` and sends + // an empty response to `Scan`. Idempotent. + void Close(zx_status_t status); + + // Queue peers to be sent to the client via `ScanResultWatcher.Watch`. + // `peers` will be filtered by the client's `ScanOptions` filters before + // being sent. + void FilterAndAddPeers(std::unordered_set peers); + + private: + std::unique_ptr scan_session_; + ScanResultWatcherServer result_watcher_; + // Callback used to send an empty response to the client's `Scan()` call. + ScanCompleteCallback scan_complete_callback_; + bt::gap::PeerCache::CallbackId peer_updated_callback_id_; + // The filters specified in `ScanOptions`. + std::vector filters_; + LowEnergyCentralServer* central_server_; + bt::gap::Adapter::WeakPtr adapter_; + WeakSelf weak_self_; + }; + + // fuchsia::bluetooth::le::Central overrides: + void Scan(fuchsia::bluetooth::le::ScanOptions options, + fidl::InterfaceRequest + result_watcher, + ScanCallback callback) override; + void Connect(fuchsia::bluetooth::PeerId id, + fuchsia::bluetooth::le::ConnectionOptions options, + fidl::InterfaceRequest<::fuchsia::bluetooth::le::Connection> + request) override; + + void GetPeripherals(::fidl::VectorPtr<::std::string> service_uuids, + GetPeripheralsCallback callback) override; + void GetPeripheral(::std::string identifier, + GetPeripheralCallback callback) override; + void StartScan(fuchsia::bluetooth::le::ScanFilterPtr filter, + StartScanCallback callback) override; + void StopScan() override; + void ConnectPeripheral( + ::std::string identifier, + fuchsia::bluetooth::le::ConnectionOptions connection_options, + ::fidl::InterfaceRequest client_request, + ConnectPeripheralCallback callback) override; + void DisconnectPeripheral(::std::string identifier, + DisconnectPeripheralCallback callback) override; + + // fuchsia::bluetooth::le::ChannelListenerRegistry overrides: + void ListenL2cap( + fuchsia::bluetooth::le::ChannelListenerRegistryListenL2capRequest request, + ListenL2capCallback callback) override; + + // Called by |scan_session_| when a device is discovered. + void OnScanResult(const bt::gap::Peer& peer); + + // Notifies the delegate that the scan state for this Central has changed. + void NotifyScanStateChanged(bool scanning); + + // Notifies the delegate that the device with the given identifier has been + // disconnected. + void NotifyPeripheralDisconnected(bt::PeerId peer_id); + + void ClearScan() { scan_instance_.reset(); } + + // GATT is used to construct GattClientServers upon connection. + bt::gatt::GATT::WeakPtr gatt_; + + // Stores active GATT client FIDL servers. Only 1 client server per peer may + // exist. + std::unordered_map> + gatt_client_servers_; + + // The currently active LE discovery session. This is initialized when a + // client requests to perform a scan. + bool requesting_scan_deprecated_; + std::unique_ptr scan_session_deprecated_; + + std::unique_ptr scan_instance_; + + // This client's connection references. A client can hold a connection to + // multiple peers. Each key is a peer identifier. Each value is + // a. nullptr, if a connect request to this device is currently pending. + // b. a valid reference if this Central is holding a connection reference to + // this device. + std::unordered_map> + connections_; + std::unordered_map> + connections_deprecated_; + + // Keep this as the last member to make sure that all weak pointers are + // invalidated before other members get destroyed. + WeakSelf weak_self_; + + BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(LowEnergyCentralServer); +}; + +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_connection_server.h b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_connection_server.h new file mode 100644 index 0000000000..91f1899736 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_connection_server.h @@ -0,0 +1,78 @@ +// Copyright 2024 The Pigweed Authors +// +// 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. + +#pragma once + +#include + +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/gatt2_client_server.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/iso_stream_server.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/server_base.h" +#include "pw_bluetooth_sapphire/internal/host/common/macros.h" +#include "pw_bluetooth_sapphire/internal/host/gap/low_energy_connection_handle.h" +#include "pw_bluetooth_sapphire/internal/host/gatt/gatt.h" +#include "pw_bluetooth_sapphire/internal/host/iso/iso_common.h" + +namespace bthost { + +class LowEnergyConnectionServer + : public ServerBase { + public: + // |closed_cb| will be called to signal the invalidation of this connection + // instance. This can be called in response to the client closing its end of + // the FIDL channel or when the LL connection is severed. |closed_cb| will be + // called at most once in response to either of these events. The owner of the + // LowEnergyConnectionServer instance is expected to destroy it. + LowEnergyConnectionServer( + bt::gap::Adapter::WeakPtr adapter, + bt::gatt::GATT::WeakPtr gatt, + std::unique_ptr connection, + zx::channel handle, + fit::callback closed_cb); + + // Return a reference to the underlying connection ref. Expected to only be + // used for testing. + const bt::gap::LowEnergyConnectionHandle* conn() const { return conn_.get(); } + + private: + void OnClosed(); + + // fuchsia::bluetooth::le::Connection overrides: + void RequestGattClient( + ::fidl::InterfaceRequest<::fuchsia::bluetooth::gatt2::Client> client) + override; + void AcceptCis( + fuchsia::bluetooth::le::ConnectionAcceptCisRequest parameters) override; + void GetCodecLocalDelayRange( + ::fuchsia::bluetooth::le::CodecDelayGetCodecLocalDelayRangeRequest + CodecDelayGetCodecLocalDelayRangeRequest, + GetCodecLocalDelayRangeCallback callback) override; + void ConnectL2cap(fuchsia::bluetooth::le::ConnectionConnectL2capRequest + parameters) override; + + std::unique_ptr conn_; + fit::callback closed_handler_; + bt::PeerId peer_id_; + bt::gap::Adapter::WeakPtr adapter_; + bt::gatt::GATT::WeakPtr gatt_; + std::optional gatt_client_server_; + + std::unordered_map> + iso_streams_; + + BT_DISALLOW_COPY_ASSIGN_AND_MOVE(LowEnergyConnectionServer); +}; + +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_peripheral_server.h b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_peripheral_server.h new file mode 100644 index 0000000000..01fe830509 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_peripheral_server.h @@ -0,0 +1,222 @@ +// Copyright 2024 The Pigweed Authors +// +// 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. + +#pragma once + +#include +#include +#include + +#include +#include + +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/low_energy_connection_server.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/server_base.h" +#include "pw_bluetooth_sapphire/internal/host/common/macros.h" +#include "pw_bluetooth_sapphire/internal/host/common/weak_self.h" +#include "pw_bluetooth_sapphire/internal/host/gap/adapter.h" +#include "pw_bluetooth_sapphire/internal/host/gap/low_energy_advertising_manager.h" +#include "pw_bluetooth_sapphire/internal/host/gap/low_energy_connection_manager.h" + +namespace bthost { + +// Implements the low_energy::Peripheral FIDL interface. +class LowEnergyPeripheralServer + : public AdapterServerBase { + public: + LowEnergyPeripheralServer( + bt::gap::Adapter::WeakPtr adapter, + bt::gatt::GATT::WeakPtr gatt, + fidl::InterfaceRequest request); + ~LowEnergyPeripheralServer() override; + + // fuchsia::bluetooth::le::Peripheral overrides: + void Advertise( + fuchsia::bluetooth::le::AdvertisingParameters parameters, + fidl::InterfaceHandle + advertised_peripheral, + AdvertiseCallback callback) override; + void StartAdvertising( + fuchsia::bluetooth::le::AdvertisingParameters parameters, + ::fidl::InterfaceRequest token, + StartAdvertisingCallback callback) override; + + // fuchsia::bluetooth::le::ChannelListenerRegistry overrides: + void ListenL2cap( + fuchsia::bluetooth::le::ChannelListenerRegistryListenL2capRequest request, + ListenL2capCallback callback) override; + + // Returns the connection handle associated with the given |id|, or nullptr if + // the peer with |id| is no longer connected. Should only be used for testing. + const bt::gap::LowEnergyConnectionHandle* FindConnectionForTesting( + bt::PeerId id) const; + + private: + using ConnectionRefPtr = std::unique_ptr; + using AdvertisementInstanceId = uint64_t; + using ConnectionServerId = uint64_t; + + // Manages state associated with a single invocation of the + // `Peripheral.Advertise` method. + class AdvertisementInstance final { + public: + using AdvertiseCompleteCallback = fit::callback; + + // |complete_cb| will be called to send a Peripheral.Advertise response to + // the client when an error occurs or this AdvertisementInstance is + // destroyed. This is done so that the FIDL client can determine when the + // server has terminated this AdvertisementInstance (this is useful for + // reconfiguring an advertisement). + AdvertisementInstance( + LowEnergyPeripheralServer* peripheral_server, + AdvertisementInstanceId id, + fuchsia::bluetooth::le::AdvertisingParameters parameters, + fidl::InterfaceHandle + handle, + AdvertiseCompleteCallback complete_cb); + ~AdvertisementInstance(); + + // This method is separate from the constructor because HCI-level + // advertising may be started many times over the life of this object. + void StartAdvertising(); + + // Called when a central connects to us. When this is called, the + // advertisement in |advertisement_id| has been stopped. + void OnConnected(bt::gap::AdvertisementId advertisement_id, + bt::gap::Adapter::LowEnergy::ConnectionResult result); + + private: + // After advertising successfully starts, the advertisement instance must be + // registered to tie advertising to the lifetime of this object. + void Register(bt::gap::AdvertisementInstance instance); + + // End the advertisement with a result. Idempotent. + // This object should be destroyed immediately after calling this method. + void CloseWith( + fpromise::result result); + + LowEnergyPeripheralServer* peripheral_server_; + AdvertisementInstanceId id_; + fuchsia::bluetooth::le::AdvertisingParameters parameters_; + + // The advertising handle set by Register. When destroyed, advertising will + // be stopped. + std::optional instance_; + + // The AdvertisedPeripheral protocol representing this advertisement. + fidl::InterfacePtr + advertised_peripheral_; + + // Callback used to send a response to the Advertise request that started + // this advertisement. + AdvertiseCompleteCallback advertise_complete_cb_; + + WeakSelf weak_self_; + + BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(AdvertisementInstance); + }; + + class AdvertisementInstanceDeprecated final { + public: + explicit AdvertisementInstanceDeprecated( + fidl::InterfaceRequest + handle); + ~AdvertisementInstanceDeprecated(); + + // Begin watching for ZX_CHANNEL_PEER_CLOSED events on the AdvertisingHandle + // this was initialized with. The returned status will indicate an error if + // wait cannot be initiated (e.g. because the peer closed its end of the + // channel). + zx_status_t Register(bt::gap::AdvertisementInstance instance); + + // Returns the ID assigned to this instance, or + // bt::gap::kInvalidAdvertisementId if one wasn't assigned. + bt::gap::AdvertisementId id() const { + return instance_ ? instance_->id() : bt::gap::kInvalidAdvertisementId; + } + + private: + std::optional instance_; + fidl::InterfaceRequest handle_; + async::Wait handle_closed_wait_; + + BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(AdvertisementInstanceDeprecated); + }; + + // Called when a central connects to us. When this is called, the + // advertisement in |advertisement_id| has been stopped. + void OnConnectedDeprecated( + bt::gap::AdvertisementId advertisement_id, + bt::gap::Adapter::LowEnergy::ConnectionResult result); + + // Sets up a Connection server and returns the client end. + fidl::InterfaceHandle + CreateConnectionServer( + std::unique_ptr connection); + + // Common advertising initiation code shared by Peripheral.{Advertise, + // StartAdvertising}. If advertising was initiated by `Advertise`, + // `advertisement_instance` must be set to the identifier of the + // `AdvertisementInstance` that connections to this advertisement should be + // routed to. Otherwise, connections will be sent in a + // `Peripheral.OnConnected` event. + void StartAdvertisingInternal( + fuchsia::bluetooth::le::AdvertisingParameters& parameters, + bt::gap::Adapter::LowEnergy::AdvertisingStatusCallback status_cb, + std::optional advertisement_instance = + std::nullopt); + + void RemoveAdvertisingInstance(AdvertisementInstanceId id) { + advertisements_.erase(id); + } + + // Represents the current advertising instance: + // - Contains no value if advertising was never requested. + // - Contains a value while advertising is being (re)enabled and during + // advertising. + // - May correspond to an invalidated advertising instance if advertising is + // stopped by closing + // the AdvertisingHandle. + std::optional advertisement_deprecated_; + + // Map of all active advertisement instances associated with a call to + // `Advertise`. bt::gap::AdvertisementId cannot be used as a map key because + // it is received asynchronously, and we need an advertisement ID to refer to + // before advertising starts. + // TODO(fxbug.dev/42157682): Support AdvertisedPeripheral protocols that + // outlive this Peripheral protocol. This may require passing + // AdvertisementInstances to HostServer. + AdvertisementInstanceId next_advertisement_instance_id_ = 0u; + std::unordered_map + advertisements_; + + // Connections that were initiated to this peripheral. A single Peripheral + // instance can hold many connections across numerous advertisements that it + // initiates during its lifetime. + ConnectionServerId next_connection_server_id_ = 0u; + std::unordered_map> + connections_; + + bt::gatt::GATT::WeakPtr gatt_; + + // Keep this as the last member to make sure that all weak pointers are + // invalidated before other members get destroyed. + WeakSelf weak_self_; + + BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(LowEnergyPeripheralServer); +}; + +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/profile_server.h b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/profile_server.h new file mode 100644 index 0000000000..61d1b23f78 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/profile_server.h @@ -0,0 +1,383 @@ +// Copyright 2024 The Pigweed Authors +// +// 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. + +#pragma once + +#include + +#include "lib/fidl/cpp/binding.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/bredr_connection_server.h" +#include "pw_bluetooth_sapphire/fuchsia/host/fidl/server_base.h" +#include "pw_bluetooth_sapphire/fuchsia/host/socket/socket_factory.h" +#include "pw_bluetooth_sapphire/internal/host/common/macros.h" +#include "pw_bluetooth_sapphire/internal/host/gap/bredr_connection_manager.h" +#include "pw_bluetooth_sapphire/internal/host/sdp/server.h" +#include "pw_intrusive_ptr/intrusive_ptr.h" + +namespace bthost { + +// Implements the bredr::Profile FIDL interface. +class ProfileServer : public ServerBase { + public: + ProfileServer( + bt::gap::Adapter::WeakPtr adapter, + fidl::InterfaceRequest request); + ~ProfileServer() override; + + // If true, use Channel.socket. If false, use Channel.connection. + // TODO(fxbug.dev/330590954): Remove once migration to Connection is complete. + void set_use_sockets(bool enable) { use_sockets_ = enable; } + + private: + class AudioDirectionExt; + + class L2capParametersExt final + : public ServerBase { + public: + L2capParametersExt( + fidl::InterfaceRequest + request, + bt::l2cap::Channel::WeakPtr channel) + : ServerBase(this, std::move(request)), + unique_id_(channel->unique_id()), + channel_(std::move(channel)) {} + + bt::l2cap::Channel::UniqueId unique_id() const { return unique_id_; } + + void RequestParameters(fuchsia::bluetooth::ChannelParameters requested, + RequestParametersCallback callback) override; + + void handle_unknown_method(uint64_t ordinal, + bool method_has_response) override; + + private: + bt::l2cap::Channel::UniqueId unique_id_; + bt::l2cap::Channel::WeakPtr channel_; + }; + + class AudioOffloadExt final + : public ServerBase { + public: + AudioOffloadExt( + ProfileServer& profile_server, + fidl::InterfaceRequest + request, + bt::l2cap::Channel::WeakPtr channel, + bt::gap::Adapter::WeakPtr adapter) + : ServerBase(this, std::move(request)), + profile_server_(profile_server), + unique_id_(channel->unique_id()), + channel_(std::move(channel)), + adapter_(std::move(adapter)) {} + void GetSupportedFeatures(GetSupportedFeaturesCallback callback) override; + void StartAudioOffload( + fuchsia::bluetooth::bredr::AudioOffloadConfiguration + audio_offload_configuration, + fidl::InterfaceRequest< + fuchsia::bluetooth::bredr::AudioOffloadController> controller) + override; + + bt::l2cap::Channel::UniqueId unique_id() const { return unique_id_; } + + void handle_unknown_method(uint64_t ordinal, + bool method_has_response) override; + + private: + std::unique_ptr + AudioOffloadConfigFromFidl( + fuchsia::bluetooth::bredr::AudioOffloadConfiguration& + audio_offload_configuration); + + ProfileServer& profile_server_; + bt::l2cap::Channel::UniqueId unique_id_; + bt::l2cap::Channel::WeakPtr channel_; + bt::gap::Adapter::WeakPtr adapter_; + }; + + class AudioOffloadController + : public ServerBase { + public: + explicit AudioOffloadController( + fidl::InterfaceRequest< + fuchsia::bluetooth::bredr::AudioOffloadController> request, + bt::l2cap::Channel::WeakPtr channel) + : ServerBase(this, std::move(request)), + unique_id_(channel->unique_id()), + channel_(std::move(channel)) {} + + void handle_unknown_method(uint64_t ordinal, + bool method_has_response) override; + + bt::l2cap::Channel::UniqueId unique_id() const { return unique_id_; } + + void Stop(StopCallback callback) override; + + void SendOnStartedEvent() { binding()->events().OnStarted(); } + + void Close(zx_status_t epitaph) { binding()->Close(epitaph); } + + WeakPtr GetWeakPtr() { + return weak_self_.GetWeakPtr(); + } + + private: + bt::l2cap::Channel::UniqueId unique_id_; + bt::l2cap::Channel::WeakPtr channel_; + WeakSelf weak_self_{this}; + }; + + class ScoConnectionServer final + : public ServerBase { + public: + explicit ScoConnectionServer( + fidl::InterfaceRequest + request, + ProfileServer* profile_server); + ~ScoConnectionServer() override; + // Call bt::gap::ScoConnection::Activate with the appropriate callbacks. On + // error, destroys this server. + void Activate(); + + void OnConnectedParams( + ::fuchsia::bluetooth::bredr::ScoConnectionParameters params) { + OnConnectionComplete( + ::fuchsia::bluetooth::bredr:: + ScoConnectionOnConnectionCompleteRequest::WithConnectedParams( + std::move(params))); + } + + // Sends the OnConnectionComplete event with `error` and shuts down the + // server. + void OnError(::fuchsia::bluetooth::bredr::ScoErrorCode error); + + const std::vector& + parameters() const { + return parameters_; + } + + void set_parameters( + std::vector + params) { + parameters_ = std::move(params); + } + + void set_request_handle(bt::gap::Adapter::BrEdr::ScoRequestHandle handle) { + request_handle_ = std::move(handle); + } + + void set_connection(bt::sco::ScoConnection::WeakPtr connection) { + connection_ = std::move(connection); + } + + WeakPtr GetWeakPtr() { + return weak_self_.GetWeakPtr(); + } + + private: + // ScoConnection overrides: + void Read(ReadCallback callback) override; + void Write(::fuchsia::bluetooth::bredr::ScoConnectionWriteRequest request, + WriteCallback callback) override; + void handle_unknown_method(uint64_t ordinal, + bool method_has_response) override; + + void TryRead(); + + // Closes the server with `epitaph` and destroys the server. + void Close(zx_status_t epitaph); + + void OnConnectionComplete( + ::fuchsia::bluetooth::bredr::ScoConnectionOnConnectionCompleteRequest + request) { + binding()->events().OnConnectionComplete(std::move(request)); + } + + std::optional request_handle_; + std::vector parameters_; + + bt::sco::ScoConnection::WeakPtr connection_; + // Non-null when a read request is waiting for an inbound packet. + fit::callback + read_cb_; + + ProfileServer* profile_server_; + + // Keep this as the last member to make sure that all weak pointers are + // invalidated before other members get destroyed. + WeakSelf weak_self_; + }; + + // fuchsia::bluetooth::bredr::Profile overrides: + void Advertise(fuchsia::bluetooth::bredr::ProfileAdvertiseRequest request, + AdvertiseCallback callback) override; + void Search( + ::fuchsia::bluetooth::bredr::ProfileSearchRequest request) override; + void Connect(fuchsia::bluetooth::PeerId peer_id, + fuchsia::bluetooth::bredr::ConnectParameters connection, + ConnectCallback callback) override; + void ConnectSco( + ::fuchsia::bluetooth::bredr::ProfileConnectScoRequest request) override; + void handle_unknown_method(uint64_t ordinal, + bool method_has_response) override; + + // Callback when clients close or revoke their connection targets + void OnConnectionReceiverClosed(uint64_t ad_id); + + // Callback when clients close their search results + void OnSearchResultError(uint64_t search_id, zx_status_t status); + + // Callback for incoming connections + void OnChannelConnected(uint64_t ad_id, + bt::l2cap::Channel::WeakPtr channel, + const bt::sdp::DataElement& protocol_list); + + // Callback for services found on connected device + void OnServiceFound( + uint64_t search_id, + bt::PeerId peer_id, + const std::map& attributes); + + // Callback for SCO connections requests. + static void OnScoConnectionResult( + WeakPtr& server, + bt::sco::ScoConnectionManager::AcceptConnectionResult result); + + // Callback when clients close their audio direction extension. + void OnAudioDirectionExtError(AudioDirectionExt* ext_server, + zx_status_t status); + + // Create an AudioDirectionExt server for the given channel and set up + // callbacks. Returns the client end of the channel. + fidl::InterfaceHandle + BindAudioDirectionExtServer(bt::l2cap::Channel::WeakPtr channel); + + // Callback when clients close their l2cap parameters extension. + void OnL2capParametersExtError(L2capParametersExt* ext_server, + zx_status_t status); + + // Create an L2capParametersExt server for the given channel and set up + // callbacks. Returns the client end of the channel. + fidl::InterfaceHandle + BindL2capParametersExtServer(bt::l2cap::Channel::WeakPtr channel); + + // Callback when clients close their audio offload extension. + void OnAudioOffloadExtError(AudioOffloadExt* ext_server, zx_status_t status); + + // Create an AudioOffloadExt server for the given channel and set up + // callbacks. Returns the client end of the channel. + fidl::InterfaceHandle + BindAudioOffloadExtServer(bt::l2cap::Channel::WeakPtr channel); + + // Create an Connection server for the given channel and set up callbacks. + // Returns the client end of the channel, or null on failure. + std::optional> + BindBrEdrConnectionServer(bt::l2cap::Channel::WeakPtr channel, + fit::callback closed_callback); + + // Create a FIDL Channel from an l2cap::Channel. A Connection relay is created + // from |channel| and returned in the FIDL Channel. + std::optional ChannelToFidl( + bt::l2cap::Channel::WeakPtr channel); + + const bt::gap::Adapter::WeakPtr& adapter() const { return adapter_; } + + // Advertised Services + struct AdvertisedService { + AdvertisedService( + fidl::InterfacePtr + receiver, + bt::sdp::Server::RegistrationHandle registration_handle) + : receiver(std::move(receiver)), + registration_handle(registration_handle) {} + fidl::InterfacePtr receiver; + bt::sdp::Server::RegistrationHandle registration_handle; + }; + + uint64_t advertised_total_; + std::map current_advertised_; + + // Searches registered + struct RegisteredSearch { + RegisteredSearch( + fidl::InterfacePtr results, + bt::gap::BrEdrConnectionManager::SearchId search_id) + : results(std::move(results)), search_id(search_id) {} + fidl::InterfacePtr results; + bt::gap::BrEdrConnectionManager::SearchId search_id; + }; + + uint64_t searches_total_; + std::map searches_; + + class AudioDirectionExt final + : public ServerBase { + public: + // Calls to SetPriority() are forwarded to |priority_cb|. + AudioDirectionExt(fidl::InterfaceRequest< + fuchsia::bluetooth::bredr::AudioDirectionExt> request, + bt::l2cap::Channel::WeakPtr channel) + : ServerBase(this, std::move(request)), + unique_id_(channel->unique_id()), + channel_(std::move(channel)) {} + + bt::l2cap::Channel::UniqueId unique_id() const { return unique_id_; } + + // fuchsia::bluetooth::bredr::AudioDirectionExt overrides: + void SetPriority(fuchsia::bluetooth::bredr::A2dpDirectionPriority priority, + SetPriorityCallback callback) override; + + void handle_unknown_method(uint64_t ordinal, + bool method_has_response) override; + + private: + bt::l2cap::Channel::UniqueId unique_id_; + bt::l2cap::Channel::WeakPtr channel_; + }; + std::unordered_map> + l2cap_parameters_ext_servers_; + + std::unordered_map> + audio_direction_ext_servers_; + + std::unordered_map> + audio_offload_ext_servers_; + + std::unique_ptr audio_offload_controller_server_; + + bt::gap::Adapter::WeakPtr adapter_; + + // If true, use Channel.socket. If false, use Channel.connection. + bool use_sockets_ = true; + + // Creates sockets that bridge L2CAP channels to profile processes. + bt::socket::SocketFactory l2cap_socket_factory_; + + std::unordered_map> + bredr_connection_servers_; + + std::unordered_map> + sco_connection_servers_; + + // Keep this as the last member to make sure that all weak pointers are + // invalidated before other members get destroyed. + WeakSelf weak_self_; + + BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(ProfileServer); +}; + +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/server_base.h b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/server_base.h new file mode 100644 index 0000000000..feae41e560 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/fidl/public/pw_bluetooth_sapphire/fuchsia/host/fidl/server_base.h @@ -0,0 +1,134 @@ +// Copyright 2024 The Pigweed Authors +// +// 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. + +#pragma once + +#include + +#include + +#include "lib/fidl/cpp/binding.h" +#include "lib/fidl/cpp/interface_request.h" +#include "pw_bluetooth_sapphire/internal/host/common/assert.h" +#include "pw_bluetooth_sapphire/internal/host/common/macros.h" +#include "pw_bluetooth_sapphire/internal/host/gap/adapter.h" + +namespace bt { + +namespace gap { +class Adapter; +} // namespace gap + +namespace gatt { +class GATT; +} // namespace gatt + +} // namespace bt + +namespace bthost { + +// This class acts as a common base type for all FIDL interface servers. Its +// main purpose is to provide type erasure for the ServerBase template below. +class Server { + public: + virtual ~Server() = default; + + virtual void set_error_handler(fit::function handler) = 0; +}; + +// ServerBase is a common base implementation for FIDL interface servers. +template +class ServerBase : public Server, public Interface { + public: + // Constructs a FIDL server by binding a fidl::InterfaceRequest. + ServerBase(Interface* impl, fidl::InterfaceRequest request) + : ServerBase(impl, request.TakeChannel()) {} + + // Constructs a FIDL server by binding a zx::channel. + ServerBase(Interface* impl, zx::channel channel) + : binding_(impl, std::move(channel)) { + BT_DEBUG_ASSERT(binding_.is_bound()); + } + + ~ServerBase() override = default; + + void set_error_handler(fit::function handler) override { + binding_.set_error_handler(std::move(handler)); + } + + protected: + ::fidl::Binding* binding() { return &binding_; } + + private: + // Holds the channel from the FIDL client. + ::fidl::Binding binding_; + + // Binding cannot be copied or moved. + BT_DISALLOW_COPY_ASSIGN_AND_MOVE(ServerBase); +}; + +// Base template for GAP FIDL interface servers. The GAP profile is accessible +// through an Adapter object. +template +class AdapterServerBase : public ServerBase { + public: + AdapterServerBase(bt::gap::Adapter::WeakPtr adapter, + Interface* impl, + fidl::InterfaceRequest request) + : AdapterServerBase(adapter, impl, request.TakeChannel()) {} + + AdapterServerBase(bt::gap::Adapter::WeakPtr adapter, + Interface* impl, + zx::channel channel) + : ServerBase(impl, std::move(channel)), + adapter_(std::move(adapter)) { + BT_DEBUG_ASSERT(adapter_.is_alive()); + } + + ~AdapterServerBase() override = default; + + protected: + const bt::gap::Adapter::WeakPtr& adapter() const { return adapter_; } + + private: + bt::gap::Adapter::WeakPtr adapter_; + + BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(AdapterServerBase); +}; + +// Base template for GATT FIDL interface servers. The GATT profile is accessible +// through an Adapter object. +template +class GattServerBase : public ServerBase { + public: + GattServerBase(bt::gatt::GATT::WeakPtr gatt, + Interface* impl, + fidl::InterfaceRequest request) + : ServerBase(impl, std::move(request)), + gatt_(std::move(gatt)) { + BT_DEBUG_ASSERT(gatt_.is_alive()); + } + + ~GattServerBase() override = default; + + protected: + bt::gatt::GATT::WeakPtr gatt() const { return gatt_; } + + private: + bt::gatt::GATT::WeakPtr gatt_; + + BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(GattServerBase); +}; + +} // namespace bthost diff --git a/pw_bluetooth_sapphire/fuchsia/host/socket/BUILD.bazel b/pw_bluetooth_sapphire/fuchsia/host/socket/BUILD.bazel new file mode 100644 index 0000000000..7c3b644326 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/socket/BUILD.bazel @@ -0,0 +1,80 @@ +# Copyright 2024 The Pigweed Authors +# +# 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. + +load( + "@fuchsia_sdk//fuchsia:defs.bzl", + "fuchsia_cc_test", + "fuchsia_unittest_package", +) + +package(default_visibility = ["//visibility:public"]) + +cc_library( + name = "public", + hdrs = [ + "public/pw_bluetooth_sapphire/fuchsia/host/socket/socket_channel_relay.h", + "public/pw_bluetooth_sapphire/fuchsia/host/socket/socket_factory.h", + ], + includes = [ + "public", + ], + tags = ["manual"], +) + +cc_library( + name = "socket", + deps = [ + ":public", + "//pw_bluetooth_sapphire/host/common", + "@fuchsia_sdk//pkg/async-default", + "@fuchsia_sdk//pkg/fit", + "@fuchsia_sdk//pkg/trace", + "@fuchsia_sdk//pkg/zx", + ], +) + +fuchsia_cc_test( + name = "socket_test", + testonly = True, + srcs = [ + "socket_channel_relay_test.cc", + "socket_factory_l2cap_integration_test.cc", + "socket_factory_test.cc", + ], + death_unittest = True, + visibility = ["//visibility:public"], + deps = [ + ":socket", + "//pw_async_fuchsia:dispatcher", + "//pw_bluetooth_sapphire/host/gap", + "//pw_bluetooth_sapphire/host/l2cap:channel_manager_mock_controller_test_fixture", + "//pw_bluetooth_sapphire/host/l2cap:testing", + "//pw_bluetooth_sapphire/host/testing", + "//pw_bluetooth_sapphire/host/testing:gtest_main", + "//pw_bluetooth_sapphire/host/testing:loop_fixture", + "//pw_bluetooth_sapphire/host/transport:testing", + "@fuchsia_sdk//pkg/async-loop-cpp", + ], +) + +fuchsia_unittest_package( + name = "test_pkg", + package_name = "socket_tests", + testonly = True, + fuchsia_api_level = "23", + unit_tests = [ + ":socket_test", + ], + visibility = ["//visibility:public"], +) diff --git a/pw_bluetooth_sapphire/fuchsia/host/socket/README.md b/pw_bluetooth_sapphire/fuchsia/host/socket/README.md new file mode 100644 index 0000000000..44ae75c91e --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/socket/README.md @@ -0,0 +1,2 @@ +# Socket library +This library provides utilities for bridging `zx::socket`s to a generic channel interface modeled on `l2cap::Channel`. diff --git a/pw_bluetooth_sapphire/fuchsia/host/socket/public/pw_bluetooth_sapphire/fuchsia/host/socket/socket_channel_relay.h b/pw_bluetooth_sapphire/fuchsia/host/socket/public/pw_bluetooth_sapphire/fuchsia/host/socket/socket_channel_relay.h new file mode 100644 index 0000000000..c244219562 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/socket/public/pw_bluetooth_sapphire/fuchsia/host/socket/socket_channel_relay.h @@ -0,0 +1,698 @@ +// Copyright 2024 The Pigweed Authors +// +// 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. + +#pragma once + +#include +#include +#include +#include + +#include +#include + +#include "lib/zx/socket.h" +#include "pw_bluetooth_sapphire/internal/host/common/assert.h" +#include "pw_bluetooth_sapphire/internal/host/common/byte_buffer.h" +#include "pw_bluetooth_sapphire/internal/host/common/log.h" +#include "pw_bluetooth_sapphire/internal/host/common/macros.h" +#include "pw_bluetooth_sapphire/internal/host/common/trace.h" +#include "pw_bluetooth_sapphire/internal/host/common/weak_self.h" + +namespace bt::socket { + +// SocketChannelRelay relays data between a zx::socket and a Channel. This class +// should not be used directly. Instead, see SocketFactory. +template +class SocketChannelRelay final { + public: + using DeactivationCallback = fit::function; + + // The kernel allows up to ~256 KB in a socket buffer, which is enough for + // about 1 second of data at 2 Mbps. Until we have a use case that requires + // more than 1 second of buffering, we allow only a small amount of buffering + // within SocketChannelRelay itself. + static constexpr size_t kDefaultSocketWriteQueueLimitFrames = 2; + + // Creates a SocketChannelRelay which executes on |dispatcher|. Note that + // |dispatcher| must be single-threaded. + // + // The relay works with SocketFactory to manage the relay's lifetime. On any + // of the "terminal events" (see below), the relay will invoke the + // DeactivationCallback. On invocation of the DeactivationCallback, the + // SocketFactory should destroy the relay. The destruction should be done + // synchronously, as a) destruction must happen on |dispatcher|'s thread, and + // b) the |dispatcher| may be shutting down. + // + // The terminal events are: + // * the zx::socket is closed + // * the Channel is closed + // * the dispatcher begins shutting down + // + // Note that requiring |dispatcher| to be single-threaded shouldn't cause + // increased latency vs. multi-threading, since a) all I/O is non-blocking (so + // we never leave the thread idle), and b) to provide in-order delivery, + // moving the data between the zx::socket and the ChannelT needs to be + // serialized even in the multi-threaded case. + SocketChannelRelay(zx::socket socket, + typename ChannelT::WeakPtr channel, + DeactivationCallback deactivation_cb, + size_t socket_write_queue_max_frames = + kDefaultSocketWriteQueueLimitFrames); + ~SocketChannelRelay(); + + // Enables read and close callbacks for the zx::socket and the + // ChannelT. (Write callbacks aren't necessary until we have data + // buffered.) Returns true on success. + // + // Activate() is guaranteed _not_ to invoke |deactivation_cb|, even in the + // event of failure. Instead, in the failure case, the caller should dispose + // of |this| directly. + [[nodiscard]] bool Activate(); + + private: + enum class RelayState { + kActivating, + kActivated, + kDeactivating, + kDeactivated, + }; + + // Deactivates and unbinds all callbacks from the zx::socket and the + // ChannelT. Drops any data still queued for transmission to the + // zx::socket. Ensures that the zx::socket is closed, and the ChannelT + // is deactivated. It is an error to call this when |state_ == kDeactivated|. + // ChannelT. + // + // Note that Deactivate() _may_ be called from the dtor. As such, this method + // avoids doing any "real work" (such as calling ServiceSocketWriteQueue()), + // and constrains itself to just tearing things down. + void Deactivate(); + + // Deactivates |this|, and invokes deactivation_cb_. + // It is an error to call this when |state_ == kDeactivated|. + void DeactivateAndRequestDestruction(); + + // Callbacks for zx::socket events. + void OnSocketReadable(zx_status_t status); + void OnSocketWritable(zx_status_t status); + void OnSocketClosed(zx_status_t status); + + // Callbacks for ChannelT events. + void OnChannelDataReceived(ByteBufferPtr rx_data); + void OnChannelClosed(); + + // Copies any data currently available on |socket_| to |channel_|. Does not + // block for data on |socket_|, and does not retry failed writes to + // |channel_|. Returns true if we should attempt to read from this socket + // again, and false otherwise. + [[nodiscard]] bool CopyFromSocketToChannel(); + + // Copies any data pending in |socket_write_queue_| to |socket_|. + void ServiceSocketWriteQueue(); + + // Binds an async::Wait to a |handler|, but does not enable the wait. + // The handler will be wrapped in code that verifies that |this| has not begun + // destruction. + void BindWait(zx_signals_t trigger, + const char* wait_name, + async::Wait* wait, + fit::function handler); + + // Begins waiting on |wait|. Returns true on success. + // Note that it is safe to BeginWait() even after a socket operation has + // returned ZX_ERR_PEER_CLOSED. This is because "if the handle is closed, the + // operation will ... be terminated". (See zx_object_wait_async().) + bool BeginWait(const char* wait_name, async::Wait* wait); + + // Clears |wait|'s handler, and cancels |wait|. + void UnbindAndCancelWait(async::Wait* wait); + + // Get a trace_flow_id given a counter. Used to make sure trace flows are + // unique while they are active. Composed of channel UniqueId and the given + // counter, with the unique_id taking the top 32 bits. + trace_flow_id_t GetTraceId(uint32_t id); + + RelayState state_; // Initial state is kActivating. + + zx::socket socket_; + const typename ChannelT::WeakPtr channel_; + async_dispatcher_t* const dispatcher_; + DeactivationCallback deactivation_cb_; + const size_t socket_write_queue_max_frames_; + + async::Wait sock_read_waiter_; + async::Wait sock_write_waiter_; + async::Wait sock_close_waiter_; + + // Count of packets received from the peer on the channel. + uint32_t channel_rx_packet_count_ = 0u; + // Count of packets sent to the peer on the channel. + uint32_t channel_tx_packet_count_ = 0u; + // Count of packets sent to the socket (should match + // |channel_rx_packet_count_| normally) + uint32_t socket_packet_sent_count_ = 0u; + // Count of packets received from the socket (should match + // |channel_Tx_packet_count_| normally) + uint32_t socket_packet_recv_count_ = 0u; + + // We use a std::deque here to minimize the number dynamic memory + // allocations (cf. std::list, which would require allocation on each + // SDU). This comes, however, at the cost of higher memory usage when the + // number of SDUs is small. (libc++ uses a minimum of 4KB per deque.) + // + // TODO(fxbug.dev/42150194): We should set an upper bound on the size of this + // queue. + std::deque socket_write_queue_; + + // Read buffer. This must be larger than the max_tx_sdu_size so that we can + // detect truncated datagrams. + DynamicByteBuffer read_buf_; + + WeakSelf weak_self_; // Keep last. + + BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(SocketChannelRelay); +}; + +template +SocketChannelRelay::SocketChannelRelay( + zx::socket socket, + typename ChannelT::WeakPtr channel, + DeactivationCallback deactivation_cb, + size_t socket_write_queue_max_frames) + : state_(RelayState::kActivating), + socket_(std::move(socket)), + channel_(std::move(channel)), + dispatcher_(async_get_default_dispatcher()), + deactivation_cb_(std::move(deactivation_cb)), + socket_write_queue_max_frames_(socket_write_queue_max_frames), + // Subtle: we make the read buffer larger than the TX MTU, so that we can + // detect truncated datagrams. + read_buf_(channel_->max_tx_sdu_size() + 1), + weak_self_(this) { + BT_ASSERT(dispatcher_); + BT_ASSERT(socket_); + BT_ASSERT(channel_.is_alive()); + + // Note: binding |this| is safe, as BindWait() wraps the bound method inside + // of a lambda which verifies that |this| hasn't been destroyed. + BindWait(ZX_SOCKET_READABLE, + "socket read waiter", + &sock_read_waiter_, + fit::bind_member<&SocketChannelRelay::OnSocketReadable>(this)); + BindWait(ZX_SOCKET_WRITE_THRESHOLD, + "socket write waiter", + &sock_write_waiter_, + fit::bind_member<&SocketChannelRelay::OnSocketWritable>(this)); + BindWait(ZX_SOCKET_PEER_CLOSED, + "socket close waiter", + &sock_close_waiter_, + fit::bind_member<&SocketChannelRelay::OnSocketClosed>(this)); +} + +template +SocketChannelRelay::~SocketChannelRelay() { + if (state_ != RelayState::kDeactivated) { + bt_log(TRACE, + "l2cap", + "Deactivating relay for channel %u in dtor", + channel_->id()); + Deactivate(); + } +} + +template +bool SocketChannelRelay::Activate() { + BT_ASSERT(state_ == RelayState::kActivating); + + // Note: we assume that BeginWait() does not synchronously dispatch any + // events. The wait handler will assert otherwise. + if (!BeginWait("socket close waiter", &sock_close_waiter_)) { + // Perhaps |dispatcher| is already stopped. + return false; + } + + if (!BeginWait("socket read waiter", &sock_read_waiter_)) { + // Perhaps |dispatcher| is already stopped. + return false; + } + + const auto self = weak_self_.GetWeakPtr(); + const auto channel_id = channel_->id(); + const bool activate_success = channel_->Activate( + [self, channel_id](ByteBufferPtr rx_data) { + // Note: this lambda _may_ be invoked immediately for buffered packets. + if (self.is_alive()) { + self->OnChannelDataReceived(std::move(rx_data)); + } else { + bt_log(TRACE, + "l2cap", + "Ignoring data received on destroyed relay (channel_id=%#.4x)", + channel_id); + } + }, + [self, channel_id] { + if (self.is_alive()) { + self->OnChannelClosed(); + } else { + bt_log( + TRACE, + "l2cap", + "Ignoring channel closure on destroyed relay (channel_id=%#.4x)", + channel_id); + } + }); + if (!activate_success) { + return false; + } + + state_ = RelayState::kActivated; + return true; +} + +template +void SocketChannelRelay::Deactivate() { + BT_ASSERT(state_ != RelayState::kDeactivated); + + state_ = RelayState::kDeactivating; + if (!socket_write_queue_.empty()) { + bt_log(DEBUG, + "l2cap", + "Dropping %zu packets from channel %u due to channel closure", + socket_write_queue_.size(), + channel_->id()); + socket_write_queue_.clear(); + } + channel_->Deactivate(); + + // We assume that UnbindAndCancelWait() will not trigger a re-entrant call + // into Deactivate(). And the RelayIsDestroyedWhenDispatcherIsShutDown test + // verifies that to be the case. (If we had re-entrant calls, a + // BT_ASSERT() in the lambda bound by BindWait() would cause an abort.) + UnbindAndCancelWait(&sock_read_waiter_); + UnbindAndCancelWait(&sock_write_waiter_); + UnbindAndCancelWait(&sock_close_waiter_); + socket_.reset(); + + // Any further callbacks are bugs. Update state_, to help us detect + // those bugs. + state_ = RelayState::kDeactivated; +} + +template +void SocketChannelRelay::DeactivateAndRequestDestruction() { + Deactivate(); + if (deactivation_cb_) { + // NOTE: deactivation_cb_ is expected to destroy |this|. Since |this| + // owns deactivation_cb_, we move() deactivation_cb_ outside of |this| + // before invoking the callback. + auto moved_deactivation_cb = std::move(deactivation_cb_); + moved_deactivation_cb(); + } +} + +template +void SocketChannelRelay::OnSocketReadable(zx_status_t status) { + BT_ASSERT(state_ == RelayState::kActivated); + if (!CopyFromSocketToChannel() || + !BeginWait("socket read waiter", &sock_read_waiter_)) { + DeactivateAndRequestDestruction(); + } +} + +template +void SocketChannelRelay::OnSocketWritable(zx_status_t status) { + BT_ASSERT(state_ == RelayState::kActivated); + if (socket_write_queue_.empty()) { + // The write queue may be emptied before this signal handler is called if + // the first packet in the write queue gets dropped and the subsequent + // smaller packets all fit into the socket. If canceling the wait fails, + // this handler may be called. + bt_log( + WARN, + "l2cap", + "socket_write_queue_ is empty in SocketChannelRelay::OnSocketWritable"); + return; + } + ServiceSocketWriteQueue(); +} + +template +void SocketChannelRelay::OnSocketClosed(zx_status_t status) { + BT_ASSERT(state_ == RelayState::kActivated); + DeactivateAndRequestDestruction(); +} + +template +void SocketChannelRelay::OnChannelDataReceived( + ByteBufferPtr rx_data) { + // Note: kActivating is deliberately permitted, as ChannelImpl::Activate() + // will synchronously deliver any queued frames. + BT_ASSERT(state_ != RelayState::kDeactivated); + TRACE_DURATION("bluetooth", + "SocketChannelRelay::OnChannelDataReceived", + "channel id", + channel_->id()); + + if (state_ == RelayState::kDeactivating) { + bt_log(DEBUG, + "l2cap", + "Ignoring %s on socket for channel %u while deactivating", + __func__, + channel_->id()); + return; + } + + BT_ASSERT(rx_data); + if (rx_data->size() == 0) { + bt_log(DEBUG, + "l2cap", + "Ignoring empty %s on socket on channel %u", + __func__, + channel_->id()); + return; + } + + BT_ASSERT(socket_write_queue_.size() <= socket_write_queue_max_frames_); + // On a full queue, we drop the oldest element, on the theory that newer data + // is more useful. This should be true, e.g., for real-time applications such + // as voice calls. In the future, we may want to make the drop-head vs. + // drop-tail choice configurable. + if (socket_write_queue_.size() == socket_write_queue_max_frames_) { + // TODO(fxbug.dev/42082614): Add a metric for number of dropped frames. + socket_write_queue_.pop_front(); + // Cancel the threshold wait, as the packet it corresponds to has been + // dropped. ServiceSocketWriteQueue() will start a new wait if necessary. + zx_status_t cancel_status = sock_write_waiter_.Cancel(); + if (cancel_status != ZX_OK) { + bt_log(WARN, + "l2cap", + "failed to cancel sock_write_waiter_ with status: %s", + zx_status_get_string(cancel_status)); + } + } + + channel_rx_packet_count_++; + TRACE_FLOW_BEGIN("bluetooth", + "SocketChannelRelay::OnChannelDataReceived queued", + GetTraceId(channel_rx_packet_count_)); + socket_write_queue_.push_back(std::move(rx_data)); + ServiceSocketWriteQueue(); +} + +template +void SocketChannelRelay::OnChannelClosed() { + BT_ASSERT(state_ != RelayState::kActivating); + BT_ASSERT(state_ != RelayState::kDeactivated); + + if (state_ == RelayState::kDeactivating) { + bt_log(DEBUG, + "l2cap", + "Ignorning %s on socket for channel %u while deactivating", + __func__, + channel_->id()); + return; + } + + BT_ASSERT(state_ == RelayState::kActivated); + if (!socket_write_queue_.empty()) { + ServiceSocketWriteQueue(); + } + DeactivateAndRequestDestruction(); +} + +template +bool SocketChannelRelay::CopyFromSocketToChannel() { + if (channel_->max_tx_sdu_size() > read_buf_.size()) { + read_buf_ = DynamicByteBuffer(channel_->max_tx_sdu_size() + 1); + } + // TODO(fxbug.dev/42153078): Consider yielding occasionally. As-is, we run the + // risk of starving other SocketChannelRelays on the same |dispatcher| (and + // anyone else on |dispatcher|), if a misbehaving process spams its + // zx::socket. And even if starvation isn't an issue, latency/jitter might be. + zx_status_t read_res; + do { + size_t n_bytes_read = 0; + read_res = socket_.read( + 0, read_buf_.mutable_data(), read_buf_.size(), &n_bytes_read); + BT_ASSERT_MSG(read_res == ZX_OK || read_res == ZX_ERR_SHOULD_WAIT || + read_res == ZX_ERR_PEER_CLOSED, + "%s", + zx_status_get_string(read_res)); + BT_ASSERT_MSG(n_bytes_read <= read_buf_.size(), + "(n_bytes_read=%zu, read_buf_size=%zu)", + n_bytes_read, + read_buf_.size()); + if (read_res == ZX_ERR_SHOULD_WAIT) { + return true; + } + + if (read_res == ZX_ERR_PEER_CLOSED) { + bt_log(TRACE, + "l2cap", + "Failed to read from socket for channel %u: %s", + channel_->id(), + zx_status_get_string(read_res)); + return false; + } + + BT_ASSERT(n_bytes_read > 0); + socket_packet_recv_count_++; + if (n_bytes_read > channel_->max_tx_sdu_size()) { + bt_log(TRACE, + "l2cap", + "Dropping %zu bytes for channel %u as max TX SDU is %u ", + n_bytes_read, + channel_->id(), + channel_->max_tx_sdu_size()); + return false; + } + + // TODO(fxbug.dev/42152967): For low latency and low jitter, IWBN to avoid + // allocating dynamic memory on every read. + bool write_success = channel_->Send( + std::make_unique(read_buf_.view(0, n_bytes_read))); + if (!write_success) { + bt_log(TRACE, + "l2cap", + "Failed to write %zu bytes to channel %u", + n_bytes_read, + channel_->id()); + } + channel_tx_packet_count_++; + } while (read_res == ZX_OK); + + return true; +} + +template +void SocketChannelRelay::ServiceSocketWriteQueue() { + // TODO(fxbug.dev/42150083): Similarly to CopyFromSocketToChannel(), we may + // want to consider yielding occasionally. The data-rate from the Channel into + // the socket write queue should be bounded by PHY layer data rates, which are + // much lower than the CPU's data processing throughput, so starvation + // shouldn't be an issue. However, latency might be. + zx_status_t write_res; + TRACE_DURATION("bluetooth", + "SocketChannelRelay::ServiceSocketWriteQueue", + "channel_id", + channel_->id()); + do { + BT_ASSERT(!socket_write_queue_.empty()); + BT_ASSERT(socket_write_queue_.front()); + TRACE_DURATION("bluetooth", + "SocketChannelRelay::ServiceSocketWriteQueue write", + "channel_id", + channel_->id()); + + const ByteBuffer& rx_data = *socket_write_queue_.front(); + BT_ASSERT_MSG(rx_data.size(), "Zero-length message on write queue"); + + socket_packet_sent_count_++; + TRACE_FLOW_END("bluetooth", + "SocketChannelRelay::OnChannelDataReceived queued", + GetTraceId(socket_packet_sent_count_)); + + // We probably need to make this unique across all profile sockets. + TRACE_FLOW_BEGIN("bluetooth", "ProfilePacket", socket_packet_sent_count_); + + size_t n_bytes_written = 0; + write_res = + socket_.write(0, rx_data.data(), rx_data.size(), &n_bytes_written); + BT_ASSERT_MSG(write_res == ZX_OK || write_res == ZX_ERR_SHOULD_WAIT || + write_res == ZX_ERR_PEER_CLOSED, + "%s", + zx_status_get_string(write_res)); + if (write_res != ZX_OK) { + BT_ASSERT(n_bytes_written == 0); + bt_log(TRACE, + "l2cap", + "Failed to write %zu bytes to socket for channel %u: %s", + rx_data.size(), + channel_->id(), + zx_status_get_string(write_res)); + break; + } + BT_ASSERT_MSG(n_bytes_written == rx_data.size(), + "(n_bytes_written=%zu, rx_data.size()=%zu)", + n_bytes_written, + rx_data.size()); + socket_write_queue_.pop_front(); + } while (write_res == ZX_OK && !socket_write_queue_.empty()); + + if (!socket_write_queue_.empty() && write_res == ZX_ERR_SHOULD_WAIT) { + // Since we hava data to write, we want to be woken when the socket has free + // space in its buffer. And, to avoid spinning, we want to be woken only + // when the free space is large enough for our first pending buffer. + // + // Note: it is safe to leave TX_THRESHOLD set, even when our queue is empty, + // because we will only be woken if we also have an active Wait for + // ZX_SOCKET_WRITE_THRESHOLD, and Waits are one-shot. + const size_t rx_data_len = socket_write_queue_.front()->size(); + const auto prop_set_res = socket_.set_property( + ZX_PROP_SOCKET_TX_THRESHOLD, &rx_data_len, sizeof(rx_data_len)); + switch (prop_set_res) { + case ZX_OK: + if (!BeginWait("socket write waiter", &sock_write_waiter_)) { + DeactivateAndRequestDestruction(); + } + break; + case ZX_ERR_PEER_CLOSED: + // Peer closed the socket after the while loop above. Nothing to do + // here, as closure event will be handled by OnSocketClosed(). + break; + default: + BT_PANIC("Unexpected zx_object_set_property() result: %s", + zx_status_get_string(prop_set_res)); + break; + } + } +} + +template +void SocketChannelRelay::BindWait( + zx_signals_t trigger, + const char* wait_name, + async::Wait* wait, + fit::function handler) { + wait->set_object(socket_.get()); + wait->set_trigger(trigger); + wait->set_handler( + [self = weak_self_.GetWeakPtr(), + channel_id = channel_->id(), + wait_name, + expected_wait = wait, + handler = std::move(handler)](async_dispatcher_t* actual_dispatcher, + async::WaitBase* actual_wait, + zx_status_t status, + const zx_packet_signal_t* signal) { + BT_ASSERT_MSG( + self.is_alive(), "(%s, channel_id=%u)", wait_name, channel_id); + BT_ASSERT_MSG(actual_dispatcher == self->dispatcher_, + "(%s, channel_id=%u)", + wait_name, + channel_id); + BT_ASSERT_MSG(actual_wait == expected_wait, + "(%s, channel_id=%u)", + wait_name, + channel_id); + BT_ASSERT_MSG(status == ZX_OK || status == ZX_ERR_CANCELED, + "(%s, channel_id=%u)", + wait_name, + channel_id); + + if (status == ZX_ERR_CANCELED) { // Dispatcher is shutting down. + bt_log(DEBUG, + "l2cap", + "%s canceled on socket for channel %u", + wait_name, + channel_id); + self->DeactivateAndRequestDestruction(); + return; + } + + BT_ASSERT_MSG(signal, "(%s, channel_id=%u)", wait_name, channel_id); + BT_ASSERT_MSG(signal->trigger == expected_wait->trigger(), + "(%s, channel_id=%u)", + wait_name, + channel_id); + BT_ASSERT_MSG(self->state_ != RelayState::kActivating, + "(%s, channel_id=%u)", + wait_name, + channel_id); + BT_ASSERT_MSG(self->state_ != RelayState::kDeactivated, + "(%s, channel_id=%u)", + wait_name, + channel_id); + + if (self->state_ == RelayState::kDeactivating) { + bt_log(DEBUG, + "l2cap", + "Ignorning %s on socket for channel %u while deactivating", + wait_name, + channel_id); + return; + } + handler(status); + }); +} + +template +bool SocketChannelRelay::BeginWait(const char* wait_name, + async::Wait* wait) { + BT_ASSERT(state_ != RelayState::kDeactivating); + BT_ASSERT(state_ != RelayState::kDeactivated); + + if (wait->is_pending()) { + return true; + } + + zx_status_t wait_res = wait->Begin(dispatcher_); + BT_ASSERT(wait_res == ZX_OK || wait_res == ZX_ERR_BAD_STATE); + + if (wait_res != ZX_OK) { + bt_log(ERROR, + "l2cap", + "Failed to enable waiting on %s: %s", + wait_name, + zx_status_get_string(wait_res)); + return false; + } + + return true; +} + +template +void SocketChannelRelay::UnbindAndCancelWait(async::Wait* wait) { + BT_ASSERT(state_ != RelayState::kActivating); + BT_ASSERT(state_ != RelayState::kDeactivated); + zx_status_t cancel_res; + wait->set_handler(nullptr); + cancel_res = wait->Cancel(); + BT_ASSERT_MSG(cancel_res == ZX_OK || cancel_res == ZX_ERR_NOT_FOUND, + "Cancel failed: %s", + zx_status_get_string(cancel_res)); +} + +template +trace_flow_id_t SocketChannelRelay::GetTraceId(uint32_t id) { + static_assert(sizeof(trace_flow_id_t) >= + (sizeof(typename ChannelT::UniqueId) + sizeof(uint32_t)), + "UniqueId needs to be small enough to make unique trace IDs"); + return (static_cast(channel_->unique_id()) + << (sizeof(uint32_t) * CHAR_BIT)) | + id; +} + +} // namespace bt::socket diff --git a/pw_bluetooth_sapphire/fuchsia/host/socket/public/pw_bluetooth_sapphire/fuchsia/host/socket/socket_factory.h b/pw_bluetooth_sapphire/fuchsia/host/socket/public/pw_bluetooth_sapphire/fuchsia/host/socket/socket_factory.h new file mode 100644 index 0000000000..6f41c8e74a --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/socket/public/pw_bluetooth_sapphire/fuchsia/host/socket/socket_factory.h @@ -0,0 +1,151 @@ +// Copyright 2024 The Pigweed Authors +// +// 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. + +#pragma once + +#include +#include +#include + +#include +#include + +#include "pw_bluetooth_sapphire/internal/host/common/assert.h" +#include "pw_bluetooth_sapphire/internal/host/common/log.h" +#include "pw_bluetooth_sapphire/internal/host/common/macros.h" +#include "pw_bluetooth_sapphire/internal/host/common/weak_self.h" +#include "socket_channel_relay.h" + +namespace bt::socket { + +// A SocketFactory vends zx::socket objects that an IPC peer can use to +// communicate with l2cap::Channels. +// +// Over time, the factory may grow more responsibility and intelligence. For +// example, the factory might manage QoS by configuring the number of packets a +// SocketChannelRelay can process before yielding control back to the +// dispatcher. +// +// THREAD-SAFETY: This class is thread-hostile. An instance must be +// created and destroyed on a single thread. Said thread must have a +// single-threaded dispatcher. Failure to follow those rules may cause the +// program to abort. +template +class SocketFactory final { + public: + SocketFactory(); + ~SocketFactory(); + + // Creates a zx::socket which can be used to read from, and write to, + // |channel|. + // + // |channel| will automatically be Deactivated() when the zx::socket is + // closed, or the creation thread's dispatcher shuts down. + // + // |closed_callback| will be called when the channel or socket is closed. This + // callback may be nullptr to ignore closures. + // + // Similarly, the local end corresponding to the returned zx::socket will + // automatically be closed when |channel| is closed, or the creation thread's + // dispatcher shuts down. + // + // It is an error to call MakeSocketForChannel() multiple times for + // the same Channel. + // + // Returns the new socket on success, and an invalid socket otherwise + // (including if |channel| is nullptr). + zx::socket MakeSocketForChannel( + typename ChannelT::WeakPtr channel, + fit::callback closed_callback = nullptr); + + private: + using RelayT = SocketChannelRelay; + using ChannelIdT = typename ChannelT::UniqueId; + + // TODO(fxbug.dev/42145980): Figure out what we need to do handle the + // possibility that a channel id is recycled. (See comment in + // LogicalLink::HandleRxPacket.) + std::unordered_map> channel_to_relay_; + + WeakSelf weak_self_; // Keep last. + + BT_DISALLOW_COPY_AND_ASSIGN_ALLOW_MOVE(SocketFactory); +}; + +template +SocketFactory::SocketFactory() : weak_self_(this) {} + +template +SocketFactory::~SocketFactory() {} + +template +zx::socket SocketFactory::MakeSocketForChannel( + typename ChannelT::WeakPtr channel, fit::callback closed_callback) { + if (!channel.is_alive()) { + return zx::socket(); + } + + const auto unique_id = channel->unique_id(); + if (channel_to_relay_.find(unique_id) != channel_to_relay_.end()) { + bt_log(ERROR, + "l2cap", + "channel %u is already bound to a socket", + channel->id()); + return zx::socket(); + } + + zx::socket local_socket, remote_socket; + const auto status = + zx::socket::create(ZX_SOCKET_DATAGRAM, &local_socket, &remote_socket); + if (status != ZX_OK) { + bt_log(ERROR, + "data", + "Failed to create socket for channel %u: %s", + channel->unique_id(), + zx_status_get_string(status)); + return zx::socket(); + } + + auto relay = std::make_unique( + std::move(local_socket), + channel, + typename RelayT::DeactivationCallback( + [self = weak_self_.GetWeakPtr(), + id = unique_id, + closed_cb = std::move(closed_callback)]() mutable { + BT_DEBUG_ASSERT_MSG(self.is_alive(), "(unique_id=%u)", id); + size_t n_erased = self->channel_to_relay_.erase(id); + BT_DEBUG_ASSERT_MSG( + n_erased == 1, "(n_erased=%zu, unique_id=%u)", n_erased, id); + + if (closed_cb) { + closed_cb(); + } + })); + + // Note: Activate() may abort, if |channel| has been Activated() without + // going through this SocketFactory. + if (!relay->Activate()) { + bt_log(ERROR, + "l2cap", + "Failed to Activate() relay for channel %u", + channel->id()); + return zx::socket(); + } + + channel_to_relay_.emplace(unique_id, std::move(relay)); + return remote_socket; +} + +} // namespace bt::socket diff --git a/pw_bluetooth_sapphire/fuchsia/host/socket/socket_channel_relay_test.cc b/pw_bluetooth_sapphire/fuchsia/host/socket/socket_channel_relay_test.cc new file mode 100644 index 0000000000..3cba172ae5 --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/socket/socket_channel_relay_test.cc @@ -0,0 +1,815 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/socket/socket_channel_relay.h" + +#include +#include +#include + +#include +#include + +#include "pw_bluetooth_sapphire/internal/host/common/assert.h" +#include "pw_bluetooth_sapphire/internal/host/common/log.h" +#include "pw_bluetooth_sapphire/internal/host/l2cap/fake_channel.h" +#include "pw_bluetooth_sapphire/internal/host/testing/test_helpers.h" +#include "pw_unit_test/framework.h" + +namespace bt::socket { +namespace { + +// We'll test the template just for L2CAP channels. +using RelayT = SocketChannelRelay; +constexpr size_t kDefaultSocketWriteQueueLimitFrames = 2; + +class SocketChannelRelayTest : public ::testing::Test { + public: + SocketChannelRelayTest() + : loop_(&kAsyncLoopConfigAttachToCurrentThread), + pw_dispatcher_(dispatcher()) { + EXPECT_EQ(ASYNC_LOOP_RUNNABLE, loop_.GetState()); + + constexpr l2cap::ChannelId kDynamicChannelIdMin = 0x0040; + constexpr l2cap::ChannelId kRemoteChannelId = 0x0050; + constexpr hci_spec::ConnectionHandle kDefaultConnectionHandle = 0x0001; + channel_ = + std::make_unique(kDynamicChannelIdMin, + kRemoteChannelId, + kDefaultConnectionHandle, + bt::LinkType::kACL); + + const auto socket_status = + zx::socket::create(ZX_SOCKET_DATAGRAM, &local_socket_, &remote_socket_); + local_socket_unowned_ = zx::unowned_socket(local_socket_); + EXPECT_EQ(ZX_OK, socket_status); + } + + pw::async::Dispatcher& pw_dispatcher() { return pw_dispatcher_; } + + // Writes data on |local_socket| until the socket is full, or an error occurs. + // Returns the number of bytes written if the socket fills, and zero + // otherwise. + [[nodiscard]] size_t StuffSocket() { + size_t n_total_bytes_written = 0; + zx_status_t write_res; + // Fill the socket buffer completely, while minimzing the number of + // syscalls required. + for (const auto spam_size_bytes : {65536, + 32768, + 16384, + 8192, + 4096, + 2048, + 1024, + 512, + 256, + 128, + 64, + 32, + 16, + 8, + 4, + 2, + 1}) { + DynamicByteBuffer spam_data(spam_size_bytes); + spam_data.Fill(kSpamChar); + do { + size_t n_iter_bytes_written = 0; + write_res = local_socket_unowned_->write( + 0, spam_data.data(), spam_data.size(), &n_iter_bytes_written); + if (write_res != ZX_OK && write_res != ZX_ERR_SHOULD_WAIT) { + bt_log(ERROR, + "l2cap", + "Failure in zx_socket_write(): %s", + zx_status_get_string(write_res)); + return 0; + } + n_total_bytes_written += n_iter_bytes_written; + } while (write_res == ZX_OK); + } + return n_total_bytes_written; + } + + // Reads and discards |n_bytes| on |remote_socket|. Returns true if at-least + // |n_bytes| were successfully discarded. (The actual number of discarded + // bytes is not known, as a pending datagram may be larger than our read + // buffer.) + [[nodiscard]] bool DiscardFromSocket(size_t n_bytes_requested) { + DynamicByteBuffer received_data(n_bytes_requested); + zx_status_t read_res; + size_t n_total_bytes_read = 0; + while (n_total_bytes_read < n_bytes_requested) { + size_t n_iter_bytes_read = 0; + read_res = remote_socket_.read(0, + received_data.mutable_data(), + received_data.size(), + &n_iter_bytes_read); + if (read_res != ZX_OK && read_res != ZX_ERR_SHOULD_WAIT) { + bt_log(ERROR, + "l2cap", + "Failure in zx_socket_read(): %s", + zx_status_get_string(read_res)); + return false; + } + if (read_res == ZX_ERR_SHOULD_WAIT) { + EXPECT_EQ(n_bytes_requested, n_total_bytes_read); + return false; + } else { + n_total_bytes_read += n_iter_bytes_read; + } + } + return true; + } + + protected: + static constexpr auto kGoodChar = 'a'; + static constexpr auto kSpamChar = 'b'; + l2cap::testing::FakeChannel* channel() { return channel_.get(); } + async_dispatcher_t* dispatcher() { return loop_.dispatcher(); } + zx::socket* local_socket() { return &local_socket_; } + zx::unowned_socket local_socket_unowned() { + return zx::unowned_socket(local_socket_unowned_); + } + zx::socket* remote_socket() { return &remote_socket_; } + zx::socket ConsumeLocalSocket() { return std::move(local_socket_); } + void CloseRemoteSocket() { remote_socket_.reset(); } + // Note: A call to RunLoopOnce() may cause multiple timer-based tasks + // to be dispatched. (When the timer expires, async_loop_run_once() dispatches + // all expired timer-based tasks.) + void RunLoopOnce() { loop_.Run(zx::time::infinite(), /*once=*/true); } + void RunLoopUntilIdle() { loop_.RunUntilIdle(); } + void ShutdownLoop() { loop_.Shutdown(); } + + private: + std::unique_ptr channel_; + zx::socket local_socket_; + zx::socket remote_socket_; + zx::unowned_socket local_socket_unowned_; + // TODO(fxbug.dev/42150969): Move to FakeChannelTest, which wraps pw_async. + async::Loop loop_; + pw::async_fuchsia::FuchsiaDispatcher pw_dispatcher_; +}; + +class SocketChannelRelayLifetimeTest : public SocketChannelRelayTest { + public: + SocketChannelRelayLifetimeTest() + : was_deactivation_callback_invoked_(false), + relay_(std::make_unique( + ConsumeLocalSocket(), channel()->GetWeakPtr(), [this]() { + was_deactivation_callback_invoked_ = true; + })) {} + + protected: + bool was_deactivation_callback_invoked() { + return was_deactivation_callback_invoked_; + } + RelayT* relay() { + BT_DEBUG_ASSERT(relay_); + return relay_.get(); + } + void DestroyRelay() { relay_ = nullptr; } + + private: + bool was_deactivation_callback_invoked_; + std::unique_ptr relay_; +}; + +TEST_F(SocketChannelRelayLifetimeTest, ActivateFailsIfGivenStoppedDispatcher) { + ShutdownLoop(); + EXPECT_FALSE(relay()->Activate()); +} + +TEST_F(SocketChannelRelayLifetimeTest, + ActivateDoesNotInvokeDeactivationCallbackOnSuccess) { + ASSERT_TRUE(relay()->Activate()); + EXPECT_FALSE(was_deactivation_callback_invoked()); +} + +TEST_F(SocketChannelRelayLifetimeTest, + ActivateDoesNotInvokeDeactivationCallbackOnFailure) { + ShutdownLoop(); + ASSERT_FALSE(relay()->Activate()); + EXPECT_FALSE(was_deactivation_callback_invoked()); +} + +TEST_F(SocketChannelRelayLifetimeTest, SocketIsClosedWhenRelayIsDestroyed) { + const char data = kGoodChar; + ASSERT_EQ(ZX_OK, remote_socket()->write(0, &data, sizeof(data), nullptr)); + DestroyRelay(); + EXPECT_EQ(ZX_ERR_PEER_CLOSED, + remote_socket()->write(0, &data, sizeof(data), nullptr)); +} + +TEST_F(SocketChannelRelayLifetimeTest, + RelayIsDeactivatedWhenDispatcherIsShutDown) { + ASSERT_TRUE(relay()->Activate()); + + ShutdownLoop(); + EXPECT_TRUE(was_deactivation_callback_invoked()); +} + +TEST_F(SocketChannelRelayLifetimeTest, + RelayActivationFailsIfChannelActivationFails) { + channel()->set_activate_fails(true); + EXPECT_FALSE(relay()->Activate()); +} + +TEST_F(SocketChannelRelayLifetimeTest, + DestructionWithPendingSdusFromChannelDoesNotCrash) { + ASSERT_TRUE(relay()->Activate()); + channel()->Receive(StaticByteBuffer('h', 'e', 'l', 'l', 'o')); + DestroyRelay(); + RunLoopUntilIdle(); +} + +TEST_F(SocketChannelRelayLifetimeTest, RelayIsDeactivatedWhenChannelIsClosed) { + ASSERT_TRUE(relay()->Activate()); + + channel()->Close(); + EXPECT_TRUE(was_deactivation_callback_invoked()); +} + +TEST_F(SocketChannelRelayLifetimeTest, + RelayIsDeactivatedWhenRemoteSocketIsClosed) { + ASSERT_TRUE(relay()->Activate()); + + CloseRemoteSocket(); + RunLoopUntilIdle(); + EXPECT_TRUE(was_deactivation_callback_invoked()); +} + +TEST_F(SocketChannelRelayLifetimeTest, + RelayIsDeactivatedWhenRemoteSocketIsClosedEvenWithPendingSocketData) { + ASSERT_TRUE(relay()->Activate()); + ASSERT_TRUE(StuffSocket()); + + channel()->Receive(StaticByteBuffer('h', 'e', 'l', 'l', 'o')); + RunLoopUntilIdle(); + ASSERT_FALSE(was_deactivation_callback_invoked()); + + CloseRemoteSocket(); + RunLoopUntilIdle(); + EXPECT_TRUE(was_deactivation_callback_invoked()); +} + +TEST_F(SocketChannelRelayLifetimeTest, OversizedDatagramDeactivatesRelay) { + const size_t kMessageBufSize = channel()->max_tx_sdu_size() * 5; + DynamicByteBuffer large_message(kMessageBufSize); + large_message.Fill('a'); + ASSERT_TRUE(relay()->Activate()); + + size_t n_bytes_written_to_socket = 0; + const auto write_res = remote_socket()->write(0, + large_message.data(), + large_message.size(), + &n_bytes_written_to_socket); + ASSERT_EQ(ZX_OK, write_res); + ASSERT_EQ(large_message.size(), n_bytes_written_to_socket); + RunLoopUntilIdle(); + + EXPECT_TRUE(was_deactivation_callback_invoked()); +} + +TEST_F(SocketChannelRelayLifetimeTest, + SocketClosureAfterChannelClosureDoesNotHangOrCrash) { + ASSERT_TRUE(relay()->Activate()); + channel()->Close(); + ASSERT_TRUE(was_deactivation_callback_invoked()); + + CloseRemoteSocket(); + RunLoopUntilIdle(); +} + +TEST_F(SocketChannelRelayLifetimeTest, + ChannelClosureAfterSocketClosureDoesNotHangOrCrash) { + ASSERT_TRUE(relay()->Activate()); + CloseRemoteSocket(); + RunLoopUntilIdle(); + + channel()->Close(); + ASSERT_TRUE(was_deactivation_callback_invoked()); +} + +TEST_F(SocketChannelRelayLifetimeTest, DeactivationClosesSocket) { + ASSERT_TRUE(relay()->Activate()); + channel()->Close(); // Triggers relay deactivation. + + const char data = kGoodChar; + EXPECT_EQ(ZX_ERR_PEER_CLOSED, + remote_socket()->write(0, &data, sizeof(data), nullptr)); +} + +class SocketChannelRelayDataPathTest : public SocketChannelRelayTest { + public: + SocketChannelRelayDataPathTest() + : relay_(ConsumeLocalSocket(), + channel()->GetWeakPtr(), + /*deactivation_cb=*/nullptr, + kDefaultSocketWriteQueueLimitFrames) { + channel()->SetSendCallback( + [&](auto data) { sent_to_channel_.push_back(std::move(data)); }, + pw_dispatcher()); + } + + protected: + RelayT* relay() { return &relay_; } + auto& sent_to_channel() { return sent_to_channel_; } + + private: + RelayT relay_; + std::vector sent_to_channel_; +}; + +// Fixture for tests which exercise the datapath from the controller. +class SocketChannelRelayRxTest : public SocketChannelRelayDataPathTest { + protected: + DynamicByteBuffer ReadDatagramFromSocket(const size_t dgram_len) { + DynamicByteBuffer socket_read_buffer(dgram_len + + 1); // +1 to detect trailing garbage. + size_t n_bytes_read = 0; + const auto read_res = + remote_socket()->read(0, + socket_read_buffer.mutable_data(), + socket_read_buffer.size(), + &n_bytes_read); + if (read_res != ZX_OK) { + bt_log(ERROR, + "l2cap", + "Failure in zx_socket_read(): %s", + zx_status_get_string(read_res)); + return {}; + } + return DynamicByteBuffer(BufferView(socket_read_buffer, n_bytes_read)); + } +}; + +TEST_F(SocketChannelRelayRxTest, + MessageFromChannelIsCopiedToSocketSynchronously) { + const StaticByteBuffer kExpectedMessage('h', 'e', 'l', 'l', 'o'); + ASSERT_TRUE(relay()->Activate()); + channel()->Receive(kExpectedMessage); + // The data should be copied synchronously, so the async loop should not be + // run here. + EXPECT_TRUE(ContainersEqual(kExpectedMessage, + ReadDatagramFromSocket(kExpectedMessage.size()))); +} + +TEST_F(SocketChannelRelayRxTest, + MultipleSdusFromChannelAreCopiedToSocketPreservingSduBoundaries) { + const StaticByteBuffer kExpectedMessage1('h', 'e', 'l', 'l', 'o'); + const StaticByteBuffer kExpectedMessage2('g', 'o', 'o', 'd', 'b', 'y', 'e'); + ASSERT_TRUE(relay()->Activate()); + channel()->Receive(kExpectedMessage1); + channel()->Receive(kExpectedMessage2); + RunLoopUntilIdle(); + + EXPECT_TRUE(ContainersEqual( + kExpectedMessage1, ReadDatagramFromSocket(kExpectedMessage1.size()))); + EXPECT_TRUE(ContainersEqual( + kExpectedMessage2, ReadDatagramFromSocket(kExpectedMessage2.size()))); +} + +TEST_F(SocketChannelRelayRxTest, + SduFromChannelIsCopiedToSocketWhenSocketUnblocks) { + size_t n_junk_bytes = StuffSocket(); + ASSERT_TRUE(n_junk_bytes); + + const StaticByteBuffer kExpectedMessage('h', 'e', 'l', 'l', 'o'); + ASSERT_TRUE(relay()->Activate()); + channel()->Receive(kExpectedMessage); + RunLoopUntilIdle(); + + ASSERT_TRUE(DiscardFromSocket(n_junk_bytes)); + RunLoopUntilIdle(); + EXPECT_TRUE(ContainersEqual(kExpectedMessage, + ReadDatagramFromSocket(kExpectedMessage.size()))); +} + +TEST_F(SocketChannelRelayRxTest, CanQueueAndWriteMultipleSDUs) { + size_t n_junk_bytes = StuffSocket(); + ASSERT_TRUE(n_junk_bytes); + + const StaticByteBuffer kExpectedMessage1('h', 'e', 'l', 'l', 'o'); + const StaticByteBuffer kExpectedMessage2('g', 'o', 'o', 'd', 'b', 'y', 'e'); + ASSERT_TRUE(relay()->Activate()); + channel()->Receive(kExpectedMessage1); + channel()->Receive(kExpectedMessage2); + RunLoopUntilIdle(); + + ASSERT_TRUE(DiscardFromSocket(n_junk_bytes)); + // Run only one task. This verifies that the relay writes both pending SDUs in + // one shot, rather than re-arming the async::Wait for each SDU. + RunLoopOnce(); + + EXPECT_TRUE(ContainersEqual( + kExpectedMessage1, ReadDatagramFromSocket(kExpectedMessage1.size()))); + EXPECT_TRUE(ContainersEqual( + kExpectedMessage2, ReadDatagramFromSocket(kExpectedMessage2.size()))); +} + +TEST_F(SocketChannelRelayRxTest, CanQueueAndIncrementallyWriteMultipleSDUs) { + // Find the socket buffer size. + const size_t socket_buffer_size = StuffSocket(); + ASSERT_TRUE(DiscardFromSocket(socket_buffer_size)); + + // Stuff the socket manually, rather than using StuffSocket(), so that we know + // exactly how much buffer space we free, as we read datagrams out of + // |remote_socket()|. + constexpr size_t kLargeSduSize = 1023; + zx_status_t write_res = ZX_ERR_INTERNAL; + DynamicByteBuffer spam_sdu(kLargeSduSize); + size_t n_junk_bytes = 0; + size_t n_junk_datagrams = 0; + spam_sdu.Fill('s'); + do { + size_t n_iter_bytes_written = 0; + write_res = local_socket_unowned()->write( + 0, spam_sdu.data(), spam_sdu.size(), &n_iter_bytes_written); + ASSERT_TRUE(write_res == ZX_OK || write_res == ZX_ERR_SHOULD_WAIT) + << "Failure in zx_socket_write: " << zx_status_get_string(write_res); + if (write_res == ZX_OK) { + ASSERT_EQ(spam_sdu.size(), n_iter_bytes_written); + n_junk_bytes += spam_sdu.size(); + n_junk_datagrams += 1; + } + } while (write_res == ZX_OK); + ASSERT_NE(socket_buffer_size, n_junk_bytes) + << "Need non-zero free space in socket buffer."; + + DynamicByteBuffer hello_sdu(kLargeSduSize); + DynamicByteBuffer goodbye_sdu(kLargeSduSize); + hello_sdu.Fill('h'); + goodbye_sdu.Fill('g'); + ASSERT_TRUE(relay()->Activate()); + channel()->Receive(hello_sdu); + channel()->Receive(goodbye_sdu); + RunLoopUntilIdle(); + + // Free up space for just the first SDU. + ASSERT_TRUE( + ContainersEqual(spam_sdu, ReadDatagramFromSocket(spam_sdu.size()))); + n_junk_datagrams -= 1; + RunLoopUntilIdle(); + + // Free up space for just the second SDU. + ASSERT_TRUE( + ContainersEqual(spam_sdu, ReadDatagramFromSocket(spam_sdu.size()))); + n_junk_datagrams -= 1; + RunLoopUntilIdle(); + + // Discard spam. + while (n_junk_datagrams) { + ASSERT_TRUE( + ContainersEqual(spam_sdu, ReadDatagramFromSocket(spam_sdu.size()))); + n_junk_datagrams -= 1; + } + + // Read out our expected datagrams, verifying that boundaries are preserved. + EXPECT_TRUE( + ContainersEqual(hello_sdu, ReadDatagramFromSocket(hello_sdu.size()))); + EXPECT_TRUE( + ContainersEqual(goodbye_sdu, ReadDatagramFromSocket(goodbye_sdu.size()))); + EXPECT_EQ(0u, ReadDatagramFromSocket(1u).size()) + << "Found unexpected datagram"; +} + +TEST_F(SocketChannelRelayRxTest, ZeroByteSDUsDropped) { + const StaticByteBuffer kMessage1('h', 'e', 'l', 'l', 'o'); + DynamicByteBuffer kMessageZero(0); + const StaticByteBuffer kMessage3('f', 'u', 'c', 'h', 's', 'i', 'a'); + + ASSERT_TRUE(relay()->Activate()); + channel()->Receive(kMessageZero); + channel()->Receive(kMessage1); + channel()->Receive(kMessageZero); + channel()->Receive(kMessage3); + channel()->Receive(kMessageZero); + RunLoopUntilIdle(); + + ASSERT_TRUE( + ContainersEqual(kMessage1, ReadDatagramFromSocket(kMessage1.size()))); + ASSERT_TRUE( + ContainersEqual(kMessage3, ReadDatagramFromSocket(kMessage3.size()))); + + EXPECT_EQ(0u, ReadDatagramFromSocket(1u).size()) + << "Found unexpected datagram"; +} + +TEST_F(SocketChannelRelayRxTest, OldestSDUIsDroppedOnOverflow) { + size_t n_junk_bytes = StuffSocket(); + ASSERT_TRUE(n_junk_bytes); + + const StaticByteBuffer kSentMessage1(1); + const StaticByteBuffer kSentMessage2(2); + const StaticByteBuffer kSentMessage3(3); + ASSERT_TRUE(relay()->Activate()); + channel()->Receive(kSentMessage1); + channel()->Receive(kSentMessage2); + channel()->Receive(kSentMessage3); + RunLoopUntilIdle(); + + ASSERT_TRUE(DiscardFromSocket(n_junk_bytes)); + RunLoopUntilIdle(); + + EXPECT_TRUE(ContainersEqual(kSentMessage2, + ReadDatagramFromSocket(kSentMessage2.size()))); + EXPECT_TRUE(ContainersEqual(kSentMessage3, + ReadDatagramFromSocket(kSentMessage3.size()))); +} + +TEST_F(SocketChannelRelayRxTest, + SdusReceivedBeforeChannelActivationAreCopiedToSocket) { + const StaticByteBuffer kExpectedMessage1('h', 'e', 'l', 'l', 'o'); + const StaticByteBuffer kExpectedMessage2('g', 'o', 'o', 'd', 'b', 'y', 'e'); + channel()->Receive(kExpectedMessage1); + channel()->Receive(kExpectedMessage2); + ASSERT_TRUE(relay()->Activate()); + // Note: we omit RunLoopOnce()/RunLoopUntilIdle(), as Channel activation + // delivers the messages synchronously. + + EXPECT_TRUE(ContainersEqual( + kExpectedMessage1, ReadDatagramFromSocket(kExpectedMessage1.size()))); + EXPECT_TRUE(ContainersEqual( + kExpectedMessage2, ReadDatagramFromSocket(kExpectedMessage2.size()))); +} + +TEST_F(SocketChannelRelayRxTest, SdusPendingAtChannelClosureAreCopiedToSocket) { + ASSERT_TRUE(StuffSocket()); + ASSERT_TRUE(relay()->Activate()); + + const StaticByteBuffer kExpectedMessage1('h'); + const StaticByteBuffer kExpectedMessage2('i'); + channel()->Receive(kExpectedMessage1); + channel()->Receive(kExpectedMessage2); + RunLoopUntilIdle(); + + // Discard two datagrams from socket, to make room for our SDUs to be copied + // over. + ASSERT_NE(0u, ReadDatagramFromSocket(1u).size()); + ASSERT_NE(0u, ReadDatagramFromSocket(1u).size()); + channel()->Close(); + + // Read past all of the spam from StuffSocket(). + DynamicByteBuffer dgram; + do { + dgram = ReadDatagramFromSocket(1u); + } while (dgram.size() && dgram[0] == kSpamChar); + + // First non-spam message should be kExpectedMessage1, and second should be + // kExpectedMessage2. + EXPECT_TRUE(ContainersEqual(kExpectedMessage1, dgram)); + EXPECT_TRUE(ContainersEqual(kExpectedMessage2, ReadDatagramFromSocket(1u))); +} + +TEST_F(SocketChannelRelayRxTest, + ReceivingFromChannelBetweenSocketCloseAndCloseWaitTriggerDoesNotCrash) { + // Note: we call Channel::Receive() first, to force FakeChannel to deliver the + // SDU synchronously to the SocketChannelRelay. Asynchronous delivery could + // compromise the test's validity, since that would allow OnSocketClosed() to + // be invoked before OnChannelDataReceived(). + channel()->Receive(StaticByteBuffer(kGoodChar)); + CloseRemoteSocket(); + ASSERT_TRUE(relay()->Activate()); +} + +TEST_F( + SocketChannelRelayRxTest, + SocketCloseBetweenReceivingFromChannelAndSocketWritabilityDoesNotCrashOrHang) { + ASSERT_TRUE(relay()->Activate()); + + size_t n_junk_bytes = StuffSocket(); + ASSERT_TRUE(n_junk_bytes); + channel()->Receive(StaticByteBuffer(kGoodChar)); + RunLoopUntilIdle(); + + ASSERT_TRUE(DiscardFromSocket(n_junk_bytes)); + CloseRemoteSocket(); + RunLoopUntilIdle(); +} + +TEST_F(SocketChannelRelayRxTest, + NoDataFromChannelIsWrittenToSocketAfterDeactivation) { + ASSERT_TRUE(relay()->Activate()); + + size_t n_junk_bytes = StuffSocket(); + ASSERT_TRUE(n_junk_bytes); + + channel()->Receive(StaticByteBuffer('h', 'e', 'l', 'l', 'o')); + RunLoopUntilIdle(); + + channel()->Close(); // Triggers relay deactivation. + ASSERT_TRUE(DiscardFromSocket(n_junk_bytes)); + RunLoopUntilIdle(); + + zx_info_socket_t info = {}; + info.rx_buf_available = std::numeric_limits::max(); + const auto status = remote_socket()->get_info(ZX_INFO_SOCKET, + &info, + sizeof(info), + /*actual_count=*/nullptr, + /*avail_count=*/nullptr); + EXPECT_EQ(ZX_OK, status); + EXPECT_EQ(0u, info.rx_buf_available); +} + +// Alias for the fixture for tests which exercise the datapath to the +// controller. +using SocketChannelRelayTxTest = SocketChannelRelayDataPathTest; + +TEST_F(SocketChannelRelayTxTest, SduFromSocketIsCopiedToChannel) { + const StaticByteBuffer kExpectedMessage('h', 'e', 'l', 'l', 'o'); + ASSERT_TRUE(relay()->Activate()); + + size_t n_bytes_written = 0; + const auto write_res = remote_socket()->write( + 0, kExpectedMessage.data(), kExpectedMessage.size(), &n_bytes_written); + ASSERT_EQ(ZX_OK, write_res); + ASSERT_EQ(kExpectedMessage.size(), n_bytes_written); + RunLoopUntilIdle(); + + const auto& sdus = sent_to_channel(); + ASSERT_FALSE(sdus.empty()); + EXPECT_EQ(1u, sdus.size()); + ASSERT_TRUE(sdus[0]); + EXPECT_EQ(kExpectedMessage.size(), sdus[0]->size()); + EXPECT_TRUE(ContainersEqual(kExpectedMessage, *sdus[0])); +} + +TEST_F(SocketChannelRelayTxTest, MultipleSdusFromSocketAreCopiedToChannel) { + const StaticByteBuffer kExpectedMessage('h', 'e', 'l', 'l', 'o'); + const size_t kNumMessages = 3; + ASSERT_TRUE(relay()->Activate()); + + for (size_t i = 0; i < kNumMessages; ++i) { + size_t n_bytes_written = 0; + const auto write_res = remote_socket()->write( + 0, kExpectedMessage.data(), kExpectedMessage.size(), &n_bytes_written); + ASSERT_EQ(ZX_OK, write_res); + ASSERT_EQ(kExpectedMessage.size(), n_bytes_written); + RunLoopUntilIdle(); + } + + const auto& sdus = sent_to_channel(); + ASSERT_FALSE(sdus.empty()); + ASSERT_EQ(3U, sdus.size()); + ASSERT_TRUE(sdus[0]); + ASSERT_TRUE(sdus[1]); + ASSERT_TRUE(sdus[2]); + EXPECT_TRUE(ContainersEqual(kExpectedMessage, *sdus[0])); + EXPECT_TRUE(ContainersEqual(kExpectedMessage, *sdus[1])); + EXPECT_TRUE(ContainersEqual(kExpectedMessage, *sdus[2])); +} + +TEST_F(SocketChannelRelayTxTest, MultipleSdusAreCopiedToChannelInOneRelayTask) { + const StaticByteBuffer kExpectedMessage('h', 'e', 'l', 'l', 'o'); + const size_t kNumMessages = 3; + ASSERT_TRUE(relay()->Activate()); + + for (size_t i = 0; i < kNumMessages; ++i) { + size_t n_bytes_written = 0; + const auto write_res = remote_socket()->write( + 0, kExpectedMessage.data(), kExpectedMessage.size(), &n_bytes_written); + ASSERT_EQ(ZX_OK, write_res); + ASSERT_EQ(kExpectedMessage.size(), n_bytes_written); + } + + RunLoopOnce(); // Runs SocketChannelRelay::OnSocketReadable(). + RunLoopOnce(); // Runs all tasks queued by FakeChannel::Send(). + + const auto& sdus = sent_to_channel(); + ASSERT_FALSE(sdus.empty()); + ASSERT_EQ(3U, sdus.size()); + ASSERT_TRUE(sdus[0]); + ASSERT_TRUE(sdus[1]); + ASSERT_TRUE(sdus[2]); + EXPECT_TRUE(ContainersEqual(kExpectedMessage, *sdus[0])); + EXPECT_TRUE(ContainersEqual(kExpectedMessage, *sdus[1])); + EXPECT_TRUE(ContainersEqual(kExpectedMessage, *sdus[2])); +} + +TEST_F(SocketChannelRelayTxTest, OversizedSduIsDropped) { + const size_t kMessageBufSize = channel()->max_tx_sdu_size() * 5; + DynamicByteBuffer large_message(kMessageBufSize); + large_message.Fill(kGoodChar); + ASSERT_TRUE(relay()->Activate()); + + size_t n_bytes_written_to_socket = 0; + const auto write_res = remote_socket()->write(0, + large_message.data(), + large_message.size(), + &n_bytes_written_to_socket); + ASSERT_EQ(ZX_OK, write_res); + ASSERT_EQ(large_message.size(), n_bytes_written_to_socket); + RunLoopUntilIdle(); + + ASSERT_TRUE(sent_to_channel().empty()); +} + +TEST_F(SocketChannelRelayTxTest, ValidSduAfterOversizedSduIsIgnored) { + const StaticByteBuffer kSentMsg('h', 'e', 'l', 'l', 'o'); + ASSERT_TRUE(relay()->Activate()); + + { + DynamicByteBuffer dropped_msg(channel()->max_tx_sdu_size() + 1); + size_t n_bytes_written = 0; + zx_status_t write_res = ZX_ERR_INTERNAL; + dropped_msg.Fill(kGoodChar); + write_res = remote_socket()->write( + 0, dropped_msg.data(), dropped_msg.size(), &n_bytes_written); + ASSERT_EQ(ZX_OK, write_res); + ASSERT_EQ(dropped_msg.size(), n_bytes_written); + } + + { + size_t n_bytes_written = 0; + zx_status_t write_res = ZX_ERR_INTERNAL; + write_res = remote_socket()->write( + 0, kSentMsg.data(), kSentMsg.size(), &n_bytes_written); + ASSERT_EQ(ZX_OK, write_res); + ASSERT_EQ(kSentMsg.size(), n_bytes_written); + } + + RunLoopUntilIdle(); + EXPECT_TRUE(sent_to_channel().empty()); +} + +TEST_F(SocketChannelRelayTxTest, + NewSocketDataAfterChannelClosureIsNotSentToChannel) { + ASSERT_TRUE(relay()->Activate()); + channel()->Close(); + + const char data = kGoodChar; + const auto write_res = + remote_socket()->write(0, &data, sizeof(data), /*actual=*/nullptr); + ASSERT_TRUE(write_res == ZX_OK || write_res == ZX_ERR_PEER_CLOSED) + << ": " << zx_status_get_string(write_res); + RunLoopUntilIdle(); + EXPECT_TRUE(sent_to_channel().empty()); +} + +TEST_F(SocketChannelRelayRxTest, + DrainWriteQueueWhileSocketWritableWaitIsPending) { + ASSERT_EQ(kDefaultSocketWriteQueueLimitFrames, 2u); + const StaticByteBuffer kSentMessage0(0); + const StaticByteBuffer kSentMessage1(1, 1, 1); + const StaticByteBuffer kSentMessage2(2); + const StaticByteBuffer kSentMessage3(3); + + // Make the first 2 datagrams 1 byte each so that we can discard exactly 2 + // bytes after stuffing the socket. + size_t n_bytes_written = 0; + zx_status_t write_res = local_socket_unowned()->write( + 0, kSentMessage0.data(), kSentMessage0.size(), &n_bytes_written); + ASSERT_EQ(write_res, ZX_OK); + ASSERT_EQ(n_bytes_written, 1u); + write_res = local_socket_unowned()->write( + 0, kSentMessage0.data(), kSentMessage0.size(), &n_bytes_written); + ASSERT_EQ(write_res, ZX_OK); + ASSERT_EQ(n_bytes_written, 1u); + + size_t n_junk_bytes = StuffSocket(); + ASSERT_TRUE(n_junk_bytes); + + ASSERT_TRUE(relay()->Activate()); + + // Drain enough space for kSentMessage2 and kSentMessage3, but not + // kSentMessage1! + ASSERT_TRUE(DiscardFromSocket(2 * kSentMessage0.size())); + + // The kSentMessage1 is too big to write to the almost-stuffed socket. The + // packet will be queued, and the "socket writable" signal wait will start. + channel()->Receive(kSentMessage1); + // The second 1-byte packet that will fit into the socket is also queued. + channel()->Receive(kSentMessage2); + // When the third 1-byte packet is queued, kSentMessage1 will be dropped from + // the queue, allowing kSentMessage2 and kSendMessage3 to be written to the + // socket. The queue will be emptied. + channel()->Receive(kSentMessage3); + + // Allow any signals to be processed. + RunLoopUntilIdle(); + + // Verify that kSentMessage2 and kSentMessage3 were actually written to the + // socket. + ASSERT_TRUE(DiscardFromSocket(n_junk_bytes)); + RunLoopUntilIdle(); + EXPECT_TRUE(ContainersEqual(kSentMessage2, + ReadDatagramFromSocket(kSentMessage2.size()))); + EXPECT_TRUE(ContainersEqual(kSentMessage3, + ReadDatagramFromSocket(kSentMessage3.size()))); +} + +} // namespace +} // namespace bt::socket diff --git a/pw_bluetooth_sapphire/fuchsia/host/socket/socket_factory_l2cap_integration_test.cc b/pw_bluetooth_sapphire/fuchsia/host/socket/socket_factory_l2cap_integration_test.cc new file mode 100644 index 0000000000..e7ba9e393a --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/socket/socket_factory_l2cap_integration_test.cc @@ -0,0 +1,327 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 + +#include "pw_bluetooth_sapphire/fuchsia/host/socket/socket_factory.h" +#include "pw_bluetooth_sapphire/internal/host/l2cap/channel.h" +#include "pw_bluetooth_sapphire/internal/host/l2cap/channel_manager.h" +#include "pw_bluetooth_sapphire/internal/host/l2cap/channel_manager_mock_controller_test_fixture.h" +#include "pw_bluetooth_sapphire/internal/host/l2cap/l2cap_defs.h" +#include "pw_bluetooth_sapphire/internal/host/l2cap/test_packets.h" +#include "pw_bluetooth_sapphire/internal/host/testing/loop_fixture.h" +#include "pw_bluetooth_sapphire/internal/host/testing/mock_controller.h" +#include "pw_unit_test/framework.h" + +namespace bt::socket { +namespace { + +using namespace bt::testing; + +using TestingBase = l2cap::ChannelManagerMockControllerTest; + +// This test harness provides test cases for interactions between SocketFactory +// and the L2cap layer. +class SocketFactoryL2capIntegrationTest : public TestLoopFixture, + public TestingBase { + public: + SocketFactoryL2capIntegrationTest() + : TestingBase(dispatcher_), dispatcher_(dispatcher()) {} + ~SocketFactoryL2capIntegrationTest() override = default; + + protected: + void SetUp() override { + TestingBase::Initialize(); + socket_factory_ = std::make_unique>(); + } + + void TearDown() override { + socket_factory_.reset(); + TestingBase::DeleteChannelManager(); + RunLoopUntilIdle(); + DeleteTransport(); + } + + zx::socket MakeSocketForChannel(l2cap::Channel::WeakPtr channel) { + return socket_factory_->MakeSocketForChannel(std::move(channel)); + } + + private: + std::unique_ptr> socket_factory_; + pw::async_fuchsia::FuchsiaDispatcher dispatcher_; + + BT_DISALLOW_COPY_ASSIGN_AND_MOVE(SocketFactoryL2capIntegrationTest); +}; + +TEST_F(SocketFactoryL2capIntegrationTest, InboundL2capSocket) { + constexpr l2cap::Psm kPsm = l2cap::kAVDTP; + constexpr l2cap::ChannelId kLocalId = 0x0040; + constexpr l2cap::ChannelId kRemoteId = 0x9042; + constexpr hci_spec::ConnectionHandle kLinkHandle = 0x0001; + + QueueAclConnection(kLinkHandle); + + l2cap::Channel::WeakPtr channel; + auto chan_cb = [&](auto cb_chan) { + EXPECT_EQ(kLinkHandle, cb_chan->link_handle()); + channel = std::move(cb_chan); + }; + chanmgr()->RegisterService(kPsm, kChannelParameters, std::move(chan_cb)); + RunLoopUntilIdle(); + + QueueInboundL2capConnection(kLinkHandle, kPsm, kLocalId, kRemoteId); + + RunLoopUntilIdle(); + ASSERT_TRUE(channel.is_alive()); + zx::socket sock = MakeSocketForChannel(channel); + + // Test basic channel<->socket interaction by verifying that an ACL packet + // gets routed to socket + test_device()->SendACLDataChannelPacket(StaticByteBuffer( + // ACL data header (handle: 1, length 8) + 0x01, + 0x00, + 0x08, + 0x00, + // L2CAP B-frame: (length: 4, channel-id: 0x0040 (kLocalId)) + 0x04, + 0x00, + 0x40, + 0x00, + // L2CAP payload + 't', + 'e', + 's', + 't')); + + // Run until the packet is written to the socket buffer. + RunLoopUntilIdle(); + + // Allocate a larger buffer than the number of SDU bytes we expect (which is + // 4). + StaticByteBuffer<10> socket_bytes; + size_t bytes_read; + zx_status_t status = sock.read( + 0, socket_bytes.mutable_data(), socket_bytes.size(), &bytes_read); + EXPECT_EQ(ZX_OK, status); + ASSERT_EQ(4u, bytes_read); + EXPECT_EQ("test", socket_bytes.view(0, bytes_read).AsString()); + + const char write_data[81] = "🚂🚃🚄🚅🚆🚈🚇🚈🚉🚊🚋🚌🚎🚝🚞🚟🚠🚡🛤🛲"; + + // Test outbound data fragments using |kMaxDataPacketLength|. + constexpr size_t kFirstFragmentPayloadSize = + kMaxDataPacketLength - sizeof(l2cap::BasicHeader); + const StaticByteBuffer + kFirstFragment( + // ACL data header (handle: 1, length 64) + 0x01, + 0x00, + 0x40, + 0x00, + // L2CAP B-frame: (length: 80, channel-id: 0x9042 (kRemoteId)) + 0x50, + 0x00, + 0x42, + 0x90, + // L2CAP payload (fragmented) + 0xf0, + 0x9f, + 0x9a, + 0x82, + 0xf0, + 0x9f, + 0x9a, + 0x83, + 0xf0, + 0x9f, + 0x9a, + 0x84, + 0xf0, + 0x9f, + 0x9a, + 0x85, + 0xf0, + 0x9f, + 0x9a, + 0x86, + 0xf0, + 0x9f, + 0x9a, + 0x88, + 0xf0, + 0x9f, + 0x9a, + 0x87, + 0xf0, + 0x9f, + 0x9a, + 0x88, + 0xf0, + 0x9f, + 0x9a, + 0x89, + 0xf0, + 0x9f, + 0x9a, + 0x8a, + 0xf0, + 0x9f, + 0x9a, + 0x8b, + 0xf0, + 0x9f, + 0x9a, + 0x8c, + 0xf0, + 0x9f, + 0x9a, + 0x8e, + 0xf0, + 0x9f, + 0x9a, + 0x9d, + 0xf0, + 0x9f, + 0x9a, + 0x9e); + + constexpr size_t kSecondFragmentPayloadSize = + sizeof(write_data) - 1 - kFirstFragmentPayloadSize; + const StaticByteBuffer + kSecondFragment( + // ACL data header (handle: 1, pbf: continuing fr., length: 20) + 0x01, + 0x10, + 0x14, + 0x00, + // L2CAP payload (final fragment) + 0xf0, + 0x9f, + 0x9a, + 0x9f, + 0xf0, + 0x9f, + 0x9a, + 0xa0, + 0xf0, + 0x9f, + 0x9a, + 0xa1, + 0xf0, + 0x9f, + 0x9b, + 0xa4, + 0xf0, + 0x9f, + 0x9b, + 0xb2); + + // The 80-byte write should be fragmented over 64- and 20-byte HCI payloads in + // order to send it to the controller. + EXPECT_ACL_PACKET_OUT(test_device(), kFirstFragment); + EXPECT_ACL_PACKET_OUT(test_device(), kSecondFragment); + + size_t bytes_written = 0; + // Write 80 outbound bytes to the socket buffer. + status = sock.write(0, write_data, sizeof(write_data) - 1, &bytes_written); + EXPECT_EQ(ZX_OK, status); + EXPECT_EQ(80u, bytes_written); + + // Run until the data is flushed out to the MockController. + RunLoopUntilIdle(); + EXPECT_TRUE(test_device()->AllExpectedDataPacketsSent()); + + // Synchronously closes channels & sockets. + chanmgr()->RemoveConnection(kLinkHandle); + acl_data_channel()->UnregisterConnection(kLinkHandle); + acl_data_channel()->ClearControllerPacketCount(kLinkHandle); + + // try resending data now that connection is closed + bytes_written = 0; + status = sock.write(0, write_data, sizeof(write_data) - 1, &bytes_written); + + EXPECT_EQ(ZX_ERR_PEER_CLOSED, status); + EXPECT_EQ(0u, bytes_written); + + // no packets should be sent + RunLoopUntilIdle(); +} + +TEST_F(SocketFactoryL2capIntegrationTest, OutboundL2capSocket) { + constexpr l2cap::Psm kPsm = l2cap::kAVCTP; + constexpr l2cap::ChannelId kLocalId = 0x0040; + constexpr l2cap::ChannelId kRemoteId = 0x9042; + constexpr hci_spec::ConnectionHandle kLinkHandle = 0x0001; + + QueueAclConnection(kLinkHandle); + RunLoopUntilIdle(); + + EXPECT_TRUE(test_device()->AllExpectedDataPacketsSent()); + + l2cap::Channel::WeakPtr chan; + auto chan_cb = [&](auto cb_chan) { + EXPECT_EQ(kLinkHandle, cb_chan->link_handle()); + chan = std::move(cb_chan); + }; + QueueOutboundL2capConnection( + kLinkHandle, kPsm, kLocalId, kRemoteId, std::move(chan_cb)); + + RunLoopUntilIdle(); + EXPECT_TRUE(test_device()->AllExpectedDataPacketsSent()); + // We should have opened a channel successfully. + ASSERT_TRUE(chan.is_alive()); + zx::socket sock = MakeSocketForChannel(chan); + + // Test basic channel<->socket interaction by verifying that an ACL packet + // gets routed to socket. + test_device()->SendACLDataChannelPacket(StaticByteBuffer( + // ACL data header (handle: 1, length 8) + 0x01, + 0x00, + 0x08, + 0x00, + // L2CAP B-frame: (length: 4, channel-id: 0x0040 (kLocalId)) + 0x04, + 0x00, + 0x40, + 0x00, + // L2CAP payload + 't', + 'e', + 's', + 't')); + + // Run until the packet is written to the socket buffer. + RunLoopUntilIdle(); + + // Allocate a larger buffer than the number of SDU bytes we expect (which is + // 4). + StaticByteBuffer<10> socket_bytes; + size_t bytes_read; + zx_status_t status = sock.read( + 0, socket_bytes.mutable_data(), socket_bytes.size(), &bytes_read); + EXPECT_EQ(ZX_OK, status); + ASSERT_EQ(4u, bytes_read); + EXPECT_EQ("test", socket_bytes.view(0, bytes_read).AsString()); + + EXPECT_ACL_PACKET_OUT(test_device(), + l2cap::testing::AclDisconnectionReq( + NextCommandId(), kLinkHandle, kLocalId, kRemoteId)); +} + +} // namespace +} // namespace bt::socket diff --git a/pw_bluetooth_sapphire/fuchsia/host/socket/socket_factory_test.cc b/pw_bluetooth_sapphire/fuchsia/host/socket/socket_factory_test.cc new file mode 100644 index 0000000000..0a71aaac0b --- /dev/null +++ b/pw_bluetooth_sapphire/fuchsia/host/socket/socket_factory_test.cc @@ -0,0 +1,165 @@ +// Copyright 2024 The Pigweed Authors +// +// 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 "pw_bluetooth_sapphire/fuchsia/host/socket/socket_factory.h" + +#include + +#include "pw_bluetooth_sapphire/internal/host/l2cap/channel.h" +#include "pw_bluetooth_sapphire/internal/host/l2cap/fake_channel.h" +#include "pw_bluetooth_sapphire/internal/host/l2cap/l2cap_defs.h" +#include "pw_bluetooth_sapphire/internal/host/testing/loop_fixture.h" +#include "pw_unit_test/framework.h" + +namespace bt::socket { +namespace { + +// We'll test the template just for L2CAP channels. +using FactoryT = SocketFactory; + +constexpr l2cap::ChannelId kDynamicChannelIdMin = 0x0040; +constexpr l2cap::ChannelId kRemoteChannelId = 0x0050; +constexpr hci_spec::ConnectionHandle kDefaultConnectionHandle = 0x0001; +constexpr hci_spec::ConnectionHandle kAnotherConnectionHandle = 0x0002; + +class SocketFactoryTest : public testing::TestLoopFixture { + public: + SocketFactoryTest() { + channel_ = + std::make_unique(kDynamicChannelIdMin, + kRemoteChannelId, + kDefaultConnectionHandle, + bt::LinkType::kACL); + } + + void TearDown() override { + // Process any pending events, to tickle any use-after-free bugs. + RunLoopUntilIdle(); + } + + protected: + l2cap::Channel::WeakPtr channel() { return channel_->GetWeakPtr(); } + l2cap::testing::FakeChannel::WeakPtr fake_channel() { + return channel_->AsWeakPtr(); + } + + private: + std::unique_ptr channel_; +}; + +TEST_F(SocketFactoryTest, TemplatesCompile) { + socket::SocketFactory l2cap_factory; +} + +TEST_F(SocketFactoryTest, CanCreateSocket) { + FactoryT socket_factory; + EXPECT_TRUE(socket_factory.MakeSocketForChannel(channel())); +} + +TEST_F(SocketFactoryTest, SocketCreationFailsIfChannelIsNullptr) { + FactoryT socket_factory; + EXPECT_FALSE(socket_factory.MakeSocketForChannel(l2cap::Channel::WeakPtr())); +} + +TEST_F(SocketFactoryTest, SocketCreationFailsIfChannelAlreadyHasASocket) { + FactoryT socket_factory; + zx::socket socket = socket_factory.MakeSocketForChannel(channel()); + ASSERT_TRUE(socket); + + EXPECT_FALSE(socket_factory.MakeSocketForChannel(channel())); +} + +TEST_F(SocketFactoryTest, SocketCreationFailsIfChannelActivationFails) { + fake_channel()->set_activate_fails(true); + EXPECT_FALSE(FactoryT().MakeSocketForChannel(channel())); +} + +TEST_F(SocketFactoryTest, CanCreateSocketForNewChannelWithRecycledId) { + FactoryT socket_factory; + auto original_channel = + std::make_unique(kDynamicChannelIdMin + 1, + kRemoteChannelId, + kDefaultConnectionHandle, + bt::LinkType::kACL); + zx::socket socket = + socket_factory.MakeSocketForChannel(original_channel->GetWeakPtr()); + ASSERT_TRUE(socket); + original_channel->Close(); + original_channel.reset(); + RunLoopUntilIdle(); // Process any events related to channel closure. + + auto new_channel = + std::make_unique(kDynamicChannelIdMin + 1, + kRemoteChannelId, + kDefaultConnectionHandle, + bt::LinkType::kACL); + EXPECT_TRUE(socket_factory.MakeSocketForChannel(new_channel->GetWeakPtr())); + new_channel->Close(); + original_channel.reset(); +} + +TEST_F(SocketFactoryTest, DestructionWithActiveRelayDoesNotCrash) { + FactoryT socket_factory; + zx::socket socket = socket_factory.MakeSocketForChannel(channel()); + ASSERT_TRUE(socket); + // |socket_factory| is destroyed implicitly. +} + +TEST_F(SocketFactoryTest, DestructionAfterDeactivatingRelayDoesNotCrash) { + FactoryT socket_factory; + zx::socket socket = socket_factory.MakeSocketForChannel(channel()); + ASSERT_TRUE(socket); + fake_channel()->Close(); + RunLoopUntilIdle(); // Process any events related to channel closure. + // |socket_factory| is destroyed implicitly. +} + +TEST_F(SocketFactoryTest, SameChannelIdDifferentHandles) { + FactoryT socket_factory; + EXPECT_TRUE(socket_factory.MakeSocketForChannel(channel())); + auto another_channel = + std::make_unique(kDynamicChannelIdMin, + kRemoteChannelId, + kAnotherConnectionHandle, + bt::LinkType::kACL); + EXPECT_TRUE( + socket_factory.MakeSocketForChannel(another_channel->GetWeakPtr())); + another_channel->Close(); +} + +TEST_F(SocketFactoryTest, ClosedCallbackCalledOnChannelClosure) { + FactoryT socket_factory; + int closed_cb_count = 0; + auto closed_cb = [&]() { closed_cb_count++; }; + zx::socket sock = + socket_factory.MakeSocketForChannel(channel(), std::move(closed_cb)); + EXPECT_TRUE(sock); + fake_channel()->Close(); + EXPECT_EQ(closed_cb_count, 1); +} + +TEST_F(SocketFactoryTest, ClosedCallbackCalledOnSocketClosure) { + FactoryT socket_factory; + int closed_cb_count = 0; + auto closed_cb = [&]() { closed_cb_count++; }; + zx::socket sock = + socket_factory.MakeSocketForChannel(channel(), std::move(closed_cb)); + EXPECT_TRUE(sock); + sock.reset(); + RunLoopUntilIdle(); + EXPECT_EQ(closed_cb_count, 1); +} + +} // namespace +} // namespace bt::socket diff --git a/pw_bluetooth_sapphire/fuchsia/lib/fidl/BUILD.bazel b/pw_bluetooth_sapphire/fuchsia/lib/fidl/BUILD.bazel index 2f999d6ffa..64d9c5e1c6 100644 --- a/pw_bluetooth_sapphire/fuchsia/lib/fidl/BUILD.bazel +++ b/pw_bluetooth_sapphire/fuchsia/lib/fidl/BUILD.bazel @@ -19,7 +19,7 @@ load( ) load("//pw_build:compatibility.bzl", "incompatible_with_mcu") -package(default_visibility = ["//pw_bluetooth_sapphire/fuchsia:__pkg__"]) +package(default_visibility = ["//pw_bluetooth_sapphire/fuchsia/host/fidl:__pkg__"]) cc_library( name = "fidl",