diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index e0e41c86..2336972a 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -31,14 +31,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - # Disbale layering_check to work around ABSL build failure. - # see https://github.com/bazelbuild/bazel/issues/15359 - name: Build Connections - run: CC=clang CXX=clang++ bazel build --features=-layering_check //connections:core --spawn_strategy=standalone + run: CC=clang CXX=clang++ bazel build //connections:core --spawn_strategy=standalone - name: Build Presence - run: CC=clang CXX=clang++ bazel build --features=-layering_check //presence --spawn_strategy=standalone + run: CC=clang CXX=clang++ bazel build //presence --spawn_strategy=standalone - name: Build Sharing - run: CC=clang CXX=clang++ bazel build --features=-layering_check //sharing/proto:all //sharing/internal/public:nearby_context //sharing/common:all //sharing/scheduling:scheduling //sharing/fast_initiation:nearby_fast_initiation --spawn_strategy=standalone + run: CC=clang CXX=clang++ bazel build //sharing/proto/... //sharing/internal/public:nearby_context //sharing/common:all //sharing/scheduling //sharing/fast_initiation:nearby_fast_initiation //sharing/analytics --spawn_strategy=standalone build-rust-linux: name: Build Rust on Linux diff --git a/WORKSPACE b/WORKSPACE index 052d8da6..43fe86b5 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -35,6 +35,12 @@ http_archive( urls = ["https://github.com/bazelbuild/platforms/archive/4ad40ef271da8176d4fc0194d2089b8a76e19d7b.zip"], ) +http_archive( + name = "rules_cc", + strip_prefix = "rules_cc-0.0.9", + urls = ["https://github.com/bazelbuild/rules_cc/archive/refs/tags/0.0.9.tar.gz"], +) + http_archive( name = "com_google_absl", strip_prefix = "abseil-cpp-4038192a57cb75f7ee671f81a3378ff4c74c4f8e", @@ -50,22 +56,29 @@ http_archive( http_archive( name = "com_google_protobuf", - strip_prefix = "protobuf-3.17.0", - urls = ["https://github.com/protocolbuffers/protobuf/archive/v3.17.0.tar.gz"], + strip_prefix = "protobuf-3.19.6", + urls = ["https://github.com/protocolbuffers/protobuf/archive/v3.19.6.tar.gz"], ) http_archive( name = "com_google_protobuf_cc", - strip_prefix = "protobuf-3.17.0", - urls = ["https://github.com/protocolbuffers/protobuf/archive/v3.17.0.tar.gz"], + strip_prefix = "protobuf-3.19.6", + urls = ["https://github.com/protocolbuffers/protobuf/archive/v3.19.6.tar.gz"], ) http_archive( name = "com_google_protobuf_java", - strip_prefix = "protobuf-3.17.0", - urls = ["https://github.com/protocolbuffers/protobuf/archive/v3.17.0.tar.gz"], + strip_prefix = "protobuf-3.19.6", + urls = ["https://github.com/protocolbuffers/protobuf/archive/v3.19.6.tar.gz"], ) +# Load common dependencies. +load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps") +protobuf_deps() +load("@rules_cc//cc:repositories.bzl", "rules_cc_dependencies", "rules_cc_toolchains") +rules_cc_dependencies() +rules_cc_toolchains() + http_archive( name = "com_google_glog", sha256 = "f28359aeba12f30d73d9e4711ef356dc842886968112162bc73002645139c39c", @@ -112,10 +125,6 @@ cc_library( ], ) -load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps") -# Load common dependencies. -protobuf_deps() - http_archive( name = "com_google_googletest", strip_prefix = "googletest-main", diff --git a/sharing/BUILD b/sharing/BUILD new file mode 100644 index 00000000..1ee9b122 --- /dev/null +++ b/sharing/BUILD @@ -0,0 +1,288 @@ +licenses(["notice"]) + +cc_library( + name = "connection_types", + hdrs = ["nearby_connections_types.h"], + deps = [ + "//internal/crypto_cros", + "//sharing/common:compatible_u8_string", + "@com_google_absl//absl/strings:string_view", + "@com_google_absl//absl/time", + ], +) + +cc_library( + name = "types", + srcs = [ + "advertisement.cc", + "attachment.cc", + "attachment_info.cc", + "file_attachment.cc", + "share_target.cc", + "text_attachment.cc", + "wifi_credentials_attachment.cc", + ], + hdrs = [ + "advertisement.h", + "attachment.h", + "attachment_info.h", + "file_attachment.h", + "nearby_connection.h", + "nearby_connections_manager.h", + "nearby_connections_types.h", + "nearby_sharing_decoder.h", + "share_target.h", + "text_attachment.h", + "transfer_metadata.h", + "transfer_metadata_builder.h", + "wifi_credentials_attachment.h", + ], + visibility = [ + "//location/nearby/cpp/sharing:__subpackages__", + "//sharing:__subpackages__", + ], + deps = [ + "//internal/crypto_cros", + "//internal/network:types", + "//sharing/common", + "//sharing/common:compatible_u8_string", + "//sharing/internal/base", + "//sharing/internal/public:logging", + "//sharing/proto:share_cc_proto", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:str_format", + "@com_google_absl//absl/time", + "@com_google_absl//absl/types:span", + ], +) + +cc_library( + name = "nearby_sharing_service", + srcs = [ + "incoming_frames_reader.cc", + "incoming_share_target_info.cc", + "nearby_connection_impl.cc", + "nearby_connections_manager.cc", + "nearby_connections_manager_factory.cc", + "nearby_connections_manager_impl.cc", + "nearby_connections_service.cc", + "nearby_connections_service_impl.cc", + "nearby_connections_stream_buffer_manager.cc", + "nearby_file_handler.cc", + "nearby_share_profile_info_provider_impl.cc", + "nearby_sharing_decoder_impl.cc", + "nearby_sharing_event_logger.cc", + "nearby_sharing_service.cc", + "nearby_sharing_service_extension.cc", + "nearby_sharing_service_factory.cc", + "nearby_sharing_service_impl.cc", + "nearby_sharing_settings.cc", + "nearby_sharing_util.cc", + "outgoing_share_target_info.cc", + "paired_key_verification_runner.cc", + "payload_tracker.cc", + "share_target_info.cc", + "transfer_manager.cc", + "transfer_metadata.cc", + "transfer_metadata_builder.cc", + ], + hdrs = [ + "connection_lifecycle_listener.h", + "constants.h", + "endpoint_discovery_listener.h", + "incoming_frames_reader.h", + "incoming_share_target_info.h", + "nearby_connection_impl.h", + "nearby_connections_manager_factory.h", + "nearby_connections_manager_impl.h", + "nearby_connections_service.h", + "nearby_connections_service_impl.h", + "nearby_connections_stream_buffer_manager.h", + "nearby_file_handler.h", + "nearby_share_profile_info_provider_impl.h", + "nearby_sharing_decoder_impl.h", + "nearby_sharing_event_logger.h", + "nearby_sharing_service.h", + "nearby_sharing_service_extension.h", + "nearby_sharing_service_factory.h", + "nearby_sharing_service_impl.h", + "nearby_sharing_settings.h", + "nearby_sharing_util.h", + "outgoing_share_target_info.h", + "paired_key_verification_runner.h", + "payload_listener.h", + "payload_tracker.h", + "share_target_discovered_callback.h", + "share_target_info.h", + "transfer_manager.h", + "transfer_update_callback.h", + "//sharing/flags:nearby_sharing_feature_flags.h", + ], + copts = [ + "-DNEARBY_SHARING_DLL", + ], + visibility = [ + "//location/nearby/cpp/sharing:__subpackages__", + "//sharing:__subpackages__", + ], + deps = [ + ":types", + "//connections:core", + "//connections:core_types", + "//connections/implementation:internal", + "//internal/analytics:event_logger", + "//internal/base", + "//internal/base:bluetooth_address", + "//internal/flags:flag_reader", + "//internal/flags:nearby_flags", + "//internal/network:nearby_http_client", + "//internal/network:types", + "//internal/platform:base", + "//internal/platform:types", + "//internal/platform/implementation:types", + "//proto:sharing_enums_cc_proto", + "//sharing/analytics", + "//sharing/certificates", + "//sharing/client", + "//sharing/common", + "//sharing/common:compatible_u8_string", + "//sharing/contacts", + "//sharing/fast_initiation:nearby_fast_initiation", + "//sharing/flags:nearby_sharing_feature_flags_cpp_consts_generated", + "//sharing/internal/api:platform", + "//sharing/internal/base", + "//sharing/internal/base:utf_utils", + "//sharing/internal/public:logging", + "//sharing/internal/public:nearby_context", + "//sharing/internal/public:types", + "//sharing/local_device_data", + "//sharing/proto:share_cc_proto", + "//sharing/scheduling", + "@com_google_absl//absl/algorithm:container", + "@com_google_absl//absl/base:core_headers", + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/hash", + "@com_google_absl//absl/meta:type_traits", + "@com_google_absl//absl/random", + "@com_google_absl//absl/status", + "@com_google_absl//absl/status:statusor", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:str_format", + "@com_google_absl//absl/synchronization", + "@com_google_absl//absl/time", + "@com_google_absl//absl/types:span", + "@com_google_protobuf//:protobuf_lite", + ], +) + +cc_library( + name = "test_support", + testonly = True, + srcs = [ + "fake_nearby_connection.cc", + "fake_nearby_connections_manager.cc", + "fake_nearby_sharing_service.cc", + ], + hdrs = [ + "fake_nearby_connection.h", + "fake_nearby_connections_manager.h", + "fake_nearby_sharing_service.h", + ], + visibility = ["//visibility:public"], + deps = [ + ":nearby_sharing_service", + ":types", + "//internal/base", + "//sharing/common:enum", + "//sharing/internal/public:logging", + "//sharing/local_device_data", + "//sharing/proto:share_cc_proto", + "@com_google_absl//absl/algorithm:container", + "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/strings", + ], +) + +cc_test( + name = "nearby_connections_types_payload_test", + srcs = ["nearby_connections_types_payload_test.cc"], + deps = [ + ":connection_types", + "@com_github_protobuf_matchers//protobuf-matchers", + "@com_google_googletest//:gtest_main", + ], +) + +cc_test( + name = "nearby_sharing_service_test", + size = "small", + timeout = "short", + srcs = [ + "fake_nearby_connections_service.h", + "incoming_frames_reader_test.cc", + "nearby_connection_impl_test.cc", + "nearby_connections_manager_impl_test.cc", + "nearby_connections_stream_buffer_manager_test.cc", + "nearby_connections_types_test.cc", + "nearby_file_handler_test.cc", + "nearby_share_profile_info_provider_impl_test.cc", + "nearby_sharing_event_logger_test.cc", + "nearby_sharing_service_extension_test.cc", + "nearby_sharing_service_impl_test.cc", + "nearby_sharing_service_test.cc", + "nearby_sharing_settings_test.cc", + "paired_key_verification_runner_test.cc", + "payload_tracker_test.cc", + "share_target_test.cc", + "text_attachment_test.cc", + "transfer_manager_test.cc", + "transfer_metadata_test.cc", + ], + shard_count = 8, + deps = [ + ":nearby_sharing_service", + ":test_support", + ":types", + "//base:casts", + "//connections:core_types", + "//internal/account", + "//internal/analytics:event_logger", + "//internal/flags:nearby_flags", + "//internal/network:types", + "//internal/platform/implementation:types", + "//internal/platform/implementation/g3", # fixdeps: keep + "//internal/test", + "//proto:sharing_enums_cc_proto", + "//sharing/certificates", + "//sharing/certificates:test_support", + "//sharing/common", + "//sharing/common:compatible_u8_string", + "//sharing/contacts", + "//sharing/contacts:test_support", + "//sharing/fast_initiation:nearby_fast_initiation", + "//sharing/fast_initiation:test_support", + "//sharing/flags:nearby_sharing_feature_flags_cpp_consts_generated", + "//sharing/internal/api:mock_sharing_platform", + "//sharing/internal/api:platform", + "//sharing/internal/public:types", + "//sharing/internal/test:nearby_test", + "//sharing/local_device_data", + "//sharing/local_device_data:test_support", + "//sharing/proto:share_cc_proto", + "//sharing/proto/analytics:sharing_log_cc_proto", + "@com_github_protobuf_matchers//protobuf-matchers", + "@com_google_absl//absl/base:core_headers", + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/memory", + "@com_google_absl//absl/status", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/synchronization", + "@com_google_absl//absl/time", + "@com_google_absl//absl/types:optional", + "@com_google_absl//absl/types:span", + "@com_google_googletest//:gtest_main", + "@com_google_protobuf//:protobuf_lite", + ], +) diff --git a/sharing/advertisement.cc b/sharing/advertisement.cc new file mode 100644 index 00000000..d9e41231 --- /dev/null +++ b/sharing/advertisement.cc @@ -0,0 +1,219 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/advertisement.h" + +#include + +#include +#include +#include +#include +#include + +#include "absl/types/span.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/internal/public/logging.h" + +namespace nearby { +namespace sharing { +namespace { + +// v1 advertisements: +// - ParseVersion() --> 0 +// +// v2 advertisements: +// - ParseVersion() --> 1 +// - Backwards compatible; no changes in advertisement data--aside from the +// version number--or parsing logic compared to v1. +// - Only used by GmsCore at the moment. +constexpr int kMaxSupportedAdvertisementParsedVersionNumber = 1; + +// The bit mask for parsing and writing Version. +constexpr uint8_t kVersionBitmask = 0b111; + +// The bit mask for parsing and writing Visibility. +constexpr uint8_t kVisibilityBitmask = 0b1; + +// The bit mask for parsing and writing Device Type. +constexpr uint8_t kDeviceTypeBitmask = 0b111; + +const uint8_t kMinimumSize = + /* Version(3 bits)|Visibility(1 bit)|Device Type(3 bits)|Reserved(1 bits)= + */ + 1 + sharing::Advertisement::kSaltSize + + sharing::Advertisement::kMetadataEncryptionKeyHashByteSize; + +uint8_t ConvertVersion(int version) { + return static_cast((version & kVersionBitmask) << 5); +} + +uint8_t ConvertDeviceType(ShareTargetType type) { + return static_cast((static_cast(type) & kDeviceTypeBitmask) + << 1); +} + +uint8_t ConvertHasDeviceName(bool hasDeviceName) { + return static_cast((hasDeviceName ? 0 : 1) << 4); +} + +int ParseVersion(uint8_t b) { return (b >> 5) & kVersionBitmask; } + +bool IsKnownDeviceValue(int32_t value) { + switch (value) { + case 0: + case 1: + case 2: + case 3: + return true; + default: + return false; + } +} + +ShareTargetType ParseDeviceType(uint8_t b) { + int32_t intermediate = static_cast(b >> 1 & kDeviceTypeBitmask); + if (IsKnownDeviceValue(intermediate)) { + return static_cast(intermediate); + } + + return ShareTargetType::kUnknown; +} + +bool ParseHasDeviceName(uint8_t b) { + return ((b >> 4) & kVisibilityBitmask) == 0; +} + +} // namespace + +// static +std::unique_ptr Advertisement::NewInstance( + std::vector salt, std::vector encrypted_metadata_key, + ShareTargetType device_type, std::optional device_name) { + if (salt.size() != Advertisement::kSaltSize) { + NL_LOG(ERROR) << "Failed to create advertisement because the salt did " + "not match the expected length " + << salt.size(); + return nullptr; + } + + if (encrypted_metadata_key.size() != + Advertisement::kMetadataEncryptionKeyHashByteSize) { + NL_LOG(ERROR) << "Failed to create advertisement because the encrypted " + "metadata key did " + "not match the expected length " + << encrypted_metadata_key.size(); + return nullptr; + } + + if (device_name.has_value() && device_name->size() > UINT8_MAX) { + NL_LOG(ERROR) << "Failed to create advertisement because device name " + "was over UINT8_MAX: " + << device_name->size(); + return nullptr; + } + + // Using `new` to access a non-public constructor. + return std::make_unique( + /* version= */ 0, std::move(salt), std::move(encrypted_metadata_key), + device_type, std::move(device_name)); +} + +std::vector Advertisement::ToEndpointInfo() { + int size = kMinimumSize + (device_name_.has_value() ? 1 : 0) + + (device_name_.has_value() ? device_name_->size() : 0); + + std::vector endpoint_info; + endpoint_info.reserve(size); + endpoint_info.push_back( + static_cast(ConvertVersion(version_) | + ConvertHasDeviceName(device_name_.has_value()) | + ConvertDeviceType(device_type_))); + endpoint_info.insert(endpoint_info.end(), salt_.begin(), salt_.end()); + endpoint_info.insert(endpoint_info.end(), encrypted_metadata_key_.begin(), + encrypted_metadata_key_.end()); + + if (device_name_.has_value()) { + endpoint_info.push_back(static_cast(device_name_->size() & 0xff)); + endpoint_info.insert(endpoint_info.end(), device_name_->begin(), + device_name_->end()); + } + + return endpoint_info; +} + +std::unique_ptr Advertisement::FromEndpointInfo( + absl::Span endpoint_info) { + if (endpoint_info.size() < kMinimumSize) { + NL_LOG(ERROR) << "Failed to parse advertisement because it was too short."; + return nullptr; + } + + auto iter = endpoint_info.begin(); + uint8_t first_byte = *iter++; + + int version = ParseVersion(first_byte); + if (version < 0 || version > kMaxSupportedAdvertisementParsedVersionNumber) { + NL_LOG(ERROR) + << "Failed to parse advertisement; unsupported version number " + << version; + return nullptr; + } + + bool has_device_name = ParseHasDeviceName(first_byte); + ShareTargetType device_type = ParseDeviceType(first_byte); + + std::vector salt(iter, iter + Advertisement::kSaltSize); + iter += Advertisement::kSaltSize; + + std::vector encrypted_metadata_key( + iter, iter + Advertisement::kMetadataEncryptionKeyHashByteSize); + iter += Advertisement::kMetadataEncryptionKeyHashByteSize; + + int device_name_length = 0; + if (iter != endpoint_info.end()) device_name_length = *iter++ & 0xff; + + if (endpoint_info.end() - iter < device_name_length || + (device_name_length == 0 && has_device_name)) { + NL_LOG(ERROR) + << "Failed to parse advertisement because the device name did " + "not match the expected length " + << device_name_length; + return nullptr; + } + + std::optional optional_device_name; + if (device_name_length > 0) { + optional_device_name = std::string(iter, iter + device_name_length); + iter += device_name_length; + } + + return Advertisement::NewInstance( + std::move(salt), std::move(encrypted_metadata_key), device_type, + std::move(optional_device_name)); +} + +// private +Advertisement::Advertisement(int version, std::vector salt, + std::vector encrypted_metadata_key, + ShareTargetType device_type, + std::optional device_name) + : version_(version), + salt_(std::move(salt)), + encrypted_metadata_key_(std::move(encrypted_metadata_key)), + device_type_(device_type), + device_name_(std::move(device_name)) {} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/advertisement.h b/sharing/advertisement.h new file mode 100644 index 00000000..7936915f --- /dev/null +++ b/sharing/advertisement.h @@ -0,0 +1,93 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_ADVERTISEMENT_H_ +#define THIRD_PARTY_NEARBY_SHARING_ADVERTISEMENT_H_ + +#include + +#include +#include +#include +#include + +#include "absl/types/span.h" +#include "sharing/common/nearby_share_enums.h" + +namespace nearby { +namespace sharing { + +// An advertisement in the form of +// [VERSION|VISIBILITY][SALT][ACCOUNT_IDENTIFIER][LEN][DEVICE_NAME]. +// A device name indicates the advertisement is visible to everyone; +// a missing device name indicates the advertisement is contacts-only. +class Advertisement { + public: + static constexpr uint8_t kSaltSize = 2; + static constexpr uint8_t kMetadataEncryptionKeyHashByteSize = 14; + + static std::unique_ptr NewInstance( + std::vector salt, std::vector encrypted_metadata_key, + ShareTargetType device_type, std::optional device_name); + + Advertisement(int version, std::vector salt, + std::vector encrypted_metadata_key, + ShareTargetType device_type, + std::optional device_name); + ~Advertisement() = default; + Advertisement(const Advertisement&) = default; + Advertisement& operator=(const Advertisement&) = default; + Advertisement(Advertisement&&) = default; + Advertisement& operator=(Advertisement&&) = default; + + std::vector ToEndpointInfo(); + + int version() const { return version_; } + const std::vector& salt() const { return salt_; } + const std::vector& encrypted_metadata_key() const { + return encrypted_metadata_key_; + } + ShareTargetType device_type() const { return device_type_; } + const std::optional& device_name() const { return device_name_; } + bool HasDeviceName() const { return device_name_.has_value(); } + + static std::unique_ptr FromEndpointInfo( + absl::Span endpoint_info); + + private: + // The version of the advertisement. Different versions can have different + // ways of parsing the endpoint id. + int version_; + + // Random bytes that were used as salt during encryption of public certificate + // metadata. + std::vector salt_ = {}; + + // An encrypted symmetric key that was used to encrypt public certificate + // metadata, including an account identifier signifying the remote device. + // The key can be decrypted using |salt| and the corresponding public + // certificate's secret/authenticity key. + std::vector encrypted_metadata_key_ = {}; + + // The type of device that the advertisement identifies. + ShareTargetType device_type_ = ShareTargetType::kUnknown; + + // The human-readable name of the remote device. + std::optional device_name_ = std::nullopt; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_ADVERTISEMENT_H_ diff --git a/sharing/analytics/BUILD b/sharing/analytics/BUILD new file mode 100644 index 00000000..dabf73c2 --- /dev/null +++ b/sharing/analytics/BUILD @@ -0,0 +1,61 @@ +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +licenses(["notice"]) + +cc_library( + name = "analytics", + srcs = [ + "analytics_recorder.cc", + ], + hdrs = [ + "analytics_device_settings.h", + "analytics_information.h", + "analytics_recorder.h", + ], + visibility = ["//visibility:public"], + deps = [ + "//internal/analytics:event_logger", + "//proto:sharing_enums_cc_proto", + "//sharing:types", + "//sharing/common", + "//sharing/internal/public:logging", + "//sharing/proto:share_cc_proto", + "//sharing/proto/analytics:sharing_log_cc_proto", + "@com_google_absl//absl/random", + "@com_google_absl//absl/strings", + "@com_google_protobuf//:protobuf", + "@com_google_protobuf//:protobuf_lite", + ], +) + +cc_test( + name = "analytics_test", + srcs = ["analytics_recorder_test.cc"], + deps = [ + ":analytics", + "//internal/analytics:event_logger", + "//internal/platform/implementation/g3", # fixdeps: keep + "//proto:sharing_enums_cc_proto", + "//sharing:types", + "//sharing/common", + "//sharing/proto:share_cc_proto", + "//sharing/proto/analytics:sharing_log_cc_proto", + "@com_github_protobuf_matchers//protobuf-matchers", + "@com_google_absl//absl/strings", + "@com_google_googletest//:gtest_main", + "@com_google_protobuf//:protobuf", + "@com_google_protobuf//:protobuf_lite", + ], +) diff --git a/sharing/analytics/analytics_device_settings.h b/sharing/analytics/analytics_device_settings.h new file mode 100644 index 00000000..60fbd3da --- /dev/null +++ b/sharing/analytics/analytics_device_settings.h @@ -0,0 +1,36 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_ANALYTICS_ANALYTICS_DEVICE_SETTINGS_H_ +#define THIRD_PARTY_NEARBY_SHARING_ANALYTICS_ANALYTICS_DEVICE_SETTINGS_H_ + +#include "sharing/common/nearby_share_enums.h" +#include "sharing/proto/enums.pb.h" + +namespace nearby { +namespace sharing { +namespace analytics { + +struct AnalyticsDeviceSettings { + bool is_fast_init_notification_enabled; + int device_name_size; + ::nearby::sharing::proto::DataUsage data_usage; + ::nearby::sharing::proto::DeviceVisibility visibility; +}; + +} // namespace analytics +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_ANALYTICS_ANALYTICS_DEVICE_SETTINGS_H_ diff --git a/sharing/analytics/analytics_information.h b/sharing/analytics/analytics_information.h new file mode 100644 index 00000000..00a24d65 --- /dev/null +++ b/sharing/analytics/analytics_information.h @@ -0,0 +1,47 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_ANALYTICS_ANALYTICS_INFORMATION_H_ +#define THIRD_PARTY_NEARBY_SHARING_ANALYTICS_ANALYTICS_INFORMATION_H_ + +namespace nearby { +namespace sharing { +namespace analytics { + +enum class SendSurfaceState : int { + // Default, invalid state. + kUnknown = -1, + // Background share sheet only listens to transfer update. + kBackground = 0, + // Foreground share sheet both scans and listens to transfer update. + kForeground = 1, + // Direct share service only scans and listens to share target onFound and + // onLost. + kDirectShareService = 2, + // Foreground share sheet both scans and listens to transfer update, but does + // not trigger FastInit HUN. + kForegroundRetry = 3, + // A foreground surface that is registered from 2nd and 3rd party apps. + kExternal = 4, +}; + +struct AnalyticsInformation { + SendSurfaceState send_surface_state; +}; + +} // namespace analytics +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_ANALYTICS_ANALYTICS_INFORMATION_H_ diff --git a/sharing/analytics/analytics_recorder.cc b/sharing/analytics/analytics_recorder.cc new file mode 100644 index 00000000..56dab633 --- /dev/null +++ b/sharing/analytics/analytics_recorder.cc @@ -0,0 +1,1076 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/analytics/analytics_recorder.h" + +#include +#include +#include +#include +#include + +#include "google/protobuf/duration.pb.h" +#include "absl/random/random.h" +#include "absl/strings/str_cat.h" +#include "proto/sharing_enums.pb.h" +#include "sharing/analytics/analytics_device_settings.h" +#include "sharing/analytics/analytics_information.h" +#include "sharing/attachment.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/file_attachment.h" +#include "sharing/internal/public/logging.h" +#include "sharing/proto/analytics/nearby_sharing_log.pb.h" +#include "sharing/proto/enums.pb.h" +#include "sharing/share_target.h" +#include "sharing/wifi_credentials_attachment.h" +#include "google/protobuf/message_lite.h" +#include "google/protobuf/repeated_ptr_field.h" + +namespace nearby { +namespace sharing { +namespace analytics { +namespace { + +using ::location::nearby::proto::sharing::DesktopNotification; +using ::location::nearby::proto::sharing::DesktopTransferEventType; +using ::location::nearby::proto::sharing::DeviceRelationship; +using ::location::nearby::proto::sharing::DeviceType; +using ::location::nearby::proto::sharing::EventCategory; +using ::location::nearby::proto::sharing::EventType; +using ::location::nearby::proto::sharing::OSType; + +using ::nearby::sharing::analytics::proto::SharingLog; +using ::nearby::sharing::proto::DataUsage; +using ::nearby::sharing::proto::DeviceVisibility; + +DeviceRelationship GetLoggerDeviceRelationship( + const ShareTarget& share_target) { + if (share_target.for_self_share) { + return DeviceRelationship::IS_SELF; + } else if (share_target.is_known) { + return DeviceRelationship::IS_CONTACT; + } else { + return DeviceRelationship::IS_STRANGER; + } +} + +DeviceType GetLoggerDeviceType(ShareTargetType type) { + switch (type) { + case ShareTargetType::kLaptop: + return DeviceType::LAPTOP; + case ShareTargetType::kPhone: + return DeviceType::PHONE; + case ShareTargetType::kTablet: + return DeviceType::TABLET; + default: + return DeviceType::UNKNOWN_DEVICE_TYPE; + } +} + +::location::nearby::proto::sharing::Visibility GetLoggerVisibility( + DeviceVisibility visibility) { + switch (visibility) { + case DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS: + return ::location::nearby::proto::sharing::Visibility::CONTACTS_ONLY; + case DeviceVisibility::DEVICE_VISIBILITY_SELECTED_CONTACTS: + return ::location::nearby::proto::sharing::Visibility:: + SELECTED_CONTACTS_ONLY; + case DeviceVisibility::DEVICE_VISIBILITY_EVERYONE: + return ::location::nearby::proto::sharing::Visibility::EVERYONE; + case DeviceVisibility::DEVICE_VISIBILITY_SELF_SHARE: + return ::location::nearby::proto::sharing::Visibility::SELF_SHARE; + default: + return ::location::nearby::proto::sharing::Visibility::UNKNOWN_VISIBILITY; + } +} + +::location::nearby::proto::sharing::DataUsage GetLoggerDataUsage( + DataUsage data_usage) { + switch (data_usage) { + case DataUsage::OFFLINE_DATA_USAGE: + return ::location::nearby::proto::sharing::DataUsage::OFFLINE; + case DataUsage::ONLINE_DATA_USAGE: + return ::location::nearby::proto::sharing::DataUsage::ONLINE; + case DataUsage::WIFI_ONLY_DATA_USAGE: + return ::location::nearby::proto::sharing::DataUsage::WIFI_ONLY; + default: + return ::location::nearby::proto::sharing::DataUsage::UNKNOWN_DATA_USAGE; + } +} + +// TODO(b/269353084): Auto-generate codes when a new source type in +// sharing_enums.proto is added. +::location::nearby::proto::sharing::AttachmentSourceType +GetLoggerAttachmentSourceType(Attachment::SourceType source_type) { + switch (source_type) { + case Attachment::SourceType::kContextMenu: + return ::location::nearby::proto::sharing::AttachmentSourceType:: + ATTACHMENT_SOURCE_CONTEXT_MENU; + case Attachment::SourceType::kDragAndDrop: + return ::location::nearby::proto::sharing::AttachmentSourceType:: + ATTACHMENT_SOURCE_DRAG_AND_DROP; + case Attachment::SourceType::kSelectFilesButton: + return ::location::nearby::proto::sharing::AttachmentSourceType:: + ATTACHMENT_SOURCE_SELECT_FILES_BUTTON; + case Attachment::SourceType::kPaste: + return ::location::nearby::proto::sharing::AttachmentSourceType:: + ATTACHMENT_SOURCE_PASTE; + case Attachment::SourceType::kSelectFoldersButton: + return ::location::nearby::proto::sharing::AttachmentSourceType:: + ATTACHMENT_SOURCE_SELECT_FOLDERS_BUTTON; + default: + return ::location::nearby::proto::sharing::AttachmentSourceType:: + ATTACHMENT_SOURCE_UNKNOWN; + } +} + +analytics::proto::SharingLog::ShareTargetInfo* GetAllocatedShareTargetInfo( + ShareTargetType device_type, DeviceRelationship relationship, + OSType os_type = OSType::UNKNOWN_OS_TYPE) { + auto share_target_info = + analytics::proto::SharingLog::ShareTargetInfo::default_instance().New(); + share_target_info->set_device_relationship(relationship); + share_target_info->set_device_type(GetLoggerDeviceType(device_type)); + if (os_type == OSType::UNKNOWN_OS_TYPE && + device_type == ShareTargetType::kPhone) { + // If the device type is phone, just set the OS type to android because + // no other phone OS for now. + share_target_info->set_os_type(OSType::ANDROID); + } else { + share_target_info->set_os_type(os_type); + } + return share_target_info; +} + +analytics::proto::SharingLog::ShareTargetInfo* GetAllocatedShareTargetInfo( + const ShareTarget& share_target, OSType os_type = OSType::UNKNOWN_OS_TYPE) { + auto share_target_info = + analytics::proto::SharingLog::ShareTargetInfo::default_instance().New(); + share_target_info->set_device_relationship( + GetLoggerDeviceRelationship(share_target)); + share_target_info->set_device_type(GetLoggerDeviceType(share_target.type)); + if (os_type == OSType::UNKNOWN_OS_TYPE && + share_target.type == ShareTargetType::kPhone) { + // If the device type is phone, just set the OS type to android because + // no other phone OS for now. + share_target_info->set_os_type(OSType::ANDROID); + } else { + share_target_info->set_os_type(os_type); + } + return share_target_info; +} + +analytics::proto::SharingLog::AttachmentsInfo* GenerateAllocatedAttachmentInfo( + const std::vector>& attachments) { + auto attachments_info = + analytics::proto::SharingLog::AttachmentsInfo::default_instance().New(); + + for (auto& attachment : attachments) { + int64_t size = attachment->size(); + if (attachment->family() == Attachment::Family::kText) { + analytics::proto::SharingLog::TextAttachment::Type type = + analytics::proto::SharingLog::TextAttachment::UNKNOWN_TEXT_TYPE; + switch (attachment->GetShareType()) { + case ShareType::kPhone: + type = analytics::proto::SharingLog::TextAttachment::PHONE_NUMBER; + break; + case ShareType::kUrl: + type = analytics::proto::SharingLog::TextAttachment::URL; + break; + case ShareType::kAddress: + type = analytics::proto::SharingLog::TextAttachment::ADDRESS; + break; + case ShareType::kText: + // Apply UNKNOWN_TEXT_TYPE for it based on analytics design. + break; + default: + break; + } + + ::google::protobuf::RepeatedPtrField* + text_attachments = attachments_info->mutable_text_attachment(); + analytics::proto::SharingLog_TextAttachment* text_attachment = + analytics::proto::SharingLog::TextAttachment::default_instance() + .New(); + text_attachment->set_type(type); + text_attachment->set_size_bytes(size); + text_attachment->set_source_type( + GetLoggerAttachmentSourceType(attachment->source_type())); + text_attachment->set_batch_id(attachment->batch_id()); + text_attachments->AddAllocated(text_attachment); + } else if (attachment->family() == Attachment::Family::kFile) { + analytics::proto::SharingLog::FileAttachment::Type type = + analytics::proto::SharingLog::FileAttachment::UNKNOWN_FILE_TYPE; + switch (attachment->GetShareType()) { + case ShareType::kImageFile: + type = analytics::proto::SharingLog::FileAttachment::IMAGE; + break; + case ShareType::kVideoFile: + type = analytics::proto::SharingLog::FileAttachment::VIDEO; + break; + case ShareType::kAudioFile: + type = analytics::proto::SharingLog::FileAttachment::AUDIO; + break; + case ShareType::kPdfFile: + case ShareType::kTextFile: + case ShareType::kGoogleDocsFile: + case ShareType::kGoogleSheetsFile: + case ShareType::kGoogleSlidesFile: + type = analytics::proto::SharingLog::FileAttachment::DOCUMENT; + break; + case ShareType::kUnknownFile: + // The default type is set to type. + break; + default: + break; + } + + ::google::protobuf::RepeatedPtrField* + file_attachments = attachments_info->mutable_file_attachment(); + analytics::proto::SharingLog_FileAttachment* file_attachment = + analytics::proto::SharingLog::FileAttachment::default_instance() + .New(); + file_attachment->set_type(type); + file_attachment->set_size_bytes(size); + file_attachment->set_offset_bytes(0); + file_attachment->set_source_type( + GetLoggerAttachmentSourceType(attachment->source_type())); + file_attachment->set_batch_id(attachment->batch_id()); + file_attachments->AddAllocated(file_attachment); + } else if (attachment->family() == Attachment::Family::kWifiCredentials) { + ::google::protobuf::RepeatedPtrField< + analytics::proto::SharingLog_WifiCredentialsAttachment>* + wifi_credentials_attachments = + attachments_info->mutable_wifi_credentials_attachment(); + analytics::proto::SharingLog_WifiCredentialsAttachment* + wifi_credentials_attachment = + analytics::proto::SharingLog::WifiCredentialsAttachment:: + default_instance() + .New(); + wifi_credentials_attachment->set_security_type( + dynamic_cast(attachment.get()) + ->security_type()); + wifi_credentials_attachment->set_source_type( + GetLoggerAttachmentSourceType(attachment->source_type())); + wifi_credentials_attachment->set_batch_id(attachment->batch_id()); + wifi_credentials_attachments->AddAllocated(wifi_credentials_attachment); + } else { + NL_LOG(WARNING) + << __func__ + << "Unable to create event for attachment info. Unknown attachment " + << attachment->id(); + continue; + } + } + + if (attachments_info->file_attachment_size() == 0 && + attachments_info->text_attachment_size() == 0 && + attachments_info->wifi_credentials_attachment_size() == 0) { + std::string type = + attachments.empty() + ? "NULL" + : absl::StrCat(static_cast(attachments[0]->GetShareType())); + NL_LOG(WARNING) << __func__ << "attachmentInfo is empty, attachment size=" + << attachments.size() << ", type=" << type; + } + + return attachments_info; +} + +} // namespace + +void AnalyticsRecorder::NewEstablishConnection( + int64_t session_id, + ::location::nearby::proto::sharing::EstablishConnectionStatus + connection_status, + ShareTarget share_target, int transfer_position, int concurrent_connections, + int64_t duration_millis, std::optional referrer_package) { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::SENDING_EVENT, EventType::ESTABLISH_CONNECTION); + + auto establish_connection = + analytics::proto::SharingLog::EstablishConnection::default_instance() + .New(); + + establish_connection->set_session_id(session_id); + establish_connection->set_status(connection_status); + establish_connection->set_allocated_share_target_info( + GetAllocatedShareTargetInfo(share_target)); + establish_connection->set_transfer_position(transfer_position); + establish_connection->set_concurrent_connections(concurrent_connections); + establish_connection->set_duration_millis(duration_millis); + if (referrer_package.has_value()) { + establish_connection->set_referrer_name(*referrer_package); + } + + sharing_log->set_allocated_establish_connection(establish_connection); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewAcceptAgreements() { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::SETTINGS_EVENT, EventType::ACCEPT_AGREEMENTS); + + auto accept_agreements = + analytics::proto::SharingLog::AcceptAgreements::default_instance().New(); + + sharing_log->set_allocated_accept_agreements(accept_agreements); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewDeclineAgreements() { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::SETTINGS_EVENT, EventType::DECLINE_AGREEMENTS); + + auto decline_agreements = + analytics::proto::SharingLog::DeclineAgreements::default_instance().New(); + + sharing_log->set_allocated_decline_agreements(decline_agreements); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewAddContact() { + std::unique_ptr sharing_log = + CreateSharingLog(EventCategory::SETTINGS_EVENT, EventType::ADD_CONTACT); + + auto add_contact = + analytics::proto::SharingLog::AddContact::default_instance().New(); + + sharing_log->set_allocated_add_contact(add_contact); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewRemoveContact() { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::SETTINGS_EVENT, EventType::REMOVE_CONTACT); + + auto remove_contact = + analytics::proto::SharingLog::RemoveContact::default_instance().New(); + + sharing_log->set_allocated_remove_contact(remove_contact); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewTapFeedback() { + std::unique_ptr sharing_log = + CreateSharingLog(EventCategory::SETTINGS_EVENT, EventType::TAP_FEEDBACK); + + auto tap_feedback = + analytics::proto::SharingLog::TapFeedback::default_instance().New(); + + sharing_log->set_allocated_tap_feedback(tap_feedback); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewTapHelp() { + std::unique_ptr sharing_log = + CreateSharingLog(EventCategory::SETTINGS_EVENT, EventType::TAP_HELP); + + auto tap_help = + analytics::proto::SharingLog::TapHelp::default_instance().New(); + + sharing_log->set_allocated_tap_help(tap_help); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewLaunchDeviceContactConsent( + ::location::nearby::proto::sharing::ConsentAcceptanceStatus status) { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::SETTINGS_EVENT, EventType::LAUNCH_CONSENT); + + auto launch_consent = + analytics::proto::SharingLog::LaunchConsent::default_instance().New(); + launch_consent->set_status(status); + + sharing_log->set_allocated_launch_consent(launch_consent); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewAdvertiseDevicePresenceEnd(int64_t session_id) { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::RECEIVING_EVENT, EventType::ADVERTISE_DEVICE_PRESENCE_END); + + auto advertise_device_presence_end = + analytics::proto::SharingLog::AdvertiseDevicePresenceEnd:: + default_instance() + .New(); + advertise_device_presence_end->set_session_id(session_id); + + sharing_log->set_allocated_advertise_device_presence_end( + advertise_device_presence_end); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewAdvertiseDevicePresenceStart( + int64_t session_id, DeviceVisibility visibility, + ::location::nearby::proto::sharing::SessionStatus status, + DataUsage data_usage, std::optional referrer_package) { + std::unique_ptr sharing_log = + CreateSharingLog(EventCategory::RECEIVING_EVENT, + EventType::ADVERTISE_DEVICE_PRESENCE_START); + + auto advertise_device_presence_start = + analytics::proto::SharingLog::AdvertiseDevicePresenceStart:: + default_instance() + .New(); + advertise_device_presence_start->set_session_id(session_id); + advertise_device_presence_start->set_visibility( + GetLoggerVisibility(visibility)); + advertise_device_presence_start->set_status(status); + advertise_device_presence_start->set_data_usage( + GetLoggerDataUsage(data_usage)); + if (referrer_package.has_value()) { + advertise_device_presence_start->set_referrer_name(*referrer_package); + } + + sharing_log->set_allocated_advertise_device_presence_start( + advertise_device_presence_start); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewDescribeAttachments( + const std::vector>& attachments) { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::SENDING_EVENT, EventType::DESCRIBE_ATTACHMENTS); + + auto describe_attachments = + analytics::proto::SharingLog::DescribeAttachments::default_instance() + .New(); + describe_attachments->set_allocated_attachments_info( + GenerateAllocatedAttachmentInfo(attachments)); + + sharing_log->set_allocated_describe_attachments(describe_attachments); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewDiscoverShareTarget( + ShareTarget share_target, int64_t session_id, + int64_t latency_since_scanning_start_millis, int64_t flow_id, + std::optional referrer_package, + int64_t latency_since_send_surface_registered_millis) { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::SENDING_EVENT, EventType::DISCOVER_SHARE_TARGET); + + auto discover_share_target = + analytics::proto::SharingLog::DiscoverShareTarget::default_instance() + .New(); + discover_share_target->set_session_id(session_id); + auto duration = google::protobuf::Duration::default_instance().New(); + duration->set_seconds(latency_since_scanning_start_millis / 1000); + duration->set_nanos((latency_since_scanning_start_millis % 1000) * 1000000); + discover_share_target->set_allocated_duration_since_scanning(duration); + discover_share_target->set_allocated_share_target_info( + GetAllocatedShareTargetInfo(share_target)); + discover_share_target->set_session_id(session_id); + discover_share_target->set_flow_id(flow_id); + + discover_share_target->set_latency_since_activity_start_millis( + latency_since_send_surface_registered_millis > 0 + ? latency_since_send_surface_registered_millis + : -1); + + sharing_log->set_allocated_discover_share_target(discover_share_target); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewEnableNearbySharing( + ::location::nearby::proto::sharing::NearbySharingStatus status) { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::SETTINGS_EVENT, EventType::ENABLE_NEARBY_SHARING); + + auto enable_nearby_sharing = + analytics::proto::SharingLog::EnableNearbySharing::default_instance() + .New(); + enable_nearby_sharing->set_status(status); + + sharing_log->set_allocated_enable_nearby_sharing(enable_nearby_sharing); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewOpenReceivedAttachments( + const std::vector>& attachments, + int64_t session_id) { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::RECEIVING_EVENT, EventType::OPEN_RECEIVED_ATTACHMENTS); + + auto open_received_attachments = + analytics::proto::SharingLog::OpenReceivedAttachments::default_instance() + .New(); + open_received_attachments->set_allocated_attachments_info( + GenerateAllocatedAttachmentInfo(attachments)); + open_received_attachments->set_session_id(session_id); + + sharing_log->set_allocated_open_received_attachments( + open_received_attachments); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewProcessReceivedAttachmentsEnd( + int64_t session_id, + ::location::nearby::proto::sharing::ProcessReceivedAttachmentsStatus + status) { + std::unique_ptr sharing_log = + CreateSharingLog(EventCategory::RECEIVING_EVENT, + EventType::PROCESS_RECEIVED_ATTACHMENTS_END); + + auto process_received_attachments_end = + analytics::proto::SharingLog::ProcessReceivedAttachmentsEnd:: + default_instance() + .New(); + process_received_attachments_end->set_status(status); + process_received_attachments_end->set_session_id(session_id); + + sharing_log->set_allocated_process_received_attachments_end( + process_received_attachments_end); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewReceiveAttachmentsEnd( + int64_t session_id, int64_t received_bytes, + ::location::nearby::proto::sharing::AttachmentTransmissionStatus status, + std::optional referrer_package) { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::RECEIVING_EVENT, EventType::RECEIVE_ATTACHMENTS_END); + + auto receive_attachments_end = + analytics::proto::SharingLog::ReceiveAttachmentsEnd::default_instance() + .New(); + receive_attachments_end->set_session_id(session_id); + receive_attachments_end->set_received_bytes(received_bytes); + receive_attachments_end->set_status(status); + if (referrer_package.has_value()) { + receive_attachments_end->set_referrer_name(*referrer_package); + } + + sharing_log->set_allocated_receive_attachments_end(receive_attachments_end); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewReceiveAttachmentsStart( + int64_t session_id, + const std::vector>& attachments) { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::RECEIVING_EVENT, EventType::RECEIVE_ATTACHMENTS_START); + + auto receive_attachments_start = + analytics::proto::SharingLog::ReceiveAttachmentsStart::default_instance() + .New(); + receive_attachments_start->set_allocated_attachments_info( + GenerateAllocatedAttachmentInfo(attachments)); + receive_attachments_start->set_session_id(session_id); + + sharing_log->set_allocated_receive_attachments_start( + receive_attachments_start); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewReceiveFastInitialization( + int64_t timeElapseSinceScreenUnlockMillis) { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::RECEIVING_EVENT, EventType::RECEIVE_FAST_INITIALIZATION); + + auto receive_fast_initialization = + analytics::proto::SharingLog::ReceiveFastInitialization:: + default_instance() + .New(); + + receive_fast_initialization->set_time_elapse_since_screen_unlock_millis( + timeElapseSinceScreenUnlockMillis); + + sharing_log->set_allocated_receive_initialization( + receive_fast_initialization); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewAcceptFastInitialization() { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::RECEIVING_EVENT, EventType::ACCEPT_FAST_INITIALIZATION); + + auto accept_fast_initialization = + analytics::proto::SharingLog::AcceptFastInitialization::default_instance() + .New(); + + sharing_log->set_allocated_accept_fast_initialization( + accept_fast_initialization); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewDismissFastInitialization() { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::RECEIVING_EVENT, EventType::DISMISS_FAST_INITIALIZATION); + + auto dismiss_fast_initialization = + analytics::proto::SharingLog::DismissFastInitialization:: + default_instance() + .New(); + + sharing_log->set_allocated_dismiss_fast_initialization( + dismiss_fast_initialization); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewReceiveIntroduction( + int64_t session_id, ShareTarget share_target, + std::optional referrer_package, + ::location::nearby::proto::sharing::OSType share_target_os_type) { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::RECEIVING_EVENT, EventType::RECEIVE_INTRODUCTION); + + auto receive_introduction = + analytics::proto::SharingLog::ReceiveIntroduction::default_instance() + .New(); + receive_introduction->set_session_id(session_id); + receive_introduction->set_allocated_share_target_info( + GetAllocatedShareTargetInfo(share_target, share_target_os_type)); + if (referrer_package.has_value()) { + receive_introduction->set_referrer_name(*referrer_package); + } + + sharing_log->set_allocated_receive_introduction(receive_introduction); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewRespondToIntroduction( + ::location::nearby::proto::sharing::ResponseToIntroduction action, + int64_t session_id) { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::RECEIVING_EVENT, EventType::RESPOND_TO_INTRODUCTION); + + auto respond_to_introduction = + analytics::proto::SharingLog::RespondToIntroduction::default_instance() + .New(); + respond_to_introduction->set_session_id(session_id); + respond_to_introduction->set_action(action); + + sharing_log->set_allocated_respond_introduction(respond_to_introduction); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewTapPrivacyNotification() { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::RECEIVING_EVENT, EventType::TAP_PRIVACY_NOTIFICATION); + + auto tap_privacy_notification = + analytics::proto::SharingLog::TapPrivacyNotification::default_instance() + .New(); + + sharing_log->set_allocated_tap_privacy_notification(tap_privacy_notification); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewDismissPrivacyNotification() { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::RECEIVING_EVENT, EventType::DISMISS_PRIVACY_NOTIFICATION); + + auto dismiss_privacy_notification = + analytics::proto::SharingLog::DismissPrivacyNotification:: + default_instance() + .New(); + + sharing_log->set_allocated_dismiss_privacy_notification( + dismiss_privacy_notification); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewScanForShareTargetsEnd(int64_t session_id) { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::SENDING_EVENT, EventType::SCAN_FOR_SHARE_TARGETS_END); + + auto scan_for_share_targets_end = + analytics::proto::SharingLog::ScanForShareTargetsEnd::default_instance() + .New(); + scan_for_share_targets_end->set_session_id(session_id); + + sharing_log->set_allocated_scan_for_share_targets_end( + scan_for_share_targets_end); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewScanForShareTargetsStart( + int64_t session_id, + ::location::nearby::proto::sharing::SessionStatus status, + AnalyticsInformation analytics_information, int64_t flow_id, + std::optional referrer_package) { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::SENDING_EVENT, EventType::SCAN_FOR_SHARE_TARGETS_START); + + auto scan_for_share_targets_start = + analytics::proto::SharingLog::ScanForShareTargetsStart::default_instance() + .New(); + scan_for_share_targets_start->set_session_id(session_id); + scan_for_share_targets_start->set_status(status); + scan_for_share_targets_start->set_scan_type( + static_cast<::location::nearby::proto::sharing::ScanType>( + analytics_information.send_surface_state)); + scan_for_share_targets_start->set_flow_id(flow_id); + if (referrer_package.has_value()) { + scan_for_share_targets_start->set_referrer_name(*referrer_package); + } + + sharing_log->set_allocated_scan_for_share_targets_start( + scan_for_share_targets_start); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewSendAttachmentsEnd( + int64_t session_id, int64_t sent_bytes, ShareTarget share_target, + ::location::nearby::proto::sharing::AttachmentTransmissionStatus status, + int transfer_position, int concurrent_connections, int64_t duration_millis, + std::optional referrer_package, + ::location::nearby::proto::sharing::ConnectionLayerStatus + connection_layer_status, + ::location::nearby::proto::sharing::OSType share_target_os_type) { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::SENDING_EVENT, EventType::SEND_ATTACHMENTS_END); + + auto send_attachments_end = + analytics::proto::SharingLog::SendAttachmentsEnd::default_instance() + .New(); + send_attachments_end->set_session_id(session_id); + send_attachments_end->set_sent_bytes(sent_bytes); + send_attachments_end->set_allocated_share_target_info( + GetAllocatedShareTargetInfo(share_target, share_target_os_type)); + send_attachments_end->set_status(status); + send_attachments_end->set_transfer_position(transfer_position); + send_attachments_end->set_concurrent_connections(concurrent_connections); + send_attachments_end->set_duration_millis(duration_millis); + if (referrer_package.has_value()) { + send_attachments_end->set_referrer_name(*referrer_package); + } + send_attachments_end->set_connection_layer_status(connection_layer_status); + + sharing_log->set_allocated_send_attachments_end(send_attachments_end); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewSendAttachmentsStart( + int64_t session_id, + const std::vector>& attachments, + int transfer_position, int concurrent_connections) { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::SENDING_EVENT, EventType::SEND_ATTACHMENTS_START); + + auto send_attachments_start = + analytics::proto::SharingLog::SendAttachmentsStart::default_instance() + .New(); + send_attachments_start->set_session_id(session_id); + send_attachments_start->set_allocated_attachments_info( + GenerateAllocatedAttachmentInfo(attachments)); + send_attachments_start->set_transfer_position(transfer_position); + send_attachments_start->set_concurrent_connections(concurrent_connections); + + sharing_log->set_allocated_send_attachments_start(send_attachments_start); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewSendFastInitialization() { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::SENDING_EVENT, EventType::SEND_FAST_INITIALIZATION); + + auto send_fast_initialization = + analytics::proto::SharingLog::SendFastInitialization::default_instance() + .New(); + + sharing_log->set_allocated_send_initialization(send_fast_initialization); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewSendStart(int64_t session_id, int transfer_position, + int concurrent_connections, + ShareTarget share_target) { + std::unique_ptr sharing_log = + CreateSharingLog(EventCategory::SENDING_EVENT, EventType::SEND_START); + + auto send_start = + analytics::proto::SharingLog::SendStart::default_instance().New(); + send_start->set_session_id(session_id); + send_start->set_transfer_position(transfer_position); + send_start->set_concurrent_connections(concurrent_connections); + send_start->set_allocated_share_target_info( + GetAllocatedShareTargetInfo(share_target)); + + sharing_log->set_allocated_send_start(send_start); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewSendIntroduction( + ShareTargetType target_type, int64_t session_id, + DeviceRelationship relationship, + ::location::nearby::proto::sharing::OSType share_target_os_type) { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::SENDING_EVENT, EventType::SEND_INTRODUCTION); + auto send_introduction = + analytics::proto::SharingLog::SendIntroduction::default_instance().New(); + send_introduction->set_allocated_share_target_info( + GetAllocatedShareTargetInfo(target_type, relationship, + share_target_os_type)); + send_introduction->set_session_id(session_id); + + sharing_log->set_allocated_send_introduction(send_introduction); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewSendIntroduction( + int64_t session_id, ShareTarget share_target, int transfer_position, + int concurrent_connections, + ::location::nearby::proto::sharing::OSType share_target_os_type) { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::SENDING_EVENT, EventType::SEND_INTRODUCTION); + + auto send_introduction = + analytics::proto::SharingLog::SendIntroduction::default_instance().New(); + send_introduction->set_allocated_share_target_info( + GetAllocatedShareTargetInfo(share_target, share_target_os_type)); + send_introduction->set_session_id(session_id); + send_introduction->set_transfer_position(transfer_position); + send_introduction->set_concurrent_connections(concurrent_connections); + + sharing_log->set_allocated_send_introduction(send_introduction); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewSetVisibility(DeviceVisibility src_visibility, + DeviceVisibility dst_visibility, + int64_t duration_millis) { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::SETTINGS_EVENT, EventType::SET_VISIBILITY); + + auto set_visibility = + analytics::proto::SharingLog::SetVisibility::default_instance().New(); + set_visibility->set_visibility(GetLoggerVisibility(dst_visibility)); + set_visibility->set_source_visibility(GetLoggerVisibility(src_visibility)); + set_visibility->set_duration_millis(duration_millis); + + sharing_log->set_allocated_set_visibility(set_visibility); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewDeviceSettings(AnalyticsDeviceSettings settings) { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::SETTINGS_EVENT, EventType::DEVICE_SETTINGS); + + auto device_settings = + analytics::proto::SharingLog::DeviceSettings::default_instance().New(); + device_settings->set_data_usage(GetLoggerDataUsage(settings.data_usage)); + device_settings->set_device_name_size(settings.device_name_size); + device_settings->set_is_show_notification_enabled( + settings.is_fast_init_notification_enabled); + device_settings->set_visibility(GetLoggerVisibility(settings.visibility)); + + sharing_log->set_allocated_device_settings(device_settings); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewFastShareServerResponse( + ::location::nearby::proto::sharing::ServerActionName name, + ::location::nearby::proto::sharing::ServerResponseState state, + int64_t latency_millis) { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::SETTINGS_EVENT, EventType::FAST_SHARE_SERVER_RESPONSE); + + auto fast_share_server_response = + analytics::proto::SharingLog::FastShareServerResponse::default_instance() + .New(); + fast_share_server_response->set_name(name); + fast_share_server_response->set_status(state); + fast_share_server_response->set_latency_millis(latency_millis); + + sharing_log->set_allocated_fast_share_server_response( + fast_share_server_response); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewSetDataUsage(DataUsage original_preference, + DataUsage preference) { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::SETTINGS_EVENT, EventType::SET_DATA_USAGE); + + auto set_data_usage = + analytics::proto::SharingLog::SetDataUsage::default_instance().New(); + set_data_usage->set_original_preference( + GetLoggerDataUsage(original_preference)); + set_data_usage->set_preference(GetLoggerDataUsage(preference)); + + sharing_log->set_allocated_set_data_usage(set_data_usage); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewAddQuickSettingsTile() { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::SETTINGS_EVENT, EventType::ADD_QUICK_SETTINGS_TILE); + + auto add_quick_settings_tile = + analytics::proto::SharingLog::AddQuickSettingsTile::default_instance() + .New(); + + sharing_log->set_allocated_add_quick_settings_tile(add_quick_settings_tile); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewRemoveQuickSettingsTile() { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::SETTINGS_EVENT, EventType::REMOVE_QUICK_SETTINGS_TILE); + + auto remove_quick_settings_tile = + analytics::proto::SharingLog::RemoveQuickSettingsTile::default_instance() + .New(); + + sharing_log->set_allocated_remove_quick_settings_tile( + remove_quick_settings_tile); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewTapQuickSettingsTile() { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::SETTINGS_EVENT, EventType::TAP_QUICK_SETTINGS_TILE); + + auto tap_quick_settings_tile = + analytics::proto::SharingLog::TapQuickSettingsTile::default_instance() + .New(); + + sharing_log->set_allocated_tap_quick_settings_tile(tap_quick_settings_tile); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewToggleShowNotification( + ::location::nearby::proto::sharing::ShowNotificationStatus prev_status, + ::location::nearby::proto::sharing::ShowNotificationStatus current_status) { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::SETTINGS_EVENT, EventType::TOGGLE_SHOW_NOTIFICATION); + + auto toggle_show_notification = + analytics::proto::SharingLog::ToggleShowNotification::default_instance() + .New(); + toggle_show_notification->set_current_status(current_status); + toggle_show_notification->set_previous_status(prev_status); + + sharing_log->set_allocated_toggle_show_notification(toggle_show_notification); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewSetDeviceName(int device_name_size) { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::SETTINGS_EVENT, EventType::SET_DEVICE_NAME); + + auto set_device_name = + analytics::proto::SharingLog::SetDeviceName::default_instance().New(); + set_device_name->set_device_name_size(device_name_size); + + sharing_log->set_allocated_set_device_name(set_device_name); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewRequestSettingPermissions( + ::location::nearby::proto::sharing::PermissionRequestType type, + ::location::nearby::proto::sharing::PermissionRequestResult result) { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::SETTINGS_EVENT, EventType::REQUEST_SETTING_PERMISSIONS); + + auto request_setting_permissions = + analytics::proto::SharingLog::RequestSettingPermissions:: + default_instance() + .New(); + request_setting_permissions->set_permission_type(type); + request_setting_permissions->set_permission_request_result(result); + + sharing_log->set_allocated_request_setting_permissions( + request_setting_permissions); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewInstallAPKStatus( + ::location::nearby::proto::sharing::InstallAPKStatus status, + ::location::nearby::proto::sharing::ApkSource source) { + std::unique_ptr sharing_log = + CreateSharingLog(EventCategory::RECEIVING_EVENT, EventType::INSTALL_APK); + + auto install_apk_status = + analytics::proto::SharingLog::InstallAPKStatus::default_instance().New(); + install_apk_status->add_status(status); + install_apk_status->add_source(source); + + sharing_log->set_allocated_install_apk_status(install_apk_status); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewVerifyAPKStatus( + ::location::nearby::proto::sharing::VerifyAPKStatus status, + ::location::nearby::proto::sharing::ApkSource source) { + std::unique_ptr sharing_log = + CreateSharingLog(EventCategory::RECEIVING_EVENT, EventType::VERIFY_APK); + + auto verify_apk_status = + analytics::proto::SharingLog::VerifyAPKStatus::default_instance().New(); + verify_apk_status->add_status(status); + verify_apk_status->add_source(source); + + sharing_log->set_allocated_verify_apk_status(verify_apk_status); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewSendDesktopNotification(DesktopNotification event) { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::SETTINGS_EVENT, EventType::SEND_DESKTOP_NOTIFICATION); + + auto send_desktop_notification = + analytics::proto::SharingLog::SendDesktopNotification::default_instance() + .New(); + send_desktop_notification->set_event(event); + + sharing_log->set_allocated_send_desktop_notification( + send_desktop_notification); + LogEvent(*sharing_log); +} + +void AnalyticsRecorder::NewSendDesktopTransferEvent( + DesktopTransferEventType event) { + std::unique_ptr sharing_log = CreateSharingLog( + EventCategory::SETTINGS_EVENT, EventType::SEND_DESKTOP_TRANSFER_EVENT); + + auto send_desktop_transfer_event = + analytics::proto::SharingLog::SendDesktopTransferEvent::default_instance() + .New(); + send_desktop_transfer_event->set_event(event); + + sharing_log->set_allocated_send_desktop_transfer_event( + send_desktop_transfer_event); + LogEvent(*sharing_log); +} + +// Start private methods. + +std::unique_ptr AnalyticsRecorder::CreateSharingLog( + EventCategory event_category, EventType event_type) { + auto sharing_log = std::make_unique(); + sharing_log->set_event_category(event_category); + sharing_log->set_event_type(event_type); + return sharing_log; +} + +void AnalyticsRecorder::LogEvent(const ::google::protobuf::MessageLite& message) { + if (event_logger_ == nullptr) { + return; + } + + event_logger_->Log(message); +} + +int64_t AnalyticsRecorder::GenerateNextId() { + absl::BitGen bit_gen; + return absl::Uniform(bit_gen, 0, INT64_MAX - 1) + 1; +} + +} // namespace analytics +} // namespace sharing +} // namespace nearby diff --git a/sharing/analytics/analytics_recorder.h b/sharing/analytics/analytics_recorder.h new file mode 100644 index 00000000..cdcb58f0 --- /dev/null +++ b/sharing/analytics/analytics_recorder.h @@ -0,0 +1,225 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_ANALYTICS_ANALYTICS_RECORDER_H_ +#define THIRD_PARTY_NEARBY_SHARING_ANALYTICS_ANALYTICS_RECORDER_H_ + +#include +#include +#include +#include +#include + +#include "internal/analytics/event_logger.h" +#include "proto/sharing_enums.pb.h" +#include "sharing/analytics/analytics_device_settings.h" +#include "sharing/analytics/analytics_information.h" +#include "sharing/attachment.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/proto/analytics/nearby_sharing_log.pb.h" +#include "sharing/proto/enums.pb.h" +#include "sharing/share_target.h" +#include "google/protobuf/message_lite.h" + +namespace nearby { +namespace sharing { +namespace analytics { + +class AnalyticsRecorder { + public: + explicit AnalyticsRecorder(::nearby::analytics::EventLogger* event_logger) + : event_logger_(event_logger) {} + ~AnalyticsRecorder() = default; + + void NewEstablishConnection( + int64_t session_id, + ::location::nearby::proto::sharing::EstablishConnectionStatus + connection_status, + ShareTarget share_target, int transfer_position, + int concurrent_connections, int64_t duration_millis, + std::optional referrer_package); + + void NewAcceptAgreements(); + + void NewDeclineAgreements(); + + void NewAddContact(); + + void NewRemoveContact(); + + void NewTapFeedback(); + + void NewTapHelp(); + + void NewLaunchDeviceContactConsent( + ::location::nearby::proto::sharing::ConsentAcceptanceStatus status); + + void NewAdvertiseDevicePresenceEnd(int64_t session_id); + + void NewAdvertiseDevicePresenceStart( + int64_t session_id, ::nearby::sharing::proto::DeviceVisibility visibility, + ::location::nearby::proto::sharing::SessionStatus status, + ::nearby::sharing::proto::DataUsage data_usage, + std::optional referrer_package); + + void NewDescribeAttachments( + const std::vector>& attachments); + + void NewDiscoverShareTarget( + ShareTarget share_target, int64_t session_id, + int64_t latency_since_scanning_start_millis, int64_t flow_id, + std::optional referrer_package, + int64_t latency_since_send_surface_registered_millis); + + void NewEnableNearbySharing( + ::location::nearby::proto::sharing::NearbySharingStatus status); + + void NewOpenReceivedAttachments( + const std::vector>& attachments, + int64_t session_id); + + void NewProcessReceivedAttachmentsEnd( + int64_t session_id, + ::location::nearby::proto::sharing::ProcessReceivedAttachmentsStatus + status); + + void NewReceiveAttachmentsEnd( + int64_t session_id, int64_t received_bytes, + ::location::nearby::proto::sharing::AttachmentTransmissionStatus status, + std::optional referrer_package); + + void NewReceiveAttachmentsStart( + int64_t session_id, + const std::vector>& attachments); + + void NewReceiveFastInitialization(int64_t timeElapseSinceScreenUnlockMillis); + + void NewAcceptFastInitialization(); + + void NewDismissFastInitialization(); + + void NewReceiveIntroduction( + int64_t session_id, ShareTarget share_target, + std::optional referrer_package, + ::location::nearby::proto::sharing::OSType share_target_os_type); + + void NewRespondToIntroduction( + ::location::nearby::proto::sharing::ResponseToIntroduction action, + int64_t session_id); + + void NewTapPrivacyNotification(); + + void NewDismissPrivacyNotification(); + + void NewScanForShareTargetsEnd(int64_t session_id); + + void NewScanForShareTargetsStart( + int64_t session_id, + ::location::nearby::proto::sharing::SessionStatus status, + AnalyticsInformation analytics_information, int64_t flow_id, + std::optional referrer_package); + + void NewSendAttachmentsEnd( + int64_t session_id, int64_t sent_bytes, ShareTarget share_target, + ::location::nearby::proto::sharing::AttachmentTransmissionStatus status, + int transfer_position, int concurrent_connections, + int64_t duration_millis, std::optional referrer_package, + ::location::nearby::proto::sharing::ConnectionLayerStatus + connection_layer_status, + ::location::nearby::proto::sharing::OSType share_target_os_type); + + void NewSendAttachmentsStart( + int64_t session_id, + const std::vector>& attachments, + int transfer_position, int concurrent_connections); + + void NewSendFastInitialization(); + + void NewSendStart(int64_t session_id, int transfer_position, + int concurrent_connections, ShareTarget share_target); + + void NewSendIntroduction( + ShareTargetType target_type, int64_t session_id, + ::location::nearby::proto::sharing::DeviceRelationship relationship, + ::location::nearby::proto::sharing::OSType share_target_os_type); + + void NewSendIntroduction( + int64_t session_id, ShareTarget share_target, int transfer_position, + int concurrent_connections, + ::location::nearby::proto::sharing::OSType share_target_os_type); + + void NewSetVisibility( + ::nearby::sharing::proto::DeviceVisibility src_visibility, + ::nearby::sharing::proto::DeviceVisibility dst_visibility, + int64_t duration_millis); + + void NewDeviceSettings(AnalyticsDeviceSettings settings); + + void NewFastShareServerResponse( + ::location::nearby::proto::sharing::ServerActionName name, + ::location::nearby::proto::sharing::ServerResponseState state, + int64_t latency_millis); + + void NewSetDataUsage(::nearby::sharing::proto::DataUsage original_preference, + ::nearby::sharing::proto::DataUsage preference); + + void NewAddQuickSettingsTile(); + + void NewRemoveQuickSettingsTile(); + + void NewTapQuickSettingsTile(); + + void NewToggleShowNotification( + ::location::nearby::proto::sharing::ShowNotificationStatus prev_status, + ::location::nearby::proto::sharing::ShowNotificationStatus + current_status); + + void NewSetDeviceName(int device_name_size); + + void NewRequestSettingPermissions( + ::location::nearby::proto::sharing::PermissionRequestType type, + ::location::nearby::proto::sharing::PermissionRequestResult result); + + void NewInstallAPKStatus( + ::location::nearby::proto::sharing::InstallAPKStatus status, + ::location::nearby::proto::sharing::ApkSource source); + + void NewVerifyAPKStatus( + ::location::nearby::proto::sharing::VerifyAPKStatus status, + ::location::nearby::proto::sharing::ApkSource source); + + void NewSendDesktopNotification( + ::location::nearby::proto::sharing::DesktopNotification event); + + void NewSendDesktopTransferEvent( + ::location::nearby::proto::sharing::DesktopTransferEventType event); + + // Generates a random number for session ID or flow ID. + int64_t GenerateNextId(); + + private: + std::unique_ptr<::nearby::sharing::analytics::proto::SharingLog> + CreateSharingLog( + ::location::nearby::proto::sharing::EventCategory event_category, + ::location::nearby::proto::sharing::EventType event_type); + void LogEvent(const ::google::protobuf::MessageLite& message); + + ::nearby::analytics::EventLogger* event_logger_ = nullptr; +}; + +} // namespace analytics +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_ANALYTICS_ANALYTICS_RECORDER_H_ diff --git a/sharing/analytics/analytics_recorder_test.cc b/sharing/analytics/analytics_recorder_test.cc new file mode 100644 index 00000000..ca7e53ca --- /dev/null +++ b/sharing/analytics/analytics_recorder_test.cc @@ -0,0 +1,1012 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/analytics/analytics_recorder.h" + +#include + +#include +#include +#include +#include + +#include "google/protobuf/duration.pb.h" +#include "gmock/gmock.h" +#include "protobuf-matchers/protocol-buffer-matchers.h" +#include "gtest/gtest.h" +#include "absl/strings/string_view.h" +#include "internal/analytics/event_logger.h" +#include "proto/sharing_enums.pb.h" +#include "sharing/analytics/analytics_device_settings.h" +#include "sharing/analytics/analytics_information.h" +#include "sharing/attachment.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/file_attachment.h" +#include "sharing/proto/analytics/nearby_sharing_log.pb.h" +#include "sharing/proto/enums.pb.h" +#include "sharing/proto/wire_format.pb.h" +#include "sharing/share_target.h" +#include "sharing/text_attachment.h" +#include "google/protobuf/message_lite.h" + +namespace nearby { +namespace sharing { +namespace analytics { +namespace { + +using ::location::nearby::proto::sharing::EventCategory; +using ::location::nearby::proto::sharing::EventType; +using ::location::nearby::proto::sharing::OSType; +using ::nearby::sharing::analytics::proto::SharingLog; +using ::nearby::sharing::proto::DataUsage; +using ::nearby::sharing::proto::DeviceVisibility; + +constexpr absl::string_view kFileName = "fileName"; +constexpr absl::string_view kTextBody = "textBody"; +constexpr absl::string_view kFileDocumentName = "abc.pdf"; +constexpr absl::string_view kFileMimeType = "application/pdf"; +constexpr absl::string_view kTextMimeType = "text/plain"; +constexpr absl::string_view kAppPackageName = "com.google.android.youtube"; + +class MockEventLogger : public ::nearby::analytics::EventLogger { + public: + MockEventLogger() = default; + ~MockEventLogger() override = default; + + MOCK_METHOD(void, Log, (const ::google::protobuf::MessageLite& message), (override)); +}; + +class AnalyticsRecorderTest : public ::testing::Test { + public: + AnalyticsRecorderTest() = default; + ~AnalyticsRecorderTest() override = default; + + const MockEventLogger& event_logger() { return event_logger_; } + + AnalyticsRecorder analytics_recoder() { return analytics_recorder_; } + + private: + MockEventLogger event_logger_; + AnalyticsRecorder analytics_recorder_{&event_logger_}; +}; + +TEST_F(AnalyticsRecorderTest, NewEstablishConnection) { + ShareTarget share_target; + share_target.device_name = "share_target"; + share_target.type = ShareTargetType::kPhone; + + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::ESTABLISH_CONNECTION); + EXPECT_EQ(log->event_category(), EventCategory::SENDING_EVENT); + EXPECT_EQ(log->establish_connection().status(), + ::location::nearby::proto::sharing:: + EstablishConnectionStatus::CONNECTION_STATUS_SUCCESS); + EXPECT_EQ(log->establish_connection().session_id(), 1); + EXPECT_EQ(log->establish_connection().transfer_position(), 1); + EXPECT_EQ(log->establish_connection().concurrent_connections(), 1); + EXPECT_EQ(log->establish_connection().duration_millis(), 100); + EXPECT_EQ(log->establish_connection().share_target_info().os_type(), + ::location::nearby::proto::sharing::OSType::ANDROID); + EXPECT_EQ(log->establish_connection().referrer_name(), kAppPackageName); + }); + + analytics_recoder().NewEstablishConnection( + 1, + ::location::nearby::proto::sharing::EstablishConnectionStatus:: + CONNECTION_STATUS_SUCCESS, + share_target, 1, 1, 100, std::string(kAppPackageName)); +} + +TEST_F(AnalyticsRecorderTest, NewAcceptAgreements) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::ACCEPT_AGREEMENTS); + EXPECT_EQ(log->event_category(), EventCategory::SETTINGS_EVENT); + }); + + analytics_recoder().NewAcceptAgreements(); +} + +TEST_F(AnalyticsRecorderTest, NewDeclineAgreements) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::DECLINE_AGREEMENTS); + EXPECT_EQ(log->event_category(), EventCategory::SETTINGS_EVENT); + }); + + analytics_recoder().NewDeclineAgreements(); +} + +TEST_F(AnalyticsRecorderTest, NewAddContact) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::ADD_CONTACT); + EXPECT_EQ(log->event_category(), EventCategory::SETTINGS_EVENT); + }); + + analytics_recoder().NewAddContact(); +} + +TEST_F(AnalyticsRecorderTest, NewRemoveContact) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::REMOVE_CONTACT); + EXPECT_EQ(log->event_category(), EventCategory::SETTINGS_EVENT); + }); + + analytics_recoder().NewRemoveContact(); +} + +TEST_F(AnalyticsRecorderTest, NewTapFeedback) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::TAP_FEEDBACK); + EXPECT_EQ(log->event_category(), EventCategory::SETTINGS_EVENT); + }); + + analytics_recoder().NewTapFeedback(); +} + +TEST_F(AnalyticsRecorderTest, NewTapHelp) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::TAP_HELP); + EXPECT_EQ(log->event_category(), EventCategory::SETTINGS_EVENT); + }); + + analytics_recoder().NewTapHelp(); +} + +TEST_F(AnalyticsRecorderTest, NewLaunchDeviceContactConsent) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::LAUNCH_CONSENT); + EXPECT_EQ(log->event_category(), EventCategory::SETTINGS_EVENT); + EXPECT_EQ(log->launch_consent().status(), + ::location::nearby::proto::sharing::ConsentAcceptanceStatus:: + CONSENT_ACCEPTED); + }); + + analytics_recoder().NewLaunchDeviceContactConsent( + ::location::nearby::proto::sharing::ConsentAcceptanceStatus:: + CONSENT_ACCEPTED); +} + +TEST_F(AnalyticsRecorderTest, NewAdvertiseDevicePresenceEnd) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::ADVERTISE_DEVICE_PRESENCE_END); + EXPECT_EQ(log->event_category(), EventCategory::RECEIVING_EVENT); + EXPECT_EQ(log->advertise_device_presence_end().session_id(), 100); + }); + + analytics_recoder().NewAdvertiseDevicePresenceEnd(100); +} + +TEST_F(AnalyticsRecorderTest, NewAdvertiseDevicePresenceStart) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), + EventType::ADVERTISE_DEVICE_PRESENCE_START); + EXPECT_EQ(log->event_category(), EventCategory::RECEIVING_EVENT); + EXPECT_EQ( + log->advertise_device_presence_start().visibility(), + ::location::nearby::proto::sharing::Visibility::CONTACTS_ONLY); + EXPECT_EQ(log->advertise_device_presence_start().status(), + ::location::nearby::proto::sharing::SessionStatus:: + SUCCEEDED_SESSION_STATUS); + EXPECT_EQ(log->advertise_device_presence_start().data_usage(), + ::location::nearby::proto::sharing::DataUsage::OFFLINE); + EXPECT_EQ(log->advertise_device_presence_start().referrer_name(), + kAppPackageName); + EXPECT_EQ(log->advertise_device_presence_start().session_id(), 100); + }); + + analytics_recoder().NewAdvertiseDevicePresenceStart( + 100, DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS, + ::location::nearby::proto::sharing::SessionStatus:: + SUCCEEDED_SESSION_STATUS, + DataUsage::OFFLINE_DATA_USAGE, std::string(kAppPackageName)); +} + +TEST_F(AnalyticsRecorderTest, NewDescribeAttachments) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::DESCRIBE_ATTACHMENTS); + EXPECT_EQ(log->event_category(), EventCategory::SENDING_EVENT); + EXPECT_EQ(log->describe_attachments() + .attachments_info() + .text_attachment_size(), + 5); + EXPECT_EQ(log->describe_attachments() + .attachments_info() + .text_attachment(0) + .size_bytes(), + kTextBody.size()); + EXPECT_EQ(log->describe_attachments() + .attachments_info() + .text_attachment(0) + .type(), + SharingLog::TextAttachment::UNKNOWN_TEXT_TYPE); + EXPECT_EQ(log->describe_attachments() + .attachments_info() + .text_attachment(1) + .type(), + SharingLog::TextAttachment::PHONE_NUMBER); + EXPECT_EQ(log->describe_attachments() + .attachments_info() + .text_attachment(2) + .type(), + SharingLog::TextAttachment::URL); + EXPECT_EQ(log->describe_attachments() + .attachments_info() + .text_attachment(3) + .type(), + SharingLog::TextAttachment::ADDRESS); + EXPECT_EQ(log->describe_attachments() + .attachments_info() + .text_attachment(4) + .type(), + SharingLog::TextAttachment::UNKNOWN_TEXT_TYPE); + EXPECT_EQ(log->describe_attachments() + .attachments_info() + .file_attachment_size(), + 4); + EXPECT_EQ(log->describe_attachments() + .attachments_info() + .file_attachment(0) + .size_bytes(), + 2); + EXPECT_EQ(log->describe_attachments() + .attachments_info() + .file_attachment(0) + .type(), + SharingLog::FileAttachment::IMAGE); + EXPECT_EQ(log->describe_attachments() + .attachments_info() + .file_attachment(1) + .type(), + SharingLog::FileAttachment::DOCUMENT); + EXPECT_EQ(log->describe_attachments() + .attachments_info() + .file_attachment(2) + .type(), + SharingLog::FileAttachment::AUDIO); + EXPECT_EQ(log->describe_attachments() + .attachments_info() + .file_attachment(3) + .type(), + SharingLog::FileAttachment::DOCUMENT); + }); + + std::vector> attachments; + attachments.push_back(std::make_unique( + 1, 2, std::string(kFileName), "", service::proto::FileMetadata::IMAGE)); + attachments.push_back(std::make_unique( + 2, 3, std::string(kFileDocumentName), std::string(kFileMimeType), + service::proto::FileMetadata::DOCUMENT)); + attachments.push_back(std::make_unique( + 3, 4, std::string(kFileName), "", service::proto::FileMetadata::AUDIO)); + attachments.push_back(std::make_unique( + 4, 5, std::string(kFileName), std::string(kTextMimeType), + service::proto::FileMetadata::DOCUMENT)); + attachments.push_back(std::make_unique( + 5, service::proto::TextMetadata::TEXT, std::string(kTextBody), + kTextBody.size())); + attachments.push_back(std::make_unique( + 6, service::proto::TextMetadata::PHONE_NUMBER, std::string(kTextBody), + kTextBody.size())); + attachments.push_back(std::make_unique( + 7, service::proto::TextMetadata::URL, std::string(kTextBody), + kTextBody.size())); + attachments.push_back(std::make_unique( + 8, service::proto::TextMetadata::ADDRESS, std::string(kTextBody), + kTextBody.size())); + attachments.push_back(std::make_unique( + 9, service::proto::TextMetadata::UNKNOWN, std::string(kTextBody), + kTextBody.size())); + + analytics_recoder().NewDescribeAttachments(attachments); +} + +TEST_F(AnalyticsRecorderTest, EmptyDescribeAttachments) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::DESCRIBE_ATTACHMENTS); + EXPECT_EQ(log->event_category(), EventCategory::SENDING_EVENT); + EXPECT_EQ(log->describe_attachments() + .attachments_info() + .text_attachment_size(), + 0); + EXPECT_EQ(log->describe_attachments() + .attachments_info() + .file_attachment_size(), + 0); + }); + + std::vector> attachments; + analytics_recoder().NewDescribeAttachments(attachments); +} + +TEST_F(AnalyticsRecorderTest, NewDiscoverShareTarget) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::DISCOVER_SHARE_TARGET); + EXPECT_EQ(log->event_category(), EventCategory::SENDING_EVENT); + EXPECT_EQ( + log->discover_share_target().duration_since_scanning().nanos(), + (2100 % 1000) * 1000000); + EXPECT_EQ( + log->discover_share_target().duration_since_scanning().seconds(), + 2100 / 1000); + EXPECT_EQ( + log->discover_share_target() + .share_target_info() + .device_relationship(), + ::location::nearby::proto::sharing::DeviceRelationship::IS_CONTACT); + EXPECT_EQ( + log->discover_share_target().share_target_info().device_type(), + ::location::nearby::proto::sharing::DeviceType::LAPTOP); + EXPECT_EQ(log->discover_share_target().share_target_info().os_type(), + ::location::nearby::proto::sharing::OSType::UNKNOWN_OS_TYPE); + EXPECT_EQ(log->discover_share_target().session_id(), 1); + EXPECT_EQ(log->discover_share_target().flow_id(), 100); + EXPECT_FALSE(log->discover_share_target().has_referrer_name()); + EXPECT_EQ( + log->discover_share_target().latency_since_activity_start_millis(), + 2); + }); + + ShareTarget share_target; + share_target.device_name = "share_target"; + share_target.type = ShareTargetType::kLaptop; + share_target.is_incoming = true; + share_target.is_known = true; + + analytics_recoder().NewDiscoverShareTarget(share_target, 1, 2100, 100, + std::nullopt, 2); +} + +TEST_F(AnalyticsRecorderTest, NewEnableNearbySharing) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::ENABLE_NEARBY_SHARING); + EXPECT_EQ(log->event_category(), EventCategory::SETTINGS_EVENT); + EXPECT_EQ(log->enable_nearby_sharing().status(), + ::location::nearby::proto::sharing::NearbySharingStatus::ON); + }); + + analytics_recoder().NewEnableNearbySharing( + ::location::nearby::proto::sharing::NearbySharingStatus::ON); +} + +TEST_F(AnalyticsRecorderTest, NewOpenReceivedAttachments) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::OPEN_RECEIVED_ATTACHMENTS); + EXPECT_EQ(log->event_category(), EventCategory::RECEIVING_EVENT); + EXPECT_EQ(log->open_received_attachments() + .attachments_info() + .text_attachment_size(), + 0); + EXPECT_EQ(log->open_received_attachments() + .attachments_info() + .file_attachment_size(), + 0); + EXPECT_EQ(log->open_received_attachments().session_id(), 1); + }); + + analytics_recoder().NewOpenReceivedAttachments( + std::vector>(), 1); +} + +TEST_F(AnalyticsRecorderTest, NewProcessReceivedAttachmentsEnd) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), + EventType::PROCESS_RECEIVED_ATTACHMENTS_END); + EXPECT_EQ(log->event_category(), EventCategory::RECEIVING_EVENT); + EXPECT_EQ(log->process_received_attachments_end().session_id(), 1); + EXPECT_EQ(log->process_received_attachments_end().status(), + ::location::nearby::proto::sharing:: + ProcessReceivedAttachmentsStatus:: + PROCESSING_STATUS_COMPLETE_PROCESSING_ATTACHMENTS); + }); + + analytics_recoder().NewProcessReceivedAttachmentsEnd( + 1, ::location::nearby::proto::sharing::ProcessReceivedAttachmentsStatus:: + PROCESSING_STATUS_COMPLETE_PROCESSING_ATTACHMENTS); +} + +TEST_F(AnalyticsRecorderTest, NewReceiveAttachmentsEnd) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::RECEIVE_ATTACHMENTS_END); + EXPECT_EQ(log->event_category(), EventCategory::RECEIVING_EVENT); + EXPECT_EQ(log->receive_attachments_end().session_id(), 1); + EXPECT_EQ(log->receive_attachments_end().received_bytes(), 2); + EXPECT_EQ( + log->receive_attachments_end().status(), + ::location::nearby::proto::sharing::AttachmentTransmissionStatus:: + COMPLETE_ATTACHMENT_TRANSMISSION_STATUS); + EXPECT_EQ(log->receive_attachments_end().referrer_name(), + kAppPackageName); + }); + + analytics_recoder().NewReceiveAttachmentsEnd( + 1, 2, + ::location::nearby::proto::sharing::AttachmentTransmissionStatus:: + COMPLETE_ATTACHMENT_TRANSMISSION_STATUS, + std::string(kAppPackageName)); +} + +TEST_F(AnalyticsRecorderTest, NewReceiveAttachmentsStart) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::RECEIVE_ATTACHMENTS_START); + EXPECT_EQ(log->event_category(), EventCategory::RECEIVING_EVENT); + EXPECT_EQ(log->receive_attachments_start().session_id(), 1); + EXPECT_EQ(log->receive_attachments_start() + .attachments_info() + .file_attachment_size(), + 0); + }); + + analytics_recoder().NewReceiveAttachmentsStart( + 1, std::vector>()); +} + +TEST_F(AnalyticsRecorderTest, NewReceiveFastInitialization) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::RECEIVE_FAST_INITIALIZATION); + EXPECT_EQ(log->event_category(), EventCategory::RECEIVING_EVENT); + EXPECT_EQ(log->receive_initialization() + .time_elapse_since_screen_unlock_millis(), + 1); + }); + + analytics_recoder().NewReceiveFastInitialization(1); +} + +TEST_F(AnalyticsRecorderTest, NewAcceptFastInitialization) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::ACCEPT_FAST_INITIALIZATION); + EXPECT_EQ(log->event_category(), EventCategory::RECEIVING_EVENT); + }); + + analytics_recoder().NewAcceptFastInitialization(); +} + +TEST_F(AnalyticsRecorderTest, NewDismissFastInitialization) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::DISMISS_FAST_INITIALIZATION); + EXPECT_EQ(log->event_category(), EventCategory::RECEIVING_EVENT); + }); + + analytics_recoder().NewDismissFastInitialization(); +} + +TEST_F(AnalyticsRecorderTest, NewReceiveIntroduction) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::RECEIVE_INTRODUCTION); + EXPECT_EQ(log->event_category(), EventCategory::RECEIVING_EVENT); + EXPECT_EQ(log->receive_introduction().session_id(), 1); + EXPECT_EQ(log->receive_introduction().share_target_info().os_type(), + ::location::nearby::proto::sharing::OSType::WINDOWS); + EXPECT_EQ(log->receive_introduction().share_target_info().device_type(), + ::location::nearby::proto::sharing::DeviceType::PHONE); + EXPECT_EQ(log->receive_introduction().referrer_name(), kAppPackageName); + }); + + ShareTarget share_target; + share_target.device_name = "share_target"; + share_target.type = ShareTargetType::kPhone; + analytics_recoder().NewReceiveIntroduction( + 1, share_target, std::string(kAppPackageName), OSType::WINDOWS); +} + +TEST_F(AnalyticsRecorderTest, NewRespondToIntroduction) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::RESPOND_TO_INTRODUCTION); + EXPECT_EQ(log->event_category(), EventCategory::RECEIVING_EVENT); + EXPECT_EQ(log->respond_introduction().session_id(), 1); + EXPECT_EQ(log->respond_introduction().action(), + ::location::nearby::proto::sharing::ResponseToIntroduction:: + ACCEPT_INTRODUCTION); + }); + + analytics_recoder().NewRespondToIntroduction( + ::location::nearby::proto::sharing::ResponseToIntroduction:: + ACCEPT_INTRODUCTION, + 1); +} + +TEST_F(AnalyticsRecorderTest, NewTapPrivacyNotification) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::TAP_PRIVACY_NOTIFICATION); + EXPECT_EQ(log->event_category(), EventCategory::RECEIVING_EVENT); + }); + + analytics_recoder().NewTapPrivacyNotification(); +} + +TEST_F(AnalyticsRecorderTest, NewDismissPrivacyNotification) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::DISMISS_PRIVACY_NOTIFICATION); + EXPECT_EQ(log->event_category(), EventCategory::RECEIVING_EVENT); + }); + + analytics_recoder().NewDismissPrivacyNotification(); +} + +TEST_F(AnalyticsRecorderTest, NewScanForShareTargetsEnd) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::SCAN_FOR_SHARE_TARGETS_END); + EXPECT_EQ(log->event_category(), EventCategory::SENDING_EVENT); + EXPECT_EQ(log->scan_for_share_targets_end().session_id(), 100); + }); + + analytics_recoder().NewScanForShareTargetsEnd(100); +} + +TEST_F(AnalyticsRecorderTest, NewScanForShareTargetsStart) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::SCAN_FOR_SHARE_TARGETS_START); + EXPECT_EQ(log->event_category(), EventCategory::SENDING_EVENT); + EXPECT_EQ(log->scan_for_share_targets_start().session_id(), 3); + EXPECT_EQ(log->scan_for_share_targets_start().status(), + ::location::nearby::proto::sharing::SessionStatus:: + FAILED_SESSION_STATUS); + EXPECT_EQ(log->scan_for_share_targets_start().flow_id(), 100); + EXPECT_EQ( + log->scan_for_share_targets_start().scan_type(), + ::location::nearby::proto::sharing::ScanType::FOREGROUND_SCAN); + EXPECT_FALSE(log->scan_for_share_targets_start().has_referrer_name()); + }); + + analytics_recoder().NewScanForShareTargetsStart( + 3, + ::location::nearby::proto::sharing::SessionStatus::FAILED_SESSION_STATUS, + AnalyticsInformation{SendSurfaceState::kForeground}, 100, std::nullopt); +} + +TEST_F(AnalyticsRecorderTest, NewSendAttachmentsEnd) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::SEND_ATTACHMENTS_END); + EXPECT_EQ(log->event_category(), EventCategory::SENDING_EVENT); + EXPECT_EQ(log->send_attachments_end().session_id(), 1); + EXPECT_EQ(log->send_attachments_end().sent_bytes(), 2); + EXPECT_EQ(log->send_attachments_end().share_target_info().os_type(), + ::location::nearby::proto::sharing::OSType::ANDROID); + EXPECT_EQ(log->send_attachments_end().share_target_info().device_type(), + ::location::nearby::proto::sharing::DeviceType::PHONE); + EXPECT_EQ(log->send_attachments_end().transfer_position(), 1); + EXPECT_EQ(log->send_attachments_end().concurrent_connections(), 2); + EXPECT_EQ(log->send_attachments_end().duration_millis(), 100); + EXPECT_EQ( + log->send_attachments_end().status(), + ::location::nearby::proto::sharing::AttachmentTransmissionStatus:: + COMPLETE_ATTACHMENT_TRANSMISSION_STATUS); + EXPECT_EQ(log->send_attachments_end().referrer_name(), kAppPackageName); + }); + + ShareTarget share_target; + share_target.device_name = "share_target"; + share_target.type = ShareTargetType::kPhone; + analytics_recoder().NewSendAttachmentsEnd( + 1, 2, share_target, + ::location::nearby::proto::sharing::AttachmentTransmissionStatus:: + COMPLETE_ATTACHMENT_TRANSMISSION_STATUS, + 1, 2, 100, std::string(kAppPackageName), + ::location::nearby::proto::sharing::ConnectionLayerStatus:: + CONNECTION_LAYER_STATUS_UNKNOWN, + OSType::UNKNOWN_OS_TYPE); +} + +TEST_F(AnalyticsRecorderTest, NewSendAttachmentsStart) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::SEND_ATTACHMENTS_START); + EXPECT_EQ(log->event_category(), EventCategory::SENDING_EVENT); + EXPECT_EQ(log->send_attachments_start().session_id(), 1); + EXPECT_EQ(log->send_attachments_start() + .attachments_info() + .file_attachment_size(), + 0); + EXPECT_EQ(log->send_attachments_start().transfer_position(), 100); + EXPECT_EQ(log->send_attachments_start().concurrent_connections(), 200); + }); + + analytics_recoder().NewSendAttachmentsStart( + 1, std::vector>(), 100, 200); +} + +TEST_F(AnalyticsRecorderTest, NewSendFastInitialization) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::SEND_FAST_INITIALIZATION); + EXPECT_EQ(log->event_category(), EventCategory::SENDING_EVENT); + }); + + analytics_recoder().NewSendFastInitialization(); +} + +TEST_F(AnalyticsRecorderTest, NewSendStart) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::SEND_START); + EXPECT_EQ(log->event_category(), EventCategory::SENDING_EVENT); + EXPECT_EQ(log->send_start().session_id(), 123); + EXPECT_EQ(log->send_start().transfer_position(), 1); + EXPECT_EQ(log->send_start().concurrent_connections(), 2); + EXPECT_EQ(log->send_start().share_target_info().device_type(), + ::location::nearby::proto::sharing::DeviceType::LAPTOP); + EXPECT_EQ(log->send_start().share_target_info().os_type(), + ::location::nearby::proto::sharing::OSType::UNKNOWN_OS_TYPE); + }); + + ShareTarget share_target; + share_target.device_name = "share_target"; + share_target.type = ShareTargetType::kLaptop; + share_target.is_known = true; + share_target.is_incoming = true; + analytics_recoder().NewSendStart(123, 1, 2, share_target); +} + +TEST_F(AnalyticsRecorderTest, NewSendIntroductionWithRelationship) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::SEND_INTRODUCTION); + EXPECT_EQ(log->event_category(), EventCategory::SENDING_EVENT); + EXPECT_EQ(log->send_introduction().session_id(), 5); + EXPECT_EQ(log->send_introduction().share_target_info().device_type(), + ::location::nearby::proto::sharing::DeviceType::LAPTOP); + EXPECT_EQ(log->send_introduction().share_target_info().os_type(), + ::location::nearby::proto::sharing::OSType::MACOS); + EXPECT_EQ( + log->send_introduction().share_target_info().device_relationship(), + ::location::nearby::proto::sharing::DeviceRelationship::IS_CONTACT); + }); + + analytics_recoder().NewSendIntroduction( + ShareTargetType::kLaptop, 5, + ::location::nearby::proto::sharing::DeviceRelationship::IS_CONTACT, + OSType::MACOS); +} + +TEST_F(AnalyticsRecorderTest, NewSendIntroduction) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::SEND_INTRODUCTION); + EXPECT_EQ(log->event_category(), EventCategory::SENDING_EVENT); + EXPECT_EQ(log->send_introduction().session_id(), 1); + EXPECT_EQ(log->send_introduction().transfer_position(), 2); + EXPECT_EQ(log->send_introduction().concurrent_connections(), 3); + EXPECT_EQ(log->send_introduction().share_target_info().device_type(), + ::location::nearby::proto::sharing::DeviceType::LAPTOP); + EXPECT_EQ(log->send_introduction().share_target_info().os_type(), + ::location::nearby::proto::sharing::OSType::UNKNOWN_OS_TYPE); + }); + + ShareTarget share_target; + share_target.device_name = "share_target"; + share_target.type = ShareTargetType::kLaptop; + analytics_recoder().NewSendIntroduction(1, share_target, 2, 3, + OSType::UNKNOWN_OS_TYPE); +} + +TEST_F(AnalyticsRecorderTest, NewSetVisibility) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::SET_VISIBILITY); + EXPECT_EQ(log->event_category(), EventCategory::SETTINGS_EVENT); + EXPECT_EQ(log->set_visibility().duration_millis(), 100); + EXPECT_EQ(log->set_visibility().source_visibility(), + ::location::nearby::proto::sharing::Visibility::EVERYONE); + EXPECT_EQ( + log->set_visibility().visibility(), + ::location::nearby::proto::sharing::Visibility::CONTACTS_ONLY); + }); + + analytics_recoder().NewSetVisibility( + DeviceVisibility::DEVICE_VISIBILITY_EVERYONE, + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS, 100); +} + +TEST_F(AnalyticsRecorderTest, NewDeviceSettings) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::DEVICE_SETTINGS); + EXPECT_EQ(log->event_category(), EventCategory::SETTINGS_EVENT); + EXPECT_EQ(log->device_settings().device_name_size(), 10); + EXPECT_EQ(log->device_settings().visibility(), + ::location::nearby::proto::sharing::Visibility::EVERYONE); + EXPECT_EQ(log->device_settings().data_usage(), + ::location::nearby::proto::sharing::DataUsage::WIFI_ONLY); + EXPECT_EQ(log->device_settings().is_show_notification_enabled(), true); + }); + + AnalyticsDeviceSettings device_settings; + device_settings.device_name_size = 10; + device_settings.data_usage = DataUsage::WIFI_ONLY_DATA_USAGE; + device_settings.is_fast_init_notification_enabled = true; + device_settings.visibility = DeviceVisibility::DEVICE_VISIBILITY_EVERYONE; + analytics_recoder().NewDeviceSettings(device_settings); +} + +TEST_F(AnalyticsRecorderTest, NewFastShareServerResponse) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::FAST_SHARE_SERVER_RESPONSE); + EXPECT_EQ(log->event_category(), EventCategory::SETTINGS_EVENT); + EXPECT_EQ(log->fast_share_server_response().latency_millis(), 200); + EXPECT_EQ(log->fast_share_server_response().name(), + ::location::nearby::proto::sharing::ServerActionName:: + UPLOAD_CONTACTS); + EXPECT_EQ(log->fast_share_server_response().status(), + ::location::nearby::proto::sharing::ServerResponseState:: + SERVER_RESPONSE_SUCCESS); + }); + + analytics_recoder().NewFastShareServerResponse( + ::location::nearby::proto::sharing::ServerActionName::UPLOAD_CONTACTS, + ::location::nearby::proto::sharing::ServerResponseState:: + SERVER_RESPONSE_SUCCESS, + 200); +} + +TEST_F(AnalyticsRecorderTest, NewSetDataUsage) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::SET_DATA_USAGE); + EXPECT_EQ(log->event_category(), EventCategory::SETTINGS_EVENT); + EXPECT_EQ(log->set_data_usage().preference(), + ::location::nearby::proto::sharing::DataUsage::OFFLINE); + EXPECT_EQ(log->set_data_usage().original_preference(), + ::location::nearby::proto::sharing::DataUsage::WIFI_ONLY); + }); + + analytics_recoder().NewSetDataUsage(DataUsage::WIFI_ONLY_DATA_USAGE, + DataUsage::OFFLINE_DATA_USAGE); +} + +TEST_F(AnalyticsRecorderTest, NewAddQuickSettingsTile) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::ADD_QUICK_SETTINGS_TILE); + EXPECT_EQ(log->event_category(), EventCategory::SETTINGS_EVENT); + }); + + analytics_recoder().NewAddQuickSettingsTile(); +} + +TEST_F(AnalyticsRecorderTest, NewRemoveQuickSettingsTile) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::REMOVE_QUICK_SETTINGS_TILE); + EXPECT_EQ(log->event_category(), EventCategory::SETTINGS_EVENT); + }); + + analytics_recoder().NewRemoveQuickSettingsTile(); +} + +TEST_F(AnalyticsRecorderTest, NewTapQuickSettingsTile) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::TAP_QUICK_SETTINGS_TILE); + EXPECT_EQ(log->event_category(), EventCategory::SETTINGS_EVENT); + }); + + analytics_recoder().NewTapQuickSettingsTile(); +} + +TEST_F(AnalyticsRecorderTest, NewToggleShowNotification) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::TOGGLE_SHOW_NOTIFICATION); + EXPECT_EQ(log->event_category(), EventCategory::SETTINGS_EVENT); + EXPECT_EQ( + log->toggle_show_notification().previous_status(), + ::location::nearby::proto::sharing::ShowNotificationStatus::SHOW); + EXPECT_EQ(log->toggle_show_notification().current_status(), + ::location::nearby::proto::sharing::ShowNotificationStatus:: + NOT_SHOW); + }); + + analytics_recoder().NewToggleShowNotification( + ::location::nearby::proto::sharing::ShowNotificationStatus::SHOW, + ::location::nearby::proto::sharing::ShowNotificationStatus::NOT_SHOW); +} + +TEST_F(AnalyticsRecorderTest, NewSetDeviceName) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::SET_DEVICE_NAME); + EXPECT_EQ(log->event_category(), EventCategory::SETTINGS_EVENT); + EXPECT_EQ(log->set_device_name().device_name_size(), 16); + }); + + analytics_recoder().NewSetDeviceName(16); +} + +TEST_F(AnalyticsRecorderTest, NewRequestSettingPermissions) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::REQUEST_SETTING_PERMISSIONS); + EXPECT_EQ(log->event_category(), EventCategory::SETTINGS_EVENT); + EXPECT_EQ(log->request_setting_permissions().permission_type(), + ::location::nearby::proto::sharing::PermissionRequestType:: + PERMISSION_BLUETOOTH); + EXPECT_EQ( + log->request_setting_permissions().permission_request_result(), + ::location::nearby::proto::sharing::PermissionRequestResult:: + PERMISSION_GRANTED); + }); + + analytics_recoder().NewRequestSettingPermissions( + ::location::nearby::proto::sharing::PermissionRequestType:: + PERMISSION_BLUETOOTH, + ::location::nearby::proto::sharing::PermissionRequestResult:: + PERMISSION_GRANTED); +} + +TEST_F(AnalyticsRecorderTest, NewInstallAPKStatus) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::INSTALL_APK); + EXPECT_EQ(log->event_category(), EventCategory::RECEIVING_EVENT); + EXPECT_EQ(log->install_apk_status().status(0), + ::location::nearby::proto::sharing::InstallAPKStatus:: + SUCCESS_INSTALLATION); + EXPECT_EQ( + log->install_apk_status().source(0), + ::location::nearby::proto::sharing::ApkSource::APK_FROM_SD_CARD); + }); + + analytics_recoder().NewInstallAPKStatus( + ::location::nearby::proto::sharing::InstallAPKStatus:: + SUCCESS_INSTALLATION, + ::location::nearby::proto::sharing::ApkSource::APK_FROM_SD_CARD); +} + +TEST_F(AnalyticsRecorderTest, NewVerifyAPKStatus) { + EXPECT_CALL(event_logger(), Log) + .WillOnce([=](const ::google::protobuf::MessageLite& message) { + auto log = dynamic_cast(&message); + ASSERT_NE(log, nullptr); + EXPECT_EQ(log->event_type(), EventType::VERIFY_APK); + EXPECT_EQ(log->event_category(), EventCategory::RECEIVING_EVENT); + EXPECT_EQ( + log->verify_apk_status().status(0), + ::location::nearby::proto::sharing::VerifyAPKStatus::INSTALLABLE); + EXPECT_EQ( + log->verify_apk_status().source(0), + ::location::nearby::proto::sharing::ApkSource::APK_FROM_SD_CARD); + }); + + analytics_recoder().NewVerifyAPKStatus( + ::location::nearby::proto::sharing::VerifyAPKStatus::INSTALLABLE, + ::location::nearby::proto::sharing::ApkSource::APK_FROM_SD_CARD); +} + +TEST_F(AnalyticsRecorderTest, GenerateID) { + int64_t id = analytics_recoder().GenerateNextId(); + EXPECT_GT(id, 0); + int64_t id2 = analytics_recoder().GenerateNextId(); + EXPECT_NE(id2, id); +} + +} // namespace +} // namespace analytics +} // namespace sharing +} // namespace nearby diff --git a/sharing/attachment.cc b/sharing/attachment.cc new file mode 100644 index 00000000..4a1aa3a5 --- /dev/null +++ b/sharing/attachment.cc @@ -0,0 +1,61 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/attachment.h" + +#include + +#include "internal/crypto_cros/random.h" + +namespace nearby { +namespace sharing { +namespace { + +int64_t CreateRandomId() { + int64_t id; + crypto::RandBytes(&id, sizeof(id)); + return id; +} + +} // namespace + +// TODO(b/258690183): Add unit tests for Attachment with same and different ids +Attachment::Attachment(Attachment::Family family, int64_t size, + int32_t batch_id, SourceType source_type) + : id_(CreateRandomId()), + family_(family), + size_(size), + batch_id_(batch_id), + source_type_(source_type) {} + +Attachment::Attachment(int64_t id, Attachment::Family family, int64_t size, + int32_t batch_id, SourceType source_type) + : id_(id), + family_(family), + size_(size), + batch_id_(batch_id), + source_type_(source_type) {} + +Attachment::Attachment(const Attachment&) = default; + +Attachment::Attachment(Attachment&&) = default; + +Attachment& Attachment::operator=(const Attachment&) = default; + +Attachment& Attachment::operator=(Attachment&&) = default; + +Attachment::~Attachment() = default; + +} // namespace sharing +} // namespace nearby diff --git a/sharing/attachment.h b/sharing/attachment.h new file mode 100644 index 00000000..83737cf4 --- /dev/null +++ b/sharing/attachment.h @@ -0,0 +1,84 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_ATTACHMENT_H_ +#define THIRD_PARTY_NEARBY_SHARING_ATTACHMENT_H_ + +#include + +#include "absl/strings/string_view.h" +#include "sharing/common/nearby_share_enums.h" + +namespace nearby { +namespace sharing { + +struct ShareTarget; + +// A single attachment to be sent by / received from a ShareTarget, can be +// either a file or text. +class Attachment { + public: + enum class Family { + kFile, + kText, + kWifiCredentials, + kMaxValue = kWifiCredentials + }; + + // TODO(b/269353084): Auto-generate codes when a new source type in + // sharing_enums.proto is added. + enum class SourceType { + kUnknown, + kContextMenu, + kDragAndDrop, + kSelectFilesButton, + kPaste, + kSelectFoldersButton, + kMaxValue = kSelectFoldersButton + }; + + Attachment(Family family, int64_t size, int32_t batch_id, + SourceType source_type); + Attachment(int64_t id, Family family, int64_t size, int32_t batch_id, + SourceType source_type); + Attachment(const Attachment&); + Attachment(Attachment&&); + Attachment& operator=(const Attachment&); + Attachment& operator=(Attachment&&); + virtual ~Attachment(); + + int64_t id() const { return id_; } + Family family() const { return family_; } + int64_t size() const { return size_; } + void set_size(int64_t size) { size_ = size; } + int32_t batch_id() const { return batch_id_; } + SourceType source_type() const { return source_type_; } + + // Move the attachment to share target. + virtual void MoveToShareTarget(ShareTarget& share_target) = 0; + virtual absl::string_view GetDescription() const = 0; + virtual ShareType GetShareType() const = 0; + + private: + int64_t id_; + Family family_; + int64_t size_; + int32_t batch_id_; + SourceType source_type_; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_ATTACHMENT_H_ diff --git a/sharing/attachment_info.cc b/sharing/attachment_info.cc new file mode 100644 index 00000000..0320a6d4 --- /dev/null +++ b/sharing/attachment_info.cc @@ -0,0 +1,29 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/attachment_info.h" + +#include + +namespace nearby { +namespace sharing { + +AttachmentInfo::AttachmentInfo() = default; +AttachmentInfo::~AttachmentInfo() = default; + +AttachmentInfo::AttachmentInfo(AttachmentInfo&&) = default; +AttachmentInfo& AttachmentInfo::operator=(AttachmentInfo&&) = default; + +} // namespace sharing +} // namespace nearby diff --git a/sharing/attachment_info.h b/sharing/attachment_info.h new file mode 100644 index 00000000..9645194e --- /dev/null +++ b/sharing/attachment_info.h @@ -0,0 +1,43 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_ATTACHMENT_INFO_H_ +#define THIRD_PARTY_NEARBY_SHARING_ATTACHMENT_INFO_H_ + +#include + +#include // NOLINT(build/c++17) +#include +#include + +namespace nearby { +namespace sharing { + +// Ties associated information to an Attachment. +struct AttachmentInfo { + AttachmentInfo(); + ~AttachmentInfo(); + + AttachmentInfo(AttachmentInfo&&); + AttachmentInfo& operator=(AttachmentInfo&&); + + std::optional payload_id; + std::string text_body; + std::filesystem::path file_path; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_ATTACHMENT_INFO_H_ diff --git a/sharing/connection_lifecycle_listener.h b/sharing/connection_lifecycle_listener.h new file mode 100644 index 00000000..024eade4 --- /dev/null +++ b/sharing/connection_lifecycle_listener.h @@ -0,0 +1,81 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +#ifndef THIRD_PARTY_NEARBY_SHARING_CONNECTION_LIFECYCLE_LISTENER_H_ +#define THIRD_PARTY_NEARBY_SHARING_CONNECTION_LIFECYCLE_LISTENER_H_ + +#include "absl/strings/string_view.h" +#include "sharing/nearby_connections_types.h" + +namespace nearby { +namespace sharing { + +// Listener for lifecycle events associated with a connection to a remote +// endpoint. Methods in this interface are called from the utility process, and +// are used by the browser process to listen for connection status associated +// with remote endpoints. +class ConnectionLifecycleListener { + public: + virtual ~ConnectionLifecycleListener() = default; + + // A basic encrypted channel has been created between this device and the + // remote endpoint. Both sides are now asked if they wish to accept or + // reject the connection before any data can be sent over this channel. + // + // Optionally, the caller can verify if this device is connected to the + // correct remote before accepting the connection. Typically, this involves + // showing ConnectionInfo::authentication_token on both devices and having the + // users manually compare and confirm. Both devices are given an identical + // authentication token. + // + // Call NearbyConnections::AcceptConnection() to accept the connection, or + // NearbyConnections::RejectConnection() to close the connection. + // + // endpoint_id - The identifier for the remote endpoint. + // info - Other relevant information about the connection. + virtual void OnConnectionInitiated(absl::string_view endpoint_id, + ConnectionInfo info) = 0; + + // Called after both sides have accepted the connection. + // + // endpoint_id - The identifier for the remote endpoint. + virtual void OnConnectionAccepted(absl::string_view endpoint_id) = 0; + + // Called when either side rejected the connection. + // Call NearbyConnections::DisconnectFromEndpoint() to terminate connection. + // + // endpoint_id - The identifier for the remote endpoint. + // status - The result of the connection. Valid values are + // Status::kSuccess and Status::kConnectionRejected}. + virtual void OnConnectionRejected(absl::string_view endpoint_id, + Status status) = 0; + + // Called when a remote endpoint is disconnected or has become unreachable. + // At this point service (re-)discovery may start again. + // + // endpoint_id - The identifier for the remote endpoint. + virtual void OnDisconnected(absl::string_view endpoint_id) = 0; + + // Called when the connection's available bandwidth has changed. + // + // endpoint_id - The identifier for the remote endpoint. + // quality - The new quality for the connection. + virtual void OnBandwidthChanged(absl::string_view endpoint_id, + Medium medium) = 0; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_CONNECTION_LIFECYCLE_LISTENER_H_ diff --git a/sharing/constants.h b/sharing/constants.h new file mode 100644 index 00000000..65b13300 --- /dev/null +++ b/sharing/constants.h @@ -0,0 +1,73 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_CONSTANTS_H_ +#define THIRD_PARTY_NEARBY_SHARING_CONSTANTS_H_ + +#include + +#include "absl/time/time.h" + +namespace nearby { +namespace sharing { + +// Timeout for reading a response frame from the remote device. +constexpr absl::Duration kReadResponseFrameTimeout = absl::Seconds(60); + +// Timeout for initiating a connection to a remote device. +constexpr absl::Duration kInitiateNearbyConnectionTimeout = absl::Seconds(60); + +// The delay before the sender will disconnect from the receiver after sending a +// file. Note that the receiver is expected to immediately disconnect, so this +// delay is a worst-effort disconnection. Disconnecting too early might +// interrupt in flight packets, especially over Wi-Fi LAN. +constexpr absl::Duration kOutgoingDisconnectionDelay = absl::Seconds(60); + +// The delay before the receiver will disconnect from the sender after rejecting +// an incoming file. The sender is expected to disconnect immediately after +// reading the rejection frame. +constexpr absl::Duration kIncomingRejectionDelay = absl::Seconds(2); + +// The delay before the initiator of the cancellation will disconnect from the +// other device. The device that did not initiate the cancellation is expected +// to disconnect immediately after reading the cancellation frame. +constexpr absl::Duration kInitiatorCancelDelay = absl::Seconds(5); + +// Timeout for reading a frame from the remote device. +constexpr absl::Duration kReadFramesTimeout = absl::Seconds(15); + +// Time to delay running the task to invalidate send and receive surfaces. +constexpr absl::Duration kInvalidateDelay = absl::Milliseconds(500); + +// Time between successive progress updates. +constexpr absl::Duration kMinProgressUpdateFrequency = absl::Milliseconds(100); + +// Attachments size threshold for transferring high quality medium. The default +// value is 1MB to match the default setting on Android. +constexpr int64_t kAttachmentsSizeThresholdOverHighQualityMedium = 1000000; + +// If true, the user will be able to accept incoming Wi-Fi Credential +// attachments and join the network when the attachment is opened. +constexpr bool kSupportReceivingWifiCredentials = true; + +// Time between successive instantaneous transfer speed in seconds. +constexpr double kTransferSpeedUpdateInterval = 1.0; + +// Time between successive transfer completion ETA in seconds. +constexpr double kEstimatedTimeRemainingUpdateInterval = 3.0; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_CONSTANTS_H_ diff --git a/sharing/endpoint_discovery_listener.h b/sharing/endpoint_discovery_listener.h new file mode 100644 index 00000000..74dc192a --- /dev/null +++ b/sharing/endpoint_discovery_listener.h @@ -0,0 +1,50 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +#ifndef THIRD_PARTY_NEARBY_SHARING_ENDPOINT_DISCOVERY_LISTENER_H_ +#define THIRD_PARTY_NEARBY_SHARING_ENDPOINT_DISCOVERY_LISTENER_H_ + +#include "absl/strings/string_view.h" +#include "sharing/nearby_connections_types.h" + +namespace nearby { +namespace sharing { + +// Listener invoked during endpoint discovery. This interface is used by the +// browser process to listen for a remote endpoint's status during endpoint +// discovery. +class EndpointDiscoveryListener { + public: + virtual ~EndpointDiscoveryListener() = default; + + // Called when a remote endpoint is discovered. + // + // endpoint_id - The ID of the remote endpoint that was discovered. + // info - Further information about the remote endpoint which may + // include the human-readable name if it is advertising in high + // visibility mode. + virtual void OnEndpointFound(absl::string_view endpoint_id, + DiscoveredEndpointInfo& info) = 0; + + // Called when a remote endpoint is no longer discoverable; only called for + // endpoints that previously had been passed to OnEndpointFound(). + // + // endpoint_id - The ID of the remote endpoint that was lost. + virtual void OnEndpointLost(absl::string_view endpoint_id) = 0; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_ENDPOINT_DISCOVERY_LISTENER_H_ diff --git a/sharing/fake_nearby_connection.cc b/sharing/fake_nearby_connection.cc new file mode 100644 index 00000000..7e96fb39 --- /dev/null +++ b/sharing/fake_nearby_connection.cc @@ -0,0 +1,94 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/fake_nearby_connection.h" + +#include + +#include +#include +#include +#include +#include + +#include "sharing/internal/public/logging.h" +#include "sharing/nearby_connection.h" + +namespace nearby { +namespace sharing { +FakeNearbyConnection::FakeNearbyConnection() = default; +FakeNearbyConnection::~FakeNearbyConnection() = default; + +void FakeNearbyConnection::Read(ReadCallback callback) { + NL_DCHECK(!closed_); + callback_ = std::move(callback); + MaybeRunCallback(); +} + +void FakeNearbyConnection::Write(std::vector bytes) { + NL_DCHECK(!closed_); + write_data_.push(std::move(bytes)); +} + +void FakeNearbyConnection::Close() { + NL_DCHECK(!closed_); + closed_ = true; + + if (disconnect_listener_) { + std::move(disconnect_listener_)(); + } + + if (callback_) { + has_read_callback_been_run_ = true; + auto callback = std::move(callback_); + callback_ = nullptr; + callback(std::nullopt); + } +} + +void FakeNearbyConnection::SetDisconnectionListener( + std::function listener) { + NL_DCHECK(!closed_); + disconnect_listener_ = std::move(listener); +} + +void FakeNearbyConnection::AppendReadableData(std::vector bytes) { + NL_DCHECK(!closed_); + read_data_.push(std::move(bytes)); + MaybeRunCallback(); +} + +std::vector FakeNearbyConnection::GetWrittenData() { + if (write_data_.empty()) return {}; + + std::vector bytes = std::move(write_data_.front()); + write_data_.pop(); + return bytes; +} + +bool FakeNearbyConnection::IsClosed() { return closed_; } + +void FakeNearbyConnection::MaybeRunCallback() { + NL_DCHECK(!closed_); + if (!callback_ || read_data_.empty()) return; + auto item = std::move(read_data_.front()); + read_data_.pop(); + has_read_callback_been_run_ = true; + auto callback = std::move(callback_); + callback_ = nullptr; + callback(std::move(item)); +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/fake_nearby_connection.h b/sharing/fake_nearby_connection.h new file mode 100644 index 00000000..c2a4e342 --- /dev/null +++ b/sharing/fake_nearby_connection.h @@ -0,0 +1,61 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_FAKE_NEARBY_CONNECTION_H_ +#define THIRD_PARTY_NEARBY_SHARING_FAKE_NEARBY_CONNECTION_H_ + +#include + +#include +#include +#include + +#include "sharing/nearby_connection.h" + +namespace nearby { +namespace sharing { + +class FakeNearbyConnection : public NearbyConnection { + public: + FakeNearbyConnection(); + ~FakeNearbyConnection() override; + + // NearbyConnection: + void Read(ReadCallback callback) override; + void Write(std::vector bytes) override; + void Close() override; + void SetDisconnectionListener(std::function listener) override; + + void AppendReadableData(std::vector bytes); + std::vector GetWrittenData(); + + bool IsClosed(); + bool has_read_callback_been_run() { return has_read_callback_been_run_; } + + private: + void MaybeRunCallback(); + + bool closed_ = false; + bool has_read_callback_been_run_ = false; + + ReadCallback callback_; + std::queue> read_data_; + std::queue> write_data_; + std::function disconnect_listener_; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_FAKE_NEARBY_CONNECTION_H_ diff --git a/sharing/fake_nearby_connections_manager.cc b/sharing/fake_nearby_connections_manager.cc new file mode 100644 index 00000000..418110f0 --- /dev/null +++ b/sharing/fake_nearby_connections_manager.cc @@ -0,0 +1,317 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/fake_nearby_connections_manager.h" + +#include + +#include // NOLINT(build/c++17) +#include +#include +#include +#include +#include +#include +#include +#include + +#include "absl/algorithm/container.h" +#include "absl/container/flat_hash_set.h" +#include "absl/strings/string_view.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/internal/public/logging.h" +#include "sharing/nearby_connections_manager.h" +#include "sharing/nearby_connections_types.h" +#include "sharing/proto/enums.pb.h" + +namespace nearby { +namespace sharing { + +using DataUsage = ::nearby::sharing::proto::DataUsage; + +FakeNearbyConnectionsManager::FakeNearbyConnectionsManager() = default; + +FakeNearbyConnectionsManager::~FakeNearbyConnectionsManager() = default; + +void FakeNearbyConnectionsManager::Shutdown() { + NL_DCHECK(!IsAdvertising()); + NL_DCHECK(!IsDiscovering()); + is_shutdown_ = true; +} + +void FakeNearbyConnectionsManager::StartAdvertising( + std::vector endpoint_info, IncomingConnectionListener* listener, + PowerLevel power_level, DataUsage data_usage, + ConnectionsCallback callback) { + NL_DCHECK(!IsAdvertising()); + is_shutdown_ = false; + advertising_listener_ = listener; + advertising_data_usage_ = data_usage; + advertising_power_level_ = power_level; + advertising_endpoint_info_ = std::move(endpoint_info); + if (capture_next_start_advertising_callback_) { + pending_start_advertising_callback_ = std::move(callback); + capture_next_start_advertising_callback_ = false; + } else { + std::move(callback)(Status::kSuccess); + } +} + +void FakeNearbyConnectionsManager::StopAdvertising( + ConnectionsCallback callback) { + NL_DCHECK(IsAdvertising()); + NL_DCHECK(!is_shutdown()); + advertising_listener_ = nullptr; + advertising_data_usage_ = DataUsage::UNKNOWN_DATA_USAGE; + advertising_power_level_ = PowerLevel::kUnknown; + advertising_endpoint_info_.reset(); + if (capture_next_stop_advertising_callback_) { + pending_stop_advertising_callback_ = std::move(callback); + capture_next_stop_advertising_callback_ = false; + } else { + std::move(callback)(Status::kSuccess); + } +} + +void FakeNearbyConnectionsManager::StartDiscovery( + DiscoveryListener* listener, DataUsage data_usage, + ConnectionsCallback callback) { + is_shutdown_ = false; + discovery_listener_ = listener; + std::move(callback)(Status::kSuccess); +} + +void FakeNearbyConnectionsManager::StopDiscovery() { + NL_DCHECK(IsDiscovering()); + NL_DCHECK(!is_shutdown()); + discovery_listener_ = nullptr; +} + +void FakeNearbyConnectionsManager::Connect( + std::vector endpoint_info, absl::string_view endpoint_id, + std::optional> bluetooth_mac_address, + DataUsage data_usage, TransportType transport_type, + NearbyConnectionCallback callback) { + NL_DCHECK(!is_shutdown()); + connected_data_usage_ = data_usage; + transport_type_ = transport_type; + connection_endpoint_infos_.emplace(endpoint_id, std::move(endpoint_info)); + std::move(callback)(connection_, Status::kUnknown); +} + +void FakeNearbyConnectionsManager::Disconnect(absl::string_view endpoint_id) { + NL_DCHECK(!is_shutdown()); + connection_endpoint_infos_.erase(std::string(endpoint_id)); +} + +void FakeNearbyConnectionsManager::Send( + absl::string_view endpoint_id, std::unique_ptr payload, + std::weak_ptr listener) { + NL_DCHECK(!is_shutdown()); + if (send_payload_callback_) + send_payload_callback_(std::move(payload), listener); +} + +void FakeNearbyConnectionsManager::RegisterPayloadStatusListener( + int64_t payload_id, std::weak_ptr listener) { + NL_DCHECK(!is_shutdown()); + + payload_status_listeners_[payload_id] = listener; +} + +void FakeNearbyConnectionsManager::RegisterPayloadPath( + int64_t payload_id, const std::filesystem::path& file_path, + ConnectionsCallback callback) { + NL_DCHECK(!is_shutdown()); + + registered_payload_paths_[payload_id] = file_path; + + auto it = payload_path_status_.find(payload_id); + if (it == payload_path_status_.end()) { + std::move(callback)(nearby::sharing::Status::kPayloadUnknown); + return; + } + + std::move(callback)(it->second); +} + +Payload* FakeNearbyConnectionsManager::GetIncomingPayload(int64_t payload_id) { + NL_DCHECK(!is_shutdown()); + auto it = incoming_payloads_.find(payload_id); + if (it == incoming_payloads_.end()) return nullptr; + + return it->second.get(); +} + +void FakeNearbyConnectionsManager::Cancel(int64_t payload_id) { + NL_DCHECK(!is_shutdown()); + std::weak_ptr listener = + GetRegisteredPayloadStatusListener(payload_id); + if (auto weak_listener = listener.lock()) { + auto status_update = std::make_unique(); + status_update->payload_id = payload_id; + status_update->status = PayloadStatus::kCanceled; + status_update->total_bytes = 0; + status_update->bytes_transferred = 0; + weak_listener->OnStatusUpdate(std::move(status_update), + /*upgraded_medium=*/std::nullopt); + payload_status_listeners_.erase(payload_id); + } + + canceled_payload_ids_.insert(payload_id); +} + +void FakeNearbyConnectionsManager::ClearIncomingPayloads() { + incoming_payloads_.clear(); + payload_status_listeners_.clear(); +} + +std::optional> +FakeNearbyConnectionsManager::GetRawAuthenticationToken( + absl::string_view endpoint_id) { + NL_DCHECK(!is_shutdown()); + + auto iter = endpoint_auth_tokens_.find(std::string(endpoint_id)); + if (iter != endpoint_auth_tokens_.end()) return iter->second; + + return std::nullopt; +} + +void FakeNearbyConnectionsManager::SetRawAuthenticationToken( + absl::string_view endpoint_id, std::vector token) { + endpoint_auth_tokens_[std::string(endpoint_id)] = std::move(token); +} + +void FakeNearbyConnectionsManager::UpgradeBandwidth( + absl::string_view endpoint_id) { + upgrade_bandwidth_endpoint_ids_.insert(std::string(endpoint_id)); +} + +void FakeNearbyConnectionsManager::OnEndpointFound( + absl::string_view endpoint_id, + std::unique_ptr info) { + if (discovery_listener_ == nullptr) return; + + discovery_listener_->OnEndpointDiscovered(endpoint_id, info->endpoint_info); +} + +void FakeNearbyConnectionsManager::OnEndpointLost( + absl::string_view endpoint_id) { + if (!discovery_listener_) return; + + discovery_listener_->OnEndpointLost(endpoint_id); +} + +bool FakeNearbyConnectionsManager::IsAdvertising() const { + return advertising_listener_ != nullptr; +} + +bool FakeNearbyConnectionsManager::IsDiscovering() const { + return discovery_listener_ != nullptr; +} + +bool FakeNearbyConnectionsManager::DidUpgradeBandwidth( + absl::string_view endpoint_id) const { + return upgrade_bandwidth_endpoint_ids_.find(endpoint_id) != + upgrade_bandwidth_endpoint_ids_.end(); +} + +void FakeNearbyConnectionsManager::SetPayloadPathStatus( + int64_t payload_id, ConnectionsStatus status) { + payload_path_status_[payload_id] = status; +} + +std::weak_ptr +FakeNearbyConnectionsManager::GetRegisteredPayloadStatusListener( + int64_t payload_id) { + auto it = payload_status_listeners_.find(payload_id); + if (it != payload_status_listeners_.end()) return it->second; + + return std::weak_ptr(); +} + +void FakeNearbyConnectionsManager::SetIncomingPayload( + int64_t payload_id, std::unique_ptr payload) { + incoming_payloads_[payload_id] = std::move(payload); +} + +bool FakeNearbyConnectionsManager::WasPayloadCanceled( + int64_t payload_id) const { + return absl::c_linear_search(canceled_payload_ids_, payload_id); +} + +std::optional +FakeNearbyConnectionsManager::GetRegisteredPayloadPath(int64_t payload_id) { + auto it = registered_payload_paths_.find(payload_id); + if (it == registered_payload_paths_.end()) return std::nullopt; + + return it->second; +} + +void FakeNearbyConnectionsManager::CleanupForProcessStopped() { + advertising_listener_ = nullptr; + advertising_data_usage_ = DataUsage::UNKNOWN_DATA_USAGE; + advertising_power_level_ = PowerLevel::kUnknown; + advertising_endpoint_info_.reset(); + + discovery_listener_ = nullptr; + + is_shutdown_ = true; +} + +FakeNearbyConnectionsManager::ConnectionsCallback +FakeNearbyConnectionsManager::GetStartAdvertisingCallback() { + capture_next_start_advertising_callback_ = true; + + FakeNearbyConnectionsManager::ConnectionsCallback callback = + [&](ConnectionsStatus status) { HandleStartAdvertisingCallback(status); }; + + return callback; +} + +FakeNearbyConnectionsManager::ConnectionsCallback +FakeNearbyConnectionsManager::GetStopAdvertisingCallback() { + capture_next_stop_advertising_callback_ = true; + + ConnectionsCallback callback = [&](ConnectionsStatus status) { + HandleStopAdvertisingCallback(status); + }; + return callback; +} + +void FakeNearbyConnectionsManager::HandleStartAdvertisingCallback( + ConnectionsStatus status) { + if (pending_start_advertising_callback_) { + std::move(pending_start_advertising_callback_)(status); + } + capture_next_start_advertising_callback_ = false; +} + +void FakeNearbyConnectionsManager::HandleStopAdvertisingCallback( + ConnectionsStatus status) { + if (pending_stop_advertising_callback_) { + std::move(pending_stop_advertising_callback_)(status); + } + capture_next_stop_advertising_callback_ = false; +} + +void FakeNearbyConnectionsManager::SetCustomSavePath( + absl::string_view custom_save_path) { + custom_save_path_ = custom_save_path; +} + +std::string FakeNearbyConnectionsManager::Dump() const { return ""; } + +} // namespace sharing +} // namespace nearby diff --git a/sharing/fake_nearby_connections_manager.h b/sharing/fake_nearby_connections_manager.h new file mode 100644 index 00000000..7e7221f6 --- /dev/null +++ b/sharing/fake_nearby_connections_manager.h @@ -0,0 +1,175 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_FAKE_NEARBY_CONNECTIONS_MANAGER_H_ +#define THIRD_PARTY_NEARBY_SHARING_FAKE_NEARBY_CONNECTIONS_MANAGER_H_ + +#include + +#include // NOLINT(build/c++17) +#include +#include +#include +#include +#include +#include +#include +#include + +#include "absl/container/flat_hash_set.h" +#include "absl/strings/string_view.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/nearby_connections_manager.h" +#include "sharing/nearby_connections_types.h" + +namespace nearby { +namespace sharing { + +// Fake NearbyConnectionsManager for testing. +class FakeNearbyConnectionsManager : public NearbyConnectionsManager { + public: + FakeNearbyConnectionsManager(); + ~FakeNearbyConnectionsManager() override; + + // NearbyConnectionsManager: + void Shutdown() override; + void StartAdvertising(std::vector endpoint_info, + IncomingConnectionListener* listener, + PowerLevel power_level, proto::DataUsage data_usage, + ConnectionsCallback callback) override; + void StopAdvertising(ConnectionsCallback callback) override; + void StartDiscovery(DiscoveryListener* listener, proto::DataUsage data_usage, + ConnectionsCallback callback) override; + void StopDiscovery() override; + void Connect(std::vector endpoint_info, + absl::string_view endpoint_id, + std::optional> bluetooth_mac_address, + proto::DataUsage data_usage, TransportType transport_type, + NearbyConnectionCallback callback) override; + void Disconnect(absl::string_view endpoint_id) override; + void Send(absl::string_view endpoint_id, std::unique_ptr payload, + std::weak_ptr listener) override; + void RegisterPayloadStatusListener( + int64_t payload_id, + std::weak_ptr listener) override; + void RegisterPayloadPath(int64_t payload_id, + const std::filesystem::path& file_path, + ConnectionsCallback callback) override; + Payload* GetIncomingPayload(int64_t payload_id) override; + void Cancel(int64_t payload_id) override; + void ClearIncomingPayloads() override; + std::optional> GetRawAuthenticationToken( + absl::string_view endpoint_id) override; + void UpgradeBandwidth(absl::string_view endpoint_id) override; + void SetCustomSavePath(absl::string_view custom_save_path) override; + + // Testing methods + void SetRawAuthenticationToken(absl::string_view endpoint_id, + std::vector token); + + void OnEndpointFound(absl::string_view endpoint_id, + std::unique_ptr info); + void OnEndpointLost(absl::string_view endpoint_id); + + bool IsAdvertising() const; + bool IsDiscovering() const; + bool DidUpgradeBandwidth(absl::string_view endpoint_id) const; + void SetPayloadPathStatus(int64_t payload_id, ConnectionsStatus status); + std::weak_ptr GetRegisteredPayloadStatusListener( + int64_t payload_id); + void SetIncomingPayload(int64_t payload_id, std::unique_ptr payload); + std::optional GetRegisteredPayloadPath( + int64_t payload_id); + bool WasPayloadCanceled(int64_t payload_id) const; + void CleanupForProcessStopped(); + ConnectionsCallback GetStartAdvertisingCallback(); + ConnectionsCallback GetStopAdvertisingCallback(); + + bool is_shutdown() const { return is_shutdown_; } + proto::DataUsage advertising_data_usage() const { + return advertising_data_usage_; + } + PowerLevel advertising_power_level() const { + return advertising_power_level_; + } + void set_nearby_connection(NearbyConnection* connection) { + connection_ = connection; + } + proto::DataUsage connected_data_usage() const { + return connected_data_usage_; + } + TransportType transport_type() const { return transport_type_; } + void set_send_payload_callback( + std::function, + std::weak_ptr)> + callback) { + send_payload_callback_ = std::move(callback); + } + const std::optional>& advertising_endpoint_info() { + return advertising_endpoint_info_; + } + + std::optional> connection_endpoint_info( + absl::string_view endpoint_id) { + auto it = connection_endpoint_infos_.find(std::string(endpoint_id)); + if (it == connection_endpoint_infos_.end()) return std::nullopt; + + return it->second; + } + + bool has_incoming_payloads() { return !incoming_payloads_.empty(); } + + private: + void HandleStartAdvertisingCallback(ConnectionsStatus status); + void HandleStopAdvertisingCallback(ConnectionsStatus status); + + IncomingConnectionListener* advertising_listener_ = nullptr; + DiscoveryListener* discovery_listener_ = nullptr; + bool is_shutdown_ = false; + proto::DataUsage advertising_data_usage_ = + proto::DataUsage::UNKNOWN_DATA_USAGE; + PowerLevel advertising_power_level_ = PowerLevel::kUnknown; + absl::flat_hash_set upgrade_bandwidth_endpoint_ids_; + std::map> endpoint_auth_tokens_; + NearbyConnection* connection_ = nullptr; + proto::DataUsage connected_data_usage_ = proto::DataUsage::UNKNOWN_DATA_USAGE; + TransportType transport_type_ = TransportType::kAny; + std::function, + std::weak_ptr)> + send_payload_callback_; + std::optional> advertising_endpoint_info_; + std::set disconnected_endpoints_; + std::set canceled_payload_ids_; + bool capture_next_stop_advertising_callback_ = false; + ConnectionsCallback pending_stop_advertising_callback_; + bool capture_next_start_advertising_callback_ = false; + ConnectionsCallback pending_start_advertising_callback_; + std::string custom_save_path_; + + // Maps endpoint_id to endpoint_info. + std::map> connection_endpoint_infos_; + + std::map payload_path_status_; + std::map> + payload_status_listeners_; + std::map> incoming_payloads_; + std::map registered_payload_paths_; + + std::string Dump() const override; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_FAKE_NEARBY_CONNECTIONS_MANAGER_H_ diff --git a/sharing/fake_nearby_connections_service.h b/sharing/fake_nearby_connections_service.h new file mode 100644 index 00000000..35084f3f --- /dev/null +++ b/sharing/fake_nearby_connections_service.h @@ -0,0 +1,119 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_FAKE_NEARBY_CONNECTIONS_SERVICE_H_ +#define THIRD_PARTY_NEARBY_SHARING_FAKE_NEARBY_CONNECTIONS_SERVICE_H_ + +#include +#include +#include +#include +#include + +#include "gmock/gmock.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "sharing/nearby_connections_service.h" +#include "sharing/nearby_connections_types.h" + +namespace nearby { +namespace sharing { + +class FakeNearbyConnectionsService : public NearbyConnectionsService { + public: + FakeNearbyConnectionsService() = default; + FakeNearbyConnectionsService(const FakeNearbyConnectionsService&) = default; + FakeNearbyConnectionsService& operator=(const FakeNearbyConnectionsService&) = + default; + FakeNearbyConnectionsService(FakeNearbyConnectionsService&&) = default; + FakeNearbyConnectionsService& operator=(FakeNearbyConnectionsService&&) = + default; + ~FakeNearbyConnectionsService() override = default; + + MOCK_METHOD(void, StartAdvertising, + (absl::string_view service_id, + const std::vector& endpoint_info, + AdvertisingOptions advertising_options, + ConnectionListener advertising_listener, + std::function callback), + (override)); + + MOCK_METHOD(void, StopAdvertising, + (absl::string_view service_id, + std::function callback), + (override)); + + MOCK_METHOD(void, StartDiscovery, + (absl::string_view service_id, DiscoveryOptions discovery_options, + DiscoveryListener discovery_listener, + std::function callback), + (override)); + + MOCK_METHOD(void, StopDiscovery, + (absl::string_view service_id, + std::function callback), + (override)); + + MOCK_METHOD(void, RequestConnection, + (absl::string_view service_id, + const std::vector& endpoint_info, + absl::string_view endpoint_id, + ConnectionOptions connection_options, + ConnectionListener connection_listener, + std::function callback), + (override)); + + MOCK_METHOD(void, DisconnectFromEndpoint, + (absl::string_view service_id, absl::string_view endpoint_id, + std::function callback), + (override)); + + MOCK_METHOD(void, SendPayload, + (absl::string_view service_id, + absl::Span endpoint_ids, + std::unique_ptr payload, + std::function callback), + (override)); + + MOCK_METHOD(void, CancelPayload, + (absl::string_view service_id, int64_t payload_id, + std::function callback), + (override)); + + MOCK_METHOD(void, InitiateBandwidthUpgrade, + (absl::string_view service_id, absl::string_view endpoint_id, + std::function callback), + (override)); + + MOCK_METHOD(void, AcceptConnection, + (absl::string_view service_id, absl::string_view endpoint_id, + PayloadListener payload_listener, + std::function callback), + (override)); + + MOCK_METHOD(void, StopAllEndpoints, + (std::function callback), (override)); + + MOCK_METHOD(void, SetCustomSavePath, + (absl::string_view path, + std::function callback), + (override)); + + MOCK_METHOD(std::string, Dump, (), (const override)); +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_FAKE_NEARBY_CONNECTIONS_SERVICE_H_ diff --git a/sharing/fake_nearby_sharing_service.cc b/sharing/fake_nearby_sharing_service.cc new file mode 100644 index 00000000..5f1070cf --- /dev/null +++ b/sharing/fake_nearby_sharing_service.cc @@ -0,0 +1,322 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/fake_nearby_sharing_service.h" + +#include +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "internal/base/observer_list.h" +#include "sharing/attachment.h" +#include "sharing/local_device_data/nearby_share_local_device_data_manager.h" +#include "sharing/nearby_sharing_service.h" +#include "sharing/nearby_sharing_settings.h" +#include "sharing/share_target.h" +#include "sharing/share_target_discovered_callback.h" +#include "sharing/transfer_metadata.h" +#include "sharing/transfer_update_callback.h" + +namespace nearby { +namespace sharing { + +void FakeNearbySharingService::AddObserver(Observer* observer) { + observers_.AddObserver(observer); +} + +void FakeNearbySharingService::RemoveObserver(Observer* observer) { + observers_.RemoveObserver(observer); +} +bool FakeNearbySharingService::HasObserver(Observer* observer) { + return observers_.HasObserver(observer); +} + +// Shutdown the Nearby Sharing service, and cleanup. +void FakeNearbySharingService::Shutdown( + std::function status_codes_callback) { + status_codes_callback(StatusCodes::kOk); +} + +// Registers a send surface for handling payload transfer status and device +// discovery. +void FakeNearbySharingService::RegisterSendSurface( + TransferUpdateCallback* transfer_callback, + ShareTargetDiscoveredCallback* discovery_callback, SendSurfaceState state, + std::function status_codes_callback) { + if (state == SendSurfaceState::kForeground) { + foreground_send_transfer_callbacks_.AddObserver(transfer_callback); + foreground_send_discovered_callbacks_.AddObserver(discovery_callback); + } else { + background_send_transfer_callbacks_.AddObserver(transfer_callback); + background_send_discovered_callbacks_.AddObserver(discovery_callback); + } + + status_codes_callback(StatusCodes::kOk); +} + +// Unregisters the current send surface. +void FakeNearbySharingService::UnregisterSendSurface( + TransferUpdateCallback* transfer_callback, + ShareTargetDiscoveredCallback* discovery_callback, + std::function status_codes_callback) { + foreground_send_transfer_callbacks_.RemoveObserver(transfer_callback); + foreground_send_discovered_callbacks_.RemoveObserver(discovery_callback); + background_send_transfer_callbacks_.RemoveObserver(transfer_callback); + background_send_discovered_callbacks_.RemoveObserver(discovery_callback); + + status_codes_callback(StatusCodes::kOk); +} + +// Registers a receiver surface for handling payload transfer status. +void FakeNearbySharingService::RegisterReceiveSurface( + TransferUpdateCallback* transfer_callback, ReceiveSurfaceState state, + std::function status_codes_callback) { + if (state == ReceiveSurfaceState::kForeground) { + foreground_receive_transfer_callbacks_.AddObserver(transfer_callback); + } else { + background_receive_transfer_callbacks_.AddObserver(transfer_callback); + } + + status_codes_callback(StatusCodes::kOk); +} + +// Unregisters the current receive surface. +void FakeNearbySharingService::UnregisterReceiveSurface( + TransferUpdateCallback* transfer_callback, + std::function status_codes_callback) { + foreground_receive_transfer_callbacks_.RemoveObserver(transfer_callback); + background_receive_transfer_callbacks_.RemoveObserver(transfer_callback); + status_codes_callback(StatusCodes::kOk); +} + +// Unregisters all foreground receive surfaces. +void FakeNearbySharingService::ClearForegroundReceiveSurfaces( + std::function status_codes_callback) { + status_codes_callback(StatusCodes::kOk); +} + +// Returns true if a foreground receive surface is registered. +bool FakeNearbySharingService::IsInHighVisibility() const { return false; } + +// Returns true if there is an ongoing file transfer. +bool FakeNearbySharingService::IsTransferring() const { return false; } + +// Returns true if we're currently receiving a file. +bool FakeNearbySharingService::IsReceivingFile() const { return false; } + +// Returns true if we're currently sending a file. +bool FakeNearbySharingService::IsSendingFile() const { return false; } + +// Returns true if we're currently attempting to connect to a +// remote device. +bool FakeNearbySharingService::IsConnecting() const { return false; } + +// Returns true if we are currently scanning for remote devices. +bool FakeNearbySharingService::IsScanning() const { return false; } + +// Sends |attachments| to the remote |share_target|. +void FakeNearbySharingService::SendAttachments( + const ShareTarget& share_target, + std::vector> attachments, + std::function status_codes_callback) { + status_codes_callback(StatusCodes::kOk); +} + +// Accepts incoming share from the remote |share_target|. +void FakeNearbySharingService::Accept( + const ShareTarget& share_target, + std::function status_codes_callback) { + status_codes_callback(StatusCodes::kOk); +} + +// Rejects incoming share from the remote |share_target|. +void FakeNearbySharingService::Reject( + const ShareTarget& share_target, + std::function status_codes_callback) { + status_codes_callback(StatusCodes::kOk); +} + +// Cancels outgoing shares to the remote |share_target|. +void FakeNearbySharingService::Cancel( + const ShareTarget& share_target, + std::function status_codes_callback) { + status_codes_callback(StatusCodes::kOk); +} + +// Returns true if the local user cancelled the transfer to remote +// |share_target|. +bool FakeNearbySharingService::DidLocalUserCancelTransfer( + const ShareTarget& share_target) { + return false; +} + +// Opens attachments from the remote |share_target|. +void FakeNearbySharingService::Open( + const ShareTarget& share_target, + std::function status_codes_callback) { + status_codes_callback(StatusCodes::kOk); +} + +// Opens an url target on a browser instance. +void FakeNearbySharingService::OpenUrl(const ::nearby::network::Url& url) {} + +// Copies text to cache/clipboard. +void FakeNearbySharingService::CopyText(absl::string_view text) {} + +// Sets a cleanup callback to be called once done with transfer for ARC. +void FakeNearbySharingService::SetArcTransferCleanupCallback( + std::function callback) {} + +std::string FakeNearbySharingService::Dump() const { return ""; } + +NearbyShareSettings* FakeNearbySharingService::GetSettings() { return nullptr; } + +NearbyShareHttpNotifier* FakeNearbySharingService::GetHttpNotifier() { + return nullptr; +} + +NearbyShareLocalDeviceDataManager* +FakeNearbySharingService::GetLocalDeviceDataManager() { + return nullptr; +} + +NearbyShareContactManager* FakeNearbySharingService::GetContactManager() { + return nullptr; +} + +NearbyShareCertificateManager* +FakeNearbySharingService::GetCertificateManager() { + return nullptr; +} + +AccountManager* FakeNearbySharingService::GetAccountManager() { + return nullptr; +} + +void FakeNearbySharingService::FireHighVisibilityChangeRequested() { + for (auto& observer : observers_.GetObservers()) { + observer->OnHighVisibilityChangeRequested(); + } +} + +void FakeNearbySharingService::FireHighVisibilityChanged( + bool in_high_visibility) { + for (auto& observer : observers_.GetObservers()) { + observer->OnHighVisibilityChanged(in_high_visibility); + } +} + +void FakeNearbySharingService::FireStartAdvertisingFailure() { + for (auto& observer : observers_.GetObservers()) { + observer->OnStartAdvertisingFailure(); + } +} + +void FakeNearbySharingService::FireStartDiscoveryResult(bool success) { + for (auto& observer : observers_.GetObservers()) { + observer->OnStartDiscoveryResult(success); + } +} + +void FakeNearbySharingService::FireFastInitiationDevicesDetected() { + for (auto& observer : observers_.GetObservers()) { + observer->OnFastInitiationDevicesDetected(); + } +} + +void FakeNearbySharingService::FireFastInitiationDevicesNotDetected() { + for (auto& observer : observers_.GetObservers()) { + observer->OnFastInitiationDevicesNotDetected(); + } +} + +void FakeNearbySharingService::FireFastInitiationScanningStopped() { + for (auto& observer : observers_.GetObservers()) { + observer->OnFastInitiationScanningStopped(); + } +} + +void FakeNearbySharingService::FireShutdown() { + for (auto& observer : observers_.GetObservers()) { + observer->OnShutdown(); + } +} + +void FakeNearbySharingService::FireSendTransferUpdate( + SendSurfaceState state, ShareTarget share_target, + TransferMetadata transfer_metadata) { + if (state == SendSurfaceState::kForeground) { + for (auto& transfer_callback : + foreground_send_transfer_callbacks_.GetObservers()) { + transfer_callback->OnTransferUpdate(share_target, transfer_metadata); + } + } else { + for (auto& transfer_callback : + background_send_transfer_callbacks_.GetObservers()) { + transfer_callback->OnTransferUpdate(share_target, transfer_metadata); + } + } +} + +void FakeNearbySharingService::FireReceiveTransferUpdate( + ReceiveSurfaceState state, ShareTarget share_target, + TransferMetadata transfer_metadata) { + if (state == ReceiveSurfaceState::kForeground) { + for (auto& transfer_callback : + foreground_receive_transfer_callbacks_.GetObservers()) { + transfer_callback->OnTransferUpdate(share_target, transfer_metadata); + } + } else { + for (auto& transfer_callback : + foreground_receive_transfer_callbacks_.GetObservers()) { + transfer_callback->OnTransferUpdate(share_target, transfer_metadata); + } + } +} + +// Fire discovery events. +void FakeNearbySharingService::FireShareTargetDiscovered( + SendSurfaceState state, ShareTarget share_target) { + if (state == SendSurfaceState::kForeground) { + for (auto& discovered_callback : + foreground_send_discovered_callbacks_.GetObservers()) { + discovered_callback->OnShareTargetDiscovered(share_target); + } + } else { + for (auto& discovered_callback : + background_send_discovered_callbacks_.GetObservers()) { + discovered_callback->OnShareTargetDiscovered(share_target); + } + } +} + +void FakeNearbySharingService::FireShareTargetLost(SendSurfaceState state, + ShareTarget share_target) { + if (state == SendSurfaceState::kForeground) { + for (auto& discovered_callback : + foreground_send_discovered_callbacks_.GetObservers()) { + discovered_callback->OnShareTargetLost(share_target); + } + } else { + for (auto& discovered_callback : + background_send_discovered_callbacks_.GetObservers()) { + discovered_callback->OnShareTargetLost(share_target); + } + } +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/fake_nearby_sharing_service.h b/sharing/fake_nearby_sharing_service.h new file mode 100644 index 00000000..8c3affc8 --- /dev/null +++ b/sharing/fake_nearby_sharing_service.h @@ -0,0 +1,183 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_FAKE_NEARBY_SHARING_SERVICE_H_ +#define THIRD_PARTY_NEARBY_SHARING_FAKE_NEARBY_SHARING_SERVICE_H_ + +#include +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "internal/base/observer_list.h" +#include "sharing/attachment.h" +#include "sharing/local_device_data/nearby_share_local_device_data_manager.h" +#include "sharing/nearby_sharing_service.h" +#include "sharing/nearby_sharing_settings.h" +#include "sharing/share_target.h" +#include "sharing/share_target_discovered_callback.h" +#include "sharing/transfer_metadata.h" +#include "sharing/transfer_update_callback.h" + +namespace nearby { +namespace sharing { + +class FakeNearbySharingService : public NearbySharingService { + public: + ~FakeNearbySharingService() override = default; + + void AddObserver(Observer* observer) override; + void RemoveObserver(Observer* observer) override; + bool HasObserver(Observer* observer) override; + + // Shutdown the Nearby Sharing service, and cleanup. + void Shutdown( + std::function status_codes_callback) override; + + // Register a send surface for handling payload transfer status and device. + // discovery. + void RegisterSendSurface( + TransferUpdateCallback* transfer_callback, + ShareTargetDiscoveredCallback* discovery_callback, SendSurfaceState state, + std::function status_codes_callback) override; + + // Unregisters the current send surface. + void UnregisterSendSurface( + TransferUpdateCallback* transfer_callback, + ShareTargetDiscoveredCallback* discovery_callback, + std::function status_codes_callback) override; + + // Registers a receiver surface for handling payload transfer status. + void RegisterReceiveSurface( + TransferUpdateCallback* transfer_callback, ReceiveSurfaceState state, + std::function status_codes_callback) override; + + // Unregisters the current receive surface. + void UnregisterReceiveSurface( + TransferUpdateCallback* transfer_callback, + std::function status_codes_callback) override; + + // Unregisters all foreground receive surfaces. + void ClearForegroundReceiveSurfaces( + std::function status_codes_callback) override; + + // Returns true if a foreground receive surface is registered. + bool IsInHighVisibility() const override; + + // Returns true if there is an ongoing file transfer. + bool IsTransferring() const override; + + // Returns true if we're currently receiving a file. + bool IsReceivingFile() const override; + + // Returns true if we're currently sending a file. + bool IsSendingFile() const override; + + // Returns true if we're currently attempting to connect to a + // remote device. + bool IsConnecting() const override; + + // Returns true if we are currently scanning for remote devices. + bool IsScanning() const override; + + // Sends |attachments| to the remote |share_target|. + void SendAttachments( + const ShareTarget& share_target, + std::vector> attachments, + std::function status_codes_callback) override; + + // Accepts incoming share from the remote |share_target|. + void Accept(const ShareTarget& share_target, + std::function + status_codes_callback) override; + + // Rejects incoming share from the remote |share_target|. + void Reject(const ShareTarget& share_target, + std::function + status_codes_callback) override; + + // Cancels outgoing shares to the remote |share_target|. + void Cancel(const ShareTarget& share_target, + std::function + status_codes_callback) override; + + // Returns true if the local user cancelled the transfer to remote + // |share_target|. + bool DidLocalUserCancelTransfer(const ShareTarget& share_target) override; + + // Opens attachments from the remote |share_target|. + void Open(const ShareTarget& share_target, + std::function status_codes_callback) + override; + + // Opens an url target on a browser instance. + void OpenUrl(const ::nearby::network::Url& url) override; + + // Copies text to cache/clipboard. + void CopyText(absl::string_view text) override; + + // Sets a cleanup callback to be called once done with transfer for ARC. + void SetArcTransferCleanupCallback(std::function callback) override; + + std::string Dump() const override; + + NearbyShareSettings* GetSettings() override; + NearbyShareHttpNotifier* GetHttpNotifier() override; + NearbyShareLocalDeviceDataManager* GetLocalDeviceDataManager() override; + NearbyShareContactManager* GetContactManager() override; + NearbyShareCertificateManager* GetCertificateManager() override; + AccountManager* GetAccountManager() override; + + // Fake methods to support test scenarios. + + // Fire observer events. + void FireHighVisibilityChangeRequested(); + void FireHighVisibilityChanged(bool in_high_visibility); + void FireStartAdvertisingFailure(); + void FireStartDiscoveryResult(bool success); + void FireFastInitiationDevicesDetected(); + void FireFastInitiationDevicesNotDetected(); + void FireFastInitiationScanningStopped(); + void FireShutdown(); + + // Fire transfer update events. + void FireSendTransferUpdate(SendSurfaceState state, ShareTarget share_target, + TransferMetadata transfer_metadata); + void FireReceiveTransferUpdate(ReceiveSurfaceState state, + ShareTarget share_target, + TransferMetadata transfer_metadata); + + // Fire discovery events. + void FireShareTargetDiscovered(SendSurfaceState state, + ShareTarget share_target); + void FireShareTargetLost(SendSurfaceState state, ShareTarget share_target); + + private: + ObserverList observers_; + ObserverList settings_observers_; + ObserverList foreground_send_transfer_callbacks_; + ObserverList background_send_transfer_callbacks_; + ObserverList + foreground_send_discovered_callbacks_; + ObserverList + background_send_discovered_callbacks_; + ObserverList foreground_receive_transfer_callbacks_; + ObserverList background_receive_transfer_callbacks_; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_FAKE_NEARBY_SHARING_SERVICE_H_ diff --git a/sharing/file_attachment.cc b/sharing/file_attachment.cc new file mode 100644 index 00000000..086c7092 --- /dev/null +++ b/sharing/file_attachment.cc @@ -0,0 +1,118 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/file_attachment.h" + +#include + +#include // NOLINT(build/c++17) +#include +#include +#include +#include + +#include "absl/strings/match.h" +#include "absl/strings/string_view.h" +#include "sharing/attachment.h" +#include "sharing/common/compatible_u8_string.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/internal/base/mime.h" +#include "sharing/proto/wire_format.pb.h" +#include "sharing/share_target.h" + +namespace nearby { +namespace sharing { +namespace { + +FileAttachment::Type FileAttachmentTypeFromMimeType( + absl::string_view mime_type) { + if (absl::StartsWith(mime_type, "image/")) + return service::proto::FileMetadata::IMAGE; + + if (absl::StartsWith(mime_type, "video/")) + return service::proto::FileMetadata::VIDEO; + + if (absl::StartsWith(mime_type, "audio/")) + return service::proto::FileMetadata::AUDIO; + + return service::proto::FileMetadata::UNKNOWN; +} + +std::string MimeTypeFromPath(const std::filesystem::path& path) { + std::string extension = path.extension().string(); + return extension.empty() ? "application/octet-stream" + : nearby::utils::GetWellKnownMimeTypeFromExtension( + extension.substr(1)); +} + +} // namespace + +FileAttachment::FileAttachment(std::filesystem::path file_path, + std::string parent_folder, int32_t batch_id, + SourceType source_type) + : Attachment(Attachment::Family::kFile, /*size=*/0, batch_id, source_type), + mime_type_(MimeTypeFromPath(file_path)), + type_(FileAttachmentTypeFromMimeType(mime_type_)), + file_path_(std::move(file_path)), + parent_folder_(std::move(parent_folder)) { + file_name_ = + GetCompatibleU8String(file_path_.value_or(L"").filename().u8string()); +} + +FileAttachment::FileAttachment(int64_t id, int64_t size, std::string file_name, + std::string mime_type, Type type, + std::string parent_folder, int32_t batch_id, + SourceType source_type) + : Attachment(id, Attachment::Family::kFile, size, batch_id, source_type), + file_name_(std::move(file_name)), + mime_type_(std::move(mime_type)), + type_(type), + parent_folder_(std::move(parent_folder)) {} + +void FileAttachment::MoveToShareTarget(ShareTarget& share_target) { + share_target.file_attachments.push_back(std::move(*this)); +} + +absl::string_view FileAttachment::GetDescription() const { return file_name_; } + +ShareType FileAttachment::GetShareType() const { + switch (type()) { + case service::proto::FileMetadata::IMAGE: + return ShareType::kImageFile; + case service::proto::FileMetadata::VIDEO: + return ShareType::kVideoFile; + case service::proto::FileMetadata::AUDIO: + return ShareType::kAudioFile; + default: + break; + } + + // Try matching on mime type if the attachment type is unrecognized. + if (mime_type() == "application/pdf") { + return ShareType::kPdfFile; + } else if (mime_type() == "application/vnd.google-apps.document") { + return ShareType::kGoogleDocsFile; + } else if (mime_type() == "application/vnd.google-apps.spreadsheet") { + return ShareType::kGoogleSheetsFile; + } else if (mime_type() == "application/vnd.google-apps.presentation") { + return ShareType::kGoogleSlidesFile; + } else if (mime_type() == "text/plain") { + return ShareType::kTextFile; + } else { + return ShareType::kUnknownFile; + } +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/file_attachment.h b/sharing/file_attachment.h new file mode 100644 index 00000000..191f729f --- /dev/null +++ b/sharing/file_attachment.h @@ -0,0 +1,83 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_FILE_ATTACHMENT_H_ +#define THIRD_PARTY_NEARBY_SHARING_FILE_ATTACHMENT_H_ + +#include + +#include // NOLINT(build/c++17) +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "sharing/attachment.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/proto/wire_format.pb.h" + +namespace nearby { +namespace sharing { + +// A single attachment to be sent by / received from a |ShareTarget|, can be +// either a file or text. +struct ShareTarget; + +class FileAttachment : public Attachment { + public: + using Type = nearby::sharing::service::proto::FileMetadata::Type; + + explicit FileAttachment(std::filesystem::path file_path, + std::string parent_folder = "", int32_t batch_id = 0, + SourceType source_type = SourceType::kUnknown); + FileAttachment(int64_t id, int64_t size, std::string file_name, + std::string mime_type, Type type, + std::string parent_folder = "", int32_t batch_id = 0, + SourceType source_type = SourceType::kUnknown); + FileAttachment(const FileAttachment&) = default; + FileAttachment(FileAttachment&&) = default; + FileAttachment& operator=(const FileAttachment&) = default; + FileAttachment& operator=(FileAttachment&&) = default; + ~FileAttachment() override = default; + + absl::string_view file_name() const { return file_name_; } + absl::string_view mime_type() const { return mime_type_; } + absl::string_view parent_folder() const { return parent_folder_; } + Type type() const { return type_; } + const std::optional& file_path() const { + return file_path_; + } + + // Attachment: + void MoveToShareTarget(ShareTarget& share_target) override; + absl::string_view GetDescription() const override; + ShareType GetShareType() const override; + + void set_file_path(std::optional path) { + file_path_ = std::move(path); + } + + private: + // File name should be in UTF8 format. + std::string file_name_; + std::string mime_type_; + Type type_; + std::optional file_path_; + std::string parent_folder_; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_FILE_ATTACHMENT_H_ diff --git a/sharing/incoming_frames_reader.cc b/sharing/incoming_frames_reader.cc new file mode 100644 index 00000000..74db3741 --- /dev/null +++ b/sharing/incoming_frames_reader.cc @@ -0,0 +1,257 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/incoming_frames_reader.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "absl/time/time.h" +#include "absl/types/span.h" +#include "internal/platform/mutex_lock.h" +#include "sharing/internal/public/context.h" +#include "sharing/internal/public/logging.h" +#include "sharing/nearby_connection.h" +#include "sharing/nearby_sharing_decoder.h" +#include "sharing/proto/wire_format.pb.h" + +namespace nearby { +namespace sharing { +namespace { + +using FrameType = ::nearby::sharing::service::proto::V1Frame_FrameType; +using V1Frame = ::nearby::sharing::service::proto::V1Frame; +using Frame = ::nearby::sharing::service::proto::Frame; + +std::ostream& operator<<(std::ostream& out, const FrameType& obj) { + out << static_cast::type>(obj); + return out; +} + +} // namespace + +IncomingFramesReader::IncomingFramesReader(Context* context, + NearbySharingDecoder* decoder, + NearbyConnection* connection) + : connection_(connection), decoder_(decoder) { + NL_DCHECK(decoder); + NL_DCHECK(connection); + timeout_timer_ = context->CreateTimer(); +} + +IncomingFramesReader::~IncomingFramesReader() { + MutexLock lock(&mutex_); + NL_LOG(INFO) << "~IncomingFramesReader is called"; + Done(std::nullopt); +} + +void IncomingFramesReader::ReadFrame( + std::function)> callback) { + MutexLock lock(&mutex_); + if (!read_frame_info_queue_.empty()) { + ReadFrameInfo read_fame_info{std::nullopt, std::move(callback), + std::nullopt}; + read_frame_info_queue_.push(std::move(read_fame_info)); + return; + } + + // Check in the cache for frame. + std::optional cached_frame = GetCachedFrame(std::nullopt); + if (cached_frame.has_value()) { + callback(std::move(cached_frame)); + return; + } + + ReadFrameInfo read_fame_info{std::nullopt, std::move(callback), std::nullopt}; + read_frame_info_queue_.push(std::move(read_fame_info)); + ReadNextFrame(); +} + +void IncomingFramesReader::ReadFrame( + FrameType frame_type, std::function)> callback, + absl::Duration timeout) { + MutexLock lock(&mutex_); + if (!read_frame_info_queue_.empty()) { + ReadFrameInfo read_fame_info{frame_type, std::move(callback), timeout}; + read_frame_info_queue_.push(std::move(read_fame_info)); + return; + } + + // Check in the cache for frame. + std::optional cached_frame = GetCachedFrame(frame_type); + if (cached_frame.has_value()) { + callback(std::move(cached_frame)); + return; + } + + ReadFrameInfo read_fame_info{frame_type, std::move(callback), timeout}; + read_frame_info_queue_.push(std::move(read_fame_info)); + + if (timeout_timer_->IsRunning()) { + timeout_timer_->Stop(); + } + + timeout_timer_->Start( + timeout / absl::Milliseconds(1), 0, [&, reader = GetWeakPtr()]() { + auto frame_reader = reader.lock(); + if (frame_reader == nullptr) { + NL_LOG(WARNING) << "IncomingFramesReader is released before."; + return; + } + OnTimeout(); + }); + + ReadNextFrame(); +} + +void IncomingFramesReader::ReadNextFrame() { + connection_->Read( + [&, reader = GetWeakPtr()](std::optional> bytes) { + auto frame_reader = reader.lock(); + if (frame_reader == nullptr) { + NL_LOG(WARNING) << "IncomingFramesReader is released before."; + return; + } + + OnDataReadFromConnection(std::move(bytes)); + }); +} + +void IncomingFramesReader::OnTimeout() { + MutexLock lock(&mutex_); + NL_LOG(WARNING) << __func__ << ": Timed out reading from NearbyConnection."; + Done(std::nullopt); +} + +void IncomingFramesReader::OnDataReadFromConnection( + std::optional> bytes) { + MutexLock lock(&mutex_); + if (read_frame_info_queue_.empty()) { + return; + } + + if (!bytes.has_value()) { + NL_LOG(WARNING) << __func__ << ": Failed to read frame"; + Done(std::nullopt); + return; + } + + std::unique_ptr frame = + decoder_->DecodeFrame(absl::MakeSpan(bytes->data(), bytes->size())); + if (frame == nullptr) { + NL_LOG(WARNING) + << __func__ + << ": Cannot decode frame. Not currently bound to nearby process"; + Done(std::nullopt); + return; + } + + OnFrameDecoded(std::move(*frame)); +} + +void IncomingFramesReader::OnFrameDecoded(std::optional frame) { + if (!frame.has_value()) { + ReadNextFrame(); + return; + } + + if (frame->version() != Frame::V1) { + NL_VLOG(1) << __func__ << ": Frame read does not have V1Frame"; + ReadNextFrame(); + return; + } + + auto v1_frame = frame->v1(); + FrameType v1_frame_type = v1_frame.type(); + + const ReadFrameInfo& frame_info = read_frame_info_queue_.front(); + if (frame_info.frame_type.has_value() && + *frame_info.frame_type != v1_frame_type) { + NL_LOG(WARNING) << __func__ << ": Failed to read frame of type " + << *frame_info.frame_type << ", but got frame of type " + << v1_frame_type << ". Cached for later."; + cached_frames_.insert({v1_frame_type, std::move(v1_frame)}); + ReadNextFrame(); + return; + } + + Done(std::move(v1_frame)); +} + +void IncomingFramesReader::Done(std::optional frame) { + if (read_frame_info_queue_.empty()) { + return; + } + + if (timeout_timer_ != nullptr) { + timeout_timer_->Stop(); + } + + bool is_empty_frame = !frame.has_value(); + ReadFrameInfo read_frame_info = std::move(read_frame_info_queue_.front()); + read_frame_info_queue_.pop(); + read_frame_info.callback(std::move(frame)); + + if (is_empty_frame) { + // should complete all pending readers. + while (!read_frame_info_queue_.empty()) { + read_frame_info = std::move(read_frame_info_queue_.front()); + read_frame_info_queue_.pop(); + read_frame_info.callback(std::nullopt); + } + return; + } + + if (!read_frame_info_queue_.empty()) { + ReadFrameInfo read_frame_info = std::move(read_frame_info_queue_.front()); + read_frame_info_queue_.pop(); + + if (read_frame_info.timeout.has_value()) { + ReadFrame(*read_frame_info.frame_type, + std::move(read_frame_info.callback), *read_frame_info.timeout); + } else { + ReadFrame(std::move(read_frame_info.callback)); + } + } +} + +std::optional IncomingFramesReader::GetCachedFrame( + std::optional + frame_type) { + NL_VLOG(1) << __func__ << ": Fetching cached frame"; + if (frame_type.has_value()) + NL_VLOG(1) << __func__ << ": Requested frame type - " << *frame_type; + + auto iter = frame_type.has_value() ? cached_frames_.find(*frame_type) + : cached_frames_.begin(); + + if (iter == cached_frames_.end()) return std::nullopt; + + NL_VLOG(1) << __func__ << ": Successfully read cached frame"; + std::optional frame = std::move(iter->second); + cached_frames_.erase(iter); + return frame; +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/incoming_frames_reader.h b/sharing/incoming_frames_reader.h new file mode 100644 index 00000000..c714f226 --- /dev/null +++ b/sharing/incoming_frames_reader.h @@ -0,0 +1,113 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_INCOMING_FRAMES_READER_H_ +#define THIRD_PARTY_NEARBY_SHARING_INCOMING_FRAMES_READER_H_ + +#include + +#include +#include +#include +#include +#include +#include + +#include "absl/time/time.h" +#include "internal/platform/mutex.h" +#include "internal/platform/timer.h" +#include "sharing/internal/public/context.h" +#include "sharing/nearby_connection.h" +#include "sharing/nearby_sharing_decoder.h" +#include "sharing/proto/wire_format.pb.h" + +namespace nearby { +namespace sharing { + +// Helper class to read incoming frames from Nearby devices. +class IncomingFramesReader + : public std::enable_shared_from_this { + public: + IncomingFramesReader(Context* context, NearbySharingDecoder* decoder, + NearbyConnection* connection); + virtual ~IncomingFramesReader(); + IncomingFramesReader(const IncomingFramesReader&) = delete; + IncomingFramesReader& operator=(IncomingFramesReader&) = delete; + + // Reads an incoming frame from |connection|. |callback| is called + // with the frame read from connection or nullopt if connection socket is + // closed. + // + // Note: Callers are expected wait for |callback| to be run before scheduling + // subsequent calls to ReadFrame(..). + virtual void ReadFrame( + std::function< + void(std::optional)> + callback); + + // Reads a frame of type |frame_type| from |connection|. |callback| is called + // with the frame read from connection or nullopt if connection socket is + // closed or |timeout| units of time have passed. + // + // Note: Callers are expected wait for |callback| to be run before scheduling + // subsequent calls to ReadFrame(..). + virtual void ReadFrame( + nearby::sharing::service::proto::V1Frame_FrameType frame_type, + std::function< + void(std::optional)> + callback, + absl::Duration timeout); + + std::weak_ptr GetWeakPtr() { + return this->weak_from_this(); + } + + private: + struct ReadFrameInfo { + std::optional + frame_type = std::nullopt; + std::function)> + callback = nullptr; + std::optional timeout = std::nullopt; + }; + + void ReadNextFrame(); + void OnDataReadFromConnection(std::optional> bytes); + void OnFrameDecoded( + std::optional frame); + void OnTimeout(); + void Done(std::optional frame); + std::optional GetCachedFrame( + std::optional + frame_type); + + NearbyConnection* connection_; + NearbySharingDecoder* decoder_ = nullptr; + + RecursiveMutex mutex_; + std::queue read_frame_info_queue_; + std::function timeout_callback_; + + // Caches frames read from NearbyConnection which are not used immediately. + std::map> + cached_frames_; + + std::unique_ptr timeout_timer_ = nullptr; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_INCOMING_FRAMES_READER_H_ diff --git a/sharing/incoming_frames_reader_test.cc b/sharing/incoming_frames_reader_test.cc new file mode 100644 index 00000000..c0492749 --- /dev/null +++ b/sharing/incoming_frames_reader_test.cc @@ -0,0 +1,317 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/incoming_frames_reader.h" + +#include + +#include +#include +#include + +#include "gtest/gtest.h" +#include "absl/synchronization/notification.h" +#include "absl/time/time.h" +#include "internal/test/fake_clock.h" +#include "internal/test/fake_task_runner.h" +#include "sharing/fake_nearby_connection.h" +#include "sharing/internal/public/context.h" +#include "sharing/internal/test/fake_context.h" +#include "sharing/nearby_sharing_decoder_impl.h" +#include "sharing/proto/wire_format.pb.h" + +namespace nearby { +namespace sharing { +namespace { + +using ::nearby::sharing::service::proto::V1Frame; + +constexpr absl::Duration kTimeout = absl::Milliseconds(1000); + +std::optional> GetIntroductionFrame() { + nearby::sharing::service::proto::Frame frame = + nearby::sharing::service::proto::Frame(); + frame.set_version(nearby::sharing::service::proto::Frame::V1); + V1Frame* v1frame = frame.mutable_v1(); + v1frame->set_type(service::proto::V1Frame::INTRODUCTION); + v1frame->mutable_introduction(); + + std::vector data; + data.resize(frame.ByteSize()); + if (frame.SerializeToArray(data.data(), data.size())) { + return data; + } + + return std::nullopt; +} + +std::optional> GetCancelFrame() { + nearby::sharing::service::proto::Frame frame = + nearby::sharing::service::proto::Frame(); + frame.set_version(nearby::sharing::service::proto::Frame::V1); + V1Frame* v1frame = frame.mutable_v1(); + v1frame->set_type(service::proto::V1Frame::CANCEL); + + std::vector data; + data.resize(frame.ByteSize()); + if (frame.SerializeToArray(data.data(), data.size())) { + return data; + } + + return std::nullopt; +} + +std::optional> GetInvalidFrame() { + std::vector data; + data.push_back(0xff); + data.push_back(0x00); + data.push_back(0x02); + return data; +} + +class IncomingFramesReaderTest : public testing::Test { + public: + IncomingFramesReaderTest() = default; + ~IncomingFramesReaderTest() override = default; + + void SetUp() override { + frames_reader_ = std::make_shared( + context(), &nearby_sharing_decoder_, &fake_nearby_connection_); + } + + FakeNearbyConnection& connection() { return fake_nearby_connection_; } + + IncomingFramesReader* frames_reader() { return frames_reader_.get(); } + + void FastForward(absl::Duration delta) { + FakeClock* fake_clock = reinterpret_cast(context()->GetClock()); + fake_clock->FastForward(delta); + } + + void Sync() { + EXPECT_TRUE( + FakeTaskRunner::WaitForRunningTasksWithTimeout(absl::Seconds(1))); + } + + void ReleaseFrameReader() { frames_reader_.reset(); } + + private: + FakeNearbyConnection fake_nearby_connection_; + NearbySharingDecoderImpl nearby_sharing_decoder_; + std::shared_ptr frames_reader_ = nullptr; + + Context* context() { + static Context* context = new FakeContext(); + return context; + } +}; + +TEST_F(IncomingFramesReaderTest, ReadTimedOut) { + absl::Notification notification; + frames_reader()->ReadFrame( + service::proto::V1Frame::INTRODUCTION, + [&](std::optional frame) { + EXPECT_EQ(frame, std::nullopt); + notification.Notify(); + }, + kTimeout); + Sync(); + FastForward(kTimeout); + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kTimeout)); + // Ensure that the OnDataReadFromConnection callback is not run since the + // read timed out. + EXPECT_FALSE(connection().has_read_callback_been_run()); + // Ensure that the IncomingFramesReader does not close the connection. + EXPECT_FALSE(connection().IsClosed()); +} + +TEST_F(IncomingFramesReaderTest, ReadAnyFrameSuccessful) { + std::optional> introduction_frame = + GetIntroductionFrame(); + ASSERT_TRUE(introduction_frame.has_value()); + connection().AppendReadableData(*introduction_frame); + + absl::Notification notification; + frames_reader()->ReadFrame([&](std::optional frame) { + EXPECT_EQ(frame->type(), service::proto::V1Frame::INTRODUCTION); + notification.Notify(); + }); + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kTimeout)); +} + +TEST_F(IncomingFramesReaderTest, ReadSuccessful) { + std::optional> introduction_frame = + GetIntroductionFrame(); + ASSERT_TRUE(introduction_frame.has_value()); + connection().AppendReadableData(*introduction_frame); + + absl::Notification notification; + frames_reader()->ReadFrame( + service::proto::V1Frame::INTRODUCTION, + [&](std::optional frame) { + EXPECT_EQ(frame->type(), service::proto::V1Frame::INTRODUCTION); + notification.Notify(); + }, + kTimeout); + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kTimeout)); +} + +TEST_F(IncomingFramesReaderTest, ReadSuccessful_JumbledFramesOrdering) { + std::optional> cancel_frame = GetCancelFrame(); + ASSERT_TRUE(cancel_frame.has_value()); + connection().AppendReadableData(*cancel_frame); + + std::optional> introduction_frame = + GetIntroductionFrame(); + ASSERT_TRUE(introduction_frame.has_value()); + connection().AppendReadableData(*introduction_frame); + + absl::Notification notification; + frames_reader()->ReadFrame( + service::proto::V1Frame::INTRODUCTION, + [&](std::optional frame) { + EXPECT_EQ(frame->type(), service::proto::V1Frame::INTRODUCTION); + notification.Notify(); + }, + kTimeout); + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kTimeout)); +} + +TEST_F(IncomingFramesReaderTest, JumbledFramesOrdering_ReadFromCache) { + std::optional> cancel_frame = GetCancelFrame(); + ASSERT_TRUE(cancel_frame.has_value()); + connection().AppendReadableData(*cancel_frame); + + std::optional> introduction_frame = + GetIntroductionFrame(); + ASSERT_TRUE(introduction_frame.has_value()); + connection().AppendReadableData(*introduction_frame); + + absl::Notification notification; + frames_reader()->ReadFrame( + service::proto::V1Frame::INTRODUCTION, + [&](std::optional frame) { + EXPECT_EQ(frame->type(), service::proto::V1Frame::INTRODUCTION); + notification.Notify(); + }, + kTimeout); + + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kTimeout)); + // Reading any frame should return CancelFrame. + absl::Notification cancel_notification; + frames_reader()->ReadFrame([&](std::optional frame) { + ASSERT_NE(frame, std::nullopt); + EXPECT_EQ(frame->type(), service::proto::V1Frame::CANCEL); + cancel_notification.Notify(); + }); + EXPECT_TRUE(cancel_notification.WaitForNotificationWithTimeout(kTimeout)); +} + +TEST_F(IncomingFramesReaderTest, ReadAfterConnectionClosed) { + absl::Notification notification; + frames_reader()->ReadFrame( + service::proto::V1Frame::INTRODUCTION, + [&](std::optional frame) { + EXPECT_EQ(frame, std::nullopt); + notification.Notify(); + }, + kTimeout); + Sync(); + connection().Close(); + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kTimeout)); +} + +TEST_F(IncomingFramesReaderTest, ReadTwoFramesWithTimeoutSuccessfully) { + absl::Notification notification; + frames_reader()->ReadFrame( + service::proto::V1Frame::INTRODUCTION, + [&](std::optional frame) { + EXPECT_EQ(frame->type(), service::proto::V1Frame::INTRODUCTION); + }, + kTimeout); + frames_reader()->ReadFrame( + service::proto::V1Frame::CANCEL, + [&](std::optional frame) { + EXPECT_EQ(frame->type(), service::proto::V1Frame::CANCEL); + notification.Notify(); + }, + kTimeout); + + std::optional> cancel_frame = GetCancelFrame(); + ASSERT_TRUE(cancel_frame.has_value()); + connection().AppendReadableData(*cancel_frame); + + std::optional> introduction_frame = + GetIntroductionFrame(); + ASSERT_TRUE(introduction_frame.has_value()); + connection().AppendReadableData(*introduction_frame); + + Sync(); + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kTimeout)); +} + +TEST_F(IncomingFramesReaderTest, ReadTwoFramesWithoutTimeoutSuccessfully) { + absl::Notification notification; + frames_reader()->ReadFrame([&](std::optional frame) { + EXPECT_EQ(frame->type(), service::proto::V1Frame::INTRODUCTION); + }); + frames_reader()->ReadFrame([&](std::optional frame) { + EXPECT_EQ(frame->type(), service::proto::V1Frame::CANCEL); + notification.Notify(); + }); + + std::optional> introduction_frame = + GetIntroductionFrame(); + ASSERT_TRUE(introduction_frame.has_value()); + connection().AppendReadableData(*introduction_frame); + + std::optional> cancel_frame = GetCancelFrame(); + ASSERT_TRUE(cancel_frame.has_value()); + connection().AppendReadableData(*cancel_frame); + + Sync(); + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kTimeout)); +} + +TEST_F(IncomingFramesReaderTest, ReleaseFrameReaderDuringRead) { + frames_reader()->ReadFrame( + service::proto::V1Frame::INTRODUCTION, + [&](std::optional frame) { EXPECT_EQ(frame, std::nullopt); }, + kTimeout); + frames_reader()->ReadFrame( + service::proto::V1Frame::INTRODUCTION, + [&](std::optional frame) { EXPECT_EQ(frame, std::nullopt); }, + kTimeout); + ReleaseFrameReader(); + EXPECT_EQ(frames_reader(), nullptr); +} + +TEST_F(IncomingFramesReaderTest, ReadInvalidFrame) { + absl::Notification notification; + frames_reader()->ReadFrame([&](std::optional frame) { + EXPECT_EQ(frame, std::nullopt); + notification.Notify(); + }); + + std::optional> invalid_frame = GetInvalidFrame(); + ASSERT_TRUE(invalid_frame.has_value()); + connection().AppendReadableData(*invalid_frame); + + Sync(); + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kTimeout)); +} + +} // namespace +} // namespace sharing +} // namespace nearby diff --git a/sharing/incoming_share_target_info.cc b/sharing/incoming_share_target_info.cc new file mode 100644 index 00000000..0ec89bc2 --- /dev/null +++ b/sharing/incoming_share_target_info.cc @@ -0,0 +1,31 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/incoming_share_target_info.h" + +namespace nearby { +namespace sharing { + +IncomingShareTargetInfo::IncomingShareTargetInfo() = default; + +IncomingShareTargetInfo::IncomingShareTargetInfo(IncomingShareTargetInfo&&) = + default; + +IncomingShareTargetInfo& IncomingShareTargetInfo::operator=( + IncomingShareTargetInfo&&) = default; + +IncomingShareTargetInfo::~IncomingShareTargetInfo() = default; + +} // namespace sharing +} // namespace nearby diff --git a/sharing/incoming_share_target_info.h b/sharing/incoming_share_target_info.h new file mode 100644 index 00000000..605a312f --- /dev/null +++ b/sharing/incoming_share_target_info.h @@ -0,0 +1,34 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_INCOMING_SHARE_TARGET_INFO_H_ +#define THIRD_PARTY_NEARBY_SHARING_INCOMING_SHARE_TARGET_INFO_H_ + +#include "sharing/share_target_info.h" + +namespace nearby { +namespace sharing { + +class IncomingShareTargetInfo : public ShareTargetInfo { + public: + IncomingShareTargetInfo(); + IncomingShareTargetInfo(IncomingShareTargetInfo&&); + IncomingShareTargetInfo& operator=(IncomingShareTargetInfo&&); + ~IncomingShareTargetInfo() override; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_INCOMING_SHARE_TARGET_INFO_H_ diff --git a/sharing/internal/base/BUILD b/sharing/internal/base/BUILD new file mode 100644 index 00000000..3967f05e --- /dev/null +++ b/sharing/internal/base/BUILD @@ -0,0 +1,67 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +licenses(["notice"]) + +cc_library( + name = "utf_utils", + srcs = [ + "utf_string_conversions.cc", + ], + hdrs = [ + "utf_string_configuration.h", + "utf_string_conversions.h", + ], + visibility = ["//visibility:public"], + deps = [ + "//sharing/internal/public:logging", + "//third_party/icu_utf:icu_utf_assistant", + ], +) + +cc_library( + name = "base", + srcs = [ + "encode.cc", + "mime.cc", + ], + hdrs = [ + "encode.h", + "mime.h", + ], + visibility = ["//visibility:public"], + deps = [ + "@com_google_absl//absl/container:flat_hash_map", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/types:span", + ], +) + +cc_test( + name = "base_test", + size = "small", + timeout = "short", + srcs = [ + "encode_test.cc", + "utf_string_conversions_test.cc", + ], + shard_count = 8, + deps = [ + ":base", + ":utf_utils", + "//internal/platform/implementation/g3", # fixdeps: keep + "@com_github_protobuf_matchers//protobuf-matchers", + "@com_google_googletest//:gtest_main", + ], +) diff --git a/sharing/internal/base/encode.cc b/sharing/internal/base/encode.cc new file mode 100644 index 00000000..03311c69 --- /dev/null +++ b/sharing/internal/base/encode.cc @@ -0,0 +1,42 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/internal/base/encode.h" + +#include + +#include +#include +#include +#include + +#include "absl/types/span.h" + +namespace nearby { +namespace utils { + +// Returns uppercase string. +std::string HexEncode(absl::Span data) { + std::ostringstream stream; + stream << std::hex << std::setfill('0') << std::uppercase; + + for (uint8_t val : data) { + stream << std::setw(2) << static_cast(val); + } + + return stream.str(); +} + +} // namespace utils +} // namespace nearby diff --git a/sharing/internal/base/encode.h b/sharing/internal/base/encode.h new file mode 100644 index 00000000..552f6e26 --- /dev/null +++ b/sharing/internal/base/encode.h @@ -0,0 +1,32 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_INTERNAL_BASE_ENCODE_H_ +#define THIRD_PARTY_NEARBY_SHARING_INTERNAL_BASE_ENCODE_H_ + +#include + +#include + +#include "absl/types/span.h" + +namespace nearby { +namespace utils { + +std::string HexEncode(absl::Span data); + +} // namespace utils +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_INTERNAL_BASE_ENCODE_H_ diff --git a/sharing/internal/base/encode_test.cc b/sharing/internal/base/encode_test.cc new file mode 100644 index 00000000..28e67f74 --- /dev/null +++ b/sharing/internal/base/encode_test.cc @@ -0,0 +1,42 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/internal/base/encode.h" + +#include + +#include + +#include "gtest/gtest.h" + +namespace nearby { +namespace utils { +namespace { + +TEST(HexEncode, HexEncodeNormal) { + std::vector data = {0x04, 0x3f, 0xf7, 0xe6, 0xf8, 0xc7, 0x0c}; + EXPECT_EQ(HexEncode(data), "043FF7E6F8C70C"); +} + +TEST(HexEncode, HexEncodeEmpty) { + EXPECT_EQ(HexEncode(std::vector({})), ""); +} + +TEST(HexEncode, HexEncodeWithPadding) { + EXPECT_EQ(HexEncode(std::vector({0, 0, 0, 0})), "00000000"); +} + +} // namespace +} // namespace utils +} // namespace nearby diff --git a/sharing/internal/base/mime.cc b/sharing/internal/base/mime.cc new file mode 100644 index 00000000..ee76e5ff --- /dev/null +++ b/sharing/internal/base/mime.cc @@ -0,0 +1,1036 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/internal/base/mime.h" + +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/strings/string_view.h" + +namespace nearby { +namespace utils { + +// Refers to +// http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types. +std::string GetWellKnownMimeTypeFromExtension(absl::string_view extension) { + static absl::flat_hash_map< + std::string, + std::string>* mime_map = new absl::flat_hash_map({ + {"123", "application/vnd.lotus-1-2-3"}, + {"3dml", "text/vnd.in3d.3dml"}, + {"3ds", "image/x-3ds"}, + {"3g2", "video/3gpp2"}, + {"3gp", "video/3gpp"}, + {"7z", "application/x-7z-compressed"}, + {"aab", "application/x-authorware-bin"}, + {"aac", "audio/x-aac"}, + {"aam", "application/x-authorware-map"}, + {"aas", "application/x-authorware-seg"}, + {"abw", "application/x-abiword"}, + {"ac", "application/pkix-attr-cert"}, + {"acc", "application/vnd.americandynamics.acc"}, + {"ace", "application/x-ace-compressed"}, + {"acu", "application/vnd.acucobol"}, + {"acutc", "application/vnd.acucorp"}, + {"adp", "audio/adpcm"}, + {"aep", "application/vnd.audiograph"}, + {"afm", "application/x-font-type1"}, + {"afp", "application/vnd.ibm.modcap"}, + {"ahead", "application/vnd.ahead.space"}, + {"ai", "application/postscript"}, + {"aif", "audio/x-aiff"}, + {"aifc", "audio/x-aiff"}, + {"aiff", "audio/x-aiff"}, + {"air", "application/vnd.adobe.air-application-installer-package+zip"}, + {"ait", "application/vnd.dvb.ait"}, + {"ami", "application/vnd.amiga.ami"}, + {"apk", "application/vnd.android.package-archive"}, + {"appcache", "text/cache-manifest"}, + {"application", "application/x-ms-application"}, + {"apr", "application/vnd.lotus-approach"}, + {"arc", "application/x-freearc"}, + {"asc", "application/pgp-signature"}, + {"asf", "video/x-ms-asf"}, + {"asm", "text/x-asm"}, + {"aso", "application/vnd.accpac.simply.aso"}, + {"asx", "video/x-ms-asf"}, + {"atc", "application/vnd.acucorp"}, + {"atom", "application/atom+xml"}, + {"atomcat", "application/atomcat+xml"}, + {"atomsvc", "application/atomsvc+xml"}, + {"atx", "application/vnd.antix.game-component"}, + {"au", "audio/basic"}, + {"avi", "video/x-msvideo"}, + {"aw", "application/applixware"}, + {"azf", "application/vnd.airzip.filesecure.azf"}, + {"azs", "application/vnd.airzip.filesecure.azs"}, + {"azw", "application/vnd.amazon.ebook"}, + {"bat", "application/x-msdownload"}, + {"bcpio", "application/x-bcpio"}, + {"bdf", "application/x-font-bdf"}, + {"bdm", "application/vnd.syncml.dm+wbxml"}, + {"bed", "application/vnd.realvnc.bed"}, + {"bh2", "application/vnd.fujitsu.oasysprs"}, + {"bin", "application/octet-stream"}, + {"blb", "application/x-blorb"}, + {"blorb", "application/x-blorb"}, + {"bmi", "application/vnd.bmi"}, + {"bmp", "image/bmp"}, + {"book", "application/vnd.framemaker"}, + {"box", "application/vnd.previewsystems.box"}, + {"boz", "application/x-bzip2"}, + {"bpk", "application/octet-stream"}, + {"btif", "image/prs.btif"}, + {"bz2", "application/x-bzip2"}, + {"bz", "application/x-bzip"}, + {"c11amc", "application/vnd.cluetrust.cartomobile-config"}, + {"c11amz", "application/vnd.cluetrust.cartomobile-config-pkg"}, + {"c4d", "application/vnd.clonk.c4group"}, + {"c4f", "application/vnd.clonk.c4group"}, + {"c4g", "application/vnd.clonk.c4group"}, + {"c4p", "application/vnd.clonk.c4group"}, + {"c4u", "application/vnd.clonk.c4group"}, + {"cab", "application/vnd.ms-cab-compressed"}, + {"caf", "audio/x-caf"}, + {"cap", "application/vnd.tcpdump.pcap"}, + {"car", "application/vnd.curl.car"}, + {"cat", "application/vnd.ms-pki.seccat"}, + {"cb7", "application/x-cbr"}, + {"cba", "application/x-cbr"}, + {"cbr", "application/x-cbr"}, + {"cbt", "application/x-cbr"}, + {"cbz", "application/x-cbr"}, + {"cct", "application/x-director"}, + {"cc", "text/x-c"}, + {"ccxml", "application/ccxml+xml"}, + {"cdbcmsg", "application/vnd.contact.cmsg"}, + {"cdf", "application/x-netcdf"}, + {"cdkey", "application/vnd.mediastation.cdkey"}, + {"cdmia", "application/cdmi-capability"}, + {"cdmic", "application/cdmi-container"}, + {"cdmid", "application/cdmi-domain"}, + {"cdmio", "application/cdmi-object"}, + {"cdmiq", "application/cdmi-queue"}, + {"cdx", "chemical/x-cdx"}, + {"cdxml", "application/vnd.chemdraw+xml"}, + {"cdy", "application/vnd.cinderella"}, + {"cer", "application/pkix-cert"}, + {"cfs", "application/x-cfs-compressed"}, + {"cgm", "image/cgm"}, + {"chat", "application/x-chat"}, + {"chm", "application/vnd.ms-htmlhelp"}, + {"chrt", "application/vnd.kde.kchart"}, + {"cif", "chemical/x-cif"}, + {"cii", "application/vnd.anser-web-certificate-issue-initiation"}, + {"cil", "application/vnd.ms-artgalry"}, + {"cla", "application/vnd.claymore"}, + {"class", "application/java-vm"}, + {"clkk", "application/vnd.crick.clicker.keyboard"}, + {"clkp", "application/vnd.crick.clicker.palette"}, + {"clkt", "application/vnd.crick.clicker.template"}, + {"clkw", "application/vnd.crick.clicker.wordbank"}, + {"clkx", "application/vnd.crick.clicker"}, + {"clp", "application/x-msclip"}, + {"cmc", "application/vnd.cosmocaller"}, + {"cmdf", "chemical/x-cmdf"}, + {"cml", "chemical/x-cml"}, + {"cmp", "application/vnd.yellowriver-custom-menu"}, + {"cmx", "image/x-cmx"}, + {"cod", "application/vnd.rim.cod"}, + {"com", "application/x-msdownload"}, + {"conf", "text/plain"}, + {"cpio", "application/x-cpio"}, + {"cpp", "text/x-c"}, + {"cpt", "application/mac-compactpro"}, + {"crd", "application/x-mscardfile"}, + {"crl", "application/pkix-crl"}, + {"crt", "application/x-x509-ca-cert"}, + {"cryptonote", "application/vnd.rig.cryptonote"}, + {"csh", "application/x-csh"}, + {"csml", "chemical/x-csml"}, + {"csp", "application/vnd.commonspace"}, + {"css", "text/css"}, + {"cst", "application/x-director"}, + {"csv", "text/csv"}, + {"c", "text/x-c"}, + {"cu", "application/cu-seeme"}, + {"curl", "text/vnd.curl"}, + {"cww", "application/prs.cww"}, + {"cxt", "application/x-director"}, + {"cxx", "text/x-c"}, + {"dae", "model/vnd.collada+xml"}, + {"daf", "application/vnd.mobius.daf"}, + {"dart", "application/vnd.dart"}, + {"dataless", "application/vnd.fdsn.seed"}, + {"davmount", "application/davmount+xml"}, + {"dbk", "application/docbook+xml"}, + {"dcr", "application/x-director"}, + {"dcurl", "text/vnd.curl.dcurl"}, + {"dd2", "application/vnd.oma.dd2+xml"}, + {"ddd", "application/vnd.fujixerox.ddd"}, + {"deb", "application/x-debian-package"}, + {"def", "text/plain"}, + {"deploy", "application/octet-stream"}, + {"der", "application/x-x509-ca-cert"}, + {"dfac", "application/vnd.dreamfactory"}, + {"dgc", "application/x-dgc-compressed"}, + {"dic", "text/x-c"}, + {"dir", "application/x-director"}, + {"dis", "application/vnd.mobius.dis"}, + {"dist", "application/octet-stream"}, + {"distz", "application/octet-stream"}, + {"djv", "image/vnd.djvu"}, + {"djvu", "image/vnd.djvu"}, + {"dll", "application/x-msdownload"}, + {"dmg", "application/x-apple-diskimage"}, + {"dmp", "application/vnd.tcpdump.pcap"}, + {"dms", "application/octet-stream"}, + {"dna", "application/vnd.dna"}, + {"doc", "application/msword"}, + {"docm", "application/vnd.ms-word.document.macroenabled.12"}, + {"docx", + "application/" + "vnd.openxmlformats-officedocument.wordprocessingml.document"}, + {"dot", "application/msword"}, + {"dotm", "application/vnd.ms-word.template.macroenabled.12"}, + {"dotx", + "application/" + "vnd.openxmlformats-officedocument.wordprocessingml.template"}, + {"dp", "application/vnd.osgi.dp"}, + {"dpg", "application/vnd.dpgraph"}, + {"dra", "audio/vnd.dra"}, + {"dsc", "text/prs.lines.tag"}, + {"dssc", "application/dssc+der"}, + {"dtb", "application/x-dtbook+xml"}, + {"dtd", "application/xml-dtd"}, + {"dts", "audio/vnd.dts"}, + {"dtshd", "audio/vnd.dts.hd"}, + {"dump", "application/octet-stream"}, + {"dvb", "video/vnd.dvb.file"}, + {"dvi", "application/x-dvi"}, + {"dwf", "model/vnd.dwf"}, + {"dwg", "image/vnd.dwg"}, + {"dxf", "image/vnd.dxf"}, + {"dxp", "application/vnd.spotfire.dxp"}, + {"dxr", "application/x-director"}, + {"ecelp4800", "audio/vnd.nuera.ecelp4800"}, + {"ecelp7470", "audio/vnd.nuera.ecelp7470"}, + {"ecelp9600", "audio/vnd.nuera.ecelp9600"}, + {"ecma", "application/ecmascript"}, + {"edm", "application/vnd.novadigm.edm"}, + {"edx", "application/vnd.novadigm.edx"}, + {"efif", "application/vnd.picsel"}, + {"ei6", "application/vnd.pg.osasli"}, + {"elc", "application/octet-stream"}, + {"emf", "application/x-msmetafile"}, + {"eml", "message/rfc822"}, + {"emma", "application/emma+xml"}, + {"emz", "application/x-msmetafile"}, + {"eol", "audio/vnd.digital-winds"}, + {"eot", "application/vnd.ms-fontobject"}, + {"eps", "application/postscript"}, + {"epub", "application/epub+zip"}, + {"es3", "application/vnd.eszigno3+xml"}, + {"esa", "application/vnd.osgi.subsystem"}, + {"esf", "application/vnd.epson.esf"}, + {"et3", "application/vnd.eszigno3+xml"}, + {"etx", "text/x-setext"}, + {"eva", "application/x-eva"}, + {"evy", "application/x-envoy"}, + {"exe", "application/x-msdownload"}, + {"exi", "application/exi"}, + {"ext", "application/vnd.novadigm.ext"}, + {"ez2", "application/vnd.ezpix-album"}, + {"ez3", "application/vnd.ezpix-package"}, + {"ez", "application/andrew-inset"}, + {"f4v", "video/x-f4v"}, + {"f77", "text/x-fortran"}, + {"f90", "text/x-fortran"}, + {"fbs", "image/vnd.fastbidsheet"}, + {"fcdt", "application/vnd.adobe.formscentral.fcdt"}, + {"fcs", "application/vnd.isac.fcs"}, + {"fdf", "application/vnd.fdf"}, + {"fe_launch", "application/vnd.denovo.fcselayout-link"}, + {"fg5", "application/vnd.fujitsu.oasysgp"}, + {"fgd", "application/x-director"}, + {"fh4", "image/x-freehand"}, + {"fh5", "image/x-freehand"}, + {"fh7", "image/x-freehand"}, + {"fhc", "image/x-freehand"}, + {"fh", "image/x-freehand"}, + {"fig", "application/x-xfig"}, + {"flac", "audio/x-flac"}, + {"fli", "video/x-fli"}, + {"flo", "application/vnd.micrografx.flo"}, + {"flv", "video/x-flv"}, + {"flw", "application/vnd.kde.kivio"}, + {"flx", "text/vnd.fmi.flexstor"}, + {"fly", "text/vnd.fly"}, + {"fm", "application/vnd.framemaker"}, + {"fnc", "application/vnd.frogans.fnc"}, + {"for", "text/x-fortran"}, + {"fpx", "image/vnd.fpx"}, + {"frame", "application/vnd.framemaker"}, + {"fsc", "application/vnd.fsc.weblaunch"}, + {"fst", "image/vnd.fst"}, + {"ftc", "application/vnd.fluxtime.clip"}, + {"f", "text/x-fortran"}, + {"fti", "application/vnd.anser-web-funds-transfer-initiation"}, + {"fvt", "video/vnd.fvt"}, + {"fxp", "application/vnd.adobe.fxp"}, + {"fxpl", "application/vnd.adobe.fxp"}, + {"fzs", "application/vnd.fuzzysheet"}, + {"g2w", "application/vnd.geoplan"}, + {"g3", "image/g3fax"}, + {"g3w", "application/vnd.geospace"}, + {"gac", "application/vnd.groove-account"}, + {"gam", "application/x-tads"}, + {"gbr", "application/rpki-ghostbusters"}, + {"gca", "application/x-gca-compressed"}, + {"gdl", "model/vnd.gdl"}, + {"geo", "application/vnd.dynageo"}, + {"gex", "application/vnd.geometry-explorer"}, + {"ggb", "application/vnd.geogebra.file"}, + {"ggt", "application/vnd.geogebra.tool"}, + {"ghf", "application/vnd.groove-help"}, + {"gif", "image/gif"}, + {"gim", "application/vnd.groove-identity-message"}, + {"gml", "application/gml+xml"}, + {"gmx", "application/vnd.gmx"}, + {"gnumeric", "application/x-gnumeric"}, + {"gph", "application/vnd.flographit"}, + {"gpx", "application/gpx+xml"}, + {"gqf", "application/vnd.grafeq"}, + {"gqs", "application/vnd.grafeq"}, + {"gram", "application/srgs"}, + {"gramps", "application/x-gramps-xml"}, + {"gre", "application/vnd.geometry-explorer"}, + {"grv", "application/vnd.groove-injector"}, + {"grxml", "application/srgs+xml"}, + {"gsf", "application/x-font-ghostscript"}, + {"gtar", "application/x-gtar"}, + {"gtm", "application/vnd.groove-tool-message"}, + {"gtw", "model/vnd.gtw"}, + {"gv", "text/vnd.graphviz"}, + {"gxf", "application/gxf"}, + {"gxt", "application/vnd.geonext"}, + {"h261", "video/h261"}, + {"h263", "video/h263"}, + {"h264", "video/h264"}, + {"hal", "application/vnd.hal+xml"}, + {"hbci", "application/vnd.hbci"}, + {"hdf", "application/x-hdf"}, + {"hh", "text/x-c"}, + {"hlp", "application/winhlp"}, + {"hpgl", "application/vnd.hp-hpgl"}, + {"hpid", "application/vnd.hp-hpid"}, + {"hps", "application/vnd.hp-hps"}, + {"hqx", "application/mac-binhex40"}, + {"h", "text/x-c"}, + {"htke", "application/vnd.kenameaapp"}, + {"html", "text/html"}, + {"htm", "text/html"}, + {"hvd", "application/vnd.yamaha.hv-dic"}, + {"hvp", "application/vnd.yamaha.hv-voice"}, + {"hvs", "application/vnd.yamaha.hv-script"}, + {"i2g", "application/vnd.intergeo"}, + {"icc", "application/vnd.iccprofile"}, + {"ice", "x-conference/x-cooltalk"}, + {"icm", "application/vnd.iccprofile"}, + {"ico", "image/x-icon"}, + {"ics", "text/calendar"}, + {"ief", "image/ief"}, + {"ifb", "text/calendar"}, + {"ifm", "application/vnd.shana.informed.formdata"}, + {"iges", "model/iges"}, + {"igl", "application/vnd.igloader"}, + {"igm", "application/vnd.insors.igm"}, + {"igs", "model/iges"}, + {"igx", "application/vnd.micrografx.igx"}, + {"iif", "application/vnd.shana.informed.interchange"}, + {"imp", "application/vnd.accpac.simply.imp"}, + {"ims", "application/vnd.ms-ims"}, + {"ink", "application/inkml+xml"}, + {"inkml", "application/inkml+xml"}, + {"install", "application/x-install-instructions"}, + {"in", "text/plain"}, + {"iota", "application/vnd.astraea-software.iota"}, + {"ipfix", "application/ipfix"}, + {"ipk", "application/vnd.shana.informed.package"}, + {"irm", "application/vnd.ibm.rights-management"}, + {"irp", "application/vnd.irepository.package+xml"}, + {"iso", "application/x-iso9660-image"}, + {"itp", "application/vnd.shana.informed.formtemplate"}, + {"ivp", "application/vnd.immervision-ivp"}, + {"ivu", "application/vnd.immervision-ivu"}, + {"jad", "text/vnd.sun.j2me.app-descriptor"}, + {"jam", "application/vnd.jam"}, + {"jar", "application/java-archive"}, + {"java", "text/x-java-source"}, + {"jisp", "application/vnd.jisp"}, + {"jlt", "application/vnd.hp-jlyt"}, + {"jnlp", "application/x-java-jnlp-file"}, + {"joda", "application/vnd.joost.joda-archive"}, + {"jpeg", "image/jpeg"}, + {"jpe", "image/jpeg"}, + {"jpg", "image/jpeg"}, + {"jpgm", "video/jpm"}, + {"jpgv", "video/jpeg"}, + {"jpm", "video/jpm"}, + {"js", "application/javascript"}, + {"json", "application/json"}, + {"jsonml", "application/jsonml+json"}, + {"kar", "audio/midi"}, + {"karbon", "application/vnd.kde.karbon"}, + {"kfo", "application/vnd.kde.kformula"}, + {"kia", "application/vnd.kidspiration"}, + {"kml", "application/vnd.google-earth.kml+xml"}, + {"kmz", "application/vnd.google-earth.kmz"}, + {"kne", "application/vnd.kinar"}, + {"knp", "application/vnd.kinar"}, + {"kon", "application/vnd.kde.kontour"}, + {"kpr", "application/vnd.kde.kpresenter"}, + {"kpt", "application/vnd.kde.kpresenter"}, + {"kpxx", "application/vnd.ds-keypoint"}, + {"ksp", "application/vnd.kde.kspread"}, + {"ktr", "application/vnd.kahootz"}, + {"ktx", "image/ktx"}, + {"ktz", "application/vnd.kahootz"}, + {"kwd", "application/vnd.kde.kword"}, + {"kwt", "application/vnd.kde.kword"}, + {"lasxml", "application/vnd.las.las+xml"}, + {"latex", "application/x-latex"}, + {"lbd", "application/vnd.llamagraphics.life-balance.desktop"}, + {"lbe", "application/vnd.llamagraphics.life-balance.exchange+xml"}, + {"les", "application/vnd.hhe.lesson-player"}, + {"lha", "application/x-lzh-compressed"}, + {"link66", "application/vnd.route66.link66+xml"}, + {"list3820", "application/vnd.ibm.modcap"}, + {"listafp", "application/vnd.ibm.modcap"}, + {"list", "text/plain"}, + {"lnk", "application/x-ms-shortcut"}, + {"log", "text/plain"}, + {"lostxml", "application/lost+xml"}, + {"lrf", "application/octet-stream"}, + {"lrm", "application/vnd.ms-lrm"}, + {"ltf", "application/vnd.frogans.ltf"}, + {"lvp", "audio/vnd.lucent.voice"}, + {"lwp", "application/vnd.lotus-wordpro"}, + {"lzh", "application/x-lzh-compressed"}, + {"m13", "application/x-msmediaview"}, + {"m14", "application/x-msmediaview"}, + {"m1v", "video/mpeg"}, + {"m21", "application/mp21"}, + {"m2a", "audio/mpeg"}, + {"m2v", "video/mpeg"}, + {"m3a", "audio/mpeg"}, + {"m3u8", "application/vnd.apple.mpegurl"}, + {"m3u", "audio/x-mpegurl"}, + {"m4a", "audio/mp4"}, + {"m4u", "video/vnd.mpegurl"}, + {"m4v", "video/x-m4v"}, + {"ma", "application/mathematica"}, + {"mads", "application/mads+xml"}, + {"mag", "application/vnd.ecowin.chart"}, + {"maker", "application/vnd.framemaker"}, + {"man", "text/troff"}, + {"mar", "application/octet-stream"}, + {"mathml", "application/mathml+xml"}, + {"mb", "application/mathematica"}, + {"mbk", "application/vnd.mobius.mbk"}, + {"mbox", "application/mbox"}, + {"mc1", "application/vnd.medcalcdata"}, + {"mcd", "application/vnd.mcd"}, + {"mcurl", "text/vnd.curl.mcurl"}, + {"mdb", "application/x-msaccess"}, + {"mdi", "image/vnd.ms-modi"}, + {"mesh", "model/mesh"}, + {"meta4", "application/metalink4+xml"}, + {"metalink", "application/metalink+xml"}, + {"me", "text/troff"}, + {"mets", "application/mets+xml"}, + {"mfm", "application/vnd.mfmp"}, + {"mft", "application/rpki-manifest"}, + {"mgp", "application/vnd.osgeo.mapguide.package"}, + {"mgz", "application/vnd.proteus.magazine"}, + {"mid", "audio/midi"}, + {"midi", "audio/midi"}, + {"mie", "application/x-mie"}, + {"mif", "application/vnd.mif"}, + {"mime", "message/rfc822"}, + {"mj2", "video/mj2"}, + {"mjp2", "video/mj2"}, + {"mk3d", "video/x-matroska"}, + {"mka", "audio/x-matroska"}, + {"mks", "video/x-matroska"}, + {"mkv", "video/x-matroska"}, + {"mlp", "application/vnd.dolby.mlp"}, + {"mmd", "application/vnd.chipnuts.karaoke-mmd"}, + {"mmf", "application/vnd.smaf"}, + {"mmr", "image/vnd.fujixerox.edmics-mmr"}, + {"mng", "video/x-mng"}, + {"mny", "application/x-msmoney"}, + {"mobi", "application/x-mobipocket-ebook"}, + {"mods", "application/mods+xml"}, + {"movie", "video/x-sgi-movie"}, + {"mov", "video/quicktime"}, + {"mp21", "application/mp21"}, + {"mp2a", "audio/mpeg"}, + {"mp2", "audio/mpeg"}, + {"mp3", "audio/mpeg"}, + {"mp4a", "audio/mp4"}, + {"mp4s", "application/mp4"}, + {"mp4", "video/mp4"}, + {"mp4v", "video/mp4"}, + {"mpc", "application/vnd.mophun.certificate"}, + {"mpeg", "video/mpeg"}, + {"mpe", "video/mpeg"}, + {"mpg4", "video/mp4"}, + {"mpga", "audio/mpeg"}, + {"mpg", "video/mpeg"}, + {"mpkg", "application/vnd.apple.installer+xml"}, + {"mpm", "application/vnd.blueice.multipass"}, + {"mpn", "application/vnd.mophun.application"}, + {"mpp", "application/vnd.ms-project"}, + {"mpt", "application/vnd.ms-project"}, + {"mpy", "application/vnd.ibm.minipay"}, + {"mqy", "application/vnd.mobius.mqy"}, + {"mrc", "application/marc"}, + {"mrcx", "application/marcxml+xml"}, + {"mscml", "application/mediaservercontrol+xml"}, + {"mseed", "application/vnd.fdsn.mseed"}, + {"mseq", "application/vnd.mseq"}, + {"msf", "application/vnd.epson.msf"}, + {"msh", "model/mesh"}, + {"msi", "application/x-msdownload"}, + {"msl", "application/vnd.mobius.msl"}, + {"ms", "text/troff"}, + {"msty", "application/vnd.muvee.style"}, + {"mts", "model/vnd.mts"}, + {"mus", "application/vnd.musician"}, + {"musicxml", "application/vnd.recordare.musicxml+xml"}, + {"mvb", "application/x-msmediaview"}, + {"mwf", "application/vnd.mfer"}, + {"mxf", "application/mxf"}, + {"mxl", "application/vnd.recordare.musicxml"}, + {"mxml", "application/xv+xml"}, + {"mxs", "application/vnd.triscape.mxs"}, + {"mxu", "video/vnd.mpegurl"}, + {"n3", "text/n3"}, + {"nb", "application/mathematica"}, + {"nbp", "application/vnd.wolfram.player"}, + {"nc", "application/x-netcdf"}, + {"ncx", "application/x-dtbncx+xml"}, + {"nfo", "text/x-nfo"}, + {"n-gage", "application/vnd.nokia.n-gage.symbian.install"}, + {"ngdat", "application/vnd.nokia.n-gage.data"}, + {"nitf", "application/vnd.nitf"}, + {"nlu", "application/vnd.neurolanguage.nlu"}, + {"nml", "application/vnd.enliven"}, + {"nnd", "application/vnd.noblenet-directory"}, + {"nns", "application/vnd.noblenet-sealer"}, + {"nnw", "application/vnd.noblenet-web"}, + {"npx", "image/vnd.net-fpx"}, + {"nsc", "application/x-conference"}, + {"nsf", "application/vnd.lotus-notes"}, + {"ntf", "application/vnd.nitf"}, + {"nzb", "application/x-nzb"}, + {"oa2", "application/vnd.fujitsu.oasys2"}, + {"oa3", "application/vnd.fujitsu.oasys3"}, + {"oas", "application/vnd.fujitsu.oasys"}, + {"obd", "application/x-msbinder"}, + {"obj", "application/x-tgif"}, + {"oda", "application/oda"}, + {"odb", "application/vnd.oasis.opendocument.database"}, + {"odc", "application/vnd.oasis.opendocument.chart"}, + {"odf", "application/vnd.oasis.opendocument.formula"}, + {"odft", "application/vnd.oasis.opendocument.formula-template"}, + {"odg", "application/vnd.oasis.opendocument.graphics"}, + {"odi", "application/vnd.oasis.opendocument.image"}, + {"odm", "application/vnd.oasis.opendocument.text-master"}, + {"odp", "application/vnd.oasis.opendocument.presentation"}, + {"ods", "application/vnd.oasis.opendocument.spreadsheet"}, + {"odt", "application/vnd.oasis.opendocument.text"}, + {"oga", "audio/ogg"}, + {"ogg", "audio/ogg"}, + {"ogv", "video/ogg"}, + {"ogx", "application/ogg"}, + {"omdoc", "application/omdoc+xml"}, + {"onepkg", "application/onenote"}, + {"onetmp", "application/onenote"}, + {"onetoc2", "application/onenote"}, + {"onetoc", "application/onenote"}, + {"opf", "application/oebps-package+xml"}, + {"opml", "text/x-opml"}, + {"oprc", "application/vnd.palm"}, + {"opus", "audio/ogg"}, + {"org", "application/vnd.lotus-organizer"}, + {"osf", "application/vnd.yamaha.openscoreformat"}, + {"osfpvg", "application/vnd.yamaha.openscoreformat.osfpvg+xml"}, + {"otc", "application/vnd.oasis.opendocument.chart-template"}, + {"otf", "font/otf"}, + {"otg", "application/vnd.oasis.opendocument.graphics-template"}, + {"oth", "application/vnd.oasis.opendocument.text-web"}, + {"oti", "application/vnd.oasis.opendocument.image-template"}, + {"otp", "application/vnd.oasis.opendocument.presentation-template"}, + {"ots", "application/vnd.oasis.opendocument.spreadsheet-template"}, + {"ott", "application/vnd.oasis.opendocument.text-template"}, + {"oxps", "application/oxps"}, + {"oxt", "application/vnd.openofficeorg.extension"}, + {"p10", "application/pkcs10"}, + {"p12", "application/x-pkcs12"}, + {"p7b", "application/x-pkcs7-certificates"}, + {"p7c", "application/pkcs7-mime"}, + {"p7m", "application/pkcs7-mime"}, + {"p7r", "application/x-pkcs7-certreqresp"}, + {"p7s", "application/pkcs7-signature"}, + {"p8", "application/pkcs8"}, + {"pas", "text/x-pascal"}, + {"paw", "application/vnd.pawaafile"}, + {"pbd", "application/vnd.powerbuilder6"}, + {"pbm", "image/x-portable-bitmap"}, + {"pcap", "application/vnd.tcpdump.pcap"}, + {"pcf", "application/x-font-pcf"}, + {"pcl", "application/vnd.hp-pcl"}, + {"pclxl", "application/vnd.hp-pclxl"}, + {"pct", "image/x-pict"}, + {"pcurl", "application/vnd.curl.pcurl"}, + {"pcx", "image/x-pcx"}, + {"pdb", "application/vnd.palm"}, + {"pdf", "application/pdf"}, + {"pfa", "application/x-font-type1"}, + {"pfb", "application/x-font-type1"}, + {"pfm", "application/x-font-type1"}, + {"pfr", "application/font-tdpfr"}, + {"pfx", "application/x-pkcs12"}, + {"pgm", "image/x-portable-graymap"}, + {"pgn", "application/x-chess-pgn"}, + {"pgp", "application/pgp-encrypted"}, + {"pic", "image/x-pict"}, + {"pkg", "application/octet-stream"}, + {"pki", "application/pkixcmp"}, + {"pkipath", "application/pkix-pkipath"}, + {"plb", "application/vnd.3gpp.pic-bw-large"}, + {"plc", "application/vnd.mobius.plc"}, + {"plf", "application/vnd.pocketlearn"}, + {"pls", "application/pls+xml"}, + {"pml", "application/vnd.ctc-posml"}, + {"png", "image/png"}, + {"pnm", "image/x-portable-anymap"}, + {"portpkg", "application/vnd.macports.portpkg"}, + {"pot", "application/vnd.ms-powerpoint"}, + {"potm", "application/vnd.ms-powerpoint.template.macroenabled.12"}, + {"potx", + "application/vnd.openxmlformats-officedocument.presentationml.template"}, + {"ppam", "application/vnd.ms-powerpoint.addin.macroenabled.12"}, + {"ppd", "application/vnd.cups-ppd"}, + {"ppm", "image/x-portable-pixmap"}, + {"pps", "application/vnd.ms-powerpoint"}, + {"ppsm", "application/vnd.ms-powerpoint.slideshow.macroenabled.12"}, + {"ppsx", + "application/" + "vnd.openxmlformats-officedocument.presentationml.slideshow"}, + {"ppt", "application/vnd.ms-powerpoint"}, + {"pptm", "application/vnd.ms-powerpoint.presentation.macroenabled.12"}, + {"pptx", + "application/" + "vnd.openxmlformats-officedocument.presentationml.presentation"}, + {"pqa", "application/vnd.palm"}, + {"prc", "application/x-mobipocket-ebook"}, + {"pre", "application/vnd.lotus-freelance"}, + {"prf", "application/pics-rules"}, + {"ps", "application/postscript"}, + {"psb", "application/vnd.3gpp.pic-bw-small"}, + {"psd", "image/vnd.adobe.photoshop"}, + {"psf", "application/x-font-linux-psf"}, + {"pskcxml", "application/pskc+xml"}, + {"p", "text/x-pascal"}, + {"ptid", "application/vnd.pvi.ptid1"}, + {"pub", "application/x-mspublisher"}, + {"pvb", "application/vnd.3gpp.pic-bw-var"}, + {"pwn", "application/vnd.3m.post-it-notes"}, + {"pya", "audio/vnd.ms-playready.media.pya"}, + {"pyv", "video/vnd.ms-playready.media.pyv"}, + {"qam", "application/vnd.epson.quickanime"}, + {"qbo", "application/vnd.intu.qbo"}, + {"qfx", "application/vnd.intu.qfx"}, + {"qps", "application/vnd.publishare-delta-tree"}, + {"qt", "video/quicktime"}, + {"qwd", "application/vnd.quark.quarkxpress"}, + {"qwt", "application/vnd.quark.quarkxpress"}, + {"qxb", "application/vnd.quark.quarkxpress"}, + {"qxd", "application/vnd.quark.quarkxpress"}, + {"qxl", "application/vnd.quark.quarkxpress"}, + {"qxt", "application/vnd.quark.quarkxpress"}, + {"ra", "audio/x-pn-realaudio"}, + {"ram", "audio/x-pn-realaudio"}, + {"rar", "application/x-rar-compressed"}, + {"ras", "image/x-cmu-raster"}, + {"rcprofile", "application/vnd.ipunplugged.rcprofile"}, + {"rdf", "application/rdf+xml"}, + {"rdz", "application/vnd.data-vision.rdz"}, + {"rep", "application/vnd.businessobjects"}, + {"res", "application/x-dtbresource+xml"}, + {"rgb", "image/x-rgb"}, + {"rif", "application/reginfo+xml"}, + {"rip", "audio/vnd.rip"}, + {"ris", "application/x-research-info-systems"}, + {"rl", "application/resource-lists+xml"}, + {"rlc", "image/vnd.fujixerox.edmics-rlc"}, + {"rld", "application/resource-lists-diff+xml"}, + {"rm", "application/vnd.rn-realmedia"}, + {"rmi", "audio/midi"}, + {"rmp", "audio/x-pn-realaudio-plugin"}, + {"rms", "application/vnd.jcp.javame.midlet-rms"}, + {"rmvb", "application/vnd.rn-realmedia-vbr"}, + {"rnc", "application/relax-ng-compact-syntax"}, + {"roa", "application/rpki-roa"}, + {"roff", "text/troff"}, + {"rp9", "application/vnd.cloanto.rp9"}, + {"rpss", "application/vnd.nokia.radio-presets"}, + {"rpst", "application/vnd.nokia.radio-preset"}, + {"rq", "application/sparql-query"}, + {"rs", "application/rls-services+xml"}, + {"rsd", "application/rsd+xml"}, + {"rss", "application/rss+xml"}, + {"rtf", "application/rtf"}, + {"rtx", "text/richtext"}, + {"s3m", "audio/s3m"}, + {"saf", "application/vnd.yamaha.smaf-audio"}, + {"sbml", "application/sbml+xml"}, + {"sc", "application/vnd.ibm.secure-container"}, + {"scd", "application/x-msschedule"}, + {"scm", "application/vnd.lotus-screencam"}, + {"scq", "application/scvp-cv-request"}, + {"scs", "application/scvp-cv-response"}, + {"scurl", "text/vnd.curl.scurl"}, + {"sda", "application/vnd.stardivision.draw"}, + {"sdc", "application/vnd.stardivision.calc"}, + {"sdd", "application/vnd.stardivision.impress"}, + {"sdkd", "application/vnd.solent.sdkm+xml"}, + {"sdkm", "application/vnd.solent.sdkm+xml"}, + {"sdp", "application/sdp"}, + {"sdw", "application/vnd.stardivision.writer"}, + {"see", "application/vnd.seemail"}, + {"seed", "application/vnd.fdsn.seed"}, + {"sema", "application/vnd.sema"}, + {"semd", "application/vnd.semd"}, + {"semf", "application/vnd.semf"}, + {"ser", "application/java-serialized-object"}, + {"setpay", "application/set-payment-initiation"}, + {"setreg", "application/set-registration-initiation"}, + {"sfd-hdstx", "application/vnd.hydrostatix.sof-data"}, + {"sfs", "application/vnd.spotfire.sfs"}, + {"sfv", "text/x-sfv"}, + {"sgi", "image/sgi"}, + {"sgl", "application/vnd.stardivision.writer-global"}, + {"sgml", "text/sgml"}, + {"sgm", "text/sgml"}, + {"sh", "application/x-sh"}, + {"shar", "application/x-shar"}, + {"shf", "application/shf+xml"}, + {"sid", "image/x-mrsid-image"}, + {"sig", "application/pgp-signature"}, + {"sil", "audio/silk"}, + {"silo", "model/mesh"}, + {"sis", "application/vnd.symbian.install"}, + {"sisx", "application/vnd.symbian.install"}, + {"sit", "application/x-stuffit"}, + {"sitx", "application/x-stuffitx"}, + {"skd", "application/vnd.koan"}, + {"skm", "application/vnd.koan"}, + {"skp", "application/vnd.koan"}, + {"skt", "application/vnd.koan"}, + {"sldm", "application/vnd.ms-powerpoint.slide.macroenabled.12"}, + {"sldx", + "application/vnd.openxmlformats-officedocument.presentationml.slide"}, + {"slt", "application/vnd.epson.salt"}, + {"sm", "application/vnd.stepmania.stepchart"}, + {"smf", "application/vnd.stardivision.math"}, + {"smi", "application/smil+xml"}, + {"smil", "application/smil+xml"}, + {"smv", "video/x-smv"}, + {"smzip", "application/vnd.stepmania.package"}, + {"snd", "audio/basic"}, + {"snf", "application/x-font-snf"}, + {"so", "application/octet-stream"}, + {"spc", "application/x-pkcs7-certificates"}, + {"spf", "application/vnd.yamaha.smaf-phrase"}, + {"spl", "application/x-futuresplash"}, + {"spot", "text/vnd.in3d.spot"}, + {"spp", "application/scvp-vp-response"}, + {"spq", "application/scvp-vp-request"}, + {"spx", "audio/ogg"}, + {"sql", "application/x-sql"}, + {"src", "application/x-wais-source"}, + {"srt", "application/x-subrip"}, + {"sru", "application/sru+xml"}, + {"srx", "application/sparql-results+xml"}, + {"ssdl", "application/ssdl+xml"}, + {"sse", "application/vnd.kodak-descriptor"}, + {"ssf", "application/vnd.epson.ssf"}, + {"ssml", "application/ssml+xml"}, + {"st", "application/vnd.sailingtracker.track"}, + {"stc", "application/vnd.sun.xml.calc.template"}, + {"std", "application/vnd.sun.xml.draw.template"}, + {"s", "text/x-asm"}, + {"stf", "application/vnd.wt.stf"}, + {"sti", "application/vnd.sun.xml.impress.template"}, + {"stk", "application/hyperstudio"}, + {"stl", "application/vnd.ms-pki.stl"}, + {"str", "application/vnd.pg.format"}, + {"stw", "application/vnd.sun.xml.writer.template"}, + {"sub", "image/vnd.dvb.subtitle"}, + {"sub", "text/vnd.dvb.subtitle"}, + {"sus", "application/vnd.sus-calendar"}, + {"susp", "application/vnd.sus-calendar"}, + {"sv4cpio", "application/x-sv4cpio"}, + {"sv4crc", "application/x-sv4crc"}, + {"svc", "application/vnd.dvb.service"}, + {"svd", "application/vnd.svd"}, + {"svg", "image/svg+xml"}, + {"svgz", "image/svg+xml"}, + {"swa", "application/x-director"}, + {"swf", "application/x-shockwave-flash"}, + {"swi", "application/vnd.aristanetworks.swi"}, + {"sxc", "application/vnd.sun.xml.calc"}, + {"sxd", "application/vnd.sun.xml.draw"}, + {"sxg", "application/vnd.sun.xml.writer.global"}, + {"sxi", "application/vnd.sun.xml.impress"}, + {"sxm", "application/vnd.sun.xml.math"}, + {"sxw", "application/vnd.sun.xml.writer"}, + {"t3", "application/x-t3vm-image"}, + {"taglet", "application/vnd.mynfc"}, + {"tao", "application/vnd.tao.intent-module-archive"}, + {"tar", "application/x-tar"}, + {"tcap", "application/vnd.3gpp2.tcap"}, + {"tcl", "application/x-tcl"}, + {"teacher", "application/vnd.smart.teacher"}, + {"tei", "application/tei+xml"}, + {"teicorpus", "application/tei+xml"}, + {"tex", "application/x-tex"}, + {"texi", "application/x-texinfo"}, + {"texinfo", "application/x-texinfo"}, + {"text", "text/plain"}, + {"tfi", "application/thraud+xml"}, + {"tfm", "application/x-tex-tfm"}, + {"tga", "image/x-tga"}, + {"thmx", "application/vnd.ms-officetheme"}, + {"tiff", "image/tiff"}, + {"tif", "image/tiff"}, + {"tmo", "application/vnd.tmobile-livetv"}, + {"torrent", "application/x-bittorrent"}, + {"tpl", "application/vnd.groove-tool-template"}, + {"tpt", "application/vnd.trid.tpt"}, + {"tra", "application/vnd.trueapp"}, + {"trm", "application/x-msterminal"}, + {"tr", "text/troff"}, + {"tsd", "application/timestamped-data"}, + {"tsv", "text/tab-separated-values"}, + {"ttc", "font/collection"}, + {"t", "text/troff"}, + {"ttf", "font/ttf"}, + {"ttl", "text/turtle"}, + {"twd", "application/vnd.simtech-mindmapper"}, + {"twds", "application/vnd.simtech-mindmapper"}, + {"txd", "application/vnd.genomatix.tuxedo"}, + {"txf", "application/vnd.mobius.txf"}, + {"txt", "text/plain"}, + {"u32", "application/x-authorware-bin"}, + {"udeb", "application/x-debian-package"}, + {"ufd", "application/vnd.ufdl"}, + {"ufdl", "application/vnd.ufdl"}, + {"ulx", "application/x-glulx"}, + {"umj", "application/vnd.umajin"}, + {"unityweb", "application/vnd.unity"}, + {"uoml", "application/vnd.uoml+xml"}, + {"uris", "text/uri-list"}, + {"uri", "text/uri-list"}, + {"urls", "text/uri-list"}, + {"ustar", "application/x-ustar"}, + {"utz", "application/vnd.uiq.theme"}, + {"uu", "text/x-uuencode"}, + {"uva", "audio/vnd.dece.audio"}, + {"uvd", "application/vnd.dece.data"}, + {"uvf", "application/vnd.dece.data"}, + {"uvg", "image/vnd.dece.graphic"}, + {"uvh", "video/vnd.dece.hd"}, + {"uvi", "image/vnd.dece.graphic"}, + {"uvm", "video/vnd.dece.mobile"}, + {"uvp", "video/vnd.dece.pd"}, + {"uvs", "video/vnd.dece.sd"}, + {"uvt", "application/vnd.dece.ttml+xml"}, + {"uvu", "video/vnd.uvvu.mp4"}, + {"uvva", "audio/vnd.dece.audio"}, + {"uvvd", "application/vnd.dece.data"}, + {"uvvf", "application/vnd.dece.data"}, + {"uvvg", "image/vnd.dece.graphic"}, + {"uvvh", "video/vnd.dece.hd"}, + {"uvvi", "image/vnd.dece.graphic"}, + {"uvvm", "video/vnd.dece.mobile"}, + {"uvvp", "video/vnd.dece.pd"}, + {"uvvs", "video/vnd.dece.sd"}, + {"uvvt", "application/vnd.dece.ttml+xml"}, + {"uvvu", "video/vnd.uvvu.mp4"}, + {"uvv", "video/vnd.dece.video"}, + {"uvvv", "video/vnd.dece.video"}, + {"uvvx", "application/vnd.dece.unspecified"}, + {"uvvz", "application/vnd.dece.zip"}, + {"uvx", "application/vnd.dece.unspecified"}, + {"uvz", "application/vnd.dece.zip"}, + {"vcard", "text/vcard"}, + {"vcd", "application/x-cdlink"}, + {"vcf", "text/x-vcard"}, + {"vcg", "application/vnd.groove-vcard"}, + {"vcs", "text/x-vcalendar"}, + {"vcx", "application/vnd.vcx"}, + {"vis", "application/vnd.visionary"}, + {"viv", "video/vnd.vivo"}, + {"vob", "video/x-ms-vob"}, + {"vor", "application/vnd.stardivision.writer"}, + {"vox", "application/x-authorware-bin"}, + {"vrml", "model/vrml"}, + {"vsd", "application/vnd.visio"}, + {"vsf", "application/vnd.vsf"}, + {"vss", "application/vnd.visio"}, + {"vst", "application/vnd.visio"}, + {"vsw", "application/vnd.visio"}, + {"vtu", "model/vnd.vtu"}, + {"vxml", "application/voicexml+xml"}, + {"w3d", "application/x-director"}, + {"wad", "application/x-doom"}, + {"wav", "audio/x-wav"}, + {"wax", "audio/x-ms-wax"}, + {"wbmp", "image/vnd.wap.wbmp"}, + {"wbs", "application/vnd.criticaltools.wbs+xml"}, + {"wbxml", "application/vnd.wap.wbxml"}, + {"wcm", "application/vnd.ms-works"}, + {"wdb", "application/vnd.ms-works"}, + {"wdp", "image/vnd.ms-photo"}, + {"weba", "audio/webm"}, + {"webm", "video/webm"}, + {"webp", "image/webp"}, + {"wg", "application/vnd.pmi.widget"}, + {"wgt", "application/widget"}, + {"wks", "application/vnd.ms-works"}, + {"wma", "audio/x-ms-wma"}, + {"wmd", "application/x-ms-wmd"}, + {"wmf", "application/x-msmetafile"}, + {"wmlc", "application/vnd.wap.wmlc"}, + {"wmlsc", "application/vnd.wap.wmlscriptc"}, + {"wmls", "text/vnd.wap.wmlscript"}, + {"wml", "text/vnd.wap.wml"}, + {"wm", "video/x-ms-wm"}, + {"wmv", "video/x-ms-wmv"}, + {"wmx", "video/x-ms-wmx"}, + {"wmz", "application/x-msmetafile"}, + {"wmz", "application/x-ms-wmz"}, + {"woff2", "font/woff2"}, + {"woff", "font/woff"}, + {"wpd", "application/vnd.wordperfect"}, + {"wpl", "application/vnd.ms-wpl"}, + {"wps", "application/vnd.ms-works"}, + {"wqd", "application/vnd.wqd"}, + {"wri", "application/x-mswrite"}, + {"wrl", "model/vrml"}, + {"wsdl", "application/wsdl+xml"}, + {"wspolicy", "application/wspolicy+xml"}, + {"wtb", "application/vnd.webturbo"}, + {"wvx", "video/x-ms-wvx"}, + {"x32", "application/x-authorware-bin"}, + {"x3db", "model/x3d+binary"}, + {"x3dbz", "model/x3d+binary"}, + {"x3d", "model/x3d+xml"}, + {"x3dv", "model/x3d+vrml"}, + {"x3dvz", "model/x3d+vrml"}, + {"x3dz", "model/x3d+xml"}, + {"xaml", "application/xaml+xml"}, + {"xap", "application/x-silverlight-app"}, + {"xar", "application/vnd.xara"}, + {"xbap", "application/x-ms-xbap"}, + {"xbd", "application/vnd.fujixerox.docuworks.binder"}, + {"xbm", "image/x-xbitmap"}, + {"xdf", "application/xcap-diff+xml"}, + {"xdm", "application/vnd.syncml.dm+xml"}, + {"xdp", "application/vnd.adobe.xdp+xml"}, + {"xdssc", "application/dssc+xml"}, + {"xdw", "application/vnd.fujixerox.docuworks"}, + {"xenc", "application/xenc+xml"}, + {"xer", "application/patch-ops-error+xml"}, + {"xfdf", "application/vnd.adobe.xfdf"}, + {"xfdl", "application/vnd.xfdl"}, + {"xht", "application/xhtml+xml"}, + {"xhtml", "application/xhtml+xml"}, + {"xhvml", "application/xv+xml"}, + {"xif", "image/vnd.xiff"}, + {"xla", "application/vnd.ms-excel"}, + {"xlam", "application/vnd.ms-excel.addin.macroenabled.12"}, + {"xlc", "application/vnd.ms-excel"}, + {"xlf", "application/x-xliff+xml"}, + {"xlm", "application/vnd.ms-excel"}, + {"xls", "application/vnd.ms-excel"}, + {"xlsb", "application/vnd.ms-excel.sheet.binary.macroenabled.12"}, + {"xlsm", "application/vnd.ms-excel.sheet.macroenabled.12"}, + {"xlsx", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, + {"xlt", "application/vnd.ms-excel"}, + {"xltm", "application/vnd.ms-excel.template.macroenabled.12"}, + {"xltx", + "application/vnd.openxmlformats-officedocument.spreadsheetml.template"}, + {"xlw", "application/vnd.ms-excel"}, + {"xm", "audio/xm"}, + {"xml", "application/xml"}, + {"xo", "application/vnd.olpc-sugar"}, + {"xop", "application/xop+xml"}, + {"xpi", "application/x-xpinstall"}, + {"xpl", "application/xproc+xml"}, + {"xpm", "image/x-xpixmap"}, + {"xpr", "application/vnd.is-xpr"}, + {"xps", "application/vnd.ms-xpsdocument"}, + {"xpw", "application/vnd.intercon.formnet"}, + {"xpx", "application/vnd.intercon.formnet"}, + {"xsl", "application/xml"}, + {"xslt", "application/xslt+xml"}, + {"xsm", "application/vnd.syncml+xml"}, + {"xspf", "application/xspf+xml"}, + {"xul", "application/vnd.mozilla.xul+xml"}, + {"xvm", "application/xv+xml"}, + {"xvml", "application/xv+xml"}, + {"xwd", "image/x-xwindowdump"}, + {"xyz", "chemical/x-xyz"}, + {"xz", "application/x-xz"}, + {"yang", "application/yang"}, + {"yin", "application/yin+xml"}, + {"z1", "application/x-zmachine"}, + {"z2", "application/x-zmachine"}, + {"z3", "application/x-zmachine"}, + {"z4", "application/x-zmachine"}, + {"z5", "application/x-zmachine"}, + {"z6", "application/x-zmachine"}, + {"z7", "application/x-zmachine"}, + {"z8", "application/x-zmachine"}, + {"zaz", "application/vnd.zzazz.deck+xml"}, + {"zip", "application/zip"}, + {"zir", "application/vnd.zul"}, + {"zirz", "application/vnd.zul"}, + {"zmm", "application/vnd.handheld-entertainment+xml"}, + }); + + return (*mime_map)[extension]; +} // NOLINT + +} // namespace utils +} // namespace nearby diff --git a/sharing/internal/base/mime.h b/sharing/internal/base/mime.h new file mode 100644 index 00000000..8e3c0c29 --- /dev/null +++ b/sharing/internal/base/mime.h @@ -0,0 +1,30 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_INTERNAL_BASE_MIME_H_ +#define THIRD_PARTY_NEARBY_SHARING_INTERNAL_BASE_MIME_H_ + +#include + +#include "absl/strings/string_view.h" + +namespace nearby { +namespace utils { + +std::string GetWellKnownMimeTypeFromExtension(absl::string_view extension); + +} // namespace utils +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_INTERNAL_BASE_MIME_H_ diff --git a/sharing/internal/base/utf_string_configuration.h b/sharing/internal/base/utf_string_configuration.h new file mode 100644 index 00000000..7f3174fc --- /dev/null +++ b/sharing/internal/base/utf_string_configuration.h @@ -0,0 +1,120 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_INTERNAL_BASE_UTF_STRING_CONFIGURATION_H_ +#define THIRD_PARTY_NEARBY_SHARING_INTERNAL_BASE_UTF_STRING_CONFIGURATION_H_ + +// A set of macros to use for platform detection. +#if defined(__native_client__) +// __native_client__ must be first, so that other OS_ defines are not set. +#define OS_NACL 1 +#define OS_NACL_SFI +#elif defined(ANDROID) +#define OS_ANDROID 1 +#elif defined(__APPLE__) +// Only include TargetConditionals after testing ANDROID as some Android builds +// on the Mac have this header available and it's not needed unless the target +// is really an Apple platform. +#include +#if defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE +#define OS_IOS 1 +#else +#define OS_MAC 1 +#endif // defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE +#elif defined(__linux__) +#if !defined(OS_CHROMEOS) +// Do not define OS_LINUX on Chrome OS build. +// The OS_CHROMEOS macro is defined in GN. +#define OS_LINUX 1 +#endif // !defined(OS_CHROMEOS) +#if defined(__GLIBC__) && !defined(__UCLIBC__) +// We really are using glibc, not uClibc pretending to be glibc. +#define LIBC_GLIBC 1 +#endif +#elif defined(_WIN32) +#define OS_WIN 1 +#elif defined(__Fuchsia__) +#define OS_FUCHSIA 1 +#elif defined(__FreeBSD__) +#define OS_FREEBSD 1 +#elif defined(__NetBSD__) +#define OS_NETBSD 1 +#elif defined(__OpenBSD__) +#define OS_OPENBSD 1 +#elif defined(__sun) +#define OS_SOLARIS 1 +#elif defined(__QNXNTO__) +#define OS_QNX 1 +#elif defined(_AIX) +#define OS_AIX 1 +#elif defined(__asmjs__) || defined(__wasm__) +#define OS_ASMJS 1 +#elif defined(__MVS__) +#define OS_ZOS 1 +#else +#error Please add support for your platform in build/build_config.h +#endif +// NOTE: Adding a new port? Please follow +// https://chromium.googlesource.com/chromium/src/+/main/docs/new_port_policy.md + +#if defined(OS_MAC) || defined(OS_IOS) +#define OS_APPLE 1 +#endif + +// For access to standard BSD features, use OS_BSD instead of a +// more specific macro. +#if defined(OS_FREEBSD) || defined(OS_NETBSD) || defined(OS_OPENBSD) +#define OS_BSD 1 +#endif + +// For access to standard POSIX features, use OS_POSIX instead of a +// more specific macro. +#if defined(OS_AIX) || defined(OS_ANDROID) || defined(OS_ASMJS) || \ + defined(OS_FREEBSD) || defined(OS_IOS) || defined(OS_LINUX) || \ + defined(OS_CHROMEOS) || defined(OS_MAC) || defined(OS_NACL) || \ + defined(OS_NETBSD) || defined(OS_OPENBSD) || defined(OS_QNX) || \ + defined(OS_SOLARIS) || defined(OS_ZOS) +#define OS_POSIX 1 +#endif + +// Compiler detection. Note: clang masquerades as GCC on POSIX and as MSVC on +// Windows. +#if defined(__GNUC__) +#define COMPILER_GCC 1 +#elif defined(_MSC_VER) +#define COMPILER_MSVC 1 +#else +#error Please add support for your compiler in build/build_config.h +#endif + +// Type detection for wchar_t. +#if defined(OS_WIN) +#define WCHAR_T_IS_UTF16 +#elif defined(OS_FUCHSIA) +#define WCHAR_T_IS_UTF32 +#elif defined(OS_POSIX) && defined(COMPILER_GCC) && defined(__WCHAR_MAX__) && \ + (__WCHAR_MAX__ == 0x7fffffff || __WCHAR_MAX__ == 0xffffffff) +#define WCHAR_T_IS_UTF32 +#elif defined(OS_POSIX) && defined(COMPILER_GCC) && defined(__WCHAR_MAX__) && \ + (__WCHAR_MAX__ == 0x7fff || __WCHAR_MAX__ == 0xffff) +// On Posix, we'll detect short wchar_t, but projects aren't guaranteed to +// compile in this mode (in particular, Chrome doesn't). This is intended for +// other projects using base who manage their own dependencies and make sure +// short wchar works for them. +#define WCHAR_T_IS_UTF16 +#else +#error Please add support for your compiler in build/build_config.h +#endif + +#endif // THIRD_PARTY_NEARBY_SHARING_INTERNAL_BASE_UTF_STRING_CONFIGURATION_H_ diff --git a/sharing/internal/base/utf_string_conversions.cc b/sharing/internal/base/utf_string_conversions.cc new file mode 100644 index 00000000..2234e8a2 --- /dev/null +++ b/sharing/internal/base/utf_string_conversions.cc @@ -0,0 +1,510 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/internal/base/utf_string_conversions.h" + +#include +#include + +#include +#include +#include +#include + +#include "third_party/icu_utf/icu_utf.h" +#include "sharing/internal/public/logging.h" + +namespace nearby { +namespace utils { +namespace { + +using MachineWord = uintptr_t; + +constexpr int32_t kErrorCodePoint = 0xFFFD; + +inline bool IsMachineWordAligned(const void* pointer) { + return !(reinterpret_cast(pointer) & (sizeof(MachineWord) - 1)); +} + +template +bool DoIsStringAscii(const Char* characters, size_t length) { + // Bitmasks to detect non-ASCII characters for character sizes of 8, 16 and 32 + // bits. + constexpr MachineWord NonASCIIMasks[] = { + 0, MachineWord(0x8080808080808080ULL), MachineWord(0xFF80FF80FF80FF80ULL), + 0, MachineWord(0xFFFFFF80FFFFFF80ULL), + }; + + if (!length) return true; + constexpr MachineWord non_ascii_bit_mask = NonASCIIMasks[sizeof(Char)]; + static_assert(non_ascii_bit_mask, "Error: Invalid Mask"); + MachineWord all_char_bits = 0; + const Char* end = characters + length; + + // Prologue: align the input. + while (!IsMachineWordAligned(characters) && characters < end) + all_char_bits |= *characters++; + if (all_char_bits & non_ascii_bit_mask) return false; + + // Compare the values of CPU word size. + constexpr size_t chars_per_word = sizeof(MachineWord) / sizeof(Char); + constexpr int batch_count = 16; + while (characters <= end - batch_count * chars_per_word) { + all_char_bits = 0; + for (int i = 0; i < batch_count; ++i) { + all_char_bits |= *(reinterpret_cast(characters)); + characters += chars_per_word; + } + if (all_char_bits & non_ascii_bit_mask) return false; + } + + // Process the remaining words. + all_char_bits = 0; + while (characters <= end - chars_per_word) { + all_char_bits |= *(reinterpret_cast(characters)); + characters += chars_per_word; + } + + // Process the remaining bytes. + while (characters < end) all_char_bits |= *characters++; + + return !(all_char_bits & non_ascii_bit_mask); +} + +inline bool IsValidCharacter(uint32_t code_point) { + // Excludes non-characters (U+FDD0..U+FDEF, and all code points + // ending in 0xFFFE or 0xFFFF) from the set of valid code points. + // https://unicode.org/faq/private_use.html#nonchar1 + return code_point < 0xD800u || + (code_point >= 0xE000u && code_point < 0xFDD0u) || + (code_point > 0xFDEFu && code_point <= 0x10FFFFu && + (code_point & 0xFFFEu) != 0xFFFEu); +} + +template +inline bool DoIsStringUtf8(std::string_view str) { + const char* src = str.data(); + int32_t src_len = static_cast(str.length()); + int32_t char_index = 0; + + while (char_index < src_len) { + int32_t code_point; + CBU8_NEXT(src, char_index, src_len, code_point); + if (!Validator(code_point)) return false; + } + return true; +} + +// Size coefficient ---------------------------------------------------------- +// The maximum number of codeunits in the destination encoding corresponding to +// one codeunit in the source encoding. + +template +struct SizeCoefficient { + static_assert(sizeof(SrcChar) < sizeof(DestChar), + "Default case: from a smaller encoding to the bigger one"); + + // ASCII symbols are encoded by one codeunit in all encodings. + static constexpr int value = 1; +}; + +template <> +struct SizeCoefficient { + // One UTF-16 code unit corresponds to at most 3 code units in UTF-8. + static constexpr int value = 3; +}; + +#if defined(WCHAR_T_IS_UTF32) +template <> +struct SizeCoefficient { + // UTF-8 uses at most 4 code units per character. + static constexpr int value = 4; +}; + +template <> +struct SizeCoefficient { + // UTF-16 uses at most 2 code units per character. + static constexpr int value = 2; +}; +#endif // defined(WCHAR_T_IS_UTF32) + +template +constexpr int size_coefficient_v = + SizeCoefficient, std::decay_t>::value; + +// UnicodeAppendUnsafe -------------------------------------------------------- +// Function overloads that write code_point to the output string. Output string +// has to have enough space for the codepoint. + +// Convenience typedef that checks whether the passed in type is integral (i.e. +// bool, char, int or their extended versions) and is of the correct size. +template +using EnableIfBitsAre = std::enable_if_t< + std::is_integral::value && CHAR_BIT * sizeof(Char) == N, bool>; + +template = true> +void UnicodeAppendUnsafe(Char* out, int32_t* size, uint32_t code_point) { + CBU8_APPEND_UNSAFE(out, *size, code_point); +} + +template = true> +void UnicodeAppendUnsafe(Char* out, int32_t* size, uint32_t code_point) { + CBU16_APPEND_UNSAFE(out, *size, code_point); +} + +template = true> +void UnicodeAppendUnsafe(Char* out, int32_t* size, uint32_t code_point) { + out[(*size)++] = code_point; +} + +// DoUtfConversion ------------------------------------------------------------ +// Main driver of UtfConversion specialized for different Src encodings. +// dest has to have enough room for the converted text. + +template +bool DoUtfConversion(const char* src, int32_t src_len, DestChar* dest, + int32_t* dest_len) { + bool success = true; + + for (int32_t i = 0; i < src_len;) { + int32_t code_point; + CBU8_NEXT(src, i, src_len, code_point); + + if (!IsValidCodepoint(code_point)) { + success = false; + code_point = kErrorCodePoint; + } + + UnicodeAppendUnsafe(dest, dest_len, code_point); + } + + return success; +} + +template +bool DoUtfConversion(const char16_t* src, int32_t src_len, DestChar* dest, + int32_t* dest_len) { + bool success = true; + + auto ConvertSingleChar = [&success](char16_t in) -> int32_t { + if (!CBU16_IS_SINGLE(in) || !IsValidCodepoint(in)) { + success = false; + return kErrorCodePoint; + } + return in; + }; + + int32_t i = 0; + + // Always have another symbol in order to avoid checking boundaries in the + // middle of the surrogate pair. + while (i < src_len - 1) { + int32_t code_point; + + if (CBU16_IS_LEAD(src[i]) && CBU16_IS_TRAIL(src[i + 1])) { + code_point = CBU16_GET_SUPPLEMENTARY(src[i], src[i + 1]); + if (!IsValidCodepoint(code_point)) { + code_point = kErrorCodePoint; + success = false; + } + i += 2; + } else { + code_point = ConvertSingleChar(src[i]); + ++i; + } + + UnicodeAppendUnsafe(dest, dest_len, code_point); + } + + if (i < src_len) + UnicodeAppendUnsafe(dest, dest_len, ConvertSingleChar(src[i])); + + return success; +} + +#if defined(WCHAR_T_IS_UTF32) + +template +bool DoUtfConversion(const wchar_t* src, int32_t src_len, DestChar* dest, + int32_t* dest_len) { + bool success = true; + + for (int32_t i = 0; i < src_len; ++i) { + int32_t code_point = src[i]; + + if (!IsValidCodepoint(code_point)) { + success = false; + code_point = kErrorCodePoint; + } + + UnicodeAppendUnsafe(dest, dest_len, code_point); + } + + return success; +} + +#endif // defined(WCHAR_T_IS_UTF32) + +// UtfConversion -------------------------------------------------------------- +// Function template for generating all UTF conversions. + +template +bool UtfConversion(const InputString& src_str, DestString* dest_str) { + if (IsStringAscii(src_str)) { + dest_str->assign(src_str.begin(), src_str.end()); + return true; + } + + dest_str->resize(src_str.length() * + size_coefficient_v); + + // Empty string is ASCII => it OK to call operator[]. + auto* dest = &(*dest_str)[0]; + + // ICU requires 32 bit numbers. + int32_t src_len32 = static_cast(src_str.length()); + int32_t dest_len32 = 0; + + bool res = DoUtfConversion(src_str.data(), src_len32, dest, &dest_len32); + + dest_str->resize(dest_len32); + dest_str->shrink_to_fit(); + + return res; +} + +#if defined(WCHAR_T_IS_UTF16) +inline const char16_t* as_u16cstr(const wchar_t* str) { + return reinterpret_cast(str); +} + +inline const char16_t* as_u16cstr(std::wstring_view str) { + return reinterpret_cast(str.data()); +} +#endif + +} // namespace + +// UTF16 <-> UTF8 -------------------------------------------------------------- + +bool Utf8ToUtf16(const char* src, size_t src_len, std::u16string* output) { + return UtfConversion(std::string_view(src, src_len), output); +} + +std::u16string Utf8ToUtf16(std::string_view utf8) { + std::u16string ret; + // Ignore the success flag of this call, it will do the best it can for + // invalid input, which is what we want here. + Utf8ToUtf16(utf8.data(), utf8.size(), &ret); + return ret; +} + +bool Utf16ToUtf8(const char16_t* src, size_t src_len, std::string* output) { + return UtfConversion(std::u16string_view(src, src_len), output); +} + +std::string Utf16ToUtf8(std::u16string_view utf16) { + std::string ret; + // Ignore the success flag of this call, it will do the best it can for + // invalid input, which is what we want here. + Utf16ToUtf8(utf16.data(), utf16.length(), &ret); + return ret; +} + +// UTF-16 <-> Wide ------------------------------------------------------------- + +#if defined(WCHAR_T_IS_UTF16) +// When wide == UTF-16 the conversions are a NOP. + +bool WideToUtf16(const wchar_t* src, size_t src_len, std::u16string* output) { + output->assign(src, src + src_len); + return true; +} + +std::u16string WideToUtf16(std::wstring_view wide) { + return std::u16string(wide.begin(), wide.end()); +} + +bool Utf16ToWide(const char16_t* src, size_t src_len, std::wstring* output) { + output->assign(src, src + src_len); + return true; +} + +std::wstring Utf16ToWide(std::u16string_view utf16) { + return std::wstring(utf16.begin(), utf16.end()); +} + +#elif defined(WCHAR_T_IS_UTF32) + +bool WideToUtf16(const wchar_t* src, size_t src_len, std::u16string* output) { + return UtfConversion(std::wstring_view(src, src_len), output); +} + +std::u16string WideToUtf16(std::wstring_view wide) { + std::u16string ret; + // Ignore the success flag of this call, it will do the best it can for + // invalid input, which is what we want here. + WideToUtf16(wide.data(), wide.length(), &ret); + return ret; +} + +bool Utf16ToWide(const char16_t* src, size_t src_len, std::wstring* output) { + return UtfConversion(std::u16string_view(src, src_len), output); +} + +std::wstring Utf16ToWide(std::u16string_view utf16) { + std::wstring ret; + // Ignore the success flag of this call, it will do the best it can for + // invalid input, which is what we want here. + Utf16ToWide(utf16.data(), utf16.length(), &ret); + return ret; +} + +#endif // defined(WCHAR_T_IS_UTF32) + +// UTF-8 <-> Wide -------------------------------------------------------------- + +// UTF8ToWide is the same code, regardless of whether wide is 16 or 32 bits + +bool Utf8ToWide(const char* src, size_t src_len, std::wstring* output) { + return UtfConversion(std::string_view(src, src_len), output); +} + +std::wstring Utf8ToWide(std::string_view utf8) { + std::wstring ret; + // Ignore the success flag of this call, it will do the best it can for + // invalid input, which is what we want here. + Utf8ToWide(utf8.data(), utf8.length(), &ret); + return ret; +} + +#if defined(WCHAR_T_IS_UTF16) +// Easy case since we can use the "utf" versions we already wrote above. + +bool WideToUtf8(const wchar_t* src, size_t src_len, std::string* output) { + return Utf16ToUtf8(as_u16cstr(src), src_len, output); +} + +std::string WideToUtf8(std::wstring_view wide) { + return Utf16ToUtf8(std::u16string_view(as_u16cstr(wide), wide.size())); +} + +#elif defined(WCHAR_T_IS_UTF32) + +bool WideToUtf8(const wchar_t* src, size_t src_len, std::string* output) { + return UtfConversion(std::wstring_view(src, src_len), output); +} + +std::string WideToUtf8(std::wstring_view wide) { + std::string ret; + // Ignore the success flag of this call, it will do the best it can for + // invalid input, which is what we want here. + WideToUtf8(wide.data(), wide.length(), &ret); + return ret; +} + +#endif // defined(WCHAR_T_IS_UTF32) + +std::u16string AsciiToUtf16(std::string_view ascii) { + NL_DCHECK(IsStringAscii(ascii)); + return std::u16string(ascii.begin(), ascii.end()); +} + +std::string Utf16ToAscii(std::u16string_view utf16) { + NL_DCHECK(IsStringAscii(utf16)); + return std::string(utf16.begin(), utf16.end()); +} + +#if defined(WCHAR_T_IS_UTF16) +std::wstring AsciiToWide(std::string_view ascii) { + NL_DCHECK(IsStringAscii(ascii)); + return std::wstring(ascii.begin(), ascii.end()); +} + +std::string WideToAscii(std::string_view wide) { + NL_DCHECK(IsStringAscii(wide)); + return std::string(wide.begin(), wide.end()); +} +#endif // defined(WCHAR_T_IS_UTF16) + +bool IsStringAscii(std::string_view str) { + return DoIsStringAscii(str.data(), str.length()); +} + +bool IsStringAscii(std::u16string_view str) { + return DoIsStringAscii(str.data(), str.length()); +} + +bool IsStringUtf8(std::string_view str) { + return DoIsStringUtf8(str); +} + +bool IsStringAscii(std::wstring_view str) { + return DoIsStringAscii(str.data(), str.length()); +} + +bool IsValidCodepoint(uint32_t code_point) { + // Excludes code points that are not Unicode scalar values, i.e. + // surrogate code points ([0xD800, 0xDFFF]). Additionally, excludes + // code points larger than 0x10FFFF (the highest codepoint allowed). + // Non-characters and unassigned code points are allowed. + // https://unicode.org/glossary/#unicode_scalar_value + return code_point < 0xD800u || + (code_point >= 0xE000u && code_point <= 0x10FFFFu); +} + +void TruncateUtf8ToByteSize(const std::string& input, size_t byte_size, + std::string* output) { + NL_DCHECK(output); + if (byte_size > input.length()) { + *output = input; + return; + } + + // Note: This cast is necessary because CBU8_NEXT uses int32_ts. + int32_t truncation_length = static_cast(byte_size); + int32_t char_index = truncation_length - 1; + const char* data = input.data(); + + // Using CBU8, we will move backwards from the truncation point + // to the beginning of the string looking for a valid UTF8 + // character. Once a full UTF8 character is found, we will + // truncate the string to the end of that character. + while (char_index >= 0) { + int32_t prev = char_index; + int32_t code_point = 0; + CBU8_NEXT(data, char_index, truncation_length, code_point); + if (!IsValidCharacter(code_point) || !IsValidCodepoint(code_point)) { + char_index = prev - 1; + } else { + break; + } + } + + if (char_index >= 0) + *output = input.substr(0, char_index); + else + output->clear(); +} + +std::string ToString(const char* str) { + if (str == nullptr) { + return ""; + } + return std::string(str); +} + +} // namespace utils +} // namespace nearby diff --git a/sharing/internal/base/utf_string_conversions.h b/sharing/internal/base/utf_string_conversions.h new file mode 100644 index 00000000..14f3acec --- /dev/null +++ b/sharing/internal/base/utf_string_conversions.h @@ -0,0 +1,117 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_INTERNAL_BASE_UTF_STRING_CONVERSIONS_H_ +#define THIRD_PARTY_NEARBY_SHARING_INTERNAL_BASE_UTF_STRING_CONVERSIONS_H_ + +#include + +#include +#include +#include + +#include "sharing/internal/base/utf_string_configuration.h" + +namespace nearby { +namespace utils { + +// These convert between UTF-8, -16, and -32 strings. They are potentially slow, +// so avoid unnecessary conversions. The low-level versions return a boolean +// indicating whether the conversion was 100% valid. In this case, it will still +// do the best it can and put the result in the output buffer. The versions that +// return strings ignore this error and just return the best conversion +// possible. +bool WideToUtf8(const wchar_t* src, size_t src_len, std::string* output); +std::string WideToUtf8(std::wstring_view wide); +bool Utf8ToWide(const char* src, size_t src_len, std::wstring* output); +std::wstring Utf8ToWide(std::string_view utf8); + +bool WideToUtf16(const wchar_t* src, size_t src_len, std::u16string* output); +std::u16string WideToUtf16(std::wstring_view wide); +bool Utf16ToWide(const char16_t* src, size_t src_len, std::wstring* output); +std::wstring Utf16ToWide(std::u16string_view utf16); + +bool Utf8ToUtf16(const char* src, size_t src_len, std::u16string* output); +std::u16string Utf8ToUtf16(std::string_view utf8); +bool Utf16ToUtf8(const char16_t* src, size_t src_len, std::string* output); +std::string Utf16ToUtf8(std::u16string_view utf16); + +// This converts an ASCII string, typically a hard coded constant, to a UTF16 +// string. +std::u16string AsciiToUtf16(std::string_view ascii); + +// Converts to 7-bit ASCII by truncating. The result must be known to be ASCII +// beforehand. +std::string Utf16ToAscii(std::u16string_view utf16); + +bool IsStringAscii(std::string_view str); +bool IsStringAscii(std::u16string_view str); +bool IsStringAscii(std::wstring_view str); +bool IsStringUtf8(std::string_view str); + +inline bool IsValidCodepoint(uint32_t code_point); + +void TruncateUtf8ToByteSize(const std::string& input, size_t byte_size, + std::string* output); + +#if defined(WCHAR_T_IS_UTF16) +// This converts an ASCII string, typically a hard coded constant, to a wide +// string. +std::wstring AsciiToWide(std::string_view ascii); + +// Converts to 7-bit ASCII by truncating. The result must be known to be ASCII +// beforehand. +std::string WideToAscii(std::wstring_view wide); +#endif // defined(WCHAR_T_IS_UTF16) + +// The conversion functions in this file should not be used to convert string +// literals. Instead, the corresponding prefixes (e.g. u"" for UTF16 or L"" for +// Wide) should be used. Deleting the overloads here catches these cases at +// compile time. +template +std::u16string WideToUtf16(const wchar_t (&str)[N]) { + static_assert(N == 0, "Error: Use the u\"...\" prefix instead."); + return std::u16string(); +} + +template +std::u16string Utf8ToUtf16(const char (&str)[N]) { + static_assert(N == 0, "Error: Use the u\"...\" prefix instead."); + return std::u16string(); +} + +template +std::u16string AsciiToUtf16(const char (&str)[N]) { + static_assert(N == 0, "Error: Use the u\"...\" prefix instead."); + return std::u16string(); +} + +// Mutable character arrays are usually only populated during runtime. Continue +// to allow this conversion. +template +std::u16string AsciiToUtf16(char (&str)[N]) { + return AsciiToUtf16(std::string_view(str)); +} + +template +constexpr size_t size(const T (&array)[N]) noexcept { + return N; +} + +std::string ToString(const char* str); + +} // namespace utils +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_INTERNAL_BASE_UTF_STRING_CONVERSIONS_H_ diff --git a/sharing/internal/base/utf_string_conversions_test.cc b/sharing/internal/base/utf_string_conversions_test.cc new file mode 100644 index 00000000..48d620e4 --- /dev/null +++ b/sharing/internal/base/utf_string_conversions_test.cc @@ -0,0 +1,375 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/internal/base/utf_string_conversions.h" + +#include +#include +#include +#include + +#include "gtest/gtest.h" + +namespace nearby { +namespace utils { +namespace { + +const wchar_t* const kConvertRoundtripCases[] = { + L"Google Video", + // "网页 图片 资讯更多 »" + L"\x7f51\x9875\x0020\x56fe\x7247\x0020\x8d44\x8baf\x66f4\x591a\x0020\x00bb", + // "Παγκόσμιος Ιστός" + L"\x03a0\x03b1\x03b3\x03ba\x03cc\x03c3\x03bc\x03b9" + L"\x03bf\x03c2\x0020\x0399\x03c3\x03c4\x03cc\x03c2", + // "Поиск страниц на русском" + L"\x041f\x043e\x0438\x0441\x043a\x0020\x0441\x0442" + L"\x0440\x0430\x043d\x0438\x0446\x0020\x043d\x0430" + L"\x0020\x0440\x0443\x0441\x0441\x043a\x043e\x043c", + // "전체서비스" + L"\xc804\xccb4\xc11c\xbe44\xc2a4", + +// Test characters that take more than 16 bits. This will depend on whether +// wchar_t is 16 or 32 bits. +#if defined(WCHAR_T_IS_UTF16) + L"\xd800\xdf00", + // ????? (Mathematical Alphanumeric Symbols (U+011d40 - U+011d44 : + // A,B,C,D,E) + L"\xd807\xdd40\xd807\xdd41\xd807\xdd42\xd807\xdd43\xd807\xdd44", +#elif defined(WCHAR_T_IS_UTF32) + L"\x10300", + // ????? (Mathematical Alphanumeric Symbols (U+011d40 - U+011d44 : + // A,B,C,D,E) + L"\x11d40\x11d41\x11d42\x11d43\x11d44", +#endif +}; + +// Helper used to test TruncateUtf8ToByteSize. +bool Truncated(const std::string& input, const size_t byte_size, + std::string* output) { + size_t prev = input.length(); + TruncateUtf8ToByteSize(input, byte_size, output); + return prev != output->length(); +} + +} // namespace + +TEST(UtfStringConversionsTest, ConvertUtf8AndWide) { + // we round-trip all the wide strings through UTF-8 to make sure everything + // agrees on the conversion. This uses the stream operators to test them + // simultaneously. + for (auto* i : kConvertRoundtripCases) { + std::ostringstream utf8; + utf8 << WideToUtf8(i); + std::wostringstream wide; + wide << Utf8ToWide(utf8.str()); + + EXPECT_EQ(i, wide.str()); + } +} + +TEST(UtfStringConversionsTest, ConvertUtf8AndWideEmptyString) { + // An empty std::wstring should be converted to an empty std::string, + // and vice versa. + std::wstring wempty; + std::string empty; + EXPECT_EQ(empty, WideToUtf8(wempty)); + EXPECT_EQ(wempty, Utf8ToWide(empty)); +} + +TEST(UtfStringConversionsTest, ConvertUtf8ToWide) { + struct Utf8ToWideCase { + const char* utf8; + const wchar_t* wide; + bool success; + } convert_cases[] = { + // Regular UTF-8 input. + {"\xe4\xbd\xa0\xe5\xa5\xbd", L"\x4f60\x597d", true}, + // Non-character is passed through. + {"\xef\xbf\xbfHello", L"\xffffHello", true}, + // Truncated UTF-8 sequence. + {"\xe4\xa0\xe5\xa5\xbd", L"\xfffd\x597d", false}, + // Truncated off the end. + {"\xe5\xa5\xbd\xe4\xa0", L"\x597d\xfffd", false}, + // Non-shortest-form UTF-8. + {"\xf0\x84\xbd\xa0\xe5\xa5\xbd", L"\xfffd\xfffd\xfffd\xfffd\x597d", false}, + // This UTF-8 character is decoded to a UTF-16 surrogate, which is illegal. + {"\xed\xb0\x80", L"\xfffd\xfffd\xfffd", false}, + // Non-BMP characters. The second is a non-character regarded as valid. + // The result will either be in UTF-16 or UTF-32. +#if defined(WCHAR_T_IS_UTF16) + {"A\xF0\x90\x8C\x80z", L"A\xd800\xdf00z", true}, + {"A\xF4\x8F\xBF\xBEz", L"A\xdbff\xdffez", true}, +#elif defined(WCHAR_T_IS_UTF32) + {"A\xF0\x90\x8C\x80z", L"A\x10300z", true}, + {"A\xF4\x8F\xBF\xBEz", L"A\x10fffez", true}, +#endif + }; + + for (const auto& i : convert_cases) { + std::wstring converted; + EXPECT_EQ(i.success, Utf8ToWide(i.utf8, strlen(i.utf8), &converted)); + std::wstring expected(i.wide); + EXPECT_EQ(expected, converted); + } + + // Manually test an embedded NULL. + std::wstring converted; + EXPECT_TRUE(Utf8ToWide("\00Z\t", 3, &converted)); + ASSERT_EQ(3U, converted.length()); + EXPECT_EQ(static_cast(0), converted[0]); + EXPECT_EQ('Z', converted[1]); + EXPECT_EQ('\t', converted[2]); + + // Make sure that conversion replaces, not appends. + EXPECT_TRUE(Utf8ToWide("B", 1, &converted)); + ASSERT_EQ(1U, converted.length()); + EXPECT_EQ('B', converted[0]); +} + +#if defined(WCHAR_T_IS_UTF16) +// This test is only valid when wchar_t == UTF-16. +TEST(UtfStringConversionsTest, ConvertUtf16ToUtf8) { + struct WideToUtf8Case { + const wchar_t* utf16; + const char* utf8; + bool success; + } convert_cases[] = { + // Regular UTF-16 input. + {L"\x4f60\x597d", "\xe4\xbd\xa0\xe5\xa5\xbd", true}, + // Test a non-BMP character. + {L"\xd800\xdf00", "\xF0\x90\x8C\x80", true}, + // Non-characters are passed through. + {L"\xffffHello", "\xEF\xBF\xBFHello", true}, + {L"\xdbff\xdffeHello", "\xF4\x8F\xBF\xBEHello", true}, + // The first character is a truncated UTF-16 character. + {L"\xd800\x597d", "\xef\xbf\xbd\xe5\xa5\xbd", false}, + // Truncated at the end. + {L"\x597d\xd800", "\xe5\xa5\xbd\xef\xbf\xbd", false}, + }; + + for (const auto& test : convert_cases) { + std::string converted; + EXPECT_EQ(test.success, + WideToUtf8(test.utf16, wcslen(test.utf16), &converted)); + std::string expected(test.utf8); + EXPECT_EQ(expected, converted); + } +} + +#elif defined(WCHAR_T_IS_UTF32) +// This test is only valid when wchar_t == UTF-32. +TEST(UtfStringConversionsTest, ConvertUtf32ToUtf8) { + struct WideToUtf8Case { + const wchar_t* utf32; + const char* utf8; + bool success; + } convert_cases[] = { + // Regular 16-bit input. + {L"\x4f60\x597d", "\xe4\xbd\xa0\xe5\xa5\xbd", true}, + // Test a non-BMP character. + {L"A\x10300z", "A\xF0\x90\x8C\x80z", true}, + // Non-characters are passed through. + {L"\xffffHello", "\xEF\xBF\xBFHello", true}, + {L"\x10fffeHello", "\xF4\x8F\xBF\xBEHello", true}, + // Invalid Unicode code points. + {L"\xfffffffHello", "\xEF\xBF\xBDHello", false}, + // The first character is a truncated UTF-16 character. + {L"\xd800\x597d", "\xef\xbf\xbd\xe5\xa5\xbd", false}, + {L"\xdc01Hello", "\xef\xbf\xbdHello", false}, + }; + + for (const auto& test : convert_cases) { + std::string converted; + EXPECT_EQ(test.success, + WideToUtf8(test.utf32, wcslen(test.utf32), &converted)); + std::string expected(test.utf8); + EXPECT_EQ(expected, converted); + } +} +#endif // defined(WCHAR_T_IS_UTF32) + +TEST(UtfStringConversionsTest, TruncateUtf8ToByteSize) { + std::string output; + + // Empty strings and invalid byte_size arguments + EXPECT_FALSE(Truncated(std::string(), 0, &output)); + EXPECT_EQ(output, ""); + EXPECT_TRUE(Truncated("\xe1\x80\xbf", 0, &output)); + EXPECT_EQ(output, ""); + EXPECT_FALSE(Truncated("\xe1\x80\xbf", static_cast(-1), &output)); + EXPECT_FALSE(Truncated("\xe1\x80\xbf", 4, &output)); + + // Testing the truncation of valid UTF8 correctly + EXPECT_TRUE(Truncated("abc", 2, &output)); + EXPECT_EQ(output, "ab"); + EXPECT_TRUE(Truncated("\xc2\x81\xc2\x81", 2, &output)); + EXPECT_EQ(output.compare("\xc2\x81"), 0); + EXPECT_TRUE(Truncated("\xc2\x81\xc2\x81", 3, &output)); + EXPECT_EQ(output.compare("\xc2\x81"), 0); + EXPECT_FALSE(Truncated("\xc2\x81\xc2\x81", 4, &output)); + EXPECT_EQ(output.compare("\xc2\x81\xc2\x81"), 0); + + { + const char array[] = "\x00\x00\xc2\x81\xc2\x81"; + const std::string array_string(array, size(array)); + EXPECT_TRUE(Truncated(array_string, 4, &output)); + EXPECT_EQ(output.compare(std::string("\x00\x00\xc2\x81", 4)), 0); + } + + { + const char array[] = "\x00\xc2\x81\xc2\x81"; + const std::string array_string(array, size(array)); + EXPECT_TRUE(Truncated(array_string, 4, &output)); + EXPECT_EQ(output.compare(std::string("\x00\xc2\x81", 3)), 0); + } + + // Testing invalid UTF8 + EXPECT_TRUE(Truncated("\xed\xa0\x80\xed\xbf\xbf", 6, &output)); + EXPECT_EQ(output.compare(""), 0); + EXPECT_TRUE(Truncated("\xed\xa0\x8f", 3, &output)); + EXPECT_EQ(output.compare(""), 0); + EXPECT_TRUE(Truncated("\xed\xbf\xbf", 3, &output)); + EXPECT_EQ(output.compare(""), 0); + + // Testing invalid UTF8 mixed with valid UTF8 + EXPECT_FALSE(Truncated("\xe1\x80\xbf", 3, &output)); + EXPECT_EQ(output.compare("\xe1\x80\xbf"), 0); + EXPECT_FALSE(Truncated("\xf1\x80\xa0\xbf", 4, &output)); + EXPECT_EQ(output.compare("\xf1\x80\xa0\xbf"), 0); + EXPECT_FALSE(Truncated("a\xc2\x81\xe1\x80\xbf\xf1\x80\xa0\xbf", 10, &output)); + EXPECT_EQ(output.compare("a\xc2\x81\xe1\x80\xbf\xf1\x80\xa0\xbf"), 0); + EXPECT_TRUE( + Truncated("a\xc2\x81\xe1\x80\xbf\xf1" + "a" + "\x80\xa0", + 10, &output)); + EXPECT_EQ(output.compare("a\xc2\x81\xe1\x80\xbf\xf1" + "a"), + 0); + EXPECT_FALSE( + Truncated("\xef\xbb\xbf" + "abc", + 6, &output)); + EXPECT_EQ(output.compare("\xef\xbb\xbf" + "abc"), + 0); + + // Overlong sequences + EXPECT_TRUE(Truncated("\xc0\x80", 2, &output)); + EXPECT_EQ(output.compare(""), 0); + EXPECT_TRUE(Truncated("\xc1\x80\xc1\x81", 4, &output)); + EXPECT_EQ(output.compare(""), 0); + EXPECT_TRUE(Truncated("\xe0\x80\x80", 3, &output)); + EXPECT_EQ(output.compare(""), 0); + EXPECT_TRUE(Truncated("\xe0\x82\x80", 3, &output)); + EXPECT_EQ(output.compare(""), 0); + EXPECT_TRUE(Truncated("\xe0\x9f\xbf", 3, &output)); + EXPECT_EQ(output.compare(""), 0); + EXPECT_TRUE(Truncated("\xf0\x80\x80\x8D", 4, &output)); + EXPECT_EQ(output.compare(""), 0); + EXPECT_TRUE(Truncated("\xf0\x80\x82\x91", 4, &output)); + EXPECT_EQ(output.compare(""), 0); + EXPECT_TRUE(Truncated("\xf0\x80\xa0\x80", 4, &output)); + EXPECT_EQ(output.compare(""), 0); + EXPECT_TRUE(Truncated("\xf0\x8f\xbb\xbf", 4, &output)); + EXPECT_EQ(output.compare(""), 0); + EXPECT_TRUE(Truncated("\xf8\x80\x80\x80\xbf", 5, &output)); + EXPECT_EQ(output.compare(""), 0); + EXPECT_TRUE(Truncated("\xfc\x80\x80\x80\xa0\xa5", 6, &output)); + EXPECT_EQ(output.compare(""), 0); + + // Beyond U+10FFFF (the upper limit of Unicode codespace) + EXPECT_TRUE(Truncated("\xf4\x90\x80\x80", 4, &output)); + EXPECT_EQ(output.compare(""), 0); + EXPECT_TRUE(Truncated("\xf8\xa0\xbf\x80\xbf", 5, &output)); + EXPECT_EQ(output.compare(""), 0); + EXPECT_TRUE(Truncated("\xfc\x9c\xbf\x80\xbf\x80", 6, &output)); + EXPECT_EQ(output.compare(""), 0); + + // BOMs in UTF-16(BE|LE) and UTF-32(BE|LE) + EXPECT_TRUE(Truncated("\xfe\xff", 2, &output)); + EXPECT_EQ(output.compare(""), 0); + EXPECT_TRUE(Truncated("\xff\xfe", 2, &output)); + EXPECT_EQ(output.compare(""), 0); + + { + const char array[] = "\x00\x00\xfe\xff"; + const std::string array_string(array, size(array)); + EXPECT_TRUE(Truncated(array_string, 4, &output)); + EXPECT_EQ(output.compare(std::string("\x00\x00", 2)), 0); + } + + // Variants on the previous test + { + const char array[] = "\xff\xfe\x00\x00"; + const std::string array_string(array, 4); + EXPECT_FALSE(Truncated(array_string, 4, &output)); + EXPECT_EQ(output.compare(std::string("\xff\xfe\x00\x00", 4)), 0); + } + { + const char array[] = "\xff\x00\x00\xfe"; + const std::string array_string(array, size(array)); + EXPECT_TRUE(Truncated(array_string, 4, &output)); + EXPECT_EQ(output.compare(std::string("\xff\x00\x00", 3)), 0); + } + + // Non-characters : U+xxFFF[EF] where xx is 0x00 through 0x10 and + EXPECT_TRUE(Truncated("\xef\xbf\xbe", 3, &output)); + EXPECT_EQ(output.compare(""), 0); + EXPECT_TRUE(Truncated("\xf0\x8f\xbf\xbe", 4, &output)); + EXPECT_EQ(output.compare(""), 0); + EXPECT_TRUE(Truncated("\xf3\xbf\xbf\xbf", 4, &output)); + EXPECT_EQ(output.compare(""), 0); + EXPECT_TRUE(Truncated("\xef\xb7\x90", 3, &output)); + EXPECT_EQ(output.compare(""), 0); + EXPECT_TRUE(Truncated("\xef\xb7\xaf", 3, &output)); + EXPECT_EQ(output.compare(""), 0); + + // Strings in legacy encodings that are valid in UTF-8, but + // are invalid as UTF-8 in real data. + EXPECT_TRUE(Truncated("caf\xe9", 4, &output)); + EXPECT_EQ(output.compare("caf"), 0); + EXPECT_TRUE(Truncated("\xb0\xa1\xb0\xa2", 4, &output)); + EXPECT_EQ(output.compare(""), 0); + EXPECT_FALSE(Truncated("\xa7\x41\xa6\x6e", 4, &output)); + EXPECT_EQ(output.compare("\xa7\x41\xa6\x6e"), 0); + EXPECT_TRUE(Truncated("\xa7\x41\xa6\x6e\xd9\xee\xe4\xee", 7, &output)); + EXPECT_EQ(output.compare("\xa7\x41\xa6\x6e"), 0); + + // Testing using the same string as input and output. + EXPECT_FALSE(Truncated(output, 4, &output)); + EXPECT_EQ(output.compare("\xa7\x41\xa6\x6e"), 0); + EXPECT_TRUE(Truncated(output, 3, &output)); + EXPECT_EQ(output.compare("\xa7\x41"), 0); + + // "abc" with U+201[CD] in windows-125[0-8] + EXPECT_TRUE( + Truncated("\x93" + "abc\x94", + 5, &output)); + EXPECT_EQ(output.compare("\x93" + "abc"), + 0); + + // U+0639 U+064E U+0644 U+064E in ISO-8859-6 + EXPECT_TRUE(Truncated("\xd9\xee\xe4\xee", 4, &output)); + EXPECT_EQ(output.compare(""), 0); + + // U+03B3 U+03B5 U+03B9 U+03AC in ISO-8859-7 + EXPECT_TRUE(Truncated("\xe3\xe5\xe9\xdC", 4, &output)); + EXPECT_EQ(output.compare(""), 0); +} + +} // namespace utils +} // namespace nearby diff --git a/sharing/nearby_connection.h b/sharing/nearby_connection.h new file mode 100644 index 00000000..518cea27 --- /dev/null +++ b/sharing/nearby_connection.h @@ -0,0 +1,58 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_NEARBY_CONNECTION_H_ +#define THIRD_PARTY_NEARBY_SHARING_NEARBY_CONNECTION_H_ + +#include +#include +#include +#include + +namespace nearby { +namespace sharing { + +// A socket-like wrapper around Nearby Connections that allows for asynchronous +// reads and writes. +class NearbyConnection { + public: + using ReadCallback = + std::function> bytes)>; + + virtual ~NearbyConnection() = default; + + // Reads a stream of bytes from the remote device. Invoke |callback| when + // there is incoming data or when the socket is closed. Previously set + // callback will be replaced by |callback|. Must not be used on an already + // closed connection. + virtual void Read(ReadCallback callback) = 0; + + // Writes an outgoing stream of bytes to the remote device asynchronously. + // Must not be used on an already closed connection. + virtual void Write(std::vector bytes) = 0; + + // Closes the socket and disconnects from the remote device. This object will + // be invalidated after |callback| in SetDisconnectionListener is invoked. + virtual void Close() = 0; + + // Listens to the socket being closed. Invoke |callback| when the socket is + // closed. This object will be invalidated after |listener| is invoked. + // Previously set listener will be replaced by |listener|. + virtual void SetDisconnectionListener(std::function listener) = 0; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_NEARBY_CONNECTION_H_ diff --git a/sharing/nearby_connection_impl.cc b/sharing/nearby_connection_impl.cc new file mode 100644 index 00000000..d67c3b0b --- /dev/null +++ b/sharing/nearby_connection_impl.cc @@ -0,0 +1,111 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/nearby_connection_impl.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "internal/platform/device_info.h" +#include "internal/platform/mutex_lock.h" +#include "sharing/internal/public/logging.h" +#include "sharing/nearby_connection.h" +#include "sharing/nearby_connections_manager.h" +#include "sharing/nearby_connections_types.h" + +namespace nearby { +namespace sharing { + +NearbyConnectionImpl::NearbyConnectionImpl( + nearby::DeviceInfo& device_info, + NearbyConnectionsManager* nearby_connections_manager, + absl::string_view endpoint_id) + : device_info_(device_info), + nearby_connections_manager_(nearby_connections_manager), + endpoint_id_(endpoint_id) { + if (!device_info_.PreventSleep()) { + NL_LOG(WARNING) << __func__ << ":Failed to prevent device sleep."; + } +} + +NearbyConnectionImpl::~NearbyConnectionImpl() { + MutexLock lock(&mutex_); + if (!device_info_.AllowSleep()) { + NL_LOG(ERROR) << __func__ << ":Failed to allow device sleep."; + } + + if (disconnect_listener_) { + disconnect_listener_(); + } + + if (read_callback_) { + read_callback_(std::nullopt); + } +} + +void NearbyConnectionImpl::Read(ReadCallback callback) { + MutexLock lock(&mutex_); + if (reads_.empty()) { + read_callback_ = std::move(callback); + return; + } + + std::vector bytes = std::move(reads_.front()); + reads_.pop(); + std::move(callback)(std::move(bytes)); +} + +void NearbyConnectionImpl::Write(std::vector bytes) { + MutexLock lock(&mutex_); + Payload payload(bytes); + nearby_connections_manager_->Send( + endpoint_id_, std::make_unique(payload), + /*listener=*/ + std::weak_ptr()); +} + +void NearbyConnectionImpl::Close() { + MutexLock lock(&mutex_); + // As [this] therefore endpoint_id_ will be destroyed in Disconnect, make a + // copy of [endpoint_id] as the parameter is a const ref. + nearby_connections_manager_->Disconnect(endpoint_id_); +} + +void NearbyConnectionImpl::SetDisconnectionListener( + std::function listener) { + MutexLock lock(&mutex_); + disconnect_listener_ = std::move(listener); +} + +void NearbyConnectionImpl::WriteMessage(std::vector bytes) { + MutexLock lock(&mutex_); + if (read_callback_) { + auto callback = std::move(read_callback_); + read_callback_ = nullptr; + callback(std::move(bytes)); + return; + } + + reads_.push(std::move(bytes)); +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/nearby_connection_impl.h b/sharing/nearby_connection_impl.h new file mode 100644 index 00000000..a944360c --- /dev/null +++ b/sharing/nearby_connection_impl.h @@ -0,0 +1,68 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_NEARBY_CONNECTION_IMPL_H_ +#define THIRD_PARTY_NEARBY_SHARING_NEARBY_CONNECTION_IMPL_H_ + +#include +#include +#include +#include +#include + +#include "absl/base/thread_annotations.h" +#include "absl/strings/string_view.h" +#include "internal/platform/device_info.h" +#include "internal/platform/mutex.h" +#include "sharing/nearby_connection.h" + +namespace nearby { +namespace sharing { + +class NearbyConnectionsManager; + +class NearbyConnectionImpl : public NearbyConnection { + public: + NearbyConnectionImpl(nearby::DeviceInfo& device_info, + NearbyConnectionsManager* nearby_connections_manager, + absl::string_view endpoint_id); + ~NearbyConnectionImpl() override; + + // NearbyConnection: + void Read(ReadCallback callback) override; + void Write(std::vector bytes) override; + void Close() override; + void SetDisconnectionListener(std::function listener) override; + + // Add bytes to the read queue, notifying ReadCallback. + void WriteMessage(std::vector bytes); + + private: + nearby::DeviceInfo& device_info_; + NearbyConnectionsManager* const nearby_connections_manager_; + std::string endpoint_id_; + + RecursiveMutex mutex_; + ReadCallback read_callback_ ABSL_GUARDED_BY(mutex_) = nullptr; + std::function disconnect_listener_ ABSL_GUARDED_BY(mutex_); + + // A read queue. The data that we've read from the remote device ends up here + // until Read() is called to dequeue it. + std::queue> reads_ ABSL_GUARDED_BY(mutex_); +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_NEARBY_CONNECTION_IMPL_H_ diff --git a/sharing/nearby_connection_impl_test.cc b/sharing/nearby_connection_impl_test.cc new file mode 100644 index 00000000..598a4827 --- /dev/null +++ b/sharing/nearby_connection_impl_test.cc @@ -0,0 +1,87 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/nearby_connection_impl.h" + +#include +#include + +#include "gtest/gtest.h" +#include "absl/synchronization/notification.h" +#include "absl/time/time.h" +#include "internal/test/fake_device_info.h" +#include "internal/test/fake_task_runner.h" +#include "sharing/fake_nearby_connections_manager.h" +#include "sharing/incoming_frames_reader.h" +#include "sharing/internal/test/fake_context.h" +#include "sharing/nearby_sharing_decoder_impl.h" +#include "sharing/proto/wire_format.pb.h" + +namespace nearby { +namespace sharing { +namespace { + +TEST(NearbyConnectionImpl, DestructorBeforeReaderDestructor) { + FakeNearbyConnectionsManager connection_manager; + FakeContext context; + FakeDeviceInfo device_info; + NearbySharingDecoderImpl decoder; + bool called = false; + + auto connection = std::make_unique( + device_info, &connection_manager, "test"); + auto frames_reader = std::make_shared( + &context, &decoder, connection.get()); + + absl::Notification notification; + frames_reader->ReadFrame( + [&](std::optional frame) { + called = true; + notification.Notify(); + }); + EXPECT_TRUE(FakeTaskRunner::WaitForRunningTasksWithTimeout(absl::Seconds(1))); + connection.reset(); + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(absl::Seconds(1))); + EXPECT_TRUE(called); +} + +TEST(NearbyConnectionImpl, DestructorAfterReaderDestructor) { + FakeNearbyConnectionsManager connection_manager; + FakeContext context; + FakeDeviceInfo device_info; + NearbySharingDecoderImpl decoder; + std::optional frame_result; + + auto connection = std::make_unique( + device_info, &connection_manager, "test"); + auto frames_reader = std::make_shared( + &context, &decoder, connection.get()); + + absl::Notification notification; + frames_reader->ReadFrame( + [&](std::optional frame) { + frame_result = frame; + notification.Notify(); + }); + + EXPECT_TRUE(FakeTaskRunner::WaitForRunningTasksWithTimeout(absl::Seconds(1))); + frames_reader.reset(); + connection.reset(); + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(absl::Seconds(1))); + EXPECT_FALSE(frame_result.has_value()); +} + +} // namespace +} // namespace sharing +} // namespace nearby diff --git a/sharing/nearby_connections_manager.cc b/sharing/nearby_connections_manager.cc new file mode 100644 index 00000000..8e12db4e --- /dev/null +++ b/sharing/nearby_connections_manager.cc @@ -0,0 +1,86 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/nearby_connections_manager.h" + +#include +#include + +#include "sharing/nearby_connections_types.h" + +namespace nearby { +namespace sharing { + +// static +// LINT.IfChange(status_enum) +std::string NearbyConnectionsManager::ConnectionsStatusToString( + ConnectionsStatus status) { + switch (status) { + case ConnectionsStatus::kSuccess: + return "kSuccess"; + case ConnectionsStatus::kError: + return "kError"; + case ConnectionsStatus::kOutOfOrderApiCall: + return "kOutOfOrderApiCall"; + case ConnectionsStatus::kAlreadyHaveActiveStrategy: + return "kAlreadyHaveActiveStrategy"; + case ConnectionsStatus::kAlreadyAdvertising: + return "kAlreadyAdvertising"; + case ConnectionsStatus::kAlreadyDiscovering: + return "kAlreadyDiscovering"; + case ConnectionsStatus::kEndpointIOError: + return "kEndpointIOError"; + case ConnectionsStatus::kEndpointUnknown: + return "kEndpointUnknown"; + case ConnectionsStatus::kConnectionRejected: + return "kConnectionRejected"; + case ConnectionsStatus::kAlreadyConnectedToEndpoint: + return "kAlreadyConnectedToEndpoint"; + case ConnectionsStatus::kNotConnectedToEndpoint: + return "kNotConnectedToEndpoint"; + case ConnectionsStatus::kBluetoothError: + return "kBluetoothError"; + case ConnectionsStatus::kBleError: + return "kBleError"; + case ConnectionsStatus::kWifiLanError: + return "kWifiLanError"; + case ConnectionsStatus::kPayloadUnknown: + return "kPayloadUnknown"; + case ConnectionsStatus::kAlreadyListening: + return "kAlreadyListening"; + case ConnectionsStatus::kReset: + return "kReset"; + case ConnectionsStatus::kTimeout: + return "kTimeout"; + case ConnectionsStatus::kUnknown: + // fall through + default: + return "Unknown"; + } +} +// LINT.ThenChange() + +NearbyConnectionsManager::PayloadStatusListener::PayloadStatusListener() = + default; + +NearbyConnectionsManager::PayloadStatusListener::~PayloadStatusListener() = + default; + +std::weak_ptr +NearbyConnectionsManager::PayloadStatusListener::GetWeakPtr() { + return this->weak_from_this(); +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/nearby_connections_manager.h b/sharing/nearby_connections_manager.h new file mode 100644 index 00000000..a1719771 --- /dev/null +++ b/sharing/nearby_connections_manager.h @@ -0,0 +1,172 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_NEARBY_CONNECTIONS_MANAGER_H_ +#define THIRD_PARTY_NEARBY_SHARING_NEARBY_CONNECTIONS_MANAGER_H_ + +#include + +#include // NOLINT(build/c++17) +#include +#include +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/nearby_connections_types.h" +#include "sharing/proto/enums.pb.h" + +namespace nearby { +namespace sharing { + +class NearbyConnection; +class PayloadTransferUpdatePtr; + +using ConnectionsStatus = nearby::sharing::Status; + +class NearbyConnectionsManager { + public: + using ConnectionsCallback = std::function; + using NearbyConnectionCallback = + std::function; + + // A callback for handling incoming connections while advertising. + class IncomingConnectionListener { + public: + virtual ~IncomingConnectionListener() = default; + + // `endpoint_info`is returned from remote devices and should be parsed in + // utility process. + virtual void OnIncomingConnection(absl::string_view endpoint_id, + absl::Span endpoint_info, + NearbyConnection* connection) = 0; + }; + + // A callback for handling discovered devices while discovering. + class DiscoveryListener { + public: + virtual ~DiscoveryListener() = default; + + // `endpoint_info` is returned from remote devices and should be parsed in + // utility process. + virtual void OnEndpointDiscovered( + absl::string_view endpoint_id, + absl::Span endpoint_info) = 0; + + // A callback triggered when the endpoint is lost. + virtual void OnEndpointLost(absl::string_view endpoint_id) = 0; + }; + + // A callback for tracking the status of a payload (both incoming and + // outgoing). + class PayloadStatusListener + : public std::enable_shared_from_this { + public: + PayloadStatusListener(); + virtual ~PayloadStatusListener(); + + std::weak_ptr GetWeakPtr(); + + // Note: `upgraded_medium` is passed in for use in metrics, and it is + // absl::nullopt if the bandwidth has not upgraded yet or if the upgrade + // status is not known. + virtual void OnStatusUpdate(std::unique_ptr update, + std::optional upgraded_medium) = 0; + }; + + // Converts the status to a logging-friendly string. + static std::string ConnectionsStatusToString(ConnectionsStatus status); + + NearbyConnectionsManager() = default; + virtual ~NearbyConnectionsManager() = default; + + // Disconnects from all endpoints and shut down Nearby Connections. + // As a side effect of this call, both StopAdvertising and StopDiscovery may + // be invoked if Nearby Connections is advertising or discovering. + virtual void Shutdown() = 0; + + // Starts advertising through Nearby Connections. Caller is expected to ensure + // `listener` remains valid until StopAdvertising is called. + virtual void StartAdvertising(std::vector endpoint_info, + IncomingConnectionListener* listener, + PowerLevel power_level, + proto::DataUsage data_usage, + ConnectionsCallback callback) = 0; + + // Stops advertising through Nearby Connections. + virtual void StopAdvertising(ConnectionsCallback callback) = 0; + + // Starts discovery through Nearby Connections. Caller is expected to ensure + // `listener` remains valid until StopDiscovery is called. + virtual void StartDiscovery(DiscoveryListener* listener, + proto::DataUsage data_usage, + ConnectionsCallback callback) = 0; + + // Stops discovery through Nearby Connections. + virtual void StopDiscovery() = 0; + + // Connects to remote `endpoint_id` through Nearby Connections. + virtual void Connect( + std::vector endpoint_info, absl::string_view endpoint_id, + std::optional> bluetooth_mac_address, + proto::DataUsage data_usage, TransportType transport_type, + NearbyConnectionCallback callback) = 0; + + // Disconnects from remote `endpoint_id` through Nearby Connections. + virtual void Disconnect(absl::string_view endpoint_id) = 0; + + // Sends `payload` through Nearby Connections. + virtual void Send(absl::string_view endpoint_id, + std::unique_ptr payload, + std::weak_ptr listener) = 0; + + // Register a `listener` with `payload_id`. + virtual void RegisterPayloadStatusListener( + int64_t payload_id, std::weak_ptr listener) = 0; + + // Register a `file_path` for receiving incoming payload with `payload_id`. + virtual void RegisterPayloadPath(int64_t payload_id, + const std::filesystem::path& file_path, + ConnectionsCallback callback) = 0; + + // Gets the payload associated with `payload_id` if available. + virtual Payload* GetIncomingPayload(int64_t payload_id) = 0; + + // Cancels a Payload currently in-flight to or from remote endpoints. + virtual void Cancel(int64_t payload_id) = 0; + + // Clears all incoming payloads. + virtual void ClearIncomingPayloads() = 0; + + // Gets the raw authentication token for the `endpoint_id`. + virtual std::optional> GetRawAuthenticationToken( + absl::string_view endpoint_id) = 0; + + // Initiates bandwidth upgrade for `endpoint_id`. + virtual void UpgradeBandwidth(absl::string_view endpoint_id) = 0; + + // Sets a custom save path. + virtual void SetCustomSavePath(absl::string_view custom_save_path) = 0; + + // Dump internal state for debugging purposes. + virtual std::string Dump() const = 0; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_NEARBY_CONNECTIONS_MANAGER_H_ diff --git a/sharing/nearby_connections_manager_factory.cc b/sharing/nearby_connections_manager_factory.cc new file mode 100644 index 00000000..821a1db8 --- /dev/null +++ b/sharing/nearby_connections_manager_factory.cc @@ -0,0 +1,40 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/nearby_connections_manager_factory.h" + +#include + +#include "internal/analytics/event_logger.h" +#include "internal/platform/device_info.h" +#include "sharing/internal/public/context.h" +#include "sharing/nearby_connections_manager.h" +#include "sharing/nearby_connections_manager_impl.h" +#include "sharing/nearby_connections_service_impl.h" + +namespace nearby { +namespace sharing { + +std::unique_ptr +NearbyConnectionsManagerFactory::CreateConnectionsManager( + LinkType link_type, Context* context, + nearby::DeviceInfo& device_info, + nearby::analytics::EventLogger* event_logger) { + return std::make_unique( + context, *context->GetConnectivityManager(), device_info, + std::make_unique(event_logger)); +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/nearby_connections_manager_factory.h b/sharing/nearby_connections_manager_factory.h new file mode 100644 index 00000000..80117a0e --- /dev/null +++ b/sharing/nearby_connections_manager_factory.h @@ -0,0 +1,46 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_NEARBY_CONNECTIONS_MANAGER_FACTORY_H_ +#define THIRD_PARTY_NEARBY_SHARING_NEARBY_CONNECTIONS_MANAGER_FACTORY_H_ + +#include + +#include "internal/analytics/event_logger.h" +#include "internal/platform/device_info.h" +#include "sharing/internal/public/context.h" +#include "sharing/nearby_connections_manager.h" +#include "sharing/nearby_sharing_service_factory.h" + +namespace nearby { +namespace sharing { + +class NearbyConnectionsManagerFactory { + public: + using LinkType = NearbySharingServiceFactory::LinkType; + + // Return a singleton instance of NearbyConnectionsManagerFactory. + static std::unique_ptr CreateConnectionsManager( + NearbySharingServiceFactory::LinkType link_type, Context* context, + nearby::DeviceInfo& device_info, + nearby::analytics::EventLogger* event_logger = nullptr); + + private: + NearbyConnectionsManagerFactory() = default; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_NEARBY_CONNECTIONS_MANAGER_FACTORY_H_ diff --git a/sharing/nearby_connections_manager_impl.cc b/sharing/nearby_connections_manager_impl.cc new file mode 100644 index 00000000..f56f47f8 --- /dev/null +++ b/sharing/nearby_connections_manager_impl.cc @@ -0,0 +1,906 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/nearby_connections_manager_impl.h" + +#include + +#include // NOLINT(build/c++17) +#include +#include +#include +#include +#include +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" +#include "absl/meta/type_traits.h" +#include "absl/strings/string_view.h" +#include "absl/time/time.h" +#include "absl/types/span.h" +#include "internal/flags/nearby_flags.h" +#include "internal/platform/device_info.h" +#include "internal/platform/mutex_lock.h" +#include "sharing/advertisement.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/constants.h" +#include "sharing/flags/nearby_sharing_feature_flags.h" +#include "sharing/internal/api/bluetooth_adapter.h" +#include "sharing/internal/base/encode.h" +#include "sharing/internal/public/connectivity_manager.h" +#include "sharing/internal/public/context.h" +#include "sharing/internal/public/logging.h" +#include "sharing/nearby_connection_impl.h" +#include "sharing/nearby_connections_manager.h" +#include "sharing/nearby_connections_service.h" +#include "sharing/nearby_connections_types.h" +#include "sharing/transfer_manager.h" + +namespace nearby { +namespace sharing { +namespace { +using ::nearby::sharing::proto::DataUsage; + +constexpr char kServiceId[] = "NearbySharing"; +constexpr char kFastAdvertisementServiceUuid[] = + "0000fef3-0000-1000-8000-00805f9b34fb"; +constexpr Strategy kStrategy = Strategy::kP2pPointToPoint; + +const uint8_t kMinimumAdvertisementSize = + /* Version(3 bits)|Visibility(1 bit)|Device Type(3 bits)|Reserved(1 bits)= + */ + 1 + Advertisement::kSaltSize + + Advertisement::kMetadataEncryptionKeyHashByteSize; + +bool ShouldUseInternet(ConnectivityManager& connectivity_manager, + DataUsage data_usage, PowerLevel power_level) { + // We won't use the internet if the user requested we don't. + if (data_usage == DataUsage::OFFLINE_DATA_USAGE) return false; + + // We won't use the internet in a low power mode. + if (power_level == PowerLevel::kLowPower) return false; + + ConnectivityManager::ConnectionType connection_type = + connectivity_manager.GetConnectionType(); + + // Verify that this network has an internet connection. + if (connection_type == ConnectivityManager::ConnectionType::kNone) { + NL_VLOG(1) << __func__ << ": No internet connection."; + return false; + } + + if (data_usage == DataUsage::WIFI_ONLY_DATA_USAGE && + connection_type != ConnectivityManager::ConnectionType::kWifi) { + return false; + } + + // We're online, the user hasn't disabled Wi-Fi, let's use it! + return true; +} + +bool ShouldEnableWebRtc(ConnectivityManager& connectivity_manager, + DataUsage data_usage, PowerLevel power_level) { + return NearbyFlags::GetInstance().GetBoolFlag( + config_package_nearby::nearby_sharing_feature:: + kEnableMediumWebRtc) && + ShouldUseInternet(connectivity_manager, data_usage, power_level); +} + +bool ShouldEnableWifiLan(ConnectivityManager& connectivity_manager) { + if (!NearbyFlags::GetInstance().GetBoolFlag( + config_package_nearby::nearby_sharing_feature:: + kEnableMediumWifiLan)) { + return false; + } + + ConnectivityManager::ConnectionType connection_type = + connectivity_manager.GetConnectionType(); + bool is_connection_wifi_or_ethernet = + connection_type == ConnectivityManager::ConnectionType::kWifi || + connection_type == ConnectivityManager::ConnectionType::kEthernet; + + return is_connection_wifi_or_ethernet; +} + +bool ShouldEnableBleForTransfers() { + return NearbyFlags::GetInstance().GetBoolFlag( + config_package_nearby::nearby_sharing_feature::kEnableBleForTransfer); +} + +std::string MediumSelectionToString(const MediumSelection& mediums) { + std::stringstream ss; + ss << "{"; + if (mediums.bluetooth) ss << "bluetooth "; + if (mediums.ble) ss << "ble "; + if (mediums.web_rtc) ss << "webrtc "; + if (mediums.wifi_lan) ss << "wifilan "; + ss << "}"; + + return ss.str(); +} + +} // namespace + +NearbyConnectionsManagerImpl::NearbyConnectionsManagerImpl( + Context* context, + ConnectivityManager& connectivity_manager, + nearby::DeviceInfo& device_info, + std::unique_ptr nearby_connections_service) + : context_(context), + connectivity_manager_(connectivity_manager), + device_info_(device_info), + nearby_connections_service_(std::move(nearby_connections_service)) {} + +NearbyConnectionsManagerImpl::~NearbyConnectionsManagerImpl() { + ClearIncomingPayloads(); +} + +void NearbyConnectionsManagerImpl::Shutdown() { Reset(); } + +void NearbyConnectionsManagerImpl::StartAdvertising( + std::vector endpoint_info, IncomingConnectionListener* listener, + PowerLevel power_level, DataUsage data_usage, + ConnectionsCallback callback) { + NL_DCHECK(listener); + NL_DCHECK(!incoming_connection_listener_); + + if (!nearby_connections_service_) { + std::move(callback)(ConnectionsStatus::kError); + return; + } + + bool is_high_power = power_level == PowerLevel::kHighPower; + bool use_ble = true; + + MediumSelection allowed_mediums = MediumSelection( + /*bluetooth=*/is_high_power, /*ble=*/use_ble, + // Using kHighPower here rather than power_level to signal that power + // level isn't a factor when deciding whether to allow WebRTC + // upgrades from this advertisement. + ShouldEnableWebRtc(connectivity_manager_, data_usage, + PowerLevel::kHighPower), + /*wifi_lan=*/ + ShouldEnableWifiLan(connectivity_manager_), + /*wifi_hotspot=*/true); + NL_VLOG(1) << __func__ << ": " + << "is_high_power=" << (is_high_power ? "yes" : "no") + << ", data_usage=" << static_cast(data_usage) + << ", allowed_mediums=" + << MediumSelectionToString(allowed_mediums); + + // Nearby Sharing manually controls Wi-Fi/Bluetooth upgrade. Frequent + // Bluetooth connection drops were observed during upgrades for Bluetooth + // transfers. Android has similar logic to handle upgrades, please check + // b/161880863 for more information. + bool auto_upgrade_bandwidth = false; + + incoming_connection_listener_ = listener; + + NearbyConnectionsService::ConnectionListener connection_listener; + connection_listener.initiated_cb = + [&](absl::string_view endpoint_id, + const ConnectionInfo& connection_info) { + OnConnectionInitiated(endpoint_id, connection_info); + }; + connection_listener.accepted_cb = [&](absl::string_view endpoint_id) { + OnConnectionAccepted(endpoint_id); + }; + connection_listener.rejected_cb = [&](absl::string_view endpoint_id, + Status status) { + OnConnectionRejected(endpoint_id, status); + }; + connection_listener.disconnected_cb = [&](absl::string_view endpoint_id) { + OnDisconnected(endpoint_id); + }; + connection_listener.bandwidth_changed_cb = [&](absl::string_view endpoint_id, + Medium medium) { + OnBandwidthChanged(endpoint_id, medium); + }; + + // Check if BLE hardware supports Extended Advertising + bool extended_advertising_supported = + context_->GetBluetoothAdapter().IsExtendedAdvertisingSupported(); + + Uuid fast_advertisement_service_uuid; + + if (NearbyFlags::GetInstance().GetBoolFlag( + config_package_nearby::nearby_sharing_feature::kEnableBleV2)) { + NL_LOG(INFO) << __func__ + << ": Nearby Sharing flag kEnableBleV2 is enabled."; + // Uses fast advertisement when advertisement data size is less than + // kMinimumAdvertisementSize. Nearby Connections will decide whether to use + // GATT server with this information. + if (endpoint_info.size() > kMinimumAdvertisementSize) { + fast_advertisement_service_uuid = Uuid(""); + } else { + fast_advertisement_service_uuid = Uuid(kFastAdvertisementServiceUuid); + } + } else { + NL_LOG(INFO) << __func__ + << ": Nearby Sharing flag kEnableBleV2 is disabled."; + // Only use Fast Advertisement if Extended Advertising is not supported + if (extended_advertising_supported) { + // Empty string to instruct Nearby Connection BLE not to use Fast + // Advertisement + fast_advertisement_service_uuid = Uuid(""); + } else { + // Handle advertisement on device without BLE advertisement extension. + if (endpoint_info.size() > kMinimumAdvertisementSize) { + // cannot use Fast Advertisement, because the endpoint info size exceeds + // the limitation of Fast Advertisement. + fast_advertisement_service_uuid = Uuid(""); + } else { + fast_advertisement_service_uuid = Uuid(kFastAdvertisementServiceUuid); + } + } + } + + nearby_connections_service_->StartAdvertising( + kServiceId, endpoint_info, + AdvertisingOptions( + kStrategy, std::move(allowed_mediums), auto_upgrade_bandwidth, + /*enforce_topology_constraints=*/true, + /*enable_bluetooth_listening=*/use_ble, + /*enable_webrtc_listening=*/ + ShouldEnableWebRtc(connectivity_manager_, data_usage, power_level), + /*fast_advertisement_service_uuid=*/ + fast_advertisement_service_uuid), + std::move(connection_listener), std::move(callback)); +} + +void NearbyConnectionsManagerImpl::StopAdvertising( + ConnectionsCallback callback) { + incoming_connection_listener_ = nullptr; + + if (!nearby_connections_service_) { + std::move(callback)(ConnectionsStatus::kSuccess); + return; + } + + nearby_connections_service_->StopAdvertising(kServiceId, std::move(callback)); +} + +void NearbyConnectionsManagerImpl::StartDiscovery( + DiscoveryListener* listener, DataUsage data_usage, + ConnectionsCallback callback) { + NL_DCHECK(listener); + + if (!nearby_connections_service_) { + std::move(callback)(ConnectionsStatus::kError); + return; + } + + MediumSelection allowed_mediums = MediumSelection( + /*bluetooth=*/true, + /*ble=*/true, + /*web_rtc=*/ + ShouldEnableWebRtc(connectivity_manager_, data_usage, + PowerLevel::kHighPower), + /*wifi_lan=*/ + ShouldEnableWifiLan(connectivity_manager_), + /*wifi_hotspot=*/true); + NL_VLOG(1) << __func__ << ": " + << "data_usage=" << static_cast(data_usage) + << ", allowed_mediums=" + << MediumSelectionToString(allowed_mediums); + + discovery_listener_ = listener; + + NearbyConnectionsService::DiscoveryListener service_discovery_listener; + service_discovery_listener.endpoint_found_cb = + [&](absl::string_view endpoint_id, const DiscoveredEndpointInfo& info) { + OnEndpointFound(endpoint_id, info); + }; + service_discovery_listener.endpoint_lost_cb = + [&](absl::string_view endpoint_id) { OnEndpointLost(endpoint_id); }; + + nearby_connections_service_->StartDiscovery( + kServiceId, + DiscoveryOptions(kStrategy, std::move(allowed_mediums), + Uuid(kFastAdvertisementServiceUuid), + /*is_out_of_band_connection=*/false), + std::move(service_discovery_listener), std::move(callback)); +} + +void NearbyConnectionsManagerImpl::StopDiscovery() { + MutexLock lock(&mutex_); + discovered_endpoints_.clear(); + discovery_listener_ = nullptr; + + if (!nearby_connections_service_) { + return; + } + + nearby_connections_service_->StopDiscovery( + kServiceId, [&](ConnectionsStatus status) { + NL_VLOG(1) << __func__ + << ": Stop discovery attempted over Nearby " + "Connections with result: " + << ConnectionsStatusToString(status); + }); +} + +void NearbyConnectionsManagerImpl::Connect( + std::vector endpoint_info, absl::string_view endpoint_id, + std::optional> bluetooth_mac_address, + DataUsage data_usage, TransportType transport_type, + NearbyConnectionCallback callback) { + MutexLock lock(&mutex_); + if (!nearby_connections_service_) { + callback(nullptr, Status::kError); + return; + } + + if (bluetooth_mac_address.has_value() && bluetooth_mac_address->size() != 6) { + bluetooth_mac_address.reset(); + } + + MediumSelection allowed_mediums = MediumSelection( + /*bluetooth=*/true, + /*ble=*/ShouldEnableBleForTransfers(), + ShouldEnableWebRtc(connectivity_manager_, data_usage, + PowerLevel::kHighPower), + /*wifi_lan=*/ + ShouldEnableWifiLan(connectivity_manager_), + /*wifi_hotspot=*/transport_type == TransportType::kHighQuality); + NL_VLOG(1) << __func__ << ": " + << "data_usage=" << static_cast(data_usage) + << ", allowed_mediums=" + << MediumSelectionToString(allowed_mediums); + [[maybe_unused]] auto result = + pending_outgoing_connections_.emplace(endpoint_id, std::move(callback)); + NL_DCHECK(result.second); + + auto timeout_timer = context_->CreateTimer(); + timeout_timer->Start( + kInitiateNearbyConnectionTimeout / absl::Milliseconds(1), 0, + [&, endpoint_id]() { OnConnectionTimedOut(endpoint_id); }); + + connect_timeout_timers_.emplace(endpoint_id, std::move(timeout_timer)); + + NearbyConnectionsService::ConnectionListener connection_listener; + connection_listener.initiated_cb = + [&](absl::string_view endpoint_id, + const ConnectionInfo& connection_info) { + OnConnectionInitiated(endpoint_id, connection_info); + }; + connection_listener.accepted_cb = [&](absl::string_view endpoint_id) { + OnConnectionAccepted(endpoint_id); + }; + connection_listener.rejected_cb = [&](absl::string_view endpoint_id, + Status status) { + OnConnectionRejected(endpoint_id, status); + }; + connection_listener.disconnected_cb = [&](absl::string_view endpoint_id) { + OnDisconnected(endpoint_id); + }; + connection_listener.bandwidth_changed_cb = [&](absl::string_view endpoint_id, + Medium medium) { + OnBandwidthChanged(endpoint_id, medium); + }; + + nearby_connections_service_->RequestConnection( + kServiceId, endpoint_info, endpoint_id, + ConnectionOptions(std::move(allowed_mediums), + std::move(bluetooth_mac_address), + /*keep_alive_interval=*/std::nullopt, + /*keep_alive_timeout=*/std::nullopt), + std::move(connection_listener), + [&, endpoint_id](ConnectionsStatus status) { + MutexLock lock(&mutex_); + if (status != ConnectionsStatus::kSuccess) { + transfer_managers_.erase(endpoint_id); + } + OnConnectionRequested(endpoint_id, status); + }); + + // Setup transfer manager. + if (transport_type == TransportType::kHighQuality) { + transfer_managers_[endpoint_id] = + std::make_unique(context_, endpoint_id); + } +} + +void NearbyConnectionsManagerImpl::OnConnectionTimedOut( + absl::string_view endpoint_id) { + MutexLock lock(&mutex_); + NL_LOG(ERROR) << "Failed to connect to the remote shareTarget: Timed out."; + if (pending_outgoing_connections_.contains(endpoint_id)) { + auto it = connection_info_map_.find(endpoint_id); + if (it != connection_info_map_.end()) { + it->second.connection_layer_status = Status::kTimeout; + } + } + Disconnect(endpoint_id); +} + +void NearbyConnectionsManagerImpl::OnConnectionRequested( + absl::string_view endpoint_id, ConnectionsStatus status) { + MutexLock lock(&mutex_); + auto it = pending_outgoing_connections_.find(endpoint_id); + if (it == pending_outgoing_connections_.end()) return; + if (status != ConnectionsStatus::kSuccess) { + NL_LOG(ERROR) << "Failed to connect to the remote shareTarget: " + << ConnectionsStatusToString(status); + auto info_it = connection_info_map_.find(endpoint_id); + if (info_it != connection_info_map_.end()) { + info_it->second.connection_layer_status = status; + } + Disconnect(endpoint_id); + return; + } +} + +void NearbyConnectionsManagerImpl::Disconnect(absl::string_view endpoint_id) { + MutexLock lock(&mutex_); + if (!pending_outgoing_connections_.contains(endpoint_id) && + !connection_info_map_.contains(endpoint_id)) { + NL_LOG(WARNING) << "No connection for endpoint " << endpoint_id; + return; + } + + if (disconnecting_endpoints_.contains(endpoint_id)) { + NL_LOG(INFO) << "Another Disconnecting is running for endpoint_id " + << endpoint_id; + return; + } + + disconnecting_endpoints_.insert(std::string(endpoint_id)); + nearby_connections_service_->DisconnectFromEndpoint( + kServiceId, endpoint_id, + [&, endpoint_id = std::string(endpoint_id)](ConnectionsStatus status) { + NL_VLOG(1) << __func__ << ": Disconnecting from endpoint " + << endpoint_id + << " attempted over Nearby Connections with result: " + << ConnectionsStatusToString(status); + + context_->GetTaskRunner()->PostTask([&, endpoint_id]() { + OnDisconnected(endpoint_id); + { + MutexLock lock(&mutex_); + disconnecting_endpoints_.erase(endpoint_id); + } + }); + NL_LOG(INFO) << "Disconnected from " << endpoint_id; + }); +} + +void NearbyConnectionsManagerImpl::Send( + absl::string_view endpoint_id, std::unique_ptr payload, + std::weak_ptr listener) { + MutexLock lock(&mutex_); + if (listener.lock()) { + RegisterPayloadStatusListener(payload->id, listener); + } + + if (transfer_managers_.contains(endpoint_id) && payload->content.is_file()) { + NL_LOG(INFO) << __func__ << ": Send payload " << payload->id << " to " + << endpoint_id + << " to transfer manager. payload is file: " + << payload->content.is_file() << ", is bytes " + << payload->content.is_bytes(); + transfer_managers_.at(endpoint_id) + ->Send([&, endpoint_id = std::string(endpoint_id), + payload_copy = *payload]() { + NL_LOG(INFO) << __func__ << ": Send payload " << payload_copy.id + << " to " << endpoint_id; + auto sent_payload = std::make_unique(payload_copy); + SendWithoutDelay(endpoint_id, std::move(sent_payload)); + }); + transfer_managers_.at(endpoint_id)->StartTransfer(); + return; + } + + SendWithoutDelay(endpoint_id, std::move(payload)); +} + +void NearbyConnectionsManagerImpl::SendWithoutDelay( + absl::string_view endpoint_id, std::unique_ptr payload) { + NL_LOG(INFO) << __func__ << ": Send payload " << payload->id << " to " + << endpoint_id; + nearby_connections_service_->SendPayload( + kServiceId, {std::string(endpoint_id)}, std::move(payload), + [endpoint_id = std::string(endpoint_id)](ConnectionsStatus status) { + NL_LOG(INFO) << __func__ << ": Sending payload to endpoint " + << endpoint_id + << " attempted over Nearby Connections with result: " + << ConnectionsStatusToString(status); + }); +} + +void NearbyConnectionsManagerImpl::RegisterPayloadStatusListener( + int64_t payload_id, std::weak_ptr listener) { + MutexLock lock(&mutex_); + payload_status_listeners_.insert_or_assign(payload_id, listener); +} + +void NearbyConnectionsManagerImpl::RegisterPayloadPath( + int64_t payload_id, const std::filesystem::path& file_path, + ConnectionsCallback callback) { + NL_DCHECK(!file_path.empty()); + + // Create file is put into Nearby Connections, don't need to create file in + // Nearby Sharing. + callback(Status::kSuccess); +} + +Payload* NearbyConnectionsManagerImpl::GetIncomingPayload(int64_t payload_id) { + MutexLock lock(&mutex_); + auto it = incoming_payloads_.find(payload_id); + if (it == incoming_payloads_.end()) return nullptr; + + return &it->second; +} + +void NearbyConnectionsManagerImpl::Cancel(int64_t payload_id) { + MutexLock lock(&mutex_); + auto it = payload_status_listeners_.find(payload_id); + if (it != payload_status_listeners_.end()) { + std::weak_ptr listener = it->second; + payload_status_listeners_.erase(payload_id); + + // Note: The listener might be invalidated, for example, if it is shared + // with another payload in the same transfer. + if (auto status_listener = listener.lock()) { + status_listener->OnStatusUpdate(std::make_unique( + payload_id, PayloadStatus::kCanceled, + /*total_bytes=*/0, + /*bytes_transferred=*/0), + /*upgraded_medium=*/std::nullopt); + } + } + + nearby_connections_service_->CancelPayload( + kServiceId, payload_id, [&, payload_id](ConnectionsStatus status) { + NL_VLOG(1) << __func__ << ": Cancelling payload to id " << payload_id + << " attempted over Nearby Connections with result: " + << ConnectionsStatusToString(status); + }); + + NL_LOG(INFO) << "Cancelling payload: " << payload_id; +} + +void NearbyConnectionsManagerImpl::ClearIncomingPayloads() { + MutexLock lock(&mutex_); + std::vector payloads; + for (auto& it : incoming_payloads_) { + payloads.push_back(std::move(it.second)); + payload_status_listeners_.erase(it.first); + } + + incoming_payloads_.clear(); +} + +std::optional> +NearbyConnectionsManagerImpl::GetRawAuthenticationToken( + absl::string_view endpoint_id) { + MutexLock lock(&mutex_); + auto it = connection_info_map_.find(endpoint_id); + if (it == connection_info_map_.end()) return std::nullopt; + + return it->second.raw_authentication_token; +} + +void NearbyConnectionsManagerImpl::UpgradeBandwidth( + absl::string_view endpoint_id) { + MutexLock lock(&mutex_); + // The only bandwidth upgrade mediums at this point are WebRTC and WifiLan. + if (!NearbyFlags::GetInstance().GetBoolFlag( + config_package_nearby::nearby_sharing_feature:: + kEnableMediumWifiLan) && + !NearbyFlags::GetInstance().GetBoolFlag( + config_package_nearby::nearby_sharing_feature::kEnableMediumWebRtc)) { + return; + } + + requested_bwu_endpoint_ids_.emplace(endpoint_id); + nearby_connections_service_->InitiateBandwidthUpgrade( + kServiceId, endpoint_id, [&, endpoint_id](ConnectionsStatus status) { + NL_VLOG(1) << __func__ << ": Bandwidth upgrade attempted to endpoint " + << endpoint_id << "over Nearby Connections with result: " + << ConnectionsStatusToString(status); + }); +} + +void NearbyConnectionsManagerImpl::OnEndpointFound( + absl::string_view endpoint_id, const DiscoveredEndpointInfo& info) { + MutexLock lock(&mutex_); + if (!discovery_listener_) { + NL_LOG(INFO) << "Ignoring discovered endpoint " + << nearby::utils::HexEncode(info.endpoint_info) + << " because we're no longer " + "in discovery mode"; + return; + } + + auto result = discovered_endpoints_.insert(std::string(endpoint_id)); + if (!result.second) { + NL_LOG(INFO) << "Ignoring discovered endpoint " + << nearby::utils::HexEncode(info.endpoint_info) + << " because we've already " + "reported this endpoint"; + return; + } + + discovery_listener_->OnEndpointDiscovered(endpoint_id, info.endpoint_info); + NL_LOG(INFO) << "Discovered " << nearby::utils::HexEncode(info.endpoint_info) + << " over Nearby Connections"; +} + +void NearbyConnectionsManagerImpl::OnEndpointLost( + absl::string_view endpoint_id) { + MutexLock lock(&mutex_); + if (!discovered_endpoints_.erase(endpoint_id)) { + NL_LOG(INFO) << "Ignoring lost endpoint " << endpoint_id + << " because we haven't reported this endpoint"; + return; + } + + if (!discovery_listener_) { + NL_LOG(INFO) << "Ignoring lost endpoint " << endpoint_id + << " because we're no longer in discovery mode"; + return; + } + + discovery_listener_->OnEndpointLost(endpoint_id); + NL_LOG(INFO) << "Endpoint " << endpoint_id << " lost over Nearby Connections"; +} + +void NearbyConnectionsManagerImpl::OnConnectionInitiated( + absl::string_view endpoint_id, const ConnectionInfo& info) { + MutexLock lock(&mutex_); + [[maybe_unused]] auto result = + connection_info_map_.emplace(endpoint_id, std::move(info)); + NL_DCHECK(result.second); + + NearbyConnectionsService::PayloadListener payload_listener; + + payload_listener.payload_cb = [&](absl::string_view endpoint_id, + Payload payload) { + OnPayloadReceived(endpoint_id, payload); + }; + + payload_listener.payload_progress_cb = + [&](absl::string_view endpoint_id, const PayloadTransferUpdate& update) { + OnPayloadTransferUpdate(endpoint_id, update); + }; + + nearby_connections_service_->AcceptConnection( + kServiceId, endpoint_id, std::move(payload_listener), + [&, endpoint_id = std::string(endpoint_id)](ConnectionsStatus status) { + NL_VLOG(1) << __func__ << ": Accept connection attempted to endpoint " + << endpoint_id << " over Nearby Connections with result: " + << ConnectionsStatusToString(status); + }); +} + +void NearbyConnectionsManagerImpl::OnConnectionAccepted( + absl::string_view endpoint_id) { + MutexLock lock(&mutex_); + auto it = connection_info_map_.find(endpoint_id); + if (it == connection_info_map_.end()) return; + + if (it->second.is_incoming_connection) { + if (!incoming_connection_listener_) { + // Not in advertising mode. + Disconnect(endpoint_id); + return; + } + + auto result = connections_.emplace(std::string(endpoint_id), + std::make_unique( + device_info_, this, endpoint_id)); + NL_DCHECK(result.second); + incoming_connection_listener_->OnIncomingConnection( + endpoint_id, it->second.endpoint_info, result.first->second.get()); + } else { + auto it = pending_outgoing_connections_.find(endpoint_id); + if (it == pending_outgoing_connections_.end()) { + Disconnect(endpoint_id); + return; + } + + auto result = connections_.emplace( + endpoint_id, std::make_unique(device_info_, this, + endpoint_id)); + NL_DCHECK(result.second); + std::move(it->second)(result.first->second.get(), Status::kSuccess); + pending_outgoing_connections_.erase(it); + connect_timeout_timers_.erase(endpoint_id); + } +} + +void NearbyConnectionsManagerImpl::OnConnectionRejected( + absl::string_view endpoint_id, Status status) { + MutexLock lock(&mutex_); + connection_info_map_.erase(endpoint_id); + + auto it = pending_outgoing_connections_.find(endpoint_id); + if (it != pending_outgoing_connections_.end()) { + std::move(it->second)(nullptr, status); + pending_outgoing_connections_.erase(it); + connect_timeout_timers_.erase(endpoint_id); + } +} + +void NearbyConnectionsManagerImpl::OnDisconnected( + absl::string_view endpoint_id) { + MutexLock lock(&mutex_); + // Remove transfer manager. + if (transfer_managers_.contains(endpoint_id)) { + transfer_managers_[endpoint_id]->CancelTransfer(); + transfer_managers_.erase(endpoint_id); + } + + Status connection_layer_status = Status::kUnknown; + auto info_it = connection_info_map_.find(endpoint_id); + if (info_it != connection_info_map_.end()) { + connection_layer_status = info_it->second.connection_layer_status; + connection_info_map_.erase(info_it); + } + + auto it = pending_outgoing_connections_.find(endpoint_id); + if (it != pending_outgoing_connections_.end()) { + std::move(it->second)(nullptr, connection_layer_status); + pending_outgoing_connections_.erase(it); + connect_timeout_timers_.erase(endpoint_id); + } + + connections_.erase(endpoint_id); + + requested_bwu_endpoint_ids_.erase(endpoint_id); + current_upgraded_mediums_.erase(endpoint_id); +} + +void NearbyConnectionsManagerImpl::OnBandwidthChanged( + absl::string_view endpoint_id, Medium medium) { + MutexLock lock(&mutex_); + NL_VLOG(1) << __func__ + << ": Bandwidth changed to medium=" << static_cast(medium) + << "; endpoint_id=" << endpoint_id; + + if (transfer_managers_.contains(endpoint_id)) { + transfer_managers_[endpoint_id]->OnMediumQualityChanged(medium); + } + + current_upgraded_mediums_.insert_or_assign(endpoint_id, medium); + // TODO(crbug/1111458): Support TransferManager. +} + +void NearbyConnectionsManagerImpl::OnPayloadReceived( + absl::string_view endpoint_id, Payload& payload) { + MutexLock lock(&mutex_); + NL_LOG(INFO) << "Received payload id=" << payload.id; + [[maybe_unused]] auto result = + incoming_payloads_.emplace(payload.id, std::move(payload)); + NL_DCHECK(result.second); +} + +void NearbyConnectionsManagerImpl::OnPayloadTransferUpdate( + absl::string_view endpoint_id, const PayloadTransferUpdate& update) { + MutexLock lock(&mutex_); + NL_LOG(INFO) << "Received payload transfer update id=" << update.payload_id + << ",status=" << update.status << ",total=" << update.total_bytes + << ",bytes_transferred=" << update.bytes_transferred + << std::endl; + + // If this is a payload we've registered for, then forward its status to + // the PayloadStatusListener if it still exists. We don't need to do + // anything more with the payload. + auto listener_it = payload_status_listeners_.find(update.payload_id); + if (listener_it != payload_status_listeners_.end()) { + std::weak_ptr listener = listener_it->second; + switch (update.status) { + case PayloadStatus::kInProgress: + break; + case PayloadStatus::kSuccess: + case PayloadStatus::kCanceled: + case PayloadStatus::kFailure: + payload_status_listeners_.erase(update.payload_id); + break; + } + // Note: The listener might be invalidated, for example, if it is shared + // with another payload in the same transfer. + if (auto status_listener = listener.lock()) { + status_listener->OnStatusUpdate( + std::make_unique(update), + GetUpgradedMedium(endpoint_id)); + } + return; + } + + // If this is an incoming payload that we have not registered for, then + // we'll treat it as a control frame (e.g. IntroductionFrame) and + // forward it to the associated NearbyConnection. + auto payload_it = incoming_payloads_.find(update.payload_id); + if (payload_it == incoming_payloads_.end()) return; + + if (payload_it->second.content.type != PayloadContent::Type::kBytes) { + NL_LOG(WARNING) << "Received unknown payload of file type. Cancelling."; + nearby_connections_service_->CancelPayload(kServiceId, payload_it->first, + [](Status status) {}); + return; + } + + if (update.status != PayloadStatus::kSuccess) return; + + auto connections_it = connections_.find(endpoint_id); + if (connections_it == connections_.end()) return; + + NL_LOG(INFO) << "Writing incoming byte message to NearbyConnection."; + connections_it->second->WriteMessage( + payload_it->second.content.bytes_payload.bytes); +} + +void NearbyConnectionsManagerImpl::Reset() { + MutexLock lock(&mutex_); + nearby_connections_service_->StopAllEndpoints([](ConnectionsStatus status) { + NL_VLOG(1) << __func__ + << ": Stop all endpoints attempted over Nearby " + "Connections with result: " + << ConnectionsStatusToString(status); + }); + + discovered_endpoints_.clear(); + payload_status_listeners_.clear(); + ClearIncomingPayloads(); + connections_.clear(); + connection_info_map_.clear(); + discovery_listener_ = nullptr; + incoming_connection_listener_ = nullptr; + connect_timeout_timers_.clear(); + requested_bwu_endpoint_ids_.clear(); + current_upgraded_mediums_.clear(); + + for (auto& transfer_manager : transfer_managers_) { + transfer_manager.second->CancelTransfer(); + } + transfer_managers_.clear(); + + for (auto& entry : pending_outgoing_connections_) + std::move(entry.second)(/*connection=*/nullptr, Status::kReset); + + pending_outgoing_connections_.clear(); +} + +std::optional NearbyConnectionsManagerImpl::GetUpgradedMedium( + absl::string_view endpoint_id) const { + MutexLock lock(&mutex_); + const auto it = current_upgraded_mediums_.find(endpoint_id); + if (it == current_upgraded_mediums_.end()) return std::nullopt; + + return it->second; +} + +void NearbyConnectionsManagerImpl::SetCustomSavePath( + absl::string_view custom_save_path) { + MutexLock lock(&mutex_); + nearby_connections_service_->SetCustomSavePath( + custom_save_path, [&](Status status) { + NL_VLOG(1) << __func__ + << ": SetCustomSavePath attempted over Nearby " + "Connections with result: " + << static_cast(status); + }); +} + +std::string NearbyConnectionsManagerImpl::Dump() const { + return nearby_connections_service_->Dump(); +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/nearby_connections_manager_impl.h b/sharing/nearby_connections_manager_impl.h new file mode 100644 index 00000000..cd4ce4e1 --- /dev/null +++ b/sharing/nearby_connections_manager_impl.h @@ -0,0 +1,184 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_NEARBY_CONNECTIONS_MANAGER_IMPL_H_ +#define THIRD_PARTY_NEARBY_SHARING_NEARBY_CONNECTIONS_MANAGER_IMPL_H_ + +#include + +#include // NOLINT(build/c++17) +#include +#include +#include +#include + +#include "absl/base/thread_annotations.h" +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" +#include "absl/strings/string_view.h" +#include "internal/platform/device_info.h" +#include "internal/platform/mutex.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/internal/public/connectivity_manager.h" +#include "sharing/internal/public/context.h" +#include "sharing/nearby_connection_impl.h" +#include "sharing/nearby_connections_manager.h" +#include "sharing/nearby_connections_service.h" +#include "sharing/nearby_connections_types.h" +#include "sharing/transfer_manager.h" + +namespace nearby { +namespace sharing { + +// Concrete NearbyConnectionsManager implementation. +class NearbyConnectionsManagerImpl : public NearbyConnectionsManager { + public: + explicit NearbyConnectionsManagerImpl( + Context* context, + nearby::ConnectivityManager& connectivity_manager, + nearby::DeviceInfo& device_info, + std::unique_ptr nearby_connections_service); + ~NearbyConnectionsManagerImpl() override; + NearbyConnectionsManagerImpl(const NearbyConnectionsManagerImpl&) = delete; + NearbyConnectionsManagerImpl& operator=(const NearbyConnectionsManagerImpl&) = + delete; + + // NearbyConnectionsManager: + void Shutdown() override; + void StartAdvertising(std::vector endpoint_info, + IncomingConnectionListener* listener, + PowerLevel power_level, proto::DataUsage data_usage, + ConnectionsCallback callback) override; + void StopAdvertising(ConnectionsCallback callback) override; + void StartDiscovery(DiscoveryListener* listener, proto::DataUsage data_usage, + ConnectionsCallback callback) override; + void StopDiscovery() override; + void Connect(std::vector endpoint_info, + absl::string_view endpoint_id, + std::optional> bluetooth_mac_address, + proto::DataUsage data_usage, TransportType transport_type, + NearbyConnectionCallback callback) override; + void Disconnect(absl::string_view endpoint_id) override; + void Send(absl::string_view endpoint_id, std::unique_ptr payload, + std::weak_ptr listener) override; + void RegisterPayloadStatusListener( + int64_t payload_id, + std::weak_ptr listener) override; + void RegisterPayloadPath(int64_t payload_id, + const std::filesystem::path& file_path, + ConnectionsCallback callback) override; + Payload* GetIncomingPayload(int64_t payload_id) override; + void Cancel(int64_t payload_id) override; + void ClearIncomingPayloads() override; + std::optional> GetRawAuthenticationToken( + absl::string_view endpoint_id) override; + void UpgradeBandwidth(absl::string_view endpoint_id) override; + void SetCustomSavePath(absl::string_view custom_save_path) override; + std::string Dump() const override; + + NearbyConnectionsService* GetNearbyConnectionsService() const { + return nearby_connections_service_.get(); + } + + private: + // EndpointDiscoveryListener: + void OnEndpointFound(absl::string_view endpoint_id, + const DiscoveredEndpointInfo& info); + void OnEndpointLost(absl::string_view endpoint_id); + + // ConnectionLifecycleListener: + void OnConnectionInitiated(absl::string_view endpoint_id, + const ConnectionInfo& info); + void OnConnectionAccepted(absl::string_view endpoint_id); + void OnConnectionRejected(absl::string_view endpoint_id, Status status); + void OnDisconnected(absl::string_view endpoint_id); + void OnBandwidthChanged(absl::string_view endpoint_id, Medium medium); + + // PayloadListener: + void OnPayloadReceived(absl::string_view endpoint_id, Payload& payload); + void OnPayloadTransferUpdate(absl::string_view endpoint_id, + const PayloadTransferUpdate& update); + void OnConnectionTimedOut(absl::string_view endpoint_id); + void OnConnectionRequested(absl::string_view endpoint_id, + ConnectionsStatus status); + + void Reset(); + + std::optional GetUpgradedMedium(absl::string_view endpoint_id) const; + + void SendWithoutDelay(absl::string_view endpoint_id, + std::unique_ptr payload); + + Context* const context_; + nearby::ConnectivityManager& connectivity_manager_; + nearby::DeviceInfo& device_info_; + + // Nearby Connections Manager is called from different threads and may have + // multiple calls to the class from one thread. To avoid deadlock and access + // violation, use a recursive mutex to protect class members. + mutable RecursiveMutex mutex_; + + std::unique_ptr nearby_connections_service_ = + nullptr; + IncomingConnectionListener* incoming_connection_listener_ = nullptr; + DiscoveryListener* discovery_listener_ = nullptr; + absl::flat_hash_set discovered_endpoints_ + ABSL_GUARDED_BY(mutex_); + + // A map of endpoint_id to NearbyConnectionCallback. + absl::flat_hash_map + pending_outgoing_connections_ ABSL_GUARDED_BY(mutex_); + + // A map of endpoint_id to ConnectionInfoPtr. + absl::flat_hash_map connection_info_map_ + ABSL_GUARDED_BY(mutex_); + + // A map of endpoint_id to NearbyConnection. + absl::flat_hash_map> + connections_ ABSL_GUARDED_BY(mutex_); + + // A map of endpoint_id to timers that timeout a connection request. + absl::flat_hash_map> + connect_timeout_timers_ ABSL_GUARDED_BY(mutex_); + + // A map of payload_id to PayloadStatusListener weak pointer. + absl::flat_hash_map> + payload_status_listeners_ ABSL_GUARDED_BY(mutex_); + + // A map of payload_id to PayloadPtr. + absl::flat_hash_map incoming_payloads_ + ABSL_GUARDED_BY(mutex_); + + // For metrics. A set of endpoint_ids for which we have requested a + // bandwidth upgrade. + absl::flat_hash_set requested_bwu_endpoint_ids_ + ABSL_GUARDED_BY(mutex_); + + // For metrics. A map of endpoint_id to the current upgraded medium. + absl::flat_hash_map current_upgraded_mediums_ + ABSL_GUARDED_BY(mutex_); + + // A map of endpoint_id to transfer manager. + absl::flat_hash_map> + transfer_managers_ ABSL_GUARDED_BY(mutex_); + + // Avoid calling to disconnect on an endpoint multiple times. + absl::flat_hash_set disconnecting_endpoints_ + ABSL_GUARDED_BY(mutex_); +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_NEARBY_CONNECTIONS_MANAGER_IMPL_H_ diff --git a/sharing/nearby_connections_manager_impl_test.cc b/sharing/nearby_connections_manager_impl_test.cc new file mode 100644 index 00000000..614e9559 --- /dev/null +++ b/sharing/nearby_connections_manager_impl_test.cc @@ -0,0 +1,1735 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/nearby_connections_manager_impl.h" + +#include + +#include +#include // NOLINT(build/c++17) +#include +#include +#include +#include +#include +#include +#include + +#include "gmock/gmock.h" +#include "protobuf-matchers/protocol-buffer-matchers.h" +#include "gtest/gtest.h" +#include "absl/strings/string_view.h" +#include "absl/synchronization/notification.h" +#include "absl/time/time.h" +#include "absl/types/optional.h" +#include "absl/types/span.h" +#include "internal/flags/nearby_flags.h" +#include "internal/test/fake_clock.h" +#include "internal/test/fake_device_info.h" +#include "internal/test/fake_task_runner.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/constants.h" +#include "sharing/fake_nearby_connections_service.h" +#include "sharing/flags/nearby_sharing_feature_flags.h" +#include "sharing/internal/public/connectivity_manager.h" +#include "sharing/internal/test/fake_connectivity_manager.h" +#include "sharing/internal/test/fake_context.h" +#include "sharing/nearby_connection.h" +#include "sharing/nearby_connections_manager.h" +#include "sharing/nearby_connections_service.h" +#include "sharing/nearby_connections_types.h" +#include "sharing/proto/enums.pb.h" +#include "sharing/transfer_manager.h" + +namespace nearby { +namespace sharing { +namespace { + +using ::nearby::sharing::proto::DataUsage; +using ::testing::ElementsAre; +using ::testing::FieldsAre; + +constexpr char kServiceId[] = "NearbySharing"; +constexpr Strategy kStrategy = Strategy::kP2pPointToPoint; +constexpr char kEndpointId[] = "endpoint_id"; +constexpr char kRemoteEndpointId[] = "remote_endpoint_id"; +constexpr char kAdvertisingServiceUuid[] = + "0000fef3-0000-1000-8000-00805f9b34fb"; + +// The byte array is just used to set the endpoint information. In the test +// codes, no any special meanings. +constexpr char kEndpointInfo[] = {0x0d, 0x07, 0x07, 0x07, 0x07}; +constexpr char kRemoteEndpointInfo[] = {0x0d, 0x07, 0x06, 0x08, 0x09}; +constexpr char kAuthenticationToken[] = "authentication_token"; +constexpr char kRawAuthenticationToken[] = {0x00, 0x05, 0x04, 0x03, 0x02}; +constexpr char kBytePayload[] = {0x08, 0x09, 0x06, 0x04, 0x0f}; +constexpr char kBytePayload2[] = {0x0a, 0x0b, 0x0c, 0x0d, 0x0e}; +constexpr int64_t kPayloadId = 689777; +constexpr int64_t kPayloadId2 = 777689; +constexpr int64_t kPayloadId3 = 986777; +constexpr uint64_t kTotalSize = 5201314; +constexpr uint64_t kBytesTransferred = 721831; +constexpr uint8_t kPayload[] = {0x0f, 0x0a, 0x0c, 0x0e}; +constexpr uint8_t kBluetoothMacAddress[] = {0x00, 0x00, 0xe6, 0x88, 0x64, 0x13}; +constexpr char kInvalidBluetoothMacAddress[] = {0x07, 0x07, 0x07}; +constexpr absl::Duration kSynchronizationTimeOut = absl::Milliseconds(200); + +void InitializeTemporaryFile(std::filesystem::path& file) { + std::FILE* output_fp = std::fopen(file.string().c_str(), "wb+"); + ASSERT_NE(output_fp, nullptr); + EXPECT_EQ(std::fwrite(kPayload, 1, sizeof(kPayload), output_fp), + sizeof(kPayload)); + std::fclose(output_fp); +} + +} // namespace + +class MockDiscoveryListener + : public NearbyConnectionsManager::DiscoveryListener { + public: + MOCK_METHOD(void, OnEndpointDiscovered, + (absl::string_view endpoint_id, + absl::Span endpoint_info), + (override)); + MOCK_METHOD(void, OnEndpointLost, (absl::string_view endpoint_id), + (override)); +}; + +class MockIncomingConnectionListener + : public NearbyConnectionsManager::IncomingConnectionListener { + public: + MOCK_METHOD(void, OnIncomingConnection, + (absl::string_view endpoint_id, + absl::Span endpoint_info, + NearbyConnection* connection), + (override)); +}; + +class MockPayloadStatusListener + : public NearbyConnectionsManager::PayloadStatusListener { + public: + MOCK_METHOD(void, OnStatusUpdate, + (std::unique_ptr update, + std::optional upgraded_medium), + (override)); +}; + +class NearbyConnectionsManagerImplTest : public testing::Test { + public: + void SetUp() override { + NearbyFlags::GetInstance().OverrideBoolFlagValue( + config_package_nearby::nearby_sharing_feature::kEnableMediumWebRtc, + false); + NearbyFlags::GetInstance().OverrideBoolFlagValue( + config_package_nearby::nearby_sharing_feature::kEnableMediumWifiLan, + true); + std::unique_ptr nearby_connections_service = + std::make_unique>(); + SetConnectionType(ConnectivityManager::ConnectionType::kWifi); + nearby_connections_ = + dynamic_cast*>( + nearby_connections_service.get()); + + nearby_connections_manager_ = + std::make_unique( + &fake_context_, fake_connectivity_manager_, fake_device_info_, + std::move(nearby_connections_service)); + } + + void TearDown() override { + NearbyFlags::GetInstance().OverrideBoolFlagValue( + config_package_nearby::nearby_sharing_feature::kEnableMediumWebRtc, + false); + NearbyFlags::GetInstance().OverrideBoolFlagValue( + config_package_nearby::nearby_sharing_feature::kEnableMediumWifiLan, + true); + + FakeTaskRunner::WaitForRunningTasksWithTimeout(absl::Seconds(1)); + } + + void SetConnectionType(ConnectivityManager::ConnectionType connection_type) { + fake_connectivity_manager_.SetConnectionType(connection_type); + } + + void Fastforward(absl::Duration duration) { + fake_context_.fake_clock()->FastForward(duration); + } + + void Sync() { + EXPECT_TRUE( + FakeTaskRunner::WaitForRunningTasksWithTimeout(absl::Seconds(1))); + } + + protected: + void StartDiscovery( + NearbyConnectionsService::DiscoveryListener& listener_remote, + testing::NiceMock& discovery_listener) { + StartDiscovery(listener_remote, default_data_usage_, discovery_listener); + } + + void StartDiscovery( + NearbyConnectionsService::DiscoveryListener& listener_remote, + DataUsage data_usage, + testing::NiceMock& discovery_listener) { + EXPECT_CALL(*nearby_connections_, StartDiscovery) + .WillOnce([&listener_remote, this]( + absl::string_view service_id, DiscoveryOptions options, + NearbyConnectionsService::DiscoveryListener listener, + std::function callback) { + EXPECT_EQ(service_id, kServiceId); + EXPECT_EQ(options.strategy, kStrategy); + EXPECT_TRUE(options.allowed_mediums.bluetooth); + EXPECT_TRUE(options.allowed_mediums.ble); + EXPECT_EQ(options.allowed_mediums.web_rtc, should_use_web_rtc_); + EXPECT_EQ(options.allowed_mediums.wifi_lan, should_use_wifilan_); + EXPECT_EQ((*options.fast_advertisement_service_uuid).uuid, + kAdvertisingServiceUuid); + + listener_remote = std::move(listener); + std::move(callback)(Status::kSuccess); + }); + + absl::Notification notification; + NearbyConnectionsManager::ConnectionsCallback callback = + [¬ification](Status status) { + EXPECT_EQ(status, Status::kSuccess); + notification.Notify(); + }; + + nearby_connections_manager_->StartDiscovery(&discovery_listener, data_usage, + std::move(callback)); + + EXPECT_TRUE( + notification.WaitForNotificationWithTimeout(kSynchronizationTimeOut)); + } + + void StartAdvertising( + NearbyConnectionsService::ConnectionListener& listener_remote, + testing::NiceMock& + incoming_connection_listener) { + const std::vector local_endpoint_info(std::begin(kEndpointInfo), + std::end(kEndpointInfo)); + EXPECT_CALL(*nearby_connections_, StartAdvertising) + .WillOnce([&](absl::string_view service_id, + const std::vector& endpoint_info, + AdvertisingOptions options, + NearbyConnectionsService::ConnectionListener listener, + std::function callback) { + EXPECT_EQ(service_id, kServiceId); + EXPECT_EQ(endpoint_info, local_endpoint_info); + EXPECT_EQ(options.strategy, kStrategy); + EXPECT_TRUE(options.enforce_topology_constraints); + + listener_remote = std::move(listener); + std::move(callback)(Status::kSuccess); + }); + + absl::Notification notification; + NearbyConnectionsManager::ConnectionsCallback callback = + [&](Status status) { + EXPECT_EQ(status, Status::kSuccess); + notification.Notify(); + }; + nearby_connections_manager_->StartAdvertising( + local_endpoint_info, &incoming_connection_listener, + PowerLevel::kHighPower, DataUsage::ONLINE_DATA_USAGE, + std::move(callback)); + EXPECT_TRUE( + notification.WaitForNotificationWithTimeout(kSynchronizationTimeOut)); + } + + enum class ConnectionResponse { kAccepted, kRejected, kDisconnected }; + + NearbyConnection* Connect( + NearbyConnectionsService::ConnectionListener& connection_listener_remote, + NearbyConnectionsService::PayloadListener& payload_listener_remote, + ConnectionResponse connection_response) { + const std::vector local_endpoint_info(std::begin(kEndpointInfo), + std::end(kEndpointInfo)); + const std::vector remote_endpoint_info( + std::begin(kRemoteEndpointInfo), std::end(kRemoteEndpointInfo)); + const std::vector raw_authentication_token( + std::begin(kRawAuthenticationToken), std::end(kRawAuthenticationToken)); + + absl::Notification request_connection_notification; + EXPECT_CALL(*nearby_connections_, RequestConnection) + .WillOnce([&](absl::string_view service_id, + const std::vector& endpoint_info, + absl::string_view endpoint_id, + ConnectionOptions connection_options, + NearbyConnectionsService::ConnectionListener listener, + std::function callback) { + EXPECT_EQ(kServiceId, service_id); + EXPECT_EQ(local_endpoint_info, endpoint_info); + EXPECT_EQ(kRemoteEndpointId, endpoint_id); + + connection_listener_remote = std::move(listener); + std::move(callback)(Status::kSuccess); + request_connection_notification.Notify(); + }); + + absl::Notification notification; + NearbyConnection* nearby_connection; + nearby_connections_manager_->Connect( + local_endpoint_info, kRemoteEndpointId, + /*bluetooth_mac_address=*/std::nullopt, DataUsage::OFFLINE_DATA_USAGE, + TransportType::kHighQuality, + [&](NearbyConnection* connection, Status status) { + nearby_connection = connection; + }); + + EXPECT_TRUE(request_connection_notification.WaitForNotificationWithTimeout( + kSynchronizationTimeOut)); + + EXPECT_CALL(*nearby_connections_, AcceptConnection) + .WillOnce([&](absl::string_view service_id, + absl::string_view endpoint_id, + NearbyConnectionsService::PayloadListener listener, + std::function callback) { + EXPECT_EQ(kServiceId, service_id); + EXPECT_EQ(kRemoteEndpointId, endpoint_id); + + payload_listener_remote = std::move(listener); + std::move(callback)(Status::kSuccess); + notification.Notify(); + }); + + ConnectionInfo connection_info; + connection_info.authentication_token = kAuthenticationToken; + connection_info.raw_authentication_token = raw_authentication_token; + connection_info.endpoint_info = remote_endpoint_info; + connection_info.is_incoming_connection = false; + + connection_listener_remote.initiated_cb(kRemoteEndpointId, connection_info); + + switch (connection_response) { + case ConnectionResponse::kAccepted: + connection_listener_remote.accepted_cb(kRemoteEndpointId); + break; + case ConnectionResponse::kRejected: + connection_listener_remote.rejected_cb(kRemoteEndpointId, + Status::kConnectionRejected); + break; + case ConnectionResponse::kDisconnected: + connection_listener_remote.disconnected_cb(kRemoteEndpointId); + break; + } + EXPECT_TRUE( + notification.WaitForNotificationWithTimeout(kSynchronizationTimeOut)); + + return nearby_connection; + } + + NearbyConnection* OnIncomingConnection( + NearbyConnectionsService::ConnectionListener& connection_listener_remote, + testing::NiceMock& + incoming_connection_listener, + NearbyConnectionsService::PayloadListener& payload_listener_remote) { + absl::Notification accept_notification; + + EXPECT_CALL(*nearby_connections_, AcceptConnection) + .WillOnce([&](absl::string_view service_id, + absl::string_view endpoint_id, + NearbyConnectionsService::PayloadListener listener, + std::function callback) { + EXPECT_EQ(service_id, kServiceId); + EXPECT_EQ(endpoint_id, kRemoteEndpointId); + payload_listener_remote = std::move(listener); + std::move(callback)(Status::kSuccess); + accept_notification.Notify(); + }); + + const std::vector remote_endpoint_info( + std::begin(kRemoteEndpointInfo), std::end(kRemoteEndpointInfo)); + const std::vector raw_authentication_token( + std::begin(kRawAuthenticationToken), std::end(kRawAuthenticationToken)); + + ConnectionInfo connection_info; + connection_info.authentication_token = kAuthenticationToken; + connection_info.raw_authentication_token = raw_authentication_token; + connection_info.endpoint_info = remote_endpoint_info; + connection_info.is_incoming_connection = true; + + connection_listener_remote.initiated_cb(kRemoteEndpointId, connection_info); + EXPECT_TRUE(accept_notification.WaitForNotificationWithTimeout( + kSynchronizationTimeOut)); + + NearbyConnection* nearby_connection; + absl::Notification incoming_connection_notification; + EXPECT_CALL(incoming_connection_listener, + OnIncomingConnection(testing::_, testing::_, testing::_)) + .WillOnce([&](absl::string_view endpoint_id, + absl::Span endpoint_info, + NearbyConnection* connection) { + nearby_connection = connection; + incoming_connection_notification.Notify(); + }); + + connection_listener_remote.accepted_cb(kRemoteEndpointId); + EXPECT_TRUE(incoming_connection_notification.WaitForNotificationWithTimeout( + kSynchronizationTimeOut)); + + EXPECT_EQ(nearby_connections_manager_->GetRawAuthenticationToken( + kRemoteEndpointId), + raw_authentication_token); + + return nearby_connection; + } + + void SendPayload(int64_t payload_id, + std::shared_ptr> + payload_listener) { + const std::vector expected_payload(std::begin(kPayload), + std::end(kPayload)); + + std::filesystem::path file(std::filesystem::temp_directory_path() / + "file.jpg"); + InitializeTemporaryFile(file); + + absl::Notification notification; + EXPECT_CALL(*nearby_connections_, SendPayload) + .WillOnce([&](absl::string_view service_id, + absl::Span endpoint_ids, + std::unique_ptr payload, + std::function callback) { + EXPECT_EQ(service_id, kServiceId); + EXPECT_THAT(endpoint_ids, ElementsAre(kRemoteEndpointId)); + ASSERT_NE(payload, nullptr); + EXPECT_EQ(payload_id, payload->id); + + FilePayload file_payload = std::move(payload->content.file_payload); + std::vector payload_bytes(file_payload.size); + std::FILE* payload_fp = + std::fopen(file_payload.file.path.string().c_str(), "rb"); + ASSERT_NE(payload_fp, nullptr); + EXPECT_EQ(std::fread(payload_bytes.data(), 1, file_payload.size, + payload_fp), + file_payload.size); + EXPECT_EQ(expected_payload, payload_bytes); + std::fclose(payload_fp); + + std::move(callback)(Status::kSuccess); + notification.Notify(); + }); + + // Manually setup payload id, because the tested id is not generated from + // file name. + auto payload = std::make_unique(InputFile(file)); + payload->id = payload_id; + + nearby_connections_manager_->Send(kRemoteEndpointId, std::move(payload), + payload_listener->GetWeakPtr()); + // Move forward timer to trigger payload sending. + Fastforward(TransferManager::kMediumUpgradeTimeout); + + EXPECT_TRUE( + notification.WaitForNotificationWithTimeout(kSynchronizationTimeOut)); + } + + nearby::FakeContext fake_context_; + nearby::FakeDeviceInfo fake_device_info_; + nearby::FakeConnectivityManager fake_connectivity_manager_; + bool should_use_web_rtc_ = false; + bool should_use_wifilan_ = true; + DataUsage default_data_usage_ = DataUsage::WIFI_ONLY_DATA_USAGE; + + testing::NiceMock* nearby_connections_; + + std::unique_ptr nearby_connections_manager_; +}; + +TEST_F(NearbyConnectionsManagerImplTest, DiscoveryFlow) { + const std::vector endpoint_info(std::begin(kEndpointInfo), + std::end(kEndpointInfo)); + + // StartDiscovery will succeed. + NearbyConnectionsService::DiscoveryListener listener_remote; + testing::NiceMock discovery_listener; + StartDiscovery(listener_remote, discovery_listener); + + // Invoking OnEndpointFound over remote will invoke OnEndpointDiscovered. + absl::Notification discovered_notification; + EXPECT_CALL(discovery_listener, + OnEndpointDiscovered(testing::Eq(kEndpointId), + testing::Eq(endpoint_info))) + .WillOnce( + [&discovered_notification]() { discovered_notification.Notify(); }); + listener_remote.endpoint_found_cb( + kEndpointId, DiscoveredEndpointInfo(endpoint_info, kServiceId)); + EXPECT_TRUE(discovered_notification.WaitForNotificationWithTimeout( + kSynchronizationTimeOut)); + + // Invoking OnEndpointFound over remote on same endpointId will do nothing. + EXPECT_CALL(discovery_listener, OnEndpointDiscovered(testing::_, testing::_)) + .Times(0); + listener_remote.endpoint_found_cb( + kEndpointId, DiscoveredEndpointInfo(endpoint_info, kServiceId)); + + // Invoking OnEndpointLost over remote will invoke OnEndpointLost. + absl::Notification lost_notification; + EXPECT_CALL(discovery_listener, OnEndpointLost(testing::Eq(kEndpointId))) + .WillOnce([&lost_notification]() { lost_notification.Notify(); }); + listener_remote.endpoint_lost_cb(kEndpointId); + EXPECT_TRUE(lost_notification.WaitForNotificationWithTimeout( + kSynchronizationTimeOut)); + + // Invoking OnEndpointLost over remote on same endpointId will do nothing. + EXPECT_CALL(discovery_listener, OnEndpointLost(testing::_)).Times(0); + listener_remote.endpoint_lost_cb(kEndpointId); + + // After OnEndpointLost the same endpointId can be discovered again. + absl::Notification discovered_notification2; + EXPECT_CALL(discovery_listener, + OnEndpointDiscovered(testing::Eq(kEndpointId), + testing::Eq(endpoint_info))) + .WillOnce( + [&discovered_notification2]() { discovered_notification2.Notify(); }); + listener_remote.endpoint_found_cb( + kEndpointId, DiscoveredEndpointInfo(endpoint_info, kServiceId)); + EXPECT_TRUE(discovered_notification2.WaitForNotificationWithTimeout( + kSynchronizationTimeOut)); + + // Stop discovery will call through nearby sharing service. + absl::Notification stop_discovery_notification; + EXPECT_CALL(*nearby_connections_, StopDiscovery) + .WillOnce([&stop_discovery_notification]( + absl::string_view service_id, + std::function callback) { + EXPECT_EQ(service_id, kServiceId); + std::move(callback)(Status::kSuccess); + stop_discovery_notification.Notify(); + }); + nearby_connections_manager_->StopDiscovery(); + EXPECT_TRUE(stop_discovery_notification.WaitForNotificationWithTimeout( + kSynchronizationTimeOut)); + + // StartDiscovery again will succeed. + StartDiscovery(listener_remote, discovery_listener); + + // Same endpointId can be discovered again. + absl::Notification discovered_notification_3; + EXPECT_CALL(discovery_listener, + OnEndpointDiscovered(testing::Eq(kEndpointId), + testing::Eq(endpoint_info))) + .WillOnce([&discovered_notification_3]() { + discovered_notification_3.Notify(); + }); + listener_remote.endpoint_found_cb( + kEndpointId, DiscoveredEndpointInfo(endpoint_info, kServiceId)); + EXPECT_TRUE(discovered_notification_3.WaitForNotificationWithTimeout( + kSynchronizationTimeOut)); +} + +/******************************************************************************/ +// Begin: NearbyConnectionsManagerImplTestConnectionMediums +/******************************************************************************/ +using ConnectionMediumsTestParam = + std::tuple; +class NearbyConnectionsManagerImplTestConnectionMediums + : public NearbyConnectionsManagerImplTest, + public testing::WithParamInterface {}; + +TEST_P(NearbyConnectionsManagerImplTestConnectionMediums, + RequestConnection_MediumSelection) { + const ConnectionMediumsTestParam& param = GetParam(); + DataUsage data_usage = std::get<0>(param); + ConnectivityManager::ConnectionType connection_type = std::get<1>(param); + bool is_webrtc_enabled = std::get<2>(GetParam()); + bool is_wifilan_enabled = std::get<3>(GetParam()); + + if (is_webrtc_enabled) { + NearbyFlags::GetInstance().OverrideBoolFlagValue( + config_package_nearby::nearby_sharing_feature::kEnableMediumWebRtc, + true); + } else { + NearbyFlags::GetInstance().OverrideBoolFlagValue( + config_package_nearby::nearby_sharing_feature::kEnableMediumWebRtc, + false); + } + if (is_wifilan_enabled) { + NearbyFlags::GetInstance().OverrideBoolFlagValue( + config_package_nearby::nearby_sharing_feature::kEnableMediumWifiLan, + true); + } else { + NearbyFlags::GetInstance().OverrideBoolFlagValue( + config_package_nearby::nearby_sharing_feature::kEnableMediumWifiLan, + false); + } + + SetConnectionType(connection_type); + bool should_use_internet = + data_usage != DataUsage::OFFLINE_DATA_USAGE && + connection_type != ConnectivityManager::ConnectionType::kNone && + !(data_usage == DataUsage::WIFI_ONLY_DATA_USAGE && + connection_type != ConnectivityManager::ConnectionType::kWifi); + bool is_connection_wifi_or_ethernet = + connection_type == ConnectivityManager::ConnectionType::kWifi || + connection_type == ConnectivityManager::ConnectionType::kEthernet; + should_use_web_rtc_ = is_webrtc_enabled && should_use_internet; + should_use_wifilan_ = is_wifilan_enabled && is_connection_wifi_or_ethernet; + + MediumSelection expected_mediums(/*bluetooth=*/true, + /*ble=*/false, + /*web_rtc=*/should_use_web_rtc_, + /*wifi_lan=*/should_use_wifilan_, + /*wifi_hotspot*/ true); + + // StartDiscovery will succeed. + NearbyConnectionsService::DiscoveryListener discovery_listener_remote; + testing::NiceMock discovery_listener; + StartDiscovery(discovery_listener_remote, data_usage, discovery_listener); + + absl::Notification notification; + const std::vector local_endpoint_info(std::begin(kEndpointInfo), + std::end(kEndpointInfo)); + EXPECT_CALL(*nearby_connections_, RequestConnection) + .WillOnce([&](absl::string_view service_id, + const std::vector& endpoint_info, + absl::string_view endpoint_id, ConnectionOptions options, + NearbyConnectionsService::ConnectionListener listener, + std::function callback) { + EXPECT_EQ(service_id, kServiceId); + EXPECT_EQ(endpoint_info, local_endpoint_info); + EXPECT_EQ(endpoint_id, kRemoteEndpointId); + EXPECT_EQ(options.allowed_mediums.ble, expected_mediums.ble); + EXPECT_EQ(options.allowed_mediums.bluetooth, + expected_mediums.bluetooth); + EXPECT_EQ(options.allowed_mediums.web_rtc, expected_mediums.web_rtc); + EXPECT_EQ(options.allowed_mediums.wifi_lan, expected_mediums.wifi_lan); + std::move(callback)(Status::kSuccess); + notification.Notify(); + }); + + NearbyConnectionsManager::NearbyConnectionCallback connections_callback; + + nearby_connections_manager_->Connect(local_endpoint_info, kRemoteEndpointId, + /*bluetooth_mac_address=*/std::nullopt, + data_usage, TransportType::kHighQuality, + connections_callback); + + EXPECT_TRUE( + notification.WaitForNotificationWithTimeout(kSynchronizationTimeOut)); +} + +INSTANTIATE_TEST_SUITE_P( + NearbyConnectionsManagerImplTestConnectionMediums, + NearbyConnectionsManagerImplTestConnectionMediums, + testing::Combine(testing::Values(DataUsage::WIFI_ONLY_DATA_USAGE, + DataUsage::OFFLINE_DATA_USAGE, + DataUsage::ONLINE_DATA_USAGE), + testing::Values(ConnectivityManager::ConnectionType::kNone, + ConnectivityManager::ConnectionType::kWifi, + ConnectivityManager::ConnectionType::k3G), + testing::Bool(), testing::Bool())); +/******************************************************************************/ +// End: NearbyConnectionsManagerImplTestConnectionMediums +/******************************************************************************/ + +/******************************************************************************/ +// Begin: NearbyConnectionsManagerImplTestConnectionBluetoothMacAddress +/******************************************************************************/ +typedef struct { + std::optional> bluetooth_mac_address; + std::optional> expected_bluetooth_mac_address; +} ConnectionBluetoothMacAddressTestData; + +const std::vector& +GetConnectionBluetoothMacAddressTestData() { + static std::vector* data = + new std::vector{ + {std::make_optional( + std::vector(std::begin(kBluetoothMacAddress), + std::end(kBluetoothMacAddress))), + std::make_optional( + std::vector(std::begin(kBluetoothMacAddress), + std::end(kBluetoothMacAddress)))}, + {std::make_optional( + std::vector(std::begin(kInvalidBluetoothMacAddress), + std::end(kInvalidBluetoothMacAddress))), + std::nullopt}, + {std::nullopt, std::nullopt}}; + return *data; +} + +class NearbyConnectionsManagerImplTestConnectionBluetoothMacAddress + : public NearbyConnectionsManagerImplTest, + public testing::WithParamInterface< + ConnectionBluetoothMacAddressTestData> {}; + +TEST_P(NearbyConnectionsManagerImplTestConnectionBluetoothMacAddress, + RequestConnection_BluetoothMacAddress) { + // StartDiscovery will succeed. + NearbyConnectionsService::DiscoveryListener discovery_listener_remote; + testing::NiceMock discovery_listener; + StartDiscovery(discovery_listener_remote, discovery_listener); + + absl::Notification notification; + const std::vector local_endpoint_info(std::begin(kEndpointInfo), + std::end(kEndpointInfo)); + EXPECT_CALL(*nearby_connections_, RequestConnection) + .WillOnce([&](absl::string_view service_id, + const std::vector& endpoint_info, + absl::string_view endpoint_id, ConnectionOptions options, + NearbyConnectionsService::ConnectionListener listener, + std::function callback) { + EXPECT_EQ(service_id, kServiceId); + EXPECT_EQ(endpoint_info, local_endpoint_info); + EXPECT_EQ(endpoint_id, kRemoteEndpointId); + EXPECT_EQ(GetParam().expected_bluetooth_mac_address, + options.remote_bluetooth_mac_address); + std::move(callback)(Status::kSuccess); + notification.Notify(); + }); + + NearbyConnectionsManager::NearbyConnectionCallback connections_callback; + nearby_connections_manager_->Connect( + local_endpoint_info, kRemoteEndpointId, GetParam().bluetooth_mac_address, + DataUsage::OFFLINE_DATA_USAGE, TransportType::kHighQuality, + connections_callback); + + EXPECT_TRUE( + notification.WaitForNotificationWithTimeout(kSynchronizationTimeOut)); +} + +INSTANTIATE_TEST_SUITE_P( + NearbyConnectionsManagerImplTestConnectionBluetoothMacAddress, + NearbyConnectionsManagerImplTestConnectionBluetoothMacAddress, + testing::ValuesIn(GetConnectionBluetoothMacAddressTestData())); +/******************************************************************************/ +// End: NearbyConnectionsManagerImplTestConnectionBluetoothMacAddress +/******************************************************************************/ + +TEST_F(NearbyConnectionsManagerImplTest, ConnectRejected) { + // StartDiscovery will succeed. + NearbyConnectionsService::DiscoveryListener discovery_listener_remote; + testing::NiceMock discovery_listener; + StartDiscovery(discovery_listener_remote, discovery_listener); + + // RequestConnection will succeed. + NearbyConnectionsService::ConnectionListener connection_listener_remote; + NearbyConnectionsService::PayloadListener payload_listener_remote; + NearbyConnection* nearby_connection = + Connect(connection_listener_remote, payload_listener_remote, + ConnectionResponse::kRejected); + EXPECT_FALSE(nearby_connection); + EXPECT_FALSE(nearby_connections_manager_->GetRawAuthenticationToken( + kRemoteEndpointId)); +} + +TEST_F(NearbyConnectionsManagerImplTest, ConnectDisconnected) { + // StartDiscovery will succeed. + NearbyConnectionsService::DiscoveryListener discovery_listener_remote; + testing::NiceMock discovery_listener; + StartDiscovery(discovery_listener_remote, discovery_listener); + + // RequestConnection will succeed. + NearbyConnectionsService::ConnectionListener connection_listener_remote; + NearbyConnectionsService::PayloadListener payload_listener_remote; + NearbyConnection* nearby_connection = + Connect(connection_listener_remote, payload_listener_remote, + ConnectionResponse::kDisconnected); + EXPECT_TRUE(FakeTaskRunner::WaitForRunningTasksWithTimeout(absl::Seconds(1))); + EXPECT_FALSE(nearby_connection); + EXPECT_FALSE(nearby_connections_manager_->GetRawAuthenticationToken( + kRemoteEndpointId)); +} + +TEST_F(NearbyConnectionsManagerImplTest, ConnectAccepted) { + const std::vector raw_authentication_token( + std::begin(kRawAuthenticationToken), std::end(kRawAuthenticationToken)); + + // StartDiscovery will succeed. + NearbyConnectionsService::DiscoveryListener discovery_listener_remote; + testing::NiceMock discovery_listener; + StartDiscovery(discovery_listener_remote, discovery_listener); + + // RequestConnection will succeed. + NearbyConnectionsService::ConnectionListener connection_listener_remote; + NearbyConnectionsService::PayloadListener payload_listener_remote; + NearbyConnection* nearby_connection = + Connect(connection_listener_remote, payload_listener_remote, + ConnectionResponse::kAccepted); + EXPECT_TRUE(nearby_connection); + EXPECT_EQ( + nearby_connections_manager_->GetRawAuthenticationToken(kRemoteEndpointId), + raw_authentication_token); +} + +TEST_F(NearbyConnectionsManagerImplTest, ConnectReadBeforeAppend) { + const std::vector byte_payload(std::begin(kBytePayload), + std::end(kBytePayload)); + + // StartDiscovery will succeed. + NearbyConnectionsService::DiscoveryListener discovery_listener_remote; + testing::NiceMock discovery_listener; + StartDiscovery(discovery_listener_remote, discovery_listener); + + // RequestConnection will succeed. + NearbyConnectionsService::ConnectionListener connection_listener_remote; + NearbyConnectionsService::PayloadListener payload_listener_remote; + NearbyConnection* nearby_connection = + Connect(connection_listener_remote, payload_listener_remote, + ConnectionResponse::kAccepted); + ASSERT_TRUE(nearby_connection); + + // Read before message is appended should also succeed. + absl::Notification notification; + nearby_connection->Read([&](std::optional> bytes) { + EXPECT_EQ(bytes, byte_payload); + notification.Notify(); + }); + Sync(); + payload_listener_remote.payload_cb(kRemoteEndpointId, + Payload(kPayloadId, byte_payload)); + payload_listener_remote.payload_progress_cb( + kRemoteEndpointId, + PayloadTransferUpdate(kPayloadId, PayloadStatus::kSuccess, kTotalSize, + /*bytes_transferred=*/kTotalSize)); + EXPECT_TRUE( + notification.WaitForNotificationWithTimeout(kSynchronizationTimeOut)); +} + +TEST_F(NearbyConnectionsManagerImplTest, ConnectReadAfterAppend) { + const std::vector byte_payload(std::begin(kBytePayload), + std::end(kBytePayload)); + const std::vector byte_payload_2(std::begin(kBytePayload2), + std::end(kBytePayload2)); + + // StartDiscovery will succeed. + NearbyConnectionsService::DiscoveryListener discovery_listener_remote; + testing::NiceMock discovery_listener; + StartDiscovery(discovery_listener_remote, discovery_listener); + + // RequestConnection will succeed. + NearbyConnectionsService::ConnectionListener connection_listener_remote; + NearbyConnectionsService::PayloadListener payload_listener_remote; + NearbyConnection* nearby_connection = + Connect(connection_listener_remote, payload_listener_remote, + ConnectionResponse::kAccepted); + ASSERT_NE(nearby_connection, nullptr); + + // Read after message is appended should succeed. + payload_listener_remote.payload_cb(kRemoteEndpointId, + Payload(kPayloadId, byte_payload)); + payload_listener_remote.payload_progress_cb( + kRemoteEndpointId, + PayloadTransferUpdate(kPayloadId, PayloadStatus::kSuccess, kTotalSize, + /*bytes_transferred=*/kTotalSize)); + payload_listener_remote.payload_cb(kRemoteEndpointId, + Payload(kPayloadId2, byte_payload_2)); + payload_listener_remote.payload_progress_cb( + kRemoteEndpointId, + PayloadTransferUpdate(kPayloadId2, PayloadStatus::kSuccess, kTotalSize, + /*bytes_transferred=*/kTotalSize)); + + absl::Notification read_notification; + nearby_connection->Read([&](std::optional> bytes) { + EXPECT_EQ(bytes, byte_payload); + read_notification.Notify(); + }); + Sync(); + EXPECT_TRUE(read_notification.WaitForNotificationWithTimeout( + kSynchronizationTimeOut)); + + absl::Notification read_notification2; + nearby_connection->Read([&](std::optional> bytes) { + EXPECT_EQ(bytes, byte_payload_2); + read_notification2.Notify(); + }); + Sync(); + EXPECT_TRUE(read_notification2.WaitForNotificationWithTimeout( + kSynchronizationTimeOut)); +} + +TEST_F(NearbyConnectionsManagerImplTest, ConnectWrite) { + const std::vector byte_payload(std::begin(kBytePayload), + std::end(kBytePayload)); + // StartDiscovery will succeed. + NearbyConnectionsService::DiscoveryListener discovery_listener_remote; + testing::NiceMock discovery_listener; + StartDiscovery(discovery_listener_remote, discovery_listener); + + // RequestConnection will succeed. + NearbyConnectionsService::ConnectionListener connection_listener_remote; + NearbyConnectionsService::PayloadListener payload_listener_remote; + NearbyConnection* nearby_connection = + Connect(connection_listener_remote, payload_listener_remote, + ConnectionResponse::kAccepted); + ASSERT_TRUE(nearby_connection); + + absl::Notification notification; + EXPECT_CALL(*nearby_connections_, SendPayload) + .WillOnce([&](absl::string_view service_id, + absl::Span endpoint_ids, + std::unique_ptr payload, + std::function callback) { + EXPECT_EQ(service_id, kServiceId); + EXPECT_THAT(endpoint_ids, ElementsAre(kRemoteEndpointId)); + ASSERT_TRUE(payload); + ASSERT_TRUE(payload->content.is_bytes()); + EXPECT_EQ(payload->content.bytes_payload.bytes, byte_payload); + + std::move(callback)(Status::kSuccess); + notification.Notify(); + }); + + nearby_connection->Write(byte_payload); + Sync(); + EXPECT_TRUE( + notification.WaitForNotificationWithTimeout(kSynchronizationTimeOut)); +} + +TEST_F(NearbyConnectionsManagerImplTest, ConnectClosed) { + // StartDiscovery will succeed. + NearbyConnectionsService::DiscoveryListener discovery_listener_remote; + testing::NiceMock discovery_listener; + StartDiscovery(discovery_listener_remote, discovery_listener); + + // RequestConnection will succeed. + NearbyConnectionsService::ConnectionListener connection_listener_remote; + NearbyConnectionsService::PayloadListener payload_listener_remote; + NearbyConnection* nearby_connection = + Connect(connection_listener_remote, payload_listener_remote, + ConnectionResponse::kAccepted); + ASSERT_TRUE(nearby_connection); + + // Close should invoke disconnection callback and read callback. + absl::Notification close_notification; + nearby_connection->SetDisconnectionListener( + [&]() { close_notification.Notify(); }); + Sync(); + absl::Notification read_notification; + nearby_connection->Read([&](std::optional> bytes) { + EXPECT_FALSE(bytes.has_value()); + read_notification.Notify(); + }); + Sync(); + + absl::Notification disconnect_notification; + EXPECT_CALL(*nearby_connections_, DisconnectFromEndpoint) + .WillOnce([&](absl::string_view service_id, absl::string_view endpoint_id, + std::function callback) { + EXPECT_EQ(service_id, kServiceId); + EXPECT_EQ(endpoint_id, kRemoteEndpointId); + std::move(callback)(Status::kSuccess); + disconnect_notification.Notify(); + }); + + nearby_connection->Close(); + Sync(); + + EXPECT_TRUE(close_notification.WaitForNotificationWithTimeout( + kSynchronizationTimeOut)); + EXPECT_TRUE(read_notification.WaitForNotificationWithTimeout( + kSynchronizationTimeOut)); + EXPECT_TRUE(disconnect_notification.WaitForNotificationWithTimeout( + kSynchronizationTimeOut)); + EXPECT_FALSE(nearby_connections_manager_->GetRawAuthenticationToken( + kRemoteEndpointId)); +} + +TEST_F(NearbyConnectionsManagerImplTest, ConnectClosedByRemote) { + // StartDiscovery will succeed. + NearbyConnectionsService::DiscoveryListener discovery_listener_remote; + testing::NiceMock discovery_listener; + StartDiscovery(discovery_listener_remote, discovery_listener); + + // RequestConnection will succeed. + NearbyConnectionsService::ConnectionListener connection_listener_remote; + NearbyConnectionsService::PayloadListener payload_listener_remote; + NearbyConnection* nearby_connection = + Connect(connection_listener_remote, payload_listener_remote, + ConnectionResponse::kAccepted); + ASSERT_TRUE(nearby_connection); + + // Remote closing should invoke disconnection callback and read callback. + absl::Notification close_notification; + nearby_connection->SetDisconnectionListener( + [&]() { close_notification.Notify(); }); + Sync(); + absl::Notification read_notification; + nearby_connection->Read([&](absl::optional> bytes) { + EXPECT_FALSE(bytes); + read_notification.Notify(); + }); + Sync(); + connection_listener_remote.disconnected_cb(kRemoteEndpointId); + EXPECT_TRUE(close_notification.WaitForNotificationWithTimeout( + kSynchronizationTimeOut)); + EXPECT_TRUE(read_notification.WaitForNotificationWithTimeout( + kSynchronizationTimeOut)); + EXPECT_FALSE(nearby_connections_manager_->GetRawAuthenticationToken( + kRemoteEndpointId)); +} + +TEST_F(NearbyConnectionsManagerImplTest, ConnectClosedByClient) { + // StartDiscovery will succeed. + NearbyConnectionsService::DiscoveryListener discovery_listener_remote; + testing::NiceMock discovery_listener; + StartDiscovery(discovery_listener_remote, discovery_listener); + + // RequestConnection will succeed. + NearbyConnectionsService::ConnectionListener connection_listener_remote; + NearbyConnectionsService::PayloadListener payload_listener_remote; + NearbyConnection* nearby_connection = + Connect(connection_listener_remote, payload_listener_remote, + ConnectionResponse::kAccepted); + ASSERT_TRUE(nearby_connection); + + // Remote closing should invoke disconnection callback and read callback. + absl::Notification close_notification; + nearby_connection->SetDisconnectionListener( + [&]() { close_notification.Notify(); }); + Sync(); + absl::Notification read_notification; + nearby_connection->Read([&](absl::optional> bytes) { + EXPECT_FALSE(bytes); + read_notification.Notify(); + }); + Sync(); + + absl::Notification disconnect_notification; + EXPECT_CALL(*nearby_connections_, DisconnectFromEndpoint) + .WillOnce([&](absl::string_view service_id, absl::string_view endpoint_id, + std::function callback) { + EXPECT_EQ(service_id, kServiceId); + EXPECT_EQ(endpoint_id, kRemoteEndpointId); + std::move(callback)(Status::kSuccess); + disconnect_notification.Notify(); + }); + nearby_connections_manager_->Disconnect(kRemoteEndpointId); + Sync(); + EXPECT_TRUE(close_notification.WaitForNotificationWithTimeout( + kSynchronizationTimeOut)); + EXPECT_TRUE(read_notification.WaitForNotificationWithTimeout( + kSynchronizationTimeOut)); + EXPECT_TRUE(disconnect_notification.WaitForNotificationWithTimeout( + kSynchronizationTimeOut)); + + EXPECT_FALSE(nearby_connections_manager_->GetRawAuthenticationToken( + kRemoteEndpointId)); +} + +TEST_F(NearbyConnectionsManagerImplTest, ConnectSendPayload) { + // StartDiscovery will succeed. + NearbyConnectionsService::DiscoveryListener discovery_listener_remote; + testing::NiceMock discovery_listener; + StartDiscovery(discovery_listener_remote, discovery_listener); + + // RequestConnection will succeed. + NearbyConnectionsService::ConnectionListener connection_listener_remote; + NearbyConnectionsService::PayloadListener payload_listener_remote; + Connect(connection_listener_remote, payload_listener_remote, + ConnectionResponse::kAccepted); + + auto payload_listener = + std::make_shared>(); + SendPayload(kPayloadId, payload_listener); + + PayloadTransferUpdate expected_update(kPayloadId, PayloadStatus::kInProgress, + kTotalSize, kBytesTransferred); + absl::Notification payload_notification; + EXPECT_CALL(*payload_listener, OnStatusUpdate) + .WillOnce([&](std::unique_ptr update, + std::optional upgraded_medium) { + EXPECT_EQ(update->payload_id, expected_update.payload_id); + EXPECT_EQ(update->bytes_transferred, expected_update.bytes_transferred); + EXPECT_EQ(update->total_bytes, expected_update.total_bytes); + EXPECT_EQ(update->status, expected_update.status); + EXPECT_FALSE(upgraded_medium.has_value()); + payload_notification.Notify(); + }); + + payload_listener_remote.payload_progress_cb(kRemoteEndpointId, + expected_update); + EXPECT_TRUE(payload_notification.WaitForNotificationWithTimeout( + kSynchronizationTimeOut)); +} + +TEST_F(NearbyConnectionsManagerImplTest, ConnectCancelPayload) { + // StartDiscovery will succeed. + NearbyConnectionsService::DiscoveryListener discovery_listener_remote; + testing::NiceMock discovery_listener; + StartDiscovery(discovery_listener_remote, discovery_listener); + + // RequestConnection will succeed. + NearbyConnectionsService::ConnectionListener connection_listener_remote; + NearbyConnectionsService::PayloadListener payload_listener_remote; + Connect(connection_listener_remote, payload_listener_remote, + ConnectionResponse::kAccepted); + + auto payload_listener = + std::make_shared>(); + SendPayload(kPayloadId, payload_listener); + + absl::Notification cancel_notification; + EXPECT_CALL(*nearby_connections_, CancelPayload) + .WillOnce([&](absl::string_view service_id, int64_t payload_id, + std::function callback) { + EXPECT_EQ(service_id, kServiceId); + EXPECT_EQ(payload_id, kPayloadId); + + std::move(callback)(Status::kSuccess); + cancel_notification.Notify(); + }); + + absl::Notification payload_notification; + EXPECT_CALL(*payload_listener, OnStatusUpdate) + .WillOnce([&](std::unique_ptr update, + std::optional upgraded_medium) { + EXPECT_EQ(update->payload_id, kPayloadId); + EXPECT_EQ(update->status, PayloadStatus::kCanceled); + EXPECT_EQ(update->total_bytes, 0u); + EXPECT_EQ(update->bytes_transferred, 0u); + EXPECT_FALSE(upgraded_medium.has_value()); + payload_notification.Notify(); + }); + + nearby_connections_manager_->Cancel(kPayloadId); + EXPECT_TRUE(payload_notification.WaitForNotificationWithTimeout( + kSynchronizationTimeOut)); + EXPECT_TRUE(cancel_notification.WaitForNotificationWithTimeout( + kSynchronizationTimeOut)); +} + +TEST_F(NearbyConnectionsManagerImplTest, + ConnectCancelPayload_MultiplePayloads_HandleDestroyedPayloadListener) { + // StartDiscovery will succeed. + NearbyConnectionsService::DiscoveryListener discovery_listener_remote; + testing::NiceMock discovery_listener; + StartDiscovery(discovery_listener_remote, discovery_listener); + + // RequestConnection will succeed. + NearbyConnectionsService::ConnectionListener connection_listener_remote; + NearbyConnectionsService::PayloadListener payload_listener_remote; + Connect(connection_listener_remote, payload_listener_remote, + ConnectionResponse::kAccepted); + + // Send two payloads with the same listener. We will eventually cancel both + // payloads, but we will reset the listener before cancelling the second + // payload. This can happen in practice: if the first payload is cancelled or + // fails, it makes sense to clean everything up before waiting for the other + // payload cancellation/failure signals. We are testing that the + // connection manager handles the missing listener gracefully. + auto payload_listener = + std::make_shared>(); + SendPayload(kPayloadId, payload_listener); + SendPayload(kPayloadId2, payload_listener); + + absl::Notification cancel_notification; + EXPECT_CALL(*nearby_connections_, CancelPayload) + .WillOnce([&](absl::string_view service_id, int64_t payload_id, + std::function callback) { + EXPECT_EQ(service_id, kServiceId); + EXPECT_EQ(payload_id, kPayloadId); + + std::move(callback)(Status::kSuccess); + }) + .WillOnce([&](absl::string_view service_id, int64_t payload_id, + std::function callback) { + EXPECT_EQ(service_id, kServiceId); + EXPECT_EQ(payload_id, kPayloadId2); + + std::move(callback)(Status::kSuccess); + cancel_notification.Notify(); + }); + + // Because the payload listener is reset before the second payload is + // cancelled, we can only receive the first status update. + absl::Notification payload_notification; + EXPECT_CALL(*payload_listener, OnStatusUpdate) + .Times(1) + .WillOnce([&](std::unique_ptr update, + std::optional upgraded_medium) { + EXPECT_EQ(update->payload_id, kPayloadId); + EXPECT_EQ(update->status, PayloadStatus::kCanceled); + EXPECT_EQ(update->total_bytes, 0u); + EXPECT_EQ(update->bytes_transferred, 0u); + EXPECT_FALSE(upgraded_medium.has_value()); + + // Destroy the PayloadStatusListener after the first payload is + // cancelled. + payload_listener.reset(); + + payload_notification.Notify(); + }); + + nearby_connections_manager_->Cancel(kPayloadId); + nearby_connections_manager_->Cancel(kPayloadId2); + EXPECT_TRUE(payload_notification.WaitForNotificationWithTimeout( + kSynchronizationTimeOut)); + EXPECT_TRUE(cancel_notification.WaitForNotificationWithTimeout( + kSynchronizationTimeOut)); +} + +TEST_F(NearbyConnectionsManagerImplTest, ConnectTimeout) { + NearbyConnectionsService::DiscoveryListener discovery_listener_remote; + testing::NiceMock discovery_listener; + StartDiscovery(discovery_listener_remote, discovery_listener); + + // RequestConnection will time out. + const std::vector local_endpoint_info(std::begin(kEndpointInfo), + std::end(kEndpointInfo)); + + NearbyConnectionsService::ConnectionListener connection_listener_remote; + std::function connect_callback; + EXPECT_CALL(*nearby_connections_, RequestConnection) + .WillOnce([&](absl::string_view service_id, + const std::vector& endpoint_info, + absl::string_view endpoint_id, + ConnectionOptions connection_options, + NearbyConnectionsService::ConnectionListener listener, + std::function callback) { + EXPECT_EQ(service_id, kServiceId); + EXPECT_EQ(endpoint_info, local_endpoint_info); + EXPECT_EQ(endpoint_id, kRemoteEndpointId); + + connection_listener_remote = std::move(listener); + // Do not call callback until connection timed out. + connect_callback = std::move(callback); + }); + + // Timing out should call disconnect. + EXPECT_CALL(*nearby_connections_, DisconnectFromEndpoint) + .WillOnce([&](absl::string_view service_id, absl::string_view endpoint_id, + std::function callback) { + EXPECT_EQ(service_id, kServiceId); + EXPECT_EQ(endpoint_id, kRemoteEndpointId); + std::move(callback)(Status::kSuccess); + }); + + absl::Notification run_notification; + NearbyConnection* nearby_connection = nullptr; + nearby_connections_manager_->Connect( + local_endpoint_info, kRemoteEndpointId, + /*bluetooth_mac_address=*/std::nullopt, DataUsage::OFFLINE_DATA_USAGE, + TransportType::kHighQuality, + [&](NearbyConnection* connection, Status status) { + nearby_connection = connection; + run_notification.Notify(); + }); + // Simulate time passing until timeout is reached. + Fastforward(kInitiateNearbyConnectionTimeout); + EXPECT_TRUE( + run_notification.WaitForNotificationWithTimeout(kSynchronizationTimeOut)); + + // Expect the callback to be called with a null connection. + EXPECT_EQ(nearby_connection, nullptr); + + // Resolving connect callback after timeout should do nothing. + std::move(connect_callback)(Status::kSuccess); +} + +TEST_F(NearbyConnectionsManagerImplTest, StartAdvertising) { + NearbyConnectionsService::ConnectionListener connection_listener_remote; + testing::NiceMock + incoming_connection_listener; + StartAdvertising(connection_listener_remote, incoming_connection_listener); + + NearbyConnectionsService::PayloadListener payload_listener_remote; + NearbyConnection* connection = OnIncomingConnection( + connection_listener_remote, incoming_connection_listener, + payload_listener_remote); + EXPECT_NE(connection, nullptr); +} + +TEST_F(NearbyConnectionsManagerImplTest, IncomingPayloadStatusListener) { + NearbyConnectionsService::ConnectionListener connection_listener_remote; + testing::NiceMock + incoming_connection_listener; + StartAdvertising(connection_listener_remote, incoming_connection_listener); + + NearbyConnectionsService::PayloadListener payload_listener_remote; + NearbyConnection* connection = OnIncomingConnection( + connection_listener_remote, incoming_connection_listener, + payload_listener_remote); + EXPECT_NE(connection, nullptr); + + auto payload_listener = + std::make_shared>(); + nearby_connections_manager_->RegisterPayloadStatusListener( + kPayloadId, payload_listener->GetWeakPtr()); + + PayloadTransferUpdate expected_update(kPayloadId, PayloadStatus::kInProgress, + kTotalSize, kBytesTransferred); + absl::Notification payload_notification; + EXPECT_CALL(*payload_listener, OnStatusUpdate) + .WillOnce([&](std::unique_ptr update, + std::optional upgraded_medium) { + EXPECT_THAT(*update, FieldsAre(kPayloadId, PayloadStatus::kInProgress, + kTotalSize, kBytesTransferred)); + EXPECT_FALSE(upgraded_medium.has_value()); + payload_notification.Notify(); + }); + + payload_listener_remote.payload_progress_cb(kRemoteEndpointId, + expected_update); + EXPECT_TRUE(payload_notification.WaitForNotificationWithTimeout( + kSynchronizationTimeOut)); + + // After success status, send another progress update. + absl::Notification payload_notification_2; + EXPECT_CALL(*payload_listener, OnStatusUpdate) + .WillOnce([&](std::unique_ptr update, + std::optional upgraded_medium) { + payload_notification_2.Notify(); + }); + + payload_listener_remote.payload_progress_cb( + kRemoteEndpointId, + PayloadTransferUpdate(kPayloadId, PayloadStatus::kSuccess, kTotalSize, + /*bytes_transferred=*/kTotalSize)); + EXPECT_TRUE(payload_notification_2.WaitForNotificationWithTimeout( + kSynchronizationTimeOut)); + + // PayloadStatusListener will be unregistered and won't receive further + // updates. + payload_listener_remote.payload_progress_cb( + kRemoteEndpointId, + PayloadTransferUpdate(kPayloadId, PayloadStatus::kSuccess, kTotalSize, + /*bytes_transferred=*/kTotalSize)); + EXPECT_CALL(*payload_listener, OnStatusUpdate).Times(0); + + payload_listener_remote.payload_progress_cb( + kRemoteEndpointId, + PayloadTransferUpdate(kPayloadId, PayloadStatus::kSuccess, kTotalSize, + /*bytes_transferred=*/kTotalSize)); +} + +TEST_F(NearbyConnectionsManagerImplTest, + IncomingPayloadStatusListener_MultiplePayloads_HandleDestroyedListener) { + NearbyConnectionsService::ConnectionListener connection_listener_remote; + testing::NiceMock + incoming_connection_listener; + StartAdvertising(connection_listener_remote, incoming_connection_listener); + + NearbyConnectionsService::PayloadListener payload_listener_remote; + NearbyConnection* connection = OnIncomingConnection( + connection_listener_remote, incoming_connection_listener, + payload_listener_remote); + EXPECT_NE(connection, nullptr); + + // Register three payloads with the same listener. This happens when multiple + // payloads are included in the same transfer. Use both file and byte payloads + // to ensure control-frame logic is not invoked for either. + auto payload_listener = + std::make_shared>(); + nearby_connections_manager_->RegisterPayloadStatusListener( + kPayloadId, payload_listener->GetWeakPtr()); + nearby_connections_manager_->RegisterPayloadStatusListener( + kPayloadId2, payload_listener->GetWeakPtr()); + nearby_connections_manager_->RegisterPayloadStatusListener( + kPayloadId3, payload_listener->GetWeakPtr()); + + std::filesystem::path file1(std::filesystem::temp_directory_path() / + "file1.jpg"); + std::filesystem::path file2(std::filesystem::temp_directory_path() / + "file2.jpg"); + + InitializeTemporaryFile(file1); + InitializeTemporaryFile(file2); + + payload_listener_remote.payload_cb(kRemoteEndpointId, + Payload(kPayloadId, InputFile(file1))); + payload_listener_remote.payload_cb(kRemoteEndpointId, + Payload(kPayloadId2, InputFile(file2))); + + const std::vector byte_payload(std::begin(kBytePayload), + std::end(kBytePayload)); + + payload_listener_remote.payload_cb( + kRemoteEndpointId, Payload(kPayloadId3, std::move(byte_payload))); + + // Fail the first payload and destroy the payload listener. Then, send updates + // that the second and third payloads succeeded; this is unlikely in practice, + // but we test to ensure that no control-frame logic is exercised. Expect that + // a status update is only sent for the first payload failure because + // the listener does not exist afterwards. + absl::Notification payload_notification; + EXPECT_CALL(*payload_listener, OnStatusUpdate) + .Times(1) + .WillOnce([&](std::unique_ptr update, + std::optional upgraded_medium) { + EXPECT_EQ(update->payload_id, kPayloadId); + EXPECT_EQ(update->status, PayloadStatus::kFailure); + EXPECT_EQ(update->total_bytes, kTotalSize); + EXPECT_EQ(update->bytes_transferred, 0u); + EXPECT_FALSE(upgraded_medium.has_value()); + + // Destroy the PayloadStatusListener after the first payload fails. + payload_listener.reset(); + payload_notification.Notify(); + }); + // Ensure that no control-frame logic is run, which can happen when a payload + // update is received for an unregistered payload. + EXPECT_CALL(*nearby_connections_, CancelPayload).Times(0); + + payload_listener_remote.payload_progress_cb( + kRemoteEndpointId, + PayloadTransferUpdate(kPayloadId, PayloadStatus::kFailure, kTotalSize, + /*bytes_transferred=*/0u)); + payload_listener_remote.payload_progress_cb( + kRemoteEndpointId, + PayloadTransferUpdate(kPayloadId2, PayloadStatus::kSuccess, kTotalSize, + /*bytes_transferred=*/0u)); + payload_listener_remote.payload_progress_cb( + kRemoteEndpointId, + PayloadTransferUpdate(kPayloadId3, PayloadStatus::kSuccess, kTotalSize, + /*bytes_transferred=*/0u)); + EXPECT_TRUE(payload_notification.WaitForNotificationWithTimeout( + kSynchronizationTimeOut)); +} + +TEST_F(NearbyConnectionsManagerImplTest, IncomingBytesPayload) { + NearbyConnectionsService::ConnectionListener connection_listener_remote; + testing::NiceMock + incoming_connection_listener; + StartAdvertising(connection_listener_remote, incoming_connection_listener); + + NearbyConnectionsService::PayloadListener payload_listener_remote; + NearbyConnection* connection = OnIncomingConnection( + connection_listener_remote, incoming_connection_listener, + payload_listener_remote); + EXPECT_TRUE(connection); + + auto payload_listener = + std::make_shared>(); + nearby_connections_manager_->RegisterPayloadStatusListener( + kPayloadId, payload_listener->GetWeakPtr()); + + const std::vector expected_payload(std::begin(kPayload), + std::end(kPayload)); + + payload_listener_remote.payload_cb(kRemoteEndpointId, + Payload(kPayloadId, expected_payload)); + + absl::Notification payload_notification; + EXPECT_CALL(*payload_listener, OnStatusUpdate(::testing::_, ::testing::_)) + .WillOnce([&]() { payload_notification.Notify(); }); + + payload_listener_remote.payload_progress_cb( + kRemoteEndpointId, + PayloadTransferUpdate(kPayloadId, PayloadStatus::kSuccess, kTotalSize, + /*bytes_transferred=*/kTotalSize)); + EXPECT_TRUE(payload_notification.WaitForNotificationWithTimeout( + kSynchronizationTimeOut)); + + Payload* payload = + nearby_connections_manager_->GetIncomingPayload(kPayloadId); + ASSERT_NE(payload, nullptr); + ASSERT_TRUE(payload->content.is_bytes()); + EXPECT_EQ(payload->content.bytes_payload.bytes, expected_payload); +} + +TEST_F(NearbyConnectionsManagerImplTest, IncomingFilePayload) { + NearbyConnectionsService::ConnectionListener connection_listener_remote; + testing::NiceMock + incoming_connection_listener; + StartAdvertising(connection_listener_remote, incoming_connection_listener); + + NearbyConnectionsService::PayloadListener payload_listener_remote; + NearbyConnection* connection = OnIncomingConnection( + connection_listener_remote, incoming_connection_listener, + payload_listener_remote); + EXPECT_NE(connection, nullptr); + + auto payload_listener = + std::make_shared>(); + nearby_connections_manager_->RegisterPayloadStatusListener( + kPayloadId, payload_listener->GetWeakPtr()); + + const std::vector expected_payload(std::begin(kPayload), + std::end(kPayload)); + + std::filesystem::path file(std::filesystem::temp_directory_path() / + "file.jpg"); + InitializeTemporaryFile(file); + + payload_listener_remote.payload_cb(kRemoteEndpointId, + Payload(kPayloadId, InputFile(file))); + + absl::Notification payload_notification; + EXPECT_CALL(*payload_listener, OnStatusUpdate(::testing::_, ::testing::_)) + .WillOnce([&]() { payload_notification.Notify(); }); + + payload_listener_remote.payload_progress_cb( + kRemoteEndpointId, + PayloadTransferUpdate(kPayloadId, PayloadStatus::kSuccess, kTotalSize, + /*bytes_transferred=*/kTotalSize)); + EXPECT_TRUE(payload_notification.WaitForNotificationWithTimeout( + kSynchronizationTimeOut)); + + Payload* payload = + nearby_connections_manager_->GetIncomingPayload(kPayloadId); + ASSERT_NE(payload, nullptr); + ASSERT_TRUE(payload->content.is_file()); + std::vector payload_bytes(payload->content.file_payload.size); + std::FILE* payload_fp = std::fopen( + payload->content.file_payload.file.path.string().c_str(), "rb"); + ASSERT_NE(payload_fp, nullptr); + EXPECT_EQ(std::fread(payload_bytes.data(), 1, + payload->content.file_payload.size, payload_fp), + payload->content.file_payload.size); + std::fclose(payload_fp); + EXPECT_EQ(payload_bytes, expected_payload); +} + +TEST_F(NearbyConnectionsManagerImplTest, ClearIncomingPayloads) { + NearbyConnectionsService::ConnectionListener connection_listener_remote; + testing::NiceMock + incoming_connection_listener; + StartAdvertising(connection_listener_remote, incoming_connection_listener); + + NearbyConnectionsService::PayloadListener payload_listener_remote; + NearbyConnection* connection = OnIncomingConnection( + connection_listener_remote, incoming_connection_listener, + payload_listener_remote); + EXPECT_NE(connection, nullptr); + + auto payload_listener = + std::make_shared>(); + nearby_connections_manager_->RegisterPayloadStatusListener( + kPayloadId, payload_listener->GetWeakPtr()); + + std::filesystem::path file(std::filesystem::temp_directory_path() / + "file.jpg"); + InitializeTemporaryFile(file); + + payload_listener_remote.payload_cb(kRemoteEndpointId, + Payload(kPayloadId, InputFile(file))); + + absl::Notification payload_notification; + EXPECT_CALL(*payload_listener, OnStatusUpdate(::testing::_, ::testing::_)) + .WillOnce([&]() { payload_notification.Notify(); }); + + payload_listener_remote.payload_progress_cb( + kRemoteEndpointId, + PayloadTransferUpdate(kPayloadId, PayloadStatus::kSuccess, kTotalSize, + /*bytes_transferred=*/kTotalSize)); + EXPECT_TRUE(payload_notification.WaitForNotificationWithTimeout( + kSynchronizationTimeOut)); + + nearby_connections_manager_->ClearIncomingPayloads(); + + EXPECT_EQ(nearby_connections_manager_->GetIncomingPayload(kPayloadId), + nullptr); +} + +/******************************************************************************/ +// Begin: NearbyConnectionsManagerImplTestMediums +/******************************************************************************/ +using MediumsTestParam = + std::tuple; +class NearbyConnectionsManagerImplTestMediums + : public NearbyConnectionsManagerImplTest, + public testing::WithParamInterface {}; + +TEST_P(NearbyConnectionsManagerImplTestMediums, StartAdvertising_Options) { + const MediumsTestParam& param = GetParam(); + PowerLevel power_level = std::get<0>(param); + DataUsage data_usage = std::get<1>(param); + ConnectivityManager::ConnectionType connection_type = std::get<2>(param); + bool is_webrtc_enabled = std::get<3>(GetParam()); + bool is_wifilan_enabled = std::get<4>(GetParam()); + + if (is_webrtc_enabled) { + NearbyFlags::GetInstance().OverrideBoolFlagValue( + config_package_nearby::nearby_sharing_feature::kEnableMediumWebRtc, + true); + } else { + NearbyFlags::GetInstance().OverrideBoolFlagValue( + config_package_nearby::nearby_sharing_feature::kEnableMediumWebRtc, + false); + } + if (is_wifilan_enabled) { + NearbyFlags::GetInstance().OverrideBoolFlagValue( + config_package_nearby::nearby_sharing_feature::kEnableMediumWifiLan, + true); + } else { + NearbyFlags::GetInstance().OverrideBoolFlagValue( + config_package_nearby::nearby_sharing_feature::kEnableMediumWifiLan, + false); + } + + SetConnectionType(connection_type); + + bool should_use_internet = + data_usage != DataUsage::OFFLINE_DATA_USAGE && + connection_type != ConnectivityManager::ConnectionType::kNone && + !(data_usage == DataUsage::WIFI_ONLY_DATA_USAGE && + connection_type != ConnectivityManager::ConnectionType::kWifi); + bool is_connection_wifi_or_ethernet = + connection_type == ConnectivityManager::ConnectionType::kWifi || + connection_type == ConnectivityManager::ConnectionType::kEthernet; + should_use_web_rtc_ = is_webrtc_enabled && should_use_internet; + should_use_wifilan_ = is_wifilan_enabled & is_connection_wifi_or_ethernet; + + bool is_high_power = power_level == PowerLevel::kHighPower; + + MediumSelection expected_mediums( + /*bluetooth=*/is_high_power, + /*ble=*/true, + /*web_rtc=*/should_use_web_rtc_, + /*wifi_lan=*/should_use_wifilan_, + /*wifi_hotspot=*/true); + + absl::Notification notification; + const std::vector local_endpoint_info(std::begin(kEndpointInfo), + std::end(kEndpointInfo)); + testing::NiceMock + incoming_connection_listener; + + NearbyConnectionsManager::ConnectionsCallback callback = [&](Status status) { + EXPECT_EQ(status, Status::kSuccess); + notification.Notify(); + }; + + EXPECT_CALL(*nearby_connections_, StartAdvertising) + .WillOnce( + [&](absl::string_view service_id, + const std::vector& endpoint_info, + AdvertisingOptions options, + NearbyConnectionsService::ConnectionListener advertising_listener, + std::function callback) { + EXPECT_EQ(options.auto_upgrade_bandwidth, false); + EXPECT_EQ(options.allowed_mediums.ble, expected_mediums.ble); + EXPECT_EQ(options.allowed_mediums.bluetooth, + expected_mediums.bluetooth); + EXPECT_EQ(options.allowed_mediums.web_rtc, + expected_mediums.web_rtc); + EXPECT_EQ(options.allowed_mediums.wifi_lan, + expected_mediums.wifi_lan); + EXPECT_EQ(options.enable_bluetooth_listening, true); + EXPECT_EQ(options.enable_webrtc_listening, + is_high_power && should_use_web_rtc_); + std::move(callback)(Status::kSuccess); + }); + + nearby_connections_manager_->StartAdvertising( + local_endpoint_info, &incoming_connection_listener, power_level, + data_usage, std::move(callback)); + + EXPECT_TRUE( + notification.WaitForNotificationWithTimeout(kSynchronizationTimeOut)); +} + +INSTANTIATE_TEST_SUITE_P( + NearbyConnectionsManagerImplTestMediums, + NearbyConnectionsManagerImplTestMediums, + testing::Combine(testing::Values(PowerLevel::kLowPower, + PowerLevel::kHighPower), + testing::Values(DataUsage::WIFI_ONLY_DATA_USAGE, + DataUsage::OFFLINE_DATA_USAGE, + DataUsage::ONLINE_DATA_USAGE), + testing::Values(ConnectivityManager::ConnectionType::kNone, + ConnectivityManager::ConnectionType::kWifi, + ConnectivityManager::ConnectionType::k3G), + testing::Bool(), testing::Bool())); + +/******************************************************************************/ +// End: NearbyConnectionsManagerImplTestMediums +/******************************************************************************/ + +TEST_F(NearbyConnectionsManagerImplTest, StopAdvertising) { + NearbyConnectionsService::ConnectionListener connection_listener_remote; + testing::NiceMock + incoming_connection_listener; + StartAdvertising(connection_listener_remote, incoming_connection_listener); + + absl::Notification notification; + EXPECT_CALL(*nearby_connections_, StopAdvertising) + .WillOnce([&](absl::string_view service_id, + std::function callback) { + EXPECT_EQ(service_id, kServiceId); + std::move(callback)(Status::kSuccess); + notification.Notify(); + }); + nearby_connections_manager_->StopAdvertising( + [](Status status) { EXPECT_EQ(status, Status::kSuccess); }); + EXPECT_TRUE( + notification.WaitForNotificationWithTimeout(kSynchronizationTimeOut)); +} + +TEST_F(NearbyConnectionsManagerImplTest, ShutdownAdvertising) { + NearbyConnectionsService::ConnectionListener connection_listener_remote; + testing::NiceMock + incoming_connection_listener; + StartAdvertising(connection_listener_remote, incoming_connection_listener); + + absl::Notification notification; + EXPECT_CALL(*nearby_connections_, StopAllEndpoints) + .WillOnce([&](std::function callback) { + std::move(callback)(Status::kSuccess); + notification.Notify(); + }); + nearby_connections_manager_->Shutdown(); + EXPECT_TRUE( + notification.WaitForNotificationWithTimeout(kSynchronizationTimeOut)); +} + +TEST_F(NearbyConnectionsManagerImplTest, + UpgradeBandwidthAfterAdvertisingSucceeds) { + NearbyConnectionsService::ConnectionListener connection_listener_remote; + testing::NiceMock + incoming_connection_listener; + StartAdvertising(connection_listener_remote, incoming_connection_listener); + + // Upgrading bandwidth will succeed. + absl::Notification notification; + EXPECT_CALL(*nearby_connections_, InitiateBandwidthUpgrade) + .WillOnce([&](absl::string_view service_id, absl::string_view endpoint_id, + std::function callback) { + EXPECT_EQ(service_id, kServiceId); + EXPECT_EQ(endpoint_id, kRemoteEndpointId); + std::move(callback)(Status::kSuccess); + notification.Notify(); + }); + nearby_connections_manager_->UpgradeBandwidth(kRemoteEndpointId); + EXPECT_TRUE( + notification.WaitForNotificationWithTimeout(kSynchronizationTimeOut)); +} + +TEST_F(NearbyConnectionsManagerImplTest, + UpgradeBandwidthAfterDiscoverySucceeds) { + // StartDiscovery will succeed. + NearbyConnectionsService::DiscoveryListener discovery_listener_remote; + testing::NiceMock discovery_listener; + StartDiscovery(discovery_listener_remote, discovery_listener); + + // RequestConnection will succeed. + NearbyConnectionsService::ConnectionListener connection_listener_remote; + NearbyConnectionsService::PayloadListener payload_listener_remote; + NearbyConnection* nearby_connection = + Connect(connection_listener_remote, payload_listener_remote, + ConnectionResponse::kAccepted); + EXPECT_NE(nearby_connection, nullptr); + + // Upgrading bandwidth will succeed. + absl::Notification notification; + EXPECT_CALL(*nearby_connections_, InitiateBandwidthUpgrade) + .WillOnce([&](absl::string_view service_id, absl::string_view endpoint_id, + std::function callback) { + EXPECT_EQ(service_id, kServiceId); + EXPECT_EQ(endpoint_id, kRemoteEndpointId); + std::move(callback)(Status::kSuccess); + notification.Notify(); + }); + nearby_connections_manager_->UpgradeBandwidth(kRemoteEndpointId); + EXPECT_TRUE( + notification.WaitForNotificationWithTimeout(kSynchronizationTimeOut)); +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/nearby_connections_service.cc b/sharing/nearby_connections_service.cc new file mode 100644 index 00000000..f56f6ce2 --- /dev/null +++ b/sharing/nearby_connections_service.cc @@ -0,0 +1,131 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/nearby_connections_service.h" + +#include + +#include +#include +#include // NOLINT(build/c++17) +#include +#include +#include +#include + +#include "sharing/internal/base/utf_string_conversions.h" +#include "sharing/nearby_connections_types.h" + +namespace nearby { +namespace sharing { + +Status ConvertToStatus(NcStatus status) { + return static_cast(status.value); +} + +Payload ConvertToPayload(NcPayload payload) { + switch (payload.GetType()) { + case NcPayloadType::kBytes: { + NcByteArray bytes = payload.AsBytes(); + std::string data = std::string(bytes); + return Payload(payload.GetId(), + std::vector(data.begin(), data.end())); + } + case NcPayloadType::kFile: { + std::filesystem::path file_path; + std::string parent_folder; + // Initialize path with UTF8 cause crash on Windows with configured + // locale. + try { + file_path = payload.AsFile()->GetFilePath(); + if (!std::filesystem::exists(file_path)) { + file_path = utils::Utf8ToWide(payload.AsFile()->GetFilePath()); + parent_folder = payload.GetParentFolder(); + } + } catch (std::exception exception) { + file_path = utils::Utf8ToWide(payload.AsFile()->GetFilePath()); + parent_folder = payload.GetParentFolder(); + } catch (...) { + file_path = utils::Utf8ToWide(payload.AsFile()->GetFilePath()); + parent_folder = payload.GetParentFolder(); + } + return Payload(payload.GetId(), InputFile(file_path), parent_folder); + NEARBY_LOGS(VERBOSE) << __func__ << ": Payload file_path=" << file_path + << ", parent_folder = " << parent_folder; + } + default: + return Payload(); + } +} + +NcPayload ConvertToServicePayload(Payload payload) { + switch (payload.content.type) { + case PayloadContent::Type::kFile: { + // On Windows, a crash may happen when access string() of path if it is + // using wchar. Apply UTF8 to avoid the cross-platform issues. + std::string file_path; + std::string file_name; + std::string parent_folder; + int64_t file_size = payload.content.file_payload.size; + try { + file_path = + utils::WideToUtf8(payload.content.file_payload.file.path.wstring()); + file_name = utils::WideToUtf8( + payload.content.file_payload.file.path.filename().wstring()); + } catch (std::exception e) { + file_path = payload.content.file_payload.file.path.string(); + file_name = payload.content.file_payload.file.path.filename().string(); + } catch (...) { + file_path = payload.content.file_payload.file.path.string(); + file_name = payload.content.file_payload.file.path.filename().string(); + } + parent_folder = payload.content.file_payload.parent_folder; + std::replace(parent_folder.begin(), parent_folder.end(), '\\', '/'); + NEARBY_LOGS(VERBOSE) << __func__ << ": NC Payload file_path=" << file_path + << ", parent_folder = " << parent_folder; + nearby::InputFile input_file(file_path, file_size); + NcPayload nc_payload(payload.id, parent_folder, file_name, + std::move(input_file)); + return nc_payload; + } + case PayloadContent::Type::kBytes: { + std::vector bytes = payload.content.bytes_payload.bytes; + return NcPayload(payload.id, + NcByteArray(std::string(bytes.begin(), bytes.end()))); + } + default: + return NcPayload(); + } +} + +NcResultCallback BuildResultCallback( + std::function callback) { + return NcResultCallback{[&, callback = std::move(callback)](NcStatus status) { + callback(ConvertToStatus(status)); + }}; +} + +NcStrategy ConvertToServiceStrategy(Strategy strategy) { + switch (strategy) { + case Strategy::kP2pCluster: + return NcStrategy::kP2pCluster; + case Strategy::kP2pPointToPoint: + return NcStrategy::kP2pPointToPoint; + case Strategy::kP2pStar: + return NcStrategy::kP2pStar; + } +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/nearby_connections_service.h b/sharing/nearby_connections_service.h new file mode 100644 index 00000000..b7d18643 --- /dev/null +++ b/sharing/nearby_connections_service.h @@ -0,0 +1,183 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_NEARBY_CONNECTIONS_SERVICE_H_ +#define THIRD_PARTY_NEARBY_SHARING_NEARBY_CONNECTIONS_SERVICE_H_ + +#include + +#include +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "connections/advertising_options.h" +#include "connections/connection_options.h" +#include "connections/core.h" +#include "connections/discovery_options.h" +#include "connections/implementation/service_controller_router.h" +#include "connections/listeners.h" +#include "connections/medium_selector.h" +#include "connections/out_of_band_connection_metadata.h" +#include "connections/params.h" +#include "connections/payload.h" +#include "connections/payload_type.h" +#include "connections/status.h" +#include "connections/strategy.h" +#include "internal/platform/listeners.h" +#include "sharing/nearby_connections_types.h" + +namespace nearby { +namespace sharing { + +using Core = ::nearby::connections::Core; +using ServiceControllerRouter = ::nearby::connections::ServiceControllerRouter; +using NcAdvertisingOptions = ::nearby::connections::AdvertisingOptions; +using NcByteArray = ::nearby::ByteArray; +using NcConnectionOptions = ::nearby::connections::ConnectionOptions; +using NcConnectionRequestInfo = ::nearby::connections::ConnectionRequestInfo; +using NcConnectionResponseInfo = ::nearby::connections::ConnectionResponseInfo; +using NcDistanceInfo = ::nearby::connections::DistanceInfo; +using NcDiscoveryListener = ::nearby::connections::DiscoveryListener; +using NcDiscoveryOptions = ::nearby::connections::DiscoveryOptions; +using NcMedium = ::nearby::connections::Medium; +using NcOutOfBandConnectionMetadata = + ::nearby::connections::OutOfBandConnectionMetadata; +using NcPayload = ::nearby::connections::Payload; +using NcPayloadType = ::nearby::connections::PayloadType; +using NcPayloadListener = ::nearby::connections::PayloadListener; +using NcPayloadProgressInfo = ::nearby::connections::PayloadProgressInfo; +using NcResultCallback = ::nearby::connections::ResultCallback; +using NcStatus = ::nearby::connections::Status; +using NcStrategy = ::nearby::connections::Strategy; + +// Main interface to control the NearbyConnections library. Implemented in a +// sandboxed process. This interface is used by the browser process to connect +// to remote devices and send / receive raw data packets. Parsing of those +// packets is not part of the NearbyConnections library and is done in a +// separate interface. +class NearbyConnectionsService { + public: + using HANDLE = void*; + virtual ~NearbyConnectionsService() = default; + + struct ConnectionListener { + std::function + initiated_cb = + DefaultFuncCallback(); + + std::function accepted_cb = + DefaultFuncCallback(); + + std::function + rejected_cb = DefaultFuncCallback(); + + std::function disconnected_cb = + DefaultFuncCallback(); + + std::function + bandwidth_changed_cb = + DefaultFuncCallback(); + }; + + struct DiscoveryListener { + std::function + endpoint_found_cb = + DefaultFuncCallback(); + + std::function endpoint_lost_cb = + DefaultFuncCallback(); + + std::function + endpoint_distance_changed_cb = + DefaultFuncCallback(); + }; + + struct PayloadListener { + std::function + payload_cb = DefaultFuncCallback(); + + std::function + payload_progress_cb = + DefaultFuncCallback(); + }; + + virtual void StartAdvertising( + absl::string_view service_id, const std::vector& endpoint_info, + AdvertisingOptions advertising_options, + ConnectionListener advertising_listener, + std::function callback) = 0; + virtual void StopAdvertising(absl::string_view service_id, + std::function callback) = 0; + + virtual void StartDiscovery(absl::string_view service_id, + DiscoveryOptions discovery_options, + DiscoveryListener discovery_listener, + std::function callback) = 0; + virtual void StopDiscovery(absl::string_view service_id, + std::function callback) = 0; + + virtual void RequestConnection( + absl::string_view service_id, const std::vector& endpoint_info, + absl::string_view endpoint_id, ConnectionOptions connection_options, + ConnectionListener connection_listener, + std::function callback) = 0; + + virtual void DisconnectFromEndpoint( + absl::string_view service_id, absl::string_view endpoint_id, + std::function callback) = 0; + + virtual void SendPayload(absl::string_view service_id, + absl::Span endpoint_ids, + std::unique_ptr payload, + std::function callback) = 0; + virtual void CancelPayload(absl::string_view service_id, int64_t payload_id, + std::function callback) = 0; + + virtual void InitiateBandwidthUpgrade( + absl::string_view service_id, absl::string_view endpoint_id, + std::function callback) = 0; + + virtual void AcceptConnection( + absl::string_view service_id, absl::string_view endpoint_id, + PayloadListener payload_listener, + std::function callback) = 0; + + virtual void StopAllEndpoints( + std::function callback) = 0; + + virtual void SetCustomSavePath( + absl::string_view path, std::function callback) = 0; + + virtual std::string Dump() const = 0; +}; + +Status ConvertToStatus(NcStatus status); +Payload ConvertToPayload(NcPayload payload); +NcPayload ConvertToServicePayload(Payload payload); +NcResultCallback BuildResultCallback( + std::function callback); +NcStrategy ConvertToServiceStrategy(Strategy strategy); + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_NEARBY_CONNECTIONS_SERVICE_H_ diff --git a/sharing/nearby_connections_service_impl.cc b/sharing/nearby_connections_service_impl.cc new file mode 100644 index 00000000..c93c3717 --- /dev/null +++ b/sharing/nearby_connections_service_impl.cc @@ -0,0 +1,340 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/nearby_connections_service_impl.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/meta/type_traits.h" +#include "absl/strings/string_view.h" +#include "absl/time/time.h" +#include "absl/types/span.h" +#include "connections/listeners.h" +#include "connections/medium_selector.h" +#include "connections/payload.h" +#include "connections/strategy.h" +#include "internal/analytics/event_logger.h" +#include "sharing/nearby_connections_service.h" +#include "sharing/nearby_connections_types.h" + +namespace nearby { +namespace sharing { +namespace { + +Core* GetService(NearbyConnectionsService::HANDLE handle) { + return reinterpret_cast(handle); +} + +} // namespace + +NearbyConnectionsServiceImpl::NearbyConnectionsServiceImpl( + nearby::analytics::EventLogger* event_logger) { + static ServiceControllerRouter* router = new ServiceControllerRouter(); + static Core* core = new Core(event_logger, router); + service_handle_ = core; +} + +NearbyConnectionsServiceImpl::~NearbyConnectionsServiceImpl() = default; + +void NearbyConnectionsServiceImpl::StartAdvertising( + absl::string_view service_id, const std::vector& endpoint_info, + AdvertisingOptions advertising_options, + ConnectionListener advertising_listener, + std::function callback) { + advertising_listener_ = std::move(advertising_listener); + + NcAdvertisingOptions options{}; + options.strategy = ConvertToServiceStrategy(advertising_options.strategy); + options.allowed.ble = advertising_options.allowed_mediums.ble; + options.allowed.bluetooth = advertising_options.allowed_mediums.bluetooth; + options.allowed.web_rtc = advertising_options.allowed_mediums.web_rtc; + options.allowed.wifi_lan = advertising_options.allowed_mediums.wifi_lan; + options.auto_upgrade_bandwidth = advertising_options.auto_upgrade_bandwidth; + options.enforce_topology_constraints = + advertising_options.enforce_topology_constraints; + options.enable_bluetooth_listening = + advertising_options.enable_bluetooth_listening; + options.enable_webrtc_listening = advertising_options.enable_webrtc_listening; + options.fast_advertisement_service_uuid = + advertising_options.fast_advertisement_service_uuid.uuid; + + NcConnectionRequestInfo connection_request_info; + connection_request_info.endpoint_info = + NcByteArray(std::string(endpoint_info.begin(), endpoint_info.end())); + connection_request_info.listener.initiated_cb = + [&](const std::string& endpoint_id, + const NcConnectionResponseInfo& info) { + ConnectionInfo connection_info; + connection_info.authentication_token = info.authentication_token; + std::string remote_end_point = std::string(info.remote_endpoint_info); + connection_info.endpoint_info = std::vector( + remote_end_point.begin(), remote_end_point.end()); + connection_info.is_incoming_connection = info.is_incoming_connection; + std::string raw_authentication_token = + std::string(info.raw_authentication_token); + connection_info.raw_authentication_token = std::vector( + raw_authentication_token.begin(), raw_authentication_token.end()); + advertising_listener_.initiated_cb(endpoint_id, connection_info); + }; + connection_request_info.listener.accepted_cb = + [&](const std::string& endpoint_id) { + advertising_listener_.accepted_cb(endpoint_id); + }; + connection_request_info.listener.rejected_cb = + [&](const std::string& endpoint_info, NcStatus status) { + advertising_listener_.rejected_cb(endpoint_info, + ConvertToStatus(status)); + }; + connection_request_info.listener.disconnected_cb = + [&](const std::string& endpoint_info) { + advertising_listener_.disconnected_cb(endpoint_info); + }; + connection_request_info.listener.bandwidth_changed_cb = + [&](const std::string& endpoint_id, NcMedium medium) { + advertising_listener_.bandwidth_changed_cb(endpoint_id, + static_cast(medium)); + }; + + GetService(service_handle_) + ->StartAdvertising(service_id, options, + std::move(connection_request_info), + BuildResultCallback(callback)); +} + +void NearbyConnectionsServiceImpl::StopAdvertising( + absl::string_view service_id, std::function callback) { + GetService(service_handle_)->StopAdvertising(BuildResultCallback(callback)); +} + +void NearbyConnectionsServiceImpl::StartDiscovery( + absl::string_view service_id, DiscoveryOptions discovery_options, + DiscoveryListener discovery_listener, + std::function callback) { + discovery_listener_ = std::move(discovery_listener); + + NcDiscoveryOptions options{}; + options.strategy = ConvertToServiceStrategy(discovery_options.strategy); + options.allowed.ble = discovery_options.allowed_mediums.ble; + options.allowed.bluetooth = discovery_options.allowed_mediums.bluetooth; + options.allowed.web_rtc = discovery_options.allowed_mediums.web_rtc; + options.allowed.wifi_lan = discovery_options.allowed_mediums.wifi_lan; + if (discovery_options.fast_advertisement_service_uuid.has_value()) { + options.fast_advertisement_service_uuid = + (*discovery_options.fast_advertisement_service_uuid).uuid; + } + + options.is_out_of_band_connection = + discovery_options.is_out_of_band_connection; + NcDiscoveryListener listener; + listener.endpoint_found_cb = [this](const std::string& endpoint_id, + const NcByteArray& endpoint_info, + const std::string& service_id) { + std::string endpoint_info_data = std::string(endpoint_info); + discovery_listener_.endpoint_found_cb( + endpoint_id, + DiscoveredEndpointInfo(std::vector(endpoint_info_data.begin(), + endpoint_info_data.end()), + service_id)); + }; + listener.endpoint_lost_cb = [this](const std::string& endpoint_id) { + discovery_listener_.endpoint_lost_cb(endpoint_id); + }; + listener.endpoint_distance_changed_cb = [this](const std::string& endpoint_id, + NcDistanceInfo distance_info) { + discovery_listener_.endpoint_distance_changed_cb( + endpoint_id, static_cast(distance_info)); + }; + + GetService(service_handle_) + ->StartDiscovery(service_id, options, std::move(listener), + BuildResultCallback(callback)); +} + +void NearbyConnectionsServiceImpl::StopDiscovery( + absl::string_view service_id, std::function callback) { + GetService(service_handle_)->StopDiscovery(BuildResultCallback(callback)); +} + +void NearbyConnectionsServiceImpl::RequestConnection( + absl::string_view service_id, const std::vector& endpoint_info, + absl::string_view endpoint_id, ConnectionOptions connection_options, + ConnectionListener connection_listener, + std::function callback) { + connection_listener_ = std::move(connection_listener); + NcConnectionOptions options{}; + options.allowed.ble = connection_options.allowed_mediums.ble; + options.allowed.bluetooth = connection_options.allowed_mediums.bluetooth; + options.allowed.web_rtc = connection_options.allowed_mediums.web_rtc; + options.allowed.wifi_lan = connection_options.allowed_mediums.wifi_lan; + options.allowed.wifi_hotspot = + connection_options.allowed_mediums.wifi_hotspot; + if (connection_options.keep_alive_interval.has_value()) { + options.keep_alive_interval_millis = + *connection_options.keep_alive_interval / absl::Milliseconds(1); + } + if (connection_options.keep_alive_timeout.has_value()) { + options.keep_alive_timeout_millis = + *connection_options.keep_alive_timeout / absl::Milliseconds(1); + } + if (connection_options.remote_bluetooth_mac_address.has_value()) { + auto mac_address = *connection_options.remote_bluetooth_mac_address; + options.remote_bluetooth_mac_address = + NcByteArray(std::string(mac_address.begin(), mac_address.end())); + } + NcConnectionRequestInfo connection_request_info; + connection_request_info.endpoint_info = + NcByteArray(std::string(endpoint_info.begin(), endpoint_info.end())); + connection_request_info.listener.initiated_cb = + [&](const std::string& endpoint_id, + const NcConnectionResponseInfo& info) { + ConnectionInfo connection_info; + connection_info.authentication_token = info.authentication_token; + std::string remote_end_point = std::string(info.remote_endpoint_info); + connection_info.endpoint_info = std::vector( + remote_end_point.begin(), remote_end_point.end()); + connection_info.is_incoming_connection = info.is_incoming_connection; + std::string raw_authentication_token = + std::string(info.raw_authentication_token); + connection_info.raw_authentication_token = std::vector( + raw_authentication_token.begin(), raw_authentication_token.end()); + connection_listener_.initiated_cb(endpoint_id, connection_info); + }; + connection_request_info.listener.accepted_cb = + [&](const std::string& endpoint_id) { + connection_listener_.accepted_cb(endpoint_id); + }; + connection_request_info.listener.rejected_cb = + [&](const std::string& endpoint_info, NcStatus status) { + connection_listener_.rejected_cb(endpoint_info, + ConvertToStatus(status)); + }; + connection_request_info.listener.disconnected_cb = + [&](const std::string& endpoint_info) { + connection_listener_.disconnected_cb(endpoint_info); + }; + connection_request_info.listener.bandwidth_changed_cb = + [&](const std::string& endpoint_id, NcMedium medium) { + connection_listener_.bandwidth_changed_cb(endpoint_id, + static_cast(medium)); + }; + + GetService(service_handle_) + ->RequestConnection(endpoint_id, std::move(connection_request_info), + options, BuildResultCallback(callback)); +} + +void NearbyConnectionsServiceImpl::DisconnectFromEndpoint( + absl::string_view service_id, absl::string_view endpoint_id, + std::function callback) { + GetService(service_handle_) + ->DisconnectFromEndpoint(endpoint_id, BuildResultCallback(callback)); +} + +void NearbyConnectionsServiceImpl::SendPayload( + absl::string_view service_id, absl::Span endpoint_ids, + std::unique_ptr payload, + std::function callback) { + GetService(service_handle_) + ->SendPayload(endpoint_ids, ConvertToServicePayload(*payload), + BuildResultCallback(callback)); +} + +void NearbyConnectionsServiceImpl::CancelPayload( + absl::string_view service_id, int64_t payload_id, + std::function callback) { + GetService(service_handle_) + ->CancelPayload(payload_id, BuildResultCallback(callback)); +} + +void NearbyConnectionsServiceImpl::InitiateBandwidthUpgrade( + absl::string_view service_id, absl::string_view endpoint_id, + std::function callback) { + GetService(service_handle_) + ->InitiateBandwidthUpgrade(endpoint_id, BuildResultCallback(callback)); +} + +void NearbyConnectionsServiceImpl::AcceptConnection( + absl::string_view service_id, absl::string_view endpoint_id, + PayloadListener payload_listener, + std::function callback) { + payload_listeners_.emplace(endpoint_id, std::move(payload_listener)); + NcPayloadListener service_payload_listener{ + .payload_cb = + [&](absl::string_view endpoint_id, NcPayload payload) { + auto payload_listener = payload_listeners_.find(endpoint_id); + if (payload_listener == payload_listeners_.end()) { + return; + } + + NEARBY_LOGS(VERBOSE) << "payload callback id=" << payload.GetId(); + + switch (payload.GetType()) { + case NcPayloadType::kBytes: + case NcPayloadType::kFile: + payload_listener->second.payload_cb( + endpoint_id, ConvertToPayload(std::move(payload))); + break; + default: + // TODO(b/219814719); support stream payload. + break; + } + }, + .payload_progress_cb = + [&](absl::string_view endpoint_id, + const NcPayloadProgressInfo& info) { + PayloadTransferUpdate transfer_update; + transfer_update.bytes_transferred = info.bytes_transferred; + transfer_update.payload_id = info.payload_id; + transfer_update.status = static_cast(info.status); + transfer_update.total_bytes = info.total_bytes; + NEARBY_LOGS(VERBOSE) + << "payload transfer update id=" << info.payload_id; + auto payload_listener = payload_listeners_.find(endpoint_id); + if (payload_listener != payload_listeners_.end()) { + payload_listener->second.payload_progress_cb(endpoint_id, + transfer_update); + } + }}; + + GetService(service_handle_) + ->AcceptConnection(endpoint_id, std::move(service_payload_listener), + BuildResultCallback(callback)); +} + +void NearbyConnectionsServiceImpl::StopAllEndpoints( + std::function callback) { + GetService(service_handle_)->StopAllEndpoints(BuildResultCallback(callback)); +} + +void NearbyConnectionsServiceImpl::SetCustomSavePath( + absl::string_view path, std::function callback) { + GetService(service_handle_) + ->SetCustomSavePath(path, BuildResultCallback(callback)); +} + +std::string NearbyConnectionsServiceImpl::Dump() const { + return GetService(service_handle_)->Dump(); +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/nearby_connections_service_impl.h b/sharing/nearby_connections_service_impl.h new file mode 100644 index 00000000..11ce97d3 --- /dev/null +++ b/sharing/nearby_connections_service_impl.h @@ -0,0 +1,103 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_NEARBY_CONNECTIONS_SERVICE_IMPL_H_ +#define THIRD_PARTY_NEARBY_SHARING_NEARBY_CONNECTIONS_SERVICE_IMPL_H_ + +#include + +#include +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "internal/analytics/event_logger.h" +#include "sharing/nearby_connections_service.h" +#include "sharing/nearby_connections_types.h" + +namespace nearby { +namespace sharing { + +class NearbyConnectionsServiceImpl : public NearbyConnectionsService { + public: + explicit NearbyConnectionsServiceImpl( + nearby::analytics::EventLogger* event_logger = nullptr); + NearbyConnectionsServiceImpl() = delete; + ~NearbyConnectionsServiceImpl() override; + + void StartAdvertising(absl::string_view service_id, + const std::vector& endpoint_info, + AdvertisingOptions advertising_options, + ConnectionListener advertising_listener, + std::function callback) override; + void StopAdvertising(absl::string_view service_id, + std::function callback) override; + + void StartDiscovery(absl::string_view service_id, + DiscoveryOptions discovery_options, + DiscoveryListener discovery_listener, + std::function callback) override; + void StopDiscovery(absl::string_view service_id, + std::function callback) override; + + void RequestConnection(absl::string_view service_id, + const std::vector& endpoint_info, + absl::string_view endpoint_id, + ConnectionOptions connection_options, + ConnectionListener connection_listener, + std::function callback) override; + + void DisconnectFromEndpoint( + absl::string_view service_id, absl::string_view endpoint_id, + std::function callback) override; + + void SendPayload(absl::string_view service_id, + absl::Span endpoint_ids, + std::unique_ptr payload, + std::function callback) override; + void CancelPayload(absl::string_view service_id, int64_t payload_id, + std::function callback) override; + + void InitiateBandwidthUpgrade( + absl::string_view service_id, absl::string_view endpoint_id, + std::function callback) override; + + void AcceptConnection(absl::string_view service_id, + absl::string_view endpoint_id, + PayloadListener payload_listener, + std::function callback) override; + + void StopAllEndpoints(std::function callback) override; + + void SetCustomSavePath(absl::string_view path, + std::function callback) override; + + std::string Dump() const override; + + private: + HANDLE service_handle_ = nullptr; + + ConnectionListener advertising_listener_; + DiscoveryListener discovery_listener_; + ConnectionListener connection_listener_; + absl::flat_hash_map payload_listeners_; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_NEARBY_CONNECTIONS_SERVICE_IMPL_H_ diff --git a/sharing/nearby_connections_stream_buffer_manager.cc b/sharing/nearby_connections_stream_buffer_manager.cc new file mode 100644 index 00000000..8d3733f8 --- /dev/null +++ b/sharing/nearby_connections_stream_buffer_manager.cc @@ -0,0 +1,121 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#include "sharing/nearby_connections_stream_buffer_manager.h" + +#include +#include + +#include +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/meta/type_traits.h" +#include "sharing/internal/public/logging.h" + +namespace nearby { +namespace sharing { + +NearbyConnectionsStreamBufferManager::PayloadWithBuffer::PayloadWithBuffer( + NcPayload payload) + : buffer_payload(std::move(payload)) {} + +NearbyConnectionsStreamBufferManager::NearbyConnectionsStreamBufferManager() = + default; + +NearbyConnectionsStreamBufferManager::~NearbyConnectionsStreamBufferManager() = + default; + +void NearbyConnectionsStreamBufferManager::StartTrackingPayload( + NcPayload payload) { + int64_t payload_id = payload.GetId(); + NL_LOG(INFO) << "Starting to track stream payload with ID " << payload_id; + + id_to_payload_with_buffer_map_[payload_id] = + std::make_unique(std::move(payload)); +} + +bool NearbyConnectionsStreamBufferManager::IsTrackingPayload( + int64_t payload_id) const { + return id_to_payload_with_buffer_map_.contains(payload_id); +} + +void NearbyConnectionsStreamBufferManager::StopTrackingFailedPayload( + int64_t payload_id) { + id_to_payload_with_buffer_map_.erase(payload_id); + NL_LOG(INFO) << "Stopped tracking payload with ID " << payload_id << " " + << "and cleared internal memory."; +} + +void NearbyConnectionsStreamBufferManager::HandleBytesTransferred( + int64_t payload_id, int64_t cumulative_bytes_transferred_so_far) { + auto it = id_to_payload_with_buffer_map_.find(payload_id); + if (it == id_to_payload_with_buffer_map_.end()) { + NL_LOG(ERROR) << "Attempted to handle stream bytes for payload with ID " + << payload_id << ", but this payload was not being tracked."; + return; + } + + PayloadWithBuffer* payload_with_buffer = it->second.get(); + + // We only need to read the new bytes which have not already been inserted + // into the buffer. + size_t bytes_to_read = + cumulative_bytes_transferred_so_far - payload_with_buffer->buffer.size(); + + NcInputStream* stream = payload_with_buffer->buffer_payload.AsStream(); + if (!stream) { + NL_LOG(ERROR) << "Payload with ID " << payload_id << " is not a stream " + << "payload; transfer has failed."; + StopTrackingFailedPayload(payload_id); + return; + } + + NcExceptionOr bytes = stream->Read(bytes_to_read); + if (!bytes.ok()) { + NL_LOG(ERROR) << "Payload with ID " << payload_id << " encountered " + << "exception while reading; transfer has failed."; + StopTrackingFailedPayload(payload_id); + return; + } + // Empty `bytes` means the End Of File. There should be at `bytes_to_read` + // bytes available in the input stream, so we should never face the EOF + // condition. + NL_DCHECK(!bytes.result().Empty()); + + payload_with_buffer->buffer += static_cast(bytes.result()); +} + +NcByteArray +NearbyConnectionsStreamBufferManager::GetCompletePayloadAndStopTracking( + int64_t payload_id) { + auto it = id_to_payload_with_buffer_map_.find(payload_id); + if (it == id_to_payload_with_buffer_map_.end()) { + NL_LOG(ERROR) << "Attempted to get complete payload with ID " << payload_id + << ", but this payload was not being tracked."; + return NcByteArray(); + } + + NcByteArray complete_payload(it->second->buffer); + + // Close stream and erase internal state before returning payload. + it->second->buffer_payload.AsStream()->Close(); + id_to_payload_with_buffer_map_.erase(it); + + return complete_payload; +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/nearby_connections_stream_buffer_manager.h b/sharing/nearby_connections_stream_buffer_manager.h new file mode 100644 index 00000000..1091abde --- /dev/null +++ b/sharing/nearby_connections_stream_buffer_manager.h @@ -0,0 +1,93 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_NEARBY_CONNECTIONS_STREAM_BUFFER_MANAGER_H_ +#define THIRD_PARTY_NEARBY_SHARING_NEARBY_CONNECTIONS_STREAM_BUFFER_MANAGER_H_ + +#include + +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "connections/core.h" +#include "connections/payload.h" +#include "internal/platform/exception.h" +#include "internal/platform/input_stream.h" + +namespace nearby { +namespace sharing { + +using NcByteArray = ::nearby::ByteArray; +using NcException = ::nearby::Exception; +template +using NcExceptionOr = ::nearby::ExceptionOr; +using NcInputStream = ::nearby::InputStream; +using NcPayload = ::nearby::connections::Payload; + +// Manages payloads with type "stream" received over Nearby Connections. Streams +// over a certain size are delivered in chunks and need to be reassembled upon +// completion. +// +// Clients should start tracking a payload via StartTrackingPayload(). When +// more bytes have been transferred, clients should invoke +// HandleBytesTransferred(), passing the cumulative number of bytes that have +// been transferred. When all bytes have finished being transferred, clients +// should invoke GetCompletePayloadAndStopTracking() to get the complete, +// reassembled payload. +// +// If a payload has failed or been canceled, clients should invoke +// StopTrackingFailedPayload() so that this class can clean up its internal +// buffer. +class NearbyConnectionsStreamBufferManager { + public: + NearbyConnectionsStreamBufferManager(); + ~NearbyConnectionsStreamBufferManager(); + + // Starts tracking the given payload. + void StartTrackingPayload(NcPayload payload); + + // Returns whether a payload with the provided ID is being tracked. + bool IsTrackingPayload(int64_t payload_id) const; + + // Stops tracking the payload with the provided ID and cleans up internal + // memory being used to buffer the partially-completed transfer. + void StopTrackingFailedPayload(int64_t payload_id); + + // Processes incoming bytes by reading from the input stream. + void HandleBytesTransferred(int64_t payload_id, + int64_t cumulative_bytes_transferred_so_far); + + // Returns the completed buffer and deletes internal buffers. + NcByteArray GetCompletePayloadAndStopTracking(int64_t payload_id); + + private: + struct PayloadWithBuffer { + explicit PayloadWithBuffer(NcPayload payload); + + NcPayload buffer_payload; + + // Partially-complete buffer which contains the bytes which have been read + // up to this point. + std::string buffer; + }; + + absl::flat_hash_map> + id_to_payload_with_buffer_map_; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_NEARBY_CONNECTIONS_STREAM_BUFFER_MANAGER_H_ diff --git a/sharing/nearby_connections_stream_buffer_manager_test.cc b/sharing/nearby_connections_stream_buffer_manager_test.cc new file mode 100644 index 00000000..e0d1715a --- /dev/null +++ b/sharing/nearby_connections_stream_buffer_manager_test.cc @@ -0,0 +1,176 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/nearby_connections_stream_buffer_manager.h" + +#include + +#include +#include +#include +#include +#include + +#include "gtest/gtest.h" + +namespace nearby { +namespace sharing { +namespace { + +class FakeStream : public NcInputStream { + public: + FakeStream() = default; + ~FakeStream() override = default; + FakeStream(const FakeStream&) = delete; + FakeStream& operator=(const FakeStream&) = delete; + + NcExceptionOr Read(std::int64_t size) override { + if (should_throw_exception_) { + return NcException::kIo; + } + return NcExceptionOr(NcByteArray(std::string(size, '\0'))); + } + + NcExceptionOr Skip(size_t offset) override { + if (should_throw_exception_) { + return NcException::kIo; + } + return NcExceptionOr(0); + } + + NcException Close() override { + if (should_throw_exception_) { + return {.value = NcException::kIo}; + } + return {.value = NcException::kSuccess}; + } + + bool should_throw_exception_ = false; +}; + +} // namespace + +struct CreatePayloadStreamResult { + NcPayload payload; + FakeStream* stream; +}; + +class NearbyConnectionsStreamBufferManagerTest : public testing::Test { + protected: + CreatePayloadStreamResult CreatePayload(int64_t payload_id) { + CreatePayloadStreamResult payload_and_stream; + auto stream = std::make_unique(); + FakeStream* stream_ptr = stream.get(); + + payload_and_stream.stream = stream_ptr; + + NcPayload payload(payload_id, std::move(stream)); + payload_and_stream.payload = std::move(payload); + + return payload_and_stream; + } + + NearbyConnectionsStreamBufferManager buffer_manager_; +}; + +TEST_F(NearbyConnectionsStreamBufferManagerTest, + SingleStreamTrackingAndCheckingTransferredSize) { + CreatePayloadStreamResult payload_and_stream = + CreatePayload(/*payload_id=*/1); + + buffer_manager_.StartTrackingPayload(std::move(payload_and_stream.payload)); + EXPECT_TRUE(buffer_manager_.IsTrackingPayload(/*payload_id=*/1)); + + buffer_manager_.HandleBytesTransferred( + /*payload_id=*/1, + /*cumulative_bytes_transferred_so_far=*/1980); + buffer_manager_.HandleBytesTransferred( + /*payload_id=*/1, + /*cumulative_bytes_transferred_so_far=*/2500); + + NcByteArray array = + buffer_manager_.GetCompletePayloadAndStopTracking(/*payload_id=*/1); + EXPECT_FALSE(buffer_manager_.IsTrackingPayload(/*payload_id=*/1)); + EXPECT_EQ(array.size(), 2500u); +} + +TEST_F(NearbyConnectionsStreamBufferManagerTest, + MultipleStreamTrackingAndCheckingTransferredSize) { + CreatePayloadStreamResult payload_and_stream_1 = + CreatePayload(/*payload_id=*/1); + CreatePayloadStreamResult payload_and_stream_2 = + CreatePayload(/*payload_id=*/2); + + buffer_manager_.StartTrackingPayload(std::move(payload_and_stream_1.payload)); + EXPECT_TRUE(buffer_manager_.IsTrackingPayload(/*payload_id=*/1)); + + buffer_manager_.StartTrackingPayload(std::move(payload_and_stream_2.payload)); + EXPECT_TRUE(buffer_manager_.IsTrackingPayload(/*payload_id=*/2)); + + buffer_manager_.HandleBytesTransferred( + /*payload_id=*/1, + /*cumulative_bytes_transferred_so_far=*/1980); + buffer_manager_.HandleBytesTransferred( + /*payload_id=*/2, + /*cumulative_bytes_transferred_so_far=*/1980); + buffer_manager_.HandleBytesTransferred( + /*payload_id=*/1, + /*cumulative_bytes_transferred_so_far=*/2500); + buffer_manager_.HandleBytesTransferred( + /*payload_id=*/2, + /*cumulative_bytes_transferred_so_far=*/3000); + + NcByteArray array1 = + buffer_manager_.GetCompletePayloadAndStopTracking(/*payload_id=*/1); + EXPECT_FALSE(buffer_manager_.IsTrackingPayload(/*payload_id=*/1)); + EXPECT_EQ(array1.size(), 2500u); + + NcByteArray array2 = + buffer_manager_.GetCompletePayloadAndStopTracking(/*payload_id=*/2); + EXPECT_FALSE(buffer_manager_.IsTrackingPayload(/*payload_id=*/2)); + EXPECT_EQ(array2.size(), 3000u); +} + +TEST_F(NearbyConnectionsStreamBufferManagerTest, + SingleStreamCheckTrackingFailure) { + CreatePayloadStreamResult payload_and_stream = + CreatePayload(/*payload_id=*/1); + + buffer_manager_.StartTrackingPayload(std::move(payload_and_stream.payload)); + EXPECT_TRUE(buffer_manager_.IsTrackingPayload(/*payload_id=*/1)); + + buffer_manager_.HandleBytesTransferred( + /*payload_id=*/1, + /*cumulative_bytes_transferred_so_far=*/1980); + buffer_manager_.StopTrackingFailedPayload(/*payload_id=*/1); + EXPECT_FALSE(buffer_manager_.IsTrackingPayload(/*payload_id=*/1)); +} + +TEST_F(NearbyConnectionsStreamBufferManagerTest, SingleStreamCheckException) { + CreatePayloadStreamResult payload_and_stream = + CreatePayload(/*payload_id=*/1); + + buffer_manager_.StartTrackingPayload(std::move(payload_and_stream.payload)); + EXPECT_TRUE(buffer_manager_.IsTrackingPayload(/*payload_id=*/1)); + + payload_and_stream.stream->should_throw_exception_ = true; + buffer_manager_.HandleBytesTransferred( + /*payload_id=*/1, + /*cumulative_bytes_transferred_so_far=*/1980); + + EXPECT_FALSE(buffer_manager_.IsTrackingPayload(/*payload_id=*/1)); +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/nearby_connections_types.h b/sharing/nearby_connections_types.h new file mode 100644 index 00000000..783625b2 --- /dev/null +++ b/sharing/nearby_connections_types.h @@ -0,0 +1,475 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_NEARBY_CONNECTIONS_TYPES_H_ +#define THIRD_PARTY_NEARBY_SHARING_NEARBY_CONNECTIONS_TYPES_H_ + +#include +#include + +#include +#include // NOLINT(build/c++17) +#include +#include +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "absl/time/time.h" +#include "internal/crypto_cros/random.h" +#include "sharing/common/compatible_u8_string.h" + +namespace nearby { +namespace sharing { + +struct Uuid { + Uuid() = default; + explicit Uuid(std::string uuid) { this->uuid = uuid; } + + std::string uuid; +}; + +// Generic result status of NearbyConnections API calls. These values are +// persisted to logs. Entries should not be renumbered and numeric values should +// never be reused. +// LINT.IfChange(status_enum) +enum class Status { + // The operation was successful. + kSuccess = 0, + // The operation failed, without any more information. + kError = 1, + // The app called an API method out of order (i.e. another method is expected + // to be called first). + kOutOfOrderApiCall = 2, + // The app already has active operations (advertising, discovering, or + // connected to other devices) with another Strategy. Stop these operations on + // the current Strategy before trying to advertise or discover with a new + // Strategy. + kAlreadyHaveActiveStrategy = 3, + // The app is already advertising; call StopAdvertising() before trying to + // advertise again. + kAlreadyAdvertising = 4, + // The app is already discovering; call StopDiscovery() before trying to + // discover again. + kAlreadyDiscovering = 5, + // NC is already listening for incoming connections from remote endpoints. + kAlreadyListening = 6, + // An attempt to read from/write to a connected remote endpoint failed. If + // this occurs repeatedly, consider invoking DisconnectFromEndpoint(). + kEndpointIOError = 7, + // An attempt to interact with a remote endpoint failed because it's unknown + // to us -- it's either an endpoint that was never discovered, or an endpoint + // that never connected to us (both of which are indicative of bad input from + // the client app). + kEndpointUnknown = 8, + // The remote endpoint rejected the connection request. + kConnectionRejected = 9, + // The app is already connected to the specified endpoint. Multiple + // connections to a remote endpoint cannot be maintained simultaneously. + kAlreadyConnectedToEndpoint = 10, + // The remote endpoint is not connected; messages cannot be sent to it. + kNotConnectedToEndpoint = 11, + // There was an error trying to use the device's Bluetooth capabilities. + kBluetoothError = 12, + // There was an error trying to use the device's Bluetooth Low Energy + // capabilities. + kBleError = 13, + // There was an error trying to use the device's Wi-Fi capabilities. + kWifiLanError = 14, + // An attempt to interact with an in-flight Payload failed because it's + // unknown to us. + kPayloadUnknown = 15, + // The connection was reset + kReset = 16, + // The connection timed out + kTimeout = 17, + // No status is available + kUnknown = 18, + // Value of the next enum variant. + kNextValue = 19, +}; +// LINT.ThenChange( +// ../connections/status.h:status_enum, +// nearby_connections_manager.cc:status_enum, +// nearby_connections_types_test.cc:status_enum +// ) + +// Information about a connection that is being initiated. +struct ConnectionInfo { + // A short human-readable authentication token that has been given to both + // devices. + std::string authentication_token; + // The raw (significantly longer) version of the authentication token of + // authentication_token -- this is intended for headless authentication, + // typically on devices with no output capabilities, where the authentication + // is purely programmatic and does not have the luxury of human intervention. + std::vector raw_authentication_token; + // Information that represents the remote device. + std::vector endpoint_info; + // True if the connection request was initiated from a remote device. False if + // this device was the one to try and initiate the connection. + bool is_incoming_connection; + // Connection status used for analytics + Status connection_layer_status = Status::kUnknown; +}; + +// Information about an endpoint when it's discovered. +struct DiscoveredEndpointInfo { + DiscoveredEndpointInfo() = default; + DiscoveredEndpointInfo(std::vector endpoint_info, + std::string service_id) { + this->endpoint_info = std::move(endpoint_info); + this->service_id = std::move(service_id); + } + + // Information advertised by the remote endpoint. + std::vector endpoint_info; + // The ID of the service advertised by the remote endpoint. + std::string service_id; +}; + +// The Strategy to be used when discovering or advertising to Nearby devices. +// The Strategy defines the connectivity requirements for the device, and the +// topology constraints of the connection. +enum class Strategy { + // Peer-to-peer strategy that supports an M-to-N, or cluster-shaped, + // connection topology. In other words, this enables connecting amorphous + // clusters of devices within radio range (~100m), where each device can both + // initiate outgoing connections to M other devices and accept incoming + // connections from N other devices. + kP2pCluster, + // Peer-to-peer strategy that supports a 1-to-N, or star-shaped, connection + // topology. In other words, this enables connecting devices within radio + // range (~100m) in a star shape, where each device can, at any given time, + // play the role of either a hub (where it can accept incoming connections + // from N other devices), or a spoke (where it can initiate an outgoing + // connection to a single hub), but not both. + kP2pStar, + // Peer-to-peer strategy that supports a 1-to-1 connection topology. In other + // words, this enables connecting to a single device within radio range + // (~100m). This strategy will give the absolute highest bandwidth, but will + // not allow multiple connections at a time. + kP2pPointToPoint, +}; + +// A selection of on/off toggles to define a set of allowed mediums. +struct MediumSelection { + MediumSelection() = default; + MediumSelection(bool bluetooth, bool ble, bool web_rtc, bool wifi_lan, + bool wifi_hotspot) { + this->bluetooth = bluetooth; + this->ble = ble; + this->web_rtc = web_rtc; + this->wifi_lan = wifi_lan; + this->wifi_hotspot = wifi_hotspot; + } + + // Whether Bluetooth should be allowed. + bool bluetooth = true; + // Whether BLE should be allowed. + bool ble = true; + // Whether WebRTC should be allowed. + bool web_rtc = true; + // Whether Wi-Fi LAN should be allowed. + bool wifi_lan = true; + // Whether Wi-Fi Hotspot should be allowed + bool wifi_hotspot = true; +}; + +// Options for a call to NearbyConnections::StartAdvertising(). +struct AdvertisingOptions { + AdvertisingOptions() = default; + AdvertisingOptions(Strategy strategy, MediumSelection allowed_mediums, + bool auto_upgrade_bandwidth, + bool enforce_topology_constraints, + bool enable_bluetooth_listening, + bool enable_webrtc_listening, + Uuid fast_advertisement_service_uuid) { + this->strategy = strategy; + this->allowed_mediums = allowed_mediums; + this->auto_upgrade_bandwidth = auto_upgrade_bandwidth; + this->enforce_topology_constraints = enforce_topology_constraints; + this->enable_bluetooth_listening = enable_bluetooth_listening; + this->enable_webrtc_listening = enable_webrtc_listening; + this->fast_advertisement_service_uuid = fast_advertisement_service_uuid; + } + + // The strategy to use for advertising. Must match the strategy used in + // DiscoveryOptions for remote devices to see this advertisement. + Strategy strategy; + // Describes which mediums are allowed to be used for advertising. Note that + // allowing an otherwise unsupported medium is ok. Only the intersection of + // allowed and supported mediums will be used to advertise. + MediumSelection allowed_mediums; + // By default, this option is true. If false, we will not attempt to upgrade + // the bandwidth until a call to InitiateBandwidthUpgrade() is made. + bool auto_upgrade_bandwidth = true; + // By default, this option is true. If false, restrictions on topology will be + // ignored. This allows you treat all strategies as kP2pCluster (N to M), + // although bandwidth will be severely throttled if you don't maintain the + // original topology. When used in conjunction with auto_upgrade_bandwidth, + // you can initially connect as a kP2pCluster and then trim connections until + // you match kP2pStar or kP2pPointToPoint before upgrading the bandwidth. + bool enforce_topology_constraints = true; + // By default, this option is false. If true, this allows listening on + // incoming Bluetooth Classic connections while BLE advertising. + bool enable_bluetooth_listening = false; + // By default, this option is false. If true, this allows listening on + // incoming WebRTC connections while advertising. + bool enable_webrtc_listening = false; + // Optional. If set, BLE advertisements will be in their "fast advertisement" + // form, use this UUID, and non-connectable; if empty, BLE advertisements + // will otherwise be normal and connectable. + Uuid fast_advertisement_service_uuid; +}; + +// Options for a call to NearbyConnections::StartDiscovery(). +struct DiscoveryOptions { + DiscoveryOptions() = default; + DiscoveryOptions(Strategy strategy, MediumSelection allowed_mediums, + std::optional fast_advertisement_service_uuid, + bool is_out_of_band_connection) { + this->strategy = strategy; + this->allowed_mediums = allowed_mediums; + this->fast_advertisement_service_uuid = fast_advertisement_service_uuid, + this->is_out_of_band_connection = is_out_of_band_connection; + } + // The strategy to use for discovering. Must match the strategy used in + // AdvertisingOptions in order to see advertisements. + Strategy strategy; + // Describes which mediums are allowed to be used for scanning/discovery. Note + // that allowing an otherwise unsupported medium is ok. Only the intersection + // of allowed and supported mediums will be used to scan. + MediumSelection allowed_mediums; + // The fast advertisement service id to scan for in BLE. + std::optional fast_advertisement_service_uuid; + // Whether this connection request skips over the normal discovery flow to + // inject discovery information synced outside the Nearby Connections library. + // Intended to be used in conjunction with InjectEndpoint(). + bool is_out_of_band_connection = false; +}; + +// Options for a call to NearbyConnections::RequestConnection(). +struct ConnectionOptions { + ConnectionOptions() = default; + ConnectionOptions( + MediumSelection allowed_mediums, + std::optional> remote_bluetooth_mac_address, + std::optional keep_alive_interval, + std::optional keep_alive_timeout) { + this->allowed_mediums = allowed_mediums; + this->remote_bluetooth_mac_address = remote_bluetooth_mac_address; + this->keep_alive_interval = keep_alive_interval; + this->keep_alive_timeout = keep_alive_timeout; + } + + // Describes which mediums are allowed to be used for connection. Note that + // allowing an otherwise unsupported medium is ok. Only the intersection of + // allowed and supported mediums will be used to connect. + MediumSelection allowed_mediums; + // Bluetooth MAC address of remote device in byte format. + std::optional> remote_bluetooth_mac_address; + // How often to send a keep alive message on the channel. An unspecified or + // negative value will result in the Nearby Connections default of 5 seconds + // being used. + std::optional keep_alive_interval; + // The connection will time out if no message is received on the channel + // for this length of time. An unspecified or negative value will result in + // the Nearby Connections default of 30 seconds being used. + std::optional keep_alive_timeout; +}; + +// The status of the payload transfer at the time of this update. +enum PayloadStatus { + // The payload transfer has completed successfully. + kSuccess, + // The payload transfer failed. + kFailure, + // The payload transfer is still in progress. + kInProgress, + // The payload transfer has been canceled. + kCanceled, +}; + +// Describes the status for an active Payload transfer, either incoming or +// outgoing. Delivered to PayloadListener::OnPayloadTransferUpdate. +struct PayloadTransferUpdate { + PayloadTransferUpdate() = default; + PayloadTransferUpdate(int64_t payload_id, PayloadStatus status, + uint64_t total_bytes, uint64_t bytes_transferred) { + this->payload_id = payload_id; + this->status = status; + this->total_bytes = total_bytes; + this->bytes_transferred = bytes_transferred; + } + + // The ID for the payload related to this update. Clients should match this + // with Payload::id. + int64_t payload_id; + // The status of this payload transfer. Always starts with kInProgress and + // ends with one of kSuccess, kFailure or kCanceled. + PayloadStatus status; + // The total expected bytes of this transfer. + uint64_t total_bytes; + // The number of bytes transferred so far. + uint64_t bytes_transferred; +}; + +// Bandwidth quality of a connection. +enum class BandwidthQuality { + // Unknown connection quality. + kUnknown, + // Low quality, e.g. connected via NFC or BLE. + kLow, + // Medium quality, e.g. connected via Bluetooth Classic. + kMedium, + // High quality, e.g. connected via WebRTC or Wi-Fi LAN. + kHigh, +}; + +// These values are persisted to logs. Entries should not be renumbered and +// numeric values should never be reused. +enum class Medium { + kUnknown = 0, + kMdns = 1, + kBluetooth = 2, + kWifiHotspot = 3, + kBle = 4, + kWifiLan = 5, + kWifiAware = 6, + kNfc = 7, + kWifiDirect = 8, + kWebRtc = 9, + kBleL2Cap = 10, +}; + +// Log severity levels. This is passed as a member of +// NearbyConnectionsDependencies to set the minimum log level in the Nearby +// Connections library. Entries should be kept in sync with the values in +// nearby::sharing::api::LogMessage::Severity. +enum class LogSeverity { + kVerbose = -1, + kInfo = 0, + kWarning = 1, + kError = 2, + kFatal = 3, +}; + +enum class DistanceInfo { + kUnknown = 1, + kVeryClose = 2, + kClose = 3, + kFar = 4, +}; + +struct InputFile { + InputFile() = default; + explicit InputFile(std::filesystem::path path) { this->path = path; } + + std::filesystem::path path; +}; + +// A simple payload containing raw bytes. +struct BytesPayload { + // The bytes of this payload. + std::vector bytes; +}; + +// A file payload representing a file. +struct FilePayload { + // The file to which this payload points to. When sending this payload, the + // NearbyConnections library reads from this file. When receiving a file + // payload it writes to this file. + InputFile file; + int64_t size; + std::string parent_folder; +}; + +// Union of all supported payload types. +struct PayloadContent { + // A Payload consisting of a single byte array. + BytesPayload bytes_payload; + // A Payload representing a file on the device. + FilePayload file_payload; + enum class Type { kUnknown = 0, kBytes = 1, kStream = 2, kFile = 3 }; + Type type; + bool is_bytes() { return type == Type::kBytes; } + bool is_file() { return type == Type::kFile; } + bool is_stream() { return type == Type::kStream; } +}; + +// A Payload sent between devices. Payloads sent with a particular content type +// will be received as that same type on the other device, e.g. the content for +// a Payload of type BytesPayload must be received by reading from the bytes +// field returned by Payload::content::bytes. +struct Payload { + // A unique identifier for this payload. Generated by the sender of the + // payload and used to keep track of the transfer progress. + int64_t id; + // The content of this payload which is one of multiple types, see + // PayloadContent for all possible types. + PayloadContent content; + + Payload() = default; + explicit Payload(std::vector bytes) + : Payload(GenerateId(), std::move(bytes)) {} + + explicit Payload(InputFile file, + absl::string_view parent_folder = absl::string_view()) { + id = std::hash()(GetCompatibleU8String(file.path.u8string())); + + content.type = PayloadContent::Type::kFile; + if (std::filesystem::exists(file.path)) { + content.file_payload.size = std::filesystem::file_size(file.path); + } + + content.file_payload.file = std::move(file); + content.file_payload.parent_folder = std::string(parent_folder); + } + + Payload(int64_t id, std::vector bytes) : id(id) { + content.type = PayloadContent::Type::kBytes; + content.bytes_payload.bytes = std::move(bytes); + } + + Payload(int64_t id, InputFile file, + absl::string_view parent_folder = absl::string_view()) + : id(id) { + content.type = PayloadContent::Type::kFile; + if (std::filesystem::exists(file.path)) { + content.file_payload.size = std::filesystem::file_size(file.path); + } + + content.file_payload.file = std::move(file); + content.file_payload.parent_folder = std::string(parent_folder); + } + + Payload(char* bytes, int size) + : Payload(GenerateId(), std::vector(bytes, bytes + size)) {} + + int64_t GenerateId() { + int64_t id; + crypto::RandBytes(&id, sizeof(id)); + return id; + } +}; + +// Transport type to decide whether to upgrade to a high quality medium. +enum class TransportType { kAny = 0, kNonDisruptive = 1, kHighQuality = 2 }; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_NEARBY_CONNECTIONS_TYPES_H_ diff --git a/sharing/nearby_connections_types_payload_test.cc b/sharing/nearby_connections_types_payload_test.cc new file mode 100644 index 00000000..ad61274d --- /dev/null +++ b/sharing/nearby_connections_types_payload_test.cc @@ -0,0 +1,58 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/nearby_connections_types.h" + +#include // NOLINT + +#include "gmock/gmock.h" +#include "protobuf-matchers/protocol-buffer-matchers.h" +#include "gtest/gtest.h" + +namespace nearby::sharing { +using ::testing::Eq; + +TEST(NearbyConnectionSharingTypesPayloadTest, FromInputFileUTF8) { + InputFile input_file(std::filesystem::u8path("/为甚么/tmp/test.txt")); + Payload payload(input_file); + EXPECT_THAT(payload.id, Eq(7724502655048749887LL)); + EXPECT_THAT(payload.content.type, Eq(PayloadContent::Type::kFile)); +} + +TEST(NearbyConnectionSharingTypesPayloadTest, FromInputFileUTF16) { + InputFile input_file(std::filesystem::path(L"/为甚么/tmp/test.txt")); + Payload payload(input_file); + EXPECT_THAT(payload.id, Eq(7724502655048749887LL)); + EXPECT_THAT(payload.content.type, Eq(PayloadContent::Type::kFile)); +} + +TEST(NearbyConnectionSharingTypesPayloadTest, FromInputFileWithId) { + InputFile input_file(std::filesystem::u8path("/为甚么/tmp/test.txt")); + Payload payload(1234, input_file); + EXPECT_THAT(payload.id, Eq(1234LL)); + EXPECT_THAT(payload.content.type, Eq(PayloadContent::Type::kFile)); +} + +TEST(NearbyConnectionSharingTypesPayloadTest, FromBytes) { + Payload payload({1, 2, 3, 4, 5}); + EXPECT_THAT(payload.content.type, Eq(PayloadContent::Type::kBytes)); +} + +TEST(NearbyConnectionSharingTypesPayloadTest, FromBytesWithId) { + Payload payload(5432, {1, 2, 3, 4, 5}); + EXPECT_THAT(payload.id, Eq(5432LL)); + EXPECT_THAT(payload.content.type, Eq(PayloadContent::Type::kBytes)); +} + +} // namespace nearby::sharing diff --git a/sharing/nearby_connections_types_test.cc b/sharing/nearby_connections_types_test.cc new file mode 100644 index 00000000..9be6d1df --- /dev/null +++ b/sharing/nearby_connections_types_test.cc @@ -0,0 +1,75 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/nearby_connections_types.h" + +#include "gtest/gtest.h" +#include "connections/status.h" +#include "sharing/nearby_connections_manager.h" +#include "sharing/nearby_connections_service.h" + +namespace nearby { +namespace sharing { +namespace { + +using NsStatus = nearby::sharing::Status; +using NcStatus = nearby::connections::Status; + +// LINT.IfChange(status_enum) +TEST(NearbyConnectionSharingTypesTest, TestStatusValueIsSame) { + EXPECT_EQ(NsStatus::kSuccess, ConvertToStatus({NcStatus::kSuccess})); + EXPECT_EQ(NsStatus::kError, ConvertToStatus({NcStatus::kError})); + EXPECT_EQ(NsStatus::kOutOfOrderApiCall, + ConvertToStatus({NcStatus::kOutOfOrderApiCall})); + EXPECT_EQ(NsStatus::kAlreadyHaveActiveStrategy, + ConvertToStatus({NcStatus::kAlreadyHaveActiveStrategy})); + EXPECT_EQ(NsStatus::kAlreadyAdvertising, + ConvertToStatus({NcStatus::kAlreadyAdvertising})); + EXPECT_EQ(NsStatus::kAlreadyDiscovering, + ConvertToStatus({NcStatus::kAlreadyDiscovering})); + EXPECT_EQ(NsStatus::kEndpointIOError, + ConvertToStatus({NcStatus::kEndpointIoError})); + EXPECT_EQ(NsStatus::kEndpointUnknown, + ConvertToStatus({NcStatus::kEndpointUnknown})); + EXPECT_EQ(NsStatus::kConnectionRejected, + ConvertToStatus({NcStatus::kConnectionRejected})); + EXPECT_EQ(NsStatus::kAlreadyConnectedToEndpoint, + ConvertToStatus({NcStatus::kAlreadyConnectedToEndpoint})); + EXPECT_EQ(NsStatus::kAlreadyListening, + ConvertToStatus({NcStatus::kAlreadyListening})); + EXPECT_EQ(NsStatus::kNotConnectedToEndpoint, + ConvertToStatus({NcStatus::kNotConnectedToEndpoint})); + EXPECT_EQ(NsStatus::kBluetoothError, + ConvertToStatus({NcStatus::kBluetoothError})); + EXPECT_EQ(NsStatus::kBleError, ConvertToStatus({NcStatus::kBleError})); + EXPECT_EQ(NsStatus::kWifiLanError, + ConvertToStatus({NcStatus::kWifiLanError})); + EXPECT_EQ(NsStatus::kPayloadUnknown, + ConvertToStatus({NcStatus::kPayloadUnknown})); + EXPECT_EQ(NsStatus::kReset, ConvertToStatus({NcStatus::kReset})); + EXPECT_EQ(NsStatus::kTimeout, ConvertToStatus({NcStatus::kTimeout})); + EXPECT_EQ(NsStatus::kUnknown, ConvertToStatus({NcStatus::kUnknown})); + EXPECT_EQ(NsStatus::kNextValue, ConvertToStatus({NcStatus::kNextValue})); +} +// LINT.ThenChange() + +TEST(NearbyConnectionSharingTypesTest, TestNoFailWithUnknownStatus) { + EXPECT_EQ( + NearbyConnectionsManager::ConnectionsStatusToString(NsStatus::kNextValue), + "Unknown"); +} + +} // namespace +} // namespace sharing +} // namespace nearby diff --git a/sharing/nearby_file_handler.cc b/sharing/nearby_file_handler.cc new file mode 100644 index 00000000..1eb93470 --- /dev/null +++ b/sharing/nearby_file_handler.cc @@ -0,0 +1,135 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/nearby_file_handler.h" + +#include +#include + +#include // NOLINT(build/c++17) +#include +#include +#include +#include + +#include "absl/time/clock.h" +#include "absl/time/time.h" +#include "absl/types/span.h" +#include "internal/platform/task_runner_impl.h" +#include "sharing/common/compatible_u8_string.h" +#include "sharing/internal/public/logging.h" + +namespace nearby { +namespace sharing { +namespace { + +// Called on the FileTaskRunner to actually open the files passed. +std::vector DoOpenFiles( + absl::Span file_paths) { + std::vector files; + for (const auto& file_path : file_paths) { + if (!std::filesystem::exists(file_path)) { + NL_LOG(ERROR) << __func__ << ": Failed to open file. File=" + << GetCompatibleU8String(file_path.u8string()); + return {}; + } + + int64_t size = std::filesystem::file_size(file_path); + if (size < 0) return {}; + + files.push_back({size, file_path}); + } + return files; +} + +std::filesystem::path GenerateUniquePath(const std::filesystem::path& path) { + NL_DCHECK(!path.empty()); + // Nearby Share is not responsible for generating unique paths, any more. + // Nearby Connections contains the logic to ensure there is no conflict. + // Just return the original file path, here. + return path; +} + +} // namespace + +NearbyFileHandler::NearbyFileHandler() { + sequenced_task_runner_ = std::make_unique(1); +} + +NearbyFileHandler::~NearbyFileHandler() = default; + +void NearbyFileHandler::OpenFiles(std::vector file_paths, + OpenFilesCallback callback) { + sequenced_task_runner_->PostTask( + [callback = std::move(callback), file_paths = std::move(file_paths)]() { + auto opened_files = DoOpenFiles(file_paths); + callback(opened_files); + }); +} + +void NearbyFileHandler::GetUniquePath(const std::filesystem::path& file_path, + GetUniquePathCallback callback) { + sequenced_task_runner_->PostTask( + [callback = std::move(callback), file_path]() { + std::filesystem::path unique_path = GenerateUniquePath(file_path); + callback(unique_path); + }); +} + +bool RemoveFile(const std::filesystem::path file) noexcept { + try { + if (!std::filesystem::remove(file)) { + return false; + } + } catch (std::exception) { + return false; + } catch (...) { + return false; + } + return true; +} + +void NearbyFileHandler::DeleteFilesFromDisk( + std::vector file_paths, + DeleteFilesFromDiskCallback callback) { + sequenced_task_runner_->PostTask([callback = std::move(callback), + file_paths = std::move(file_paths)]() { + // wait 1 second to make the file being released from another process. + absl::SleepFor(absl::Seconds(1)); + for (const auto& file_path : file_paths) { + if (!std::filesystem::exists(file_path)) { + continue; + } + if (RemoveFile(file_path)) { + NL_VLOG(1) << __func__ << ": Removed partial file. File=" + << GetCompatibleU8String(file_path.u8string()); + } else { + // Try once more after 3 seconds. + absl::SleepFor(absl::Seconds(3)); + if (RemoveFile(file_path)) { + NL_VLOG(1) << __func__ + << ": Removed partial file after additional delay. File=" + << GetCompatibleU8String(file_path.u8string()); + } else { + NL_LOG(ERROR) << __func__ << "Can't remove file: " + << GetCompatibleU8String(file_path.u8string()); + } + } + } + callback(); + }); +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/nearby_file_handler.h b/sharing/nearby_file_handler.h new file mode 100644 index 00000000..757febc8 --- /dev/null +++ b/sharing/nearby_file_handler.h @@ -0,0 +1,65 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_NEARBY_FILE_HANDLER_H_ +#define THIRD_PARTY_NEARBY_SHARING_NEARBY_FILE_HANDLER_H_ + +#include + +#include // NOLINT(build/c++17) +#include +#include +#include + +#include "internal/platform/task_runner.h" + +namespace nearby { +namespace sharing { + +// This class manages async File IO for Nearby Share file payloads. Opening and +// releasing files need to run on a MayBlock task runner. +class NearbyFileHandler { + public: + struct FileInfo { + int64_t size; + std::filesystem::path file_path; + }; + + using OpenFilesCallback = std::function)>; + using GetUniquePathCallback = std::function; + using DeleteFilesFromDiskCallback = std::function; + + NearbyFileHandler(); + ~NearbyFileHandler(); + + // Open the files given in |file_paths| and return the opened files sizes via + // |callback|. If any file fails to open, return an empty list. + void OpenFiles(std::vector file_paths, + OpenFilesCallback callback); + + void DeleteFilesFromDisk(std::vector file_paths, + DeleteFilesFromDiskCallback callback); + + // Finds a unique path name for |file_path| and runs |callback| with the same. + void GetUniquePath(const std::filesystem::path& file_path, + GetUniquePathCallback callback); + + private: + std::unique_ptr sequenced_task_runner_ = nullptr; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_NEARBY_FILE_HANDLER_H_ diff --git a/sharing/nearby_file_handler_test.cc b/sharing/nearby_file_handler_test.cc new file mode 100644 index 00000000..374b48f1 --- /dev/null +++ b/sharing/nearby_file_handler_test.cc @@ -0,0 +1,151 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/nearby_file_handler.h" + +#include +#include // NOLINT(build/c++17) +#include + +#include "gtest/gtest.h" +#include "absl/synchronization/notification.h" +#include "absl/time/clock.h" +#include "absl/time/time.h" + +namespace nearby { +namespace sharing { +namespace { + +bool CreateFile(std::filesystem::path file_path) { + std::FILE* file = std::fopen(file_path.string().c_str(), "w+"); + if (file == nullptr) { + return false; + } + std::fclose(file); + return true; +} + +bool DeleteFile(std::filesystem::path file_path) { + if (std::filesystem::exists(file_path)) { + return std::filesystem::remove(file_path); + } + + return true; +} + +bool ExistFile(std::filesystem::path file_path) { + if (std::filesystem::exists(file_path)) { + return true; + } + + return false; +} + +TEST(NearbyFileHandler, GetUniquePath) { + NearbyFileHandler nearby_file_handler; + std::filesystem::path unique_path; + absl::Notification notification; + + std::filesystem::path test_file = + std::filesystem::temp_directory_path() / "nearby_nfh_test_abc.jpg"; + std::filesystem::path expected_file = + std::filesystem::temp_directory_path() / "nearby_nfh_test_abc.jpg"; + + ASSERT_TRUE(CreateFile(test_file)); + ASSERT_TRUE(DeleteFile(expected_file)); + + nearby_file_handler.GetUniquePath( + test_file, [¬ification, &unique_path](std::filesystem::path path) { + unique_path = path; + notification.Notify(); + }); + + notification.WaitForNotificationWithTimeout(absl::Seconds(1)); + EXPECT_EQ(unique_path, expected_file); +} + +TEST(NearbyFileHandler, OpenFiles) { + NearbyFileHandler nearby_file_handler; + absl::Notification notification; + std::vector result; + std::filesystem::path test_file = + std::filesystem::temp_directory_path() / "nearby_nfh_test_abc.jpg"; + + ASSERT_TRUE(CreateFile(test_file)); + nearby_file_handler.OpenFiles( + {test_file}, [&result, ¬ification]( + std::vector file_infos) { + result = file_infos; + notification.Notify(); + }); + + notification.WaitForNotificationWithTimeout(absl::Seconds(1)); + EXPECT_EQ(result.size(), 1); + ASSERT_TRUE(DeleteFile(test_file)); +} + +TEST(NearbyFileHandler, DeleteAFileFromDisk) { + NearbyFileHandler nearby_file_handler; + std::filesystem::path test_file = + std::filesystem::temp_directory_path() / "nearby_nfh_test_abc.jpg"; + ASSERT_TRUE(CreateFile(test_file)); + std::vector file_paths; + file_paths.push_back(test_file); + nearby_file_handler.DeleteFilesFromDisk(file_paths, []() {}); + ASSERT_TRUE(ExistFile(test_file)); + absl::SleepFor(absl::Seconds(2)); + ASSERT_FALSE(ExistFile(test_file)); +} + +TEST(NearbyFileHandler, DeleteMultipleFilesFromDisk) { + NearbyFileHandler nearby_file_handler; + std::filesystem::path test_file = + std::filesystem::temp_directory_path() / "nearby_nfh_test_abc.jpg"; + std::filesystem::path test_file2 = + std::filesystem::temp_directory_path() / "nearby_nfh_test_def.jpg"; + std::filesystem::path test_file3 = + std::filesystem::temp_directory_path() / "nearby_nfh_test_ghi.jpg"; + std::vector file_paths; + file_paths = {test_file, test_file2, test_file3}; + // Check it doesn't throw an exception. + nearby_file_handler.DeleteFilesFromDisk(file_paths, []() {}); + ASSERT_FALSE(ExistFile(test_file)); + ASSERT_FALSE(ExistFile(test_file2)); + ASSERT_FALSE(ExistFile(test_file3)); + absl::SleepFor(absl::Seconds(2)); + ASSERT_FALSE(ExistFile(test_file)); + ASSERT_FALSE(ExistFile(test_file2)); + ASSERT_FALSE(ExistFile(test_file3)); +} + +TEST(NearbyFileHandler, TestCallback) { + bool received_callback = false; + NearbyFileHandler nearby_file_handler; + std::filesystem::path test_file = + std::filesystem::temp_directory_path() / "nearby_nfh_test_abc.jpg"; + ASSERT_TRUE(CreateFile(test_file)); + std::vector file_paths; + file_paths.push_back(test_file); + nearby_file_handler.DeleteFilesFromDisk( + file_paths, [&received_callback]() { received_callback = true; }); + ASSERT_FALSE(received_callback); + ASSERT_TRUE(ExistFile(test_file)); + absl::SleepFor(absl::Seconds(2)); + ASSERT_TRUE(received_callback); + ASSERT_FALSE(ExistFile(test_file)); +} + +} // namespace +} // namespace sharing +} // namespace nearby diff --git a/sharing/nearby_share_profile_info_provider_impl.cc b/sharing/nearby_share_profile_info_provider_impl.cc new file mode 100644 index 00000000..e90d1790 --- /dev/null +++ b/sharing/nearby_share_profile_info_provider_impl.cc @@ -0,0 +1,52 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/nearby_share_profile_info_provider_impl.h" + +#include +#include + +#include "internal/platform/device_info.h" +#include "internal/platform/implementation/account_manager.h" + +namespace nearby { +namespace sharing { + +NearbyShareProfileInfoProviderImpl::NearbyShareProfileInfoProviderImpl( + nearby::DeviceInfo& device_info, AccountManager& account_manager) + : device_info_(device_info), account_manager_(account_manager) {} + +NearbyShareProfileInfoProviderImpl::~NearbyShareProfileInfoProviderImpl() = + default; + +std::optional NearbyShareProfileInfoProviderImpl::GetGivenName() + const { + // Use the given name when the user logs in to the backend. + std::optional account = + account_manager_.GetCurrentAccount(); + + if (account.has_value() && !account->given_name.empty()) { + return account->given_name; + } + + return device_info_.GetGivenName(); +} + +std::optional +NearbyShareProfileInfoProviderImpl::GetProfileUserName() const { + return device_info_.GetProfileUserName(); +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/nearby_share_profile_info_provider_impl.h b/sharing/nearby_share_profile_info_provider_impl.h new file mode 100644 index 00000000..b3133f2e --- /dev/null +++ b/sharing/nearby_share_profile_info_provider_impl.h @@ -0,0 +1,53 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_NEARBY_SHARE_PROFILE_INFO_PROVIDER_IMPL_H_ +#define THIRD_PARTY_NEARBY_SHARING_NEARBY_SHARE_PROFILE_INFO_PROVIDER_IMPL_H_ + +#include +#include + +#include "internal/platform/device_info.h" +#include "internal/platform/implementation/account_manager.h" +#include "sharing/common/nearby_share_profile_info_provider.h" + +namespace nearby { +namespace sharing { + +// An implementation of NearbyShareProfileInfoProvider that accesses the actual +// profile data. +class NearbyShareProfileInfoProviderImpl + : public NearbyShareProfileInfoProvider { + public: + NearbyShareProfileInfoProviderImpl(nearby::DeviceInfo& device_info, + AccountManager& account_manager); + NearbyShareProfileInfoProviderImpl( + const NearbyShareProfileInfoProviderImpl&) = delete; + NearbyShareProfileInfoProviderImpl& operator=( + const NearbyShareProfileInfoProviderImpl&) = delete; + ~NearbyShareProfileInfoProviderImpl() override; + + // NearbyShareProfileInfoProvider: + std::optional GetGivenName() const override; + std::optional GetProfileUserName() const override; + + private: + nearby::DeviceInfo& device_info_; + AccountManager& account_manager_; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_NEARBY_SHARE_PROFILE_INFO_PROVIDER_IMPL_H_ diff --git a/sharing/nearby_share_profile_info_provider_impl_test.cc b/sharing/nearby_share_profile_info_provider_impl_test.cc new file mode 100644 index 00000000..5c498a97 --- /dev/null +++ b/sharing/nearby_share_profile_info_provider_impl_test.cc @@ -0,0 +1,125 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/nearby_share_profile_info_provider_impl.h" + +#include +#include + +#include "gtest/gtest.h" +#include "internal/platform/implementation/account_manager.h" +#include "internal/test/fake_account_manager.h" +#include "internal/test/fake_device_info.h" + +namespace nearby { +namespace sharing { +namespace { + +constexpr char kTestAccountId[] = "test_account_id"; +constexpr char kTestAccountGivenName[] = "given_name"; +constexpr char kExpectedTestAccountGivenName[] = "given_name"; +constexpr char kProfileGivenName[] = "Barack"; +constexpr char kProfileProfileUserName[] = "test@gmail.com"; + +} // namespace + +class NearbyShareProfileInfoProviderImplTest : public ::testing::Test { + protected: + NearbyShareProfileInfoProviderImplTest() = default; + ~NearbyShareProfileInfoProviderImplTest() override = default; + + void SetUp() override { + fake_device_info_.SetGivenName(std::nullopt); + fake_device_info_.SetProfileUserName(std::nullopt); + } + + void SetUserGivenName(const std::string& name) { + fake_device_info_.SetGivenName(name); + } + + void SetProfileUserName(const std::string& profile_user_name) { + fake_device_info_.SetProfileUserName(profile_user_name); + } + + FakeDeviceInfo& fake_device_info() { + return fake_device_info_; + } + + FakeAccountManager& fake_account_manager() { return fake_account_manager_; } + + private: + FakeAccountManager fake_account_manager_; + FakeDeviceInfo fake_device_info_; +}; + +TEST_F(NearbyShareProfileInfoProviderImplTest, GivenName) { + SetProfileUserName(kProfileProfileUserName); + NearbyShareProfileInfoProviderImpl profile_info_provider( + fake_device_info(), fake_account_manager()); + + // If no user, return std::nullopt. + EXPECT_FALSE(profile_info_provider.GetGivenName()); + + // If given name is empty, return std::nullopt. + SetUserGivenName(std::string()); + EXPECT_FALSE(profile_info_provider.GetGivenName()); + + SetUserGivenName(kProfileGivenName); + EXPECT_EQ(profile_info_provider.GetGivenName(), kProfileGivenName); +} + +TEST_F(NearbyShareProfileInfoProviderImplTest, ProfileUserName) { + { + // If profile username is empty, return std::nullopt. + SetProfileUserName(std::string()); + NearbyShareProfileInfoProviderImpl profile_info_provider( + fake_device_info(), fake_account_manager()); + EXPECT_FALSE(profile_info_provider.GetProfileUserName()); + } + { + SetProfileUserName(kProfileProfileUserName); + NearbyShareProfileInfoProviderImpl profile_info_provider( + fake_device_info(), fake_account_manager()); + EXPECT_EQ(profile_info_provider.GetProfileUserName(), + kProfileProfileUserName); + } +} + +TEST_F(NearbyShareProfileInfoProviderImplTest, GivenNameUseLoginAccount) { + AccountManager::Account account; + account.id = kTestAccountId; + account.given_name = kTestAccountGivenName; + fake_account_manager().SetAccount(account); + SetUserGivenName(kProfileGivenName); + + NearbyShareProfileInfoProviderImpl profile_info_provider( + fake_device_info(), fake_account_manager()); + EXPECT_EQ(profile_info_provider.GetGivenName(), + kExpectedTestAccountGivenName); +} + +TEST_F(NearbyShareProfileInfoProviderImplTest, + GivenNameNotUseLoginAccountWhenGivenNameEmpty) { + AccountManager::Account account; + account.id = kTestAccountId; + fake_account_manager().SetAccount(account); + SetUserGivenName(kProfileGivenName); + + NearbyShareProfileInfoProviderImpl profile_info_provider( + fake_device_info(), fake_account_manager()); + EXPECT_EQ(profile_info_provider.GetGivenName(), kProfileGivenName); +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/nearby_sharing_decoder.h b/sharing/nearby_sharing_decoder.h new file mode 100644 index 00000000..c3f5ff7c --- /dev/null +++ b/sharing/nearby_sharing_decoder.h @@ -0,0 +1,42 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_NEARBY_SHARING_DECODER_H_ +#define THIRD_PARTY_NEARBY_SHARING_NEARBY_SHARING_DECODER_H_ + +#include + +#include + +#include "absl/types/span.h" +#include "sharing/advertisement.h" +#include "sharing/proto/wire_format.pb.h" + +namespace nearby { +namespace sharing { + +class NearbySharingDecoder { + public: + virtual ~NearbySharingDecoder() = default; + + virtual std::unique_ptr DecodeAdvertisement( + absl::Span data) = 0; + virtual std::unique_ptr DecodeFrame( + absl::Span data) = 0; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_NEARBY_SHARING_DECODER_H_ diff --git a/sharing/nearby_sharing_decoder_impl.cc b/sharing/nearby_sharing_decoder_impl.cc new file mode 100644 index 00000000..b6540e04 --- /dev/null +++ b/sharing/nearby_sharing_decoder_impl.cc @@ -0,0 +1,47 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/nearby_sharing_decoder_impl.h" + +#include + +#include + +#include "absl/types/span.h" +#include "sharing/advertisement.h" +#include "sharing/proto/wire_format.pb.h" + +namespace nearby { +namespace sharing { + +using Frame = ::nearby::sharing::service::proto::Frame; + +std::unique_ptr NearbySharingDecoderImpl::DecodeAdvertisement( + absl::Span data) { + return Advertisement::FromEndpointInfo(data); +} + +std::unique_ptr NearbySharingDecoderImpl::DecodeFrame( + absl::Span data) { + auto frame = std::make_unique(); + + if (frame->ParseFromArray(data.data(), data.size())) { + return frame; + } else { + return nullptr; + } +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/nearby_sharing_decoder_impl.h b/sharing/nearby_sharing_decoder_impl.h new file mode 100644 index 00000000..b6257c87 --- /dev/null +++ b/sharing/nearby_sharing_decoder_impl.h @@ -0,0 +1,41 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_NEARBY_SHARING_DECODER_IMPL_H_ +#define THIRD_PARTY_NEARBY_SHARING_NEARBY_SHARING_DECODER_IMPL_H_ + +#include + +#include + +#include "absl/types/span.h" +#include "sharing/advertisement.h" +#include "sharing/nearby_sharing_decoder.h" +#include "sharing/proto/wire_format.pb.h" + +namespace nearby { +namespace sharing { + +class NearbySharingDecoderImpl : public NearbySharingDecoder { + public: + std::unique_ptr DecodeAdvertisement( + absl::Span data) override; + std::unique_ptr DecodeFrame( + absl::Span data) override; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_NEARBY_SHARING_DECODER_IMPL_H_ diff --git a/sharing/nearby_sharing_event_logger.cc b/sharing/nearby_sharing_event_logger.cc new file mode 100644 index 00000000..f96c5fd7 --- /dev/null +++ b/sharing/nearby_sharing_event_logger.cc @@ -0,0 +1,52 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/nearby_sharing_event_logger.h" + +#include +#include + +#include "internal/analytics/event_logger.h" +#include "sharing/common/nearby_share_prefs.h" +#include "sharing/internal/api/preference_manager.h" +#include "google/protobuf/message_lite.h" + +namespace nearby { +namespace sharing { + +using ::nearby::sharing::api::PreferenceManager; + +NearbySharingEventLogger::NearbySharingEventLogger( + PreferenceManager& preference_manager, + std::unique_ptr event_logger) + : preference_manager_(preference_manager), + event_logger_(std::move(event_logger)) {} + +NearbySharingEventLogger::~NearbySharingEventLogger() = default; + +void NearbySharingEventLogger::Log(const proto2::MessageLite& message) { + if (event_logger_ == nullptr) { + return; + } + + if (!preference_manager_.GetBoolean( + prefs::kNearbySharingIsAnalyticsEnabledName, false)) { + return; + } + + event_logger_->Log(message); +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/nearby_sharing_event_logger.h b/sharing/nearby_sharing_event_logger.h new file mode 100644 index 00000000..c3911f7b --- /dev/null +++ b/sharing/nearby_sharing_event_logger.h @@ -0,0 +1,47 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_NEARBY_SHARING_EVENT_LOGGER_H_ +#define THIRD_PARTY_NEARBY_SHARING_NEARBY_SHARING_EVENT_LOGGER_H_ + +#include + +#include "internal/analytics/event_logger.h" +#include "sharing/internal/api/preference_manager.h" +#include "google/protobuf/message_lite.h" + +namespace nearby { +namespace sharing { + +// Nearby Sharing SDK needs to enable/disable of the event logger according to +// user settings. NearbySharingEventLogger skips analytics logging if analytics +// logging is disabled. +class NearbySharingEventLogger : public nearby::analytics::EventLogger { + public: + NearbySharingEventLogger( + nearby::sharing::api::PreferenceManager& preference_manager, + std::unique_ptr event_logger); + ~NearbySharingEventLogger() override; + + void Log(const proto2::MessageLite& message) override; + + private: + nearby::sharing::api::PreferenceManager& preference_manager_; + std::unique_ptr event_logger_; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_NEARBY_SHARING_EVENT_LOGGER_H_ diff --git a/sharing/nearby_sharing_event_logger_test.cc b/sharing/nearby_sharing_event_logger_test.cc new file mode 100644 index 00000000..c6a4a0ed --- /dev/null +++ b/sharing/nearby_sharing_event_logger_test.cc @@ -0,0 +1,111 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/nearby_sharing_event_logger.h" + +#include +#include + +#include "gmock/gmock.h" +#include "protobuf-matchers/protocol-buffer-matchers.h" +#include "gtest/gtest.h" +#include "internal/analytics/event_logger.h" +#include "proto/sharing_enums.pb.h" +#include "sharing/common/nearby_share_prefs.h" +#include "sharing/internal/test/fake_preference_manager.h" +#include "sharing/proto/analytics/nearby_sharing_log.pb.h" +#include "google/protobuf/message_lite.h" + +namespace nearby { +namespace sharing { +namespace { +using ::location::nearby::proto::sharing::EventCategory; +using ::location::nearby::proto::sharing::EventType; +using ::nearby::analytics::EventLogger; +using ::nearby::sharing::analytics::proto::SharingLog; + +class MockEventLogger : public EventLogger { + public: + ~MockEventLogger() override = default; + + MOCK_METHOD(void, Log, (const proto2::MessageLite& message), (override)); +}; + +class NearbySharingEventLoggerTest : public ::testing::Test { + public: + NearbySharingEventLoggerTest() = default; + + void SetUp() override { + auto event_logger = std::make_unique(); + raw_event_logger_ = event_logger.get(); + sharing_event_logger_ = std::make_unique( + preference_manager_, std::move(event_logger)); + } + + void SetEventLogger(bool enabled) { + preference_manager_.SetBoolean(prefs::kNearbySharingIsAnalyticsEnabledName, + enabled); + } + + const MockEventLogger* event_logger() { return raw_event_logger_; } + + std::unique_ptr GetTestEvent() { + auto sharing_log = + std::unique_ptr(SharingLog::default_instance().New()); + sharing_log->set_event_category(EventCategory::SETTINGS_EVENT); + sharing_log->set_event_type(EventType::TAP_HELP); + + auto tap_help = + analytics::proto::SharingLog::TapHelp::default_instance().New(); + + sharing_log->set_allocated_tap_help(tap_help); + return sharing_log; + } + + NearbySharingEventLogger* sharing_event_logger() { + return sharing_event_logger_.get(); + } + + private: + nearby::FakePreferenceManager preference_manager_; + MockEventLogger* raw_event_logger_ = nullptr; + std::unique_ptr sharing_event_logger_; +}; + +TEST_F(NearbySharingEventLoggerTest, LogEventWhenEnabled) { + SetEventLogger(true); + EXPECT_CALL(*event_logger(), Log) + .WillOnce([&](const ::google::protobuf::MessageLite& message) { + const SharingLog* sharing_log = + dynamic_cast(&message); + ASSERT_NE(sharing_log, nullptr); + EXPECT_EQ(sharing_log->event_category(), EventCategory::SETTINGS_EVENT); + EXPECT_EQ(sharing_log->event_type(), EventType::TAP_HELP); + }); + + std::unique_ptr event = GetTestEvent(); + sharing_event_logger()->Log(*event); +} + +TEST_F(NearbySharingEventLoggerTest, NoLogEventWhenDisabled) { + SetEventLogger(false); + EXPECT_CALL(*event_logger(), Log).Times(0); + + std::unique_ptr event = GetTestEvent(); + sharing_event_logger()->Log(*event); +} + +} // namespace +} // namespace sharing +} // namespace nearby diff --git a/sharing/nearby_sharing_service.cc b/sharing/nearby_sharing_service.cc new file mode 100644 index 00000000..8b245ab3 --- /dev/null +++ b/sharing/nearby_sharing_service.cc @@ -0,0 +1,56 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/nearby_sharing_service.h" + +#include +#include + +#include "sharing/internal/public/logging.h" + +namespace nearby { +namespace sharing { + +namespace { + +using StatusCodes = NearbySharingService::StatusCodes; +constexpr char kUnknownStatusCodesString[] = "Unknown_StatusCodes"; + +} // namespace + +// static +std::string NearbySharingService::StatusCodeToString(StatusCodes status_code) { + switch (status_code) { + case StatusCodes::kOk: + return "kOk"; + case StatusCodes::kError: + return "kError"; + case StatusCodes::kOutOfOrderApiCall: + return "kOutOfOrderApiCall"; + case StatusCodes::kStatusAlreadyStopped: + return "kStatusAlreadyStopped"; + case StatusCodes::kTransferAlreadyInProgress: + return "kTransferAlreadyInProgress"; + case StatusCodes::kNoAvailableConnectionMedium: + return "kNoAvailableConnectionMedium"; + case StatusCodes::kIrrecoverableHardwareError: + return "kIrrecoverableHardwareError"; + } + NL_LOG(ERROR) << "Unexpected value for StatusCodes: " + << static_cast(status_code); + return kUnknownStatusCodesString; +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/nearby_sharing_service.h b/sharing/nearby_sharing_service.h new file mode 100644 index 00000000..9be2f794 --- /dev/null +++ b/sharing/nearby_sharing_service.h @@ -0,0 +1,252 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_NEARBY_SHARING_SERVICE_H_ +#define THIRD_PARTY_NEARBY_SHARING_NEARBY_SHARING_SERVICE_H_ + +#include +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "internal/network/url.h" +#include "sharing/attachment.h" +#include "sharing/local_device_data/nearby_share_local_device_data_manager.h" +#include "sharing/nearby_sharing_settings.h" +#include "sharing/share_target.h" +#include "sharing/share_target_discovered_callback.h" +#include "sharing/transfer_update_callback.h" + +namespace nearby { + +class AccountManager; + +namespace sharing { + +class NearbyNotificationDelegate; +class NearbyShareCertificateManager; +class NearbyShareContactManager; +class NearbyShareHttpNotifier; + +// This service implements Nearby Sharing on top of the Nearby Connections mojo. +// Currently, only single profile will be allowed to be bound at a time and only +// after the user has enabled Nearby Sharing in prefs. +class NearbySharingService { + public: + // These values are persisted to logs. Entries should not be renumbered and + // numeric values should never be reused. If entries are added, kMaxValue + // should be updated. + enum class StatusCodes { + // The operation was successful. + kOk = 0, + // The operation failed, without any more information. + kError = 1, + // The operation failed since it was called in an invalid order. + kOutOfOrderApiCall = 2, + // Tried to stop something that was already stopped. + kStatusAlreadyStopped = 3, + // Tried to register an opposite foreground surface in the midst of a + // transfer or connection. + // (Tried to register Send Surface when receiving a file or tried to + // register Receive Surface when + // sending a file.) + kTransferAlreadyInProgress = 4, + // There is no available connection medium to use. + kNoAvailableConnectionMedium = 5, + // Bluetooth or WiFi hardware ran into an irrecoverable state. User PC needs + // to be restarted. + kIrrecoverableHardwareError = 6, + kMaxValue = kIrrecoverableHardwareError + }; + + enum class ReceiveSurfaceState { + // Default, invalid state. + kUnknown, + // Background receive surface advertises only to contacts. + kBackground, + // Foreground receive surface advertises to everyone. + kForeground, + }; + + enum class SendSurfaceState { + // Default, invalid state. + kUnknown, + // Background send surface only listens to transfer update. + kBackground, + // Foreground send surface both scans and listens to transfer update. + kForeground, + }; + + class Observer { + public: + virtual ~Observer() = default; + virtual void OnHighVisibilityChangeRequested() {} + virtual void OnHighVisibilityChanged(bool in_high_visibility) = 0; + + virtual void OnStartAdvertisingFailure() {} + virtual void OnStartDiscoveryResult(bool success) {} + + virtual void OnFastInitiationDevicesDetected() {} + virtual void OnFastInitiationDevicesNotDetected() {} + virtual void OnFastInitiationScanningStopped() {} + + virtual void OnBluetoothStatusChanged() {} + virtual void OnWifiStatusChanged() {} + virtual void OnLanStatusChanged() {} + virtual void OnIrrecoverableHardwareErrorReported() {} + + // Called during the |KeyedService| shutdown, but before everything has been + // cleaned up. It is safe to remove any observers on this event. + virtual void OnShutdown() = 0; + }; + + static std::string StatusCodeToString(StatusCodes status_code); + + virtual ~NearbySharingService() = default; + + virtual void AddObserver(Observer* observer) = 0; + virtual void RemoveObserver(Observer* observer) = 0; + virtual bool HasObserver(Observer* observer) = 0; + + // Shutdown the Nearby Sharing service, and cleanup. + virtual void Shutdown( + std::function status_codes_callback) = 0; + + // Registers a send surface for handling payload transfer status and device + // discovery. + virtual void RegisterSendSurface( + TransferUpdateCallback* transfer_callback, + ShareTargetDiscoveredCallback* discovery_callback, SendSurfaceState state, + std::function status_codes_callback) = 0; + + // Unregisters the current send surface. + virtual void UnregisterSendSurface( + TransferUpdateCallback* transfer_callback, + ShareTargetDiscoveredCallback* discovery_callback, + std::function status_codes_callback) = 0; + + // Registers a receiver surface for handling payload transfer status. + virtual void RegisterReceiveSurface( + TransferUpdateCallback* transfer_callback, ReceiveSurfaceState state, + std::function status_codes_callback) = 0; + + // Unregisters the current receive surface. + virtual void UnregisterReceiveSurface( + TransferUpdateCallback* transfer_callback, + std::function status_codes_callback) = 0; + + // Unregisters all foreground receive surfaces. + virtual void ClearForegroundReceiveSurfaces( + std::function status_codes_callback) = 0; + + // Returns true if a foreground receive surface is registered. + virtual bool IsInHighVisibility() const = 0; + + // Returns true if there is an ongoing file transfer. + virtual bool IsTransferring() const = 0; + + // Returns true if we're currently receiving a file. + virtual bool IsReceivingFile() const = 0; + + // Returns true if we're currently sending a file. + virtual bool IsSendingFile() const = 0; + + // Returns true if we're currently attempting to connect to a + // remote device. + virtual bool IsConnecting() const = 0; + + // Returns true if we are currently scanning for remote devices. + virtual bool IsScanning() const = 0; + + // Returns true if the bluetooth adapter is present. + virtual bool IsBluetoothPresent() const = 0; + + // Returns true if the bluetooth adapter is powered. + virtual bool IsBluetoothPowered() const = 0; + + // Returns true if extended advertising is supported by the BLE adapter + virtual bool IsExtendedAdvertisingSupported() const = 0; + + // Returns true if the PC is connected to LAN (wifi/ethernet). + virtual bool IsLanConnected() const = 0; + + // Returns true if the Wi-Fi adapter is present. + virtual bool IsWifiPresent() const = 0; + + // Returns true if the Wi-Fi adapter is powered. + virtual bool IsWifiPowered() const = 0; + + // Returns the QR Code Url. + virtual std::string GetQrCodeUrl() const = 0; + + // Sends |attachments| to the remote |share_target|. + virtual void SendAttachments( + const ShareTarget& share_target, + std::vector> attachments, + std::function status_codes_callback) = 0; + + // Accepts incoming share from the remote |share_target|. + virtual void Accept( + const ShareTarget& share_target, + std::function status_codes_callback) = 0; + + // Rejects incoming share from the remote |share_target|. + virtual void Reject( + const ShareTarget& share_target, + std::function status_codes_callback) = 0; + + // Cancels outgoing shares to the remote |share_target|. + virtual void Cancel( + const ShareTarget& share_target, + std::function status_codes_callback) = 0; + + // Returns true if the local user cancelled the transfer to remote + // |share_target|. + virtual bool DidLocalUserCancelTransfer(const ShareTarget& share_target) = 0; + + // Opens attachments from the remote |share_target|. + virtual void Open( + const ShareTarget& share_target, + std::function status_codes_callback) = 0; + + // Opens an url target on a browser instance. + virtual void OpenUrl(const ::nearby::network::Url& url) = 0; + + // Copies text to cache/clipboard. + virtual void CopyText(absl::string_view text) = 0; + + // Persists and joins the Wi-Fi network. + virtual void JoinWifiNetwork(absl::string_view ssid, + absl::string_view password) = 0; + + // Sets a cleanup callback to be called once done with transfer for ARC. + virtual void SetArcTransferCleanupCallback( + std::function callback) = 0; + + virtual std::string Dump() const = 0; + virtual void UpdateFilePathsInProgress(bool update_file_paths) = 0; + + virtual NearbyShareSettings* GetSettings() = 0; + virtual NearbyShareHttpNotifier* GetHttpNotifier() = 0; + virtual NearbyShareLocalDeviceDataManager* GetLocalDeviceDataManager() = 0; + virtual NearbyShareContactManager* GetContactManager() = 0; + virtual NearbyShareCertificateManager* GetCertificateManager() = 0; + virtual AccountManager* GetAccountManager() = 0; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_NEARBY_SHARING_SERVICE_H_ diff --git a/sharing/nearby_sharing_service_extension.cc b/sharing/nearby_sharing_service_extension.cc new file mode 100644 index 00000000..2a809131 --- /dev/null +++ b/sharing/nearby_sharing_service_extension.cc @@ -0,0 +1,200 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/nearby_sharing_service_extension.h" + +#include // NOLINT(build/c++17) +#include + +#include "absl/status/status.h" +#include "absl/strings/string_view.h" +#include "absl/synchronization/notification.h" +#include "internal/network/url.h" +#include "sharing/file_attachment.h" +#include "sharing/internal/base/utf_string_conversions.h" +#include "sharing/internal/public/logging.h" +#include "sharing/nearby_sharing_service.h" +#include "sharing/proto/wire_format.pb.h" +#include "sharing/share_target.h" +#include "sharing/text_attachment.h" +#include "sharing/wifi_credentials_attachment.h" + +namespace nearby { +namespace sharing { + +namespace { +using ::nearby::sharing::service::proto::FileMetadata; +using ::nearby::sharing::service::proto::TextMetadata; +using StatusCodes = ::nearby::sharing::NearbySharingService::StatusCodes; +} // namespace + +NearbySharingService::StatusCodes NearbySharingServiceExtension::Open( + const ShareTarget& share_target) { + if (share_target.file_attachments.empty() && + share_target.text_attachments.empty() && + share_target.wifi_credentials_attachments.empty()) { + return StatusCodes::kOk; + } + + if (!share_target.file_attachments.empty() && + !share_target.text_attachments.empty()) { + NL_LOG(ERROR) + << __func__ + << ": Text attachments and file attachments can't come together."; + return StatusCodes::kError; + } + + if (share_target.text_attachments.size() == 1) { + const TextAttachment& text_attachment = share_target.text_attachments[0]; + + switch (text_attachment.type()) { + case TextMetadata::TEXT: { + CopyText(text_attachment.text_body()); + break; + } + case TextMetadata::URL: { + OpenUrl(*nearby::network::Url::Create(text_attachment.text_body())); + break; + } + default: { + // Copy text for all other text types. + CopyText(text_attachment.text_title()); + break; + } + } + return StatusCodes::kOk; + } + + if (share_target.text_attachments.size() > 1) { + NL_LOG(ERROR) << __func__ + << ": Multiple text attachments are not supported currently."; + return StatusCodes::kError; + } + + if (share_target.wifi_credentials_attachments.size() == 1) { + const WifiCredentialsAttachment& wifi_credentials_attachment = + share_target.wifi_credentials_attachments[0]; + JoinWifiNetwork(wifi_credentials_attachment.ssid(), + wifi_credentials_attachment.password()); + return StatusCodes::kOk; + } + + if (share_target.wifi_credentials_attachments.size() > 1) { + NL_LOG(ERROR) << __func__ + << ": Multiple WiFi credentials attachments are not " + "supported currently."; + return StatusCodes::kError; + } + + const FileAttachment& file_attachment = share_target.file_attachments[0]; + + if ((share_target.file_attachments.size() > 1) || + ((file_attachment.type() != FileMetadata::AUDIO) && + (file_attachment.type() != FileMetadata::VIDEO) && + (file_attachment.type() != FileMetadata::IMAGE))) { + // Opens download folder. + NearbySharingService::StatusCodes status_codes = StatusCodes::kOk; + absl::Notification notification; + context_->GetShell().Open( + std::filesystem::path( + utils::Utf8ToWide(settings_->GetCustomSavePath())), + [&status_codes, ¬ification](absl::Status status) { + if (!status.ok()) { + NL_LOG(ERROR) + << "Failed to open download folder with error message:" + << status; + status_codes = StatusCodes::kError; + } else { + status_codes = StatusCodes::kOk; + } + notification.Notify(); + }); + notification.WaitForNotification(); + return status_codes; + } + + // Opens the file with default application. + std::filesystem::path file_path; + if (file_attachment.file_path().has_value()) { + file_path = *file_attachment.file_path(); + } else { + file_path = std::filesystem::path( + utils::Utf8ToWide(settings_->GetCustomSavePath())) / + // NOLINTNEXTLINE cannot build without the new string creation + utils::Utf8ToWide(std::string(file_attachment.file_name())); + } + + NearbySharingService::StatusCodes status_codes = StatusCodes::kOk; + absl::Notification notification; + context_->GetShell().Open( + file_path, [file_name = file_attachment.file_name(), &status_codes, + ¬ification](absl::Status status) { + if (!status.ok()) { + NL_LOG(ERROR) << "Failed to open file " << file_name; + status_codes = StatusCodes::kError; + } else { + status_codes = StatusCodes::kOk; + } + notification.Notify(); + }); + notification.WaitForNotification(); + return status_codes; +} + +// Opens an url target on a browser instance. +void NearbySharingServiceExtension::OpenUrl(const ::nearby::network::Url& url) { + absl::Notification notification; + context_->OpenUrl(url, [url, ¬ification](absl::Status status) { + if (!status.ok()) { + NL_LOG(ERROR) << "Failed to open URL " << url.GetUrlPath() + << " with error " << status.message(); + } + notification.Notify(); + }); + + notification.WaitForNotification(); +} + +// Copies text to cache/clipboard. +void NearbySharingServiceExtension::CopyText(absl::string_view text) { + absl::Notification notification; + context_->CopyText( + text, [text = std::string(text), ¬ification](absl::Status status) { + if (!status.ok()) { + NL_LOG(ERROR) << "Failed to copy text " << text << " with error " + << status.message(); + } + notification.Notify(); + }); + notification.WaitForNotification(); +} + +// Persists and joins the Wi-Fi network. +void NearbySharingServiceExtension::JoinWifiNetwork( + absl::string_view ssid, absl::string_view password) { + absl::Notification notification; + context_->GetWifiAdapter().JoinNetwork( + ssid, password, + [ssid = std::string(ssid), ¬ification](absl::Status status) { + if (!status.ok()) { + NL_LOG(ERROR) << "Failed to join network " << ssid << " with error " + << status.message(); + } + notification.Notify(); + }); + notification.WaitForNotification(); +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/nearby_sharing_service_extension.h b/sharing/nearby_sharing_service_extension.h new file mode 100644 index 00000000..9f4af66c --- /dev/null +++ b/sharing/nearby_sharing_service_extension.h @@ -0,0 +1,62 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_NEARBY_SHARING_SERVICE_EXTENSION_H_ +#define THIRD_PARTY_NEARBY_SHARING_NEARBY_SHARING_SERVICE_EXTENSION_H_ + +#include + +#include "absl/strings/string_view.h" +#include "internal/network/url.h" +#include "sharing/internal/public/context.h" +#include "sharing/nearby_sharing_service.h" +#include "sharing/nearby_sharing_settings.h" +#include "sharing/share_target.h" + +namespace nearby { +namespace sharing { + +class NearbySharingServiceExtension { + public: + NearbySharingServiceExtension(Context* context, NearbyShareSettings* settings) + : context_(context), settings_(settings) {} + + // Opens attachments from the remote |share_target|. + NearbySharingService::StatusCodes Open(const ShareTarget& share_target); + + // Opens an url target on a browser instance. + void OpenUrl(const ::nearby::network::Url& url); + + // Copies text to cache/clipboard. + void CopyText(absl::string_view text); + + // Persists and joins the Wi-Fi network. + void JoinWifiNetwork(absl::string_view ssid, absl::string_view password); + + // Returns the QR Code Url. + std::string GetQrCodeUrl() const { return qr_code_url_; } + + private: + Context* context_ = nullptr; + NearbyShareSettings* settings_ = nullptr; + + // The qr code url for the current session containing Advertising token, + // Connection token and Sender Public Key. + std::string qr_code_url_ = "http://near.by/launch_by_qrcode"; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_NEARBY_SHARING_SERVICE_EXTENSION_H_ diff --git a/sharing/nearby_sharing_service_extension_test.cc b/sharing/nearby_sharing_service_extension_test.cc new file mode 100644 index 00000000..b08d0a6c --- /dev/null +++ b/sharing/nearby_sharing_service_extension_test.cc @@ -0,0 +1,193 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/nearby_sharing_service_extension.h" + +#include // NOLINT(build/c++17) +#include +#include + +#include "gtest/gtest.h" +#include "internal/test/fake_device_info.h" +#include "sharing/file_attachment.h" +#include "sharing/internal/test/fake_context.h" +#include "sharing/internal/test/fake_preference_manager.h" +#include "sharing/internal/test/fake_shell.h" +#include "sharing/local_device_data/fake_nearby_share_local_device_data_manager.h" +#include "sharing/nearby_sharing_service.h" +#include "sharing/nearby_sharing_settings.h" +#include "sharing/proto/wire_format.pb.h" +#include "sharing/share_target.h" +#include "sharing/text_attachment.h" +#include "sharing/wifi_credentials_attachment.h" + +namespace nearby { +namespace sharing { +namespace { + +using StatusCodes = NearbySharingService::StatusCodes; +using ::nearby::sharing::service::proto::FileMetadata; +using ::nearby::sharing::service::proto::TextMetadata; + +class NearbySharingServiceExtensionTest : public ::testing::Test { + public: + NearbySharingServiceExtensionTest() = default; + + void SetUp() override { + service_extension_ = std::make_unique( + &context_, &nearby_share_settings_); + } + + NearbySharingServiceExtension* service_extension() { + return service_extension_.get(); + } + + FakeContext* context() { return &context_; } + + private: + std::unique_ptr service_extension_; + nearby::FakeDeviceInfo device_info_; + nearby::FakePreferenceManager preference_manager_; + FakeContext context_; + FakeNearbyShareLocalDeviceDataManager local_device_data_manager_{"test"}; + NearbyShareSettings nearby_share_settings_{&context_, context_.GetClock(), + device_info_, preference_manager_, + &local_device_data_manager_}; +}; + +TEST_F(NearbySharingServiceExtensionTest, + OpenSharedTargetNoFileAndTextAttachment) { + ShareTarget share_target; + StatusCodes status_codes = service_extension()->Open(share_target); + EXPECT_EQ(status_codes, StatusCodes::kOk); +} + +TEST_F(NearbySharingServiceExtensionTest, + OpenSharedTargetBothFileAndTextAttachments) { + ShareTarget share_target; + share_target.text_attachments = { + TextAttachment(TextMetadata::TEXT, "body", "title", "mime")}; + share_target.file_attachments = { + FileAttachment(std::filesystem::temp_directory_path() / "test.g1")}; + StatusCodes status_codes = service_extension()->Open(share_target); + EXPECT_EQ(status_codes, StatusCodes::kError); +} + +TEST_F(NearbySharingServiceExtensionTest, OpenSharedTargetOneTextAttachment) { + ShareTarget share_target; + share_target.text_attachments = { + TextAttachment(TextMetadata::TEXT, "body", "title", "mime")}; + StatusCodes status_codes = service_extension()->Open(share_target); + EXPECT_EQ(status_codes, StatusCodes::kOk); +} + +TEST_F(NearbySharingServiceExtensionTest, + OpenSharedMoreThanOneTextAttachments) { + ShareTarget share_target; + share_target.text_attachments = { + TextAttachment(TextMetadata::TEXT, "body", "title1", "mime"), + TextAttachment(TextMetadata::TEXT, "body", "title2", "mime")}; + StatusCodes status_codes = service_extension()->Open(share_target); + EXPECT_EQ(status_codes, StatusCodes::kError); +} + +TEST_F(NearbySharingServiceExtensionTest, OpenShareTargetWithUrlAttacchment) { + ShareTarget share_target; + share_target.text_attachments = {TextAttachment( + TextMetadata::URL, "http://www.google.com", std::nullopt, std::nullopt)}; + StatusCodes status_codes = service_extension()->Open(share_target); + EXPECT_EQ(status_codes, StatusCodes::kOk); +} + +TEST_F(NearbySharingServiceExtensionTest, + OpenShareTargetWithTextAddressAttacchment) { + ShareTarget share_target; + share_target.text_attachments = {TextAttachment(TextMetadata::ADDRESS, "body", + std::nullopt, std::nullopt)}; + StatusCodes status_codes = service_extension()->Open(share_target); + EXPECT_EQ(status_codes, StatusCodes::kOk); +} + +TEST_F(NearbySharingServiceExtensionTest, OpenShareTargetWithWifiAttacchment) { + ShareTarget share_target; + share_target.wifi_credentials_attachments = {WifiCredentialsAttachment( + "ssid", service::proto::WifiCredentialsMetadata::WPA_PSK)}; + StatusCodes status_codes = service_extension()->Open(share_target); + EXPECT_EQ(status_codes, StatusCodes::kOk); +} + +TEST_F(NearbySharingServiceExtensionTest, + OpenShareTargetWithMultipleWifiAttacchments) { + ShareTarget share_target; + share_target.wifi_credentials_attachments = { + WifiCredentialsAttachment( + "ssid1", service::proto::WifiCredentialsMetadata::WPA_PSK), + WifiCredentialsAttachment( + "ssid2", service::proto::WifiCredentialsMetadata::WPA_PSK)}; + StatusCodes status_codes = service_extension()->Open(share_target); + EXPECT_EQ(status_codes, StatusCodes::kError); +} + +TEST_F(NearbySharingServiceExtensionTest, + OpenShareTargetWithOneFileAttacchment) { + ShareTarget share_target; + share_target.file_attachments = {FileAttachment( + /*id=*/1234, /*size=*/1000, /*file_name=*/"test.png", + /*mime_type=*/"image", /*type=*/FileMetadata::IMAGE)}; + StatusCodes status_codes = service_extension()->Open(share_target); + EXPECT_EQ(status_codes, StatusCodes::kOk); +} + +TEST_F(NearbySharingServiceExtensionTest, OpenSharedTargetUseDownloadFolder) { + ShareTarget share_target; + share_target.file_attachments = { + FileAttachment(std::filesystem::temp_directory_path() / "test.g1"), + FileAttachment(std::filesystem::temp_directory_path() / "test.g2")}; + StatusCodes status_codes = service_extension()->Open(share_target); + EXPECT_EQ(status_codes, StatusCodes::kOk); + auto& shell = dynamic_cast(context()->GetShell()); + shell.set_return_error(true); + status_codes = service_extension()->Open(share_target); + EXPECT_NE(status_codes, StatusCodes::kOk); + shell.set_return_error(false); + status_codes = service_extension()->Open(share_target); + EXPECT_EQ(status_codes, StatusCodes::kOk); +} + +TEST_F(NearbySharingServiceExtensionTest, + OpenSharedTargetUseDefaultApplication) { + ShareTarget share_target; + NearbySharingService::StatusCodes status_codes; + share_target.file_attachments = { + FileAttachment(std::filesystem::temp_directory_path() / "test.jpg")}; + status_codes = service_extension()->Open(share_target); + EXPECT_EQ(status_codes, StatusCodes::kOk); + share_target.file_attachments = { + FileAttachment(std::filesystem::temp_directory_path() / "test.wav")}; + status_codes = service_extension()->Open(share_target); + EXPECT_EQ(status_codes, StatusCodes::kOk); + share_target.file_attachments = { + FileAttachment(std::filesystem::temp_directory_path() / "test.wmv")}; + status_codes = service_extension()->Open(share_target); + EXPECT_EQ(status_codes, StatusCodes::kOk); +} + +TEST_F(NearbySharingServiceExtensionTest, GetQrCodeUrl) { + EXPECT_EQ(service_extension()->GetQrCodeUrl(), + "http://near.by/launch_by_qrcode"); +} + +} // namespace +} // namespace sharing +} // namespace nearby diff --git a/sharing/nearby_sharing_service_factory.cc b/sharing/nearby_sharing_service_factory.cc new file mode 100644 index 00000000..3bf7ea69 --- /dev/null +++ b/sharing/nearby_sharing_service_factory.cc @@ -0,0 +1,71 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/nearby_sharing_service_factory.h" + +#include +#include + +#include "internal/analytics/event_logger.h" +#include "internal/network/http_client_factory_impl.h" +#include "sharing/internal/api/sharing_platform.h" +#include "sharing/internal/public/context_impl.h" +#include "sharing/nearby_connections_manager_factory.h" +#include "sharing/nearby_sharing_decoder_impl.h" +#include "sharing/nearby_sharing_event_logger.h" +#include "sharing/nearby_sharing_service.h" +#include "sharing/nearby_sharing_service_impl.h" + +namespace nearby { +namespace sharing { + +using ::nearby::sharing::api::SharingPlatform; + +NearbySharingServiceFactory* NearbySharingServiceFactory::GetInstance() { + static NearbySharingServiceFactory* instance = + new NearbySharingServiceFactory(); + return instance; +} + +NearbySharingService* NearbySharingServiceFactory::CreateSharingService( + LinkType link_type, + SharingPlatform& sharing_platform, + std::unique_ptr<::nearby::analytics::EventLogger> event_logger) { + if (nearby_sharing_service_ != nullptr) { + return nullptr; + } + + context_ = + std::make_unique(sharing_platform); + event_logger_ = std::make_unique( + sharing_platform.GetPreferenceManager(), + std::move(event_logger)); + decoder_ = std::make_unique(); + http_client_factory_ = + std::make_unique(); + nearby_connections_manager_ = + NearbyConnectionsManagerFactory::CreateConnectionsManager( + link_type, context_.get(), sharing_platform.GetDeviceInfo(), + event_logger_.get()); + + nearby_sharing_service_ = std::make_unique( + context_.get(), sharing_platform, decoder_.get(), + http_client_factory_.get(), std::move(nearby_connections_manager_), + event_logger_.get()); + + return nearby_sharing_service_.get(); +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/nearby_sharing_service_factory.h b/sharing/nearby_sharing_service_factory.h new file mode 100644 index 00000000..1dc72961 --- /dev/null +++ b/sharing/nearby_sharing_service_factory.h @@ -0,0 +1,57 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_NEARBY_SHARING_SERVICE_FACTORY_H_ +#define THIRD_PARTY_NEARBY_SHARING_NEARBY_SHARING_SERVICE_FACTORY_H_ + +#include + +#include "internal/analytics/event_logger.h" +#include "internal/network/http_client_factory.h" +#include "sharing/internal/api/sharing_platform.h" +#include "sharing/internal/public/context.h" +#include "sharing/nearby_connections_manager.h" +#include "sharing/nearby_sharing_decoder.h" +#include "sharing/nearby_sharing_service.h" + +namespace nearby { +namespace sharing { + +class NearbySharingServiceFactory { + public: + enum class LinkType { kStatic, kDynamic }; + + // Return a singleton instance of NearbySharingServiceFactory. + static NearbySharingServiceFactory* GetInstance(); + + NearbySharingService* CreateSharingService( + LinkType link_type, + nearby::sharing::api::SharingPlatform& sharing_platform, + std::unique_ptr<::nearby::analytics::EventLogger> event_logger = nullptr); + + private: + NearbySharingServiceFactory() = default; + + std::unique_ptr context_; + std::unique_ptr<::nearby::analytics::EventLogger> event_logger_; + std::unique_ptr decoder_; + std::unique_ptr http_client_factory_; + std::unique_ptr nearby_connections_manager_; + std::unique_ptr nearby_sharing_service_; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_NEARBY_SHARING_SERVICE_FACTORY_H_ diff --git a/sharing/nearby_sharing_service_impl.cc b/sharing/nearby_sharing_service_impl.cc new file mode 100644 index 00000000..24d01c87 --- /dev/null +++ b/sharing/nearby_sharing_service_impl.cc @@ -0,0 +1,5021 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/nearby_sharing_service_impl.h" + +#include + +#include +#include +#include // NOLINT(build/c++17) +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "absl/algorithm/container.h" +#include "absl/container/flat_hash_map.h" +#include "absl/random/random.h" +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "absl/time/time.h" +#include "absl/types/span.h" +#include "internal/analytics/event_logger.h" +#include "internal/base/bluetooth_address.h" +#include "internal/base/observer_list.h" +#include "internal/flags/nearby_flags.h" +#include "internal/network/http_client_factory.h" +#include "internal/network/url.h" +#include "internal/platform/device_info.h" +#include "internal/platform/implementation/account_manager.h" +#include "internal/platform/implementation/device_info.h" +#include "proto/sharing_enums.pb.h" +#include "sharing/advertisement.h" +#include "sharing/analytics/analytics_information.h" +#include "sharing/analytics/analytics_recorder.h" +#include "sharing/attachment.h" +#include "sharing/attachment_info.h" +#include "sharing/certificates/common.h" +#include "sharing/certificates/nearby_share_certificate_manager.h" +#include "sharing/certificates/nearby_share_certificate_manager_impl.h" +#include "sharing/certificates/nearby_share_decrypted_public_certificate.h" +#include "sharing/certificates/nearby_share_encrypted_metadata_key.h" +#include "sharing/client/nearby_share_client_impl.h" +#include "sharing/client/nearby_share_http_notifier.h" +#include "sharing/common/compatible_u8_string.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/common/nearby_share_prefs.h" +#include "sharing/constants.h" +#include "sharing/contacts/nearby_share_contact_manager.h" +#include "sharing/contacts/nearby_share_contact_manager_impl.h" +#include "sharing/fast_initiation/nearby_fast_initiation.h" +#include "sharing/fast_initiation/nearby_fast_initiation_impl.h" +#include "sharing/file_attachment.h" +#include "sharing/flags/nearby_sharing_feature_flags.h" +#include "sharing/incoming_frames_reader.h" +#include "sharing/incoming_share_target_info.h" +#include "sharing/internal/api/bluetooth_adapter.h" +#include "sharing/internal/api/sharing_platform.h" +#include "sharing/internal/api/wifi_adapter.h" +#include "sharing/internal/base/encode.h" +#include "sharing/internal/public/connectivity_manager.h" +#include "sharing/internal/public/context.h" +#include "sharing/internal/public/logging.h" +#include "sharing/local_device_data/nearby_share_local_device_data_manager.h" +#include "sharing/local_device_data/nearby_share_local_device_data_manager_impl.h" +#include "sharing/nearby_connection.h" +#include "sharing/nearby_connections_manager.h" +#include "sharing/nearby_connections_types.h" +#include "sharing/nearby_file_handler.h" +#include "sharing/nearby_share_profile_info_provider_impl.h" +#include "sharing/nearby_sharing_decoder.h" +#include "sharing/nearby_sharing_service.h" +#include "sharing/nearby_sharing_service_extension.h" +#include "sharing/nearby_sharing_settings.h" +#include "sharing/nearby_sharing_util.h" +#include "sharing/outgoing_share_target_info.h" +#include "sharing/paired_key_verification_runner.h" +#include "sharing/payload_tracker.h" +#include "sharing/proto/encrypted_metadata.pb.h" +#include "sharing/proto/enums.pb.h" +#include "sharing/proto/wire_format.pb.h" +#include "sharing/scheduling/nearby_share_scheduler_utils.h" +#include "sharing/share_target.h" +#include "sharing/share_target_discovered_callback.h" +#include "sharing/share_target_info.h" +#include "sharing/text_attachment.h" +#include "sharing/transfer_metadata.h" +#include "sharing/transfer_metadata_builder.h" +#include "sharing/transfer_update_callback.h" +#include "sharing/wifi_credentials_attachment.h" + +namespace nearby { +namespace sharing { +namespace { + +using ::nearby::sharing::api::SharingPlatform; +using ::nearby::sharing::proto::DataUsage; +using ::nearby::sharing::proto::DeviceVisibility; +using ::nearby::sharing::proto::FastInitiationNotificationState; +using Type = ::nearby::sharing::service::proto::TextMetadata; +using ::location::nearby::proto::sharing::AttachmentTransmissionStatus; +using ::location::nearby::proto::sharing::EstablishConnectionStatus; +using ::location::nearby::proto::sharing::OSType; +using ::location::nearby::proto::sharing::ResponseToIntroduction; +using ::location::nearby::proto::sharing::SessionStatus; + +constexpr absl::Duration kBackgroundAdvertisementRotationDelayMin = + absl::Minutes(12); +// 870 seconds represents 14:30 minutes +constexpr absl::Duration kBackgroundAdvertisementRotationDelayMax = + absl::Seconds(870); +constexpr absl::Duration kInvalidateSurfaceStateDelayAfterTransferDone = + absl::Milliseconds(3000); +constexpr absl::Duration kProcessShutdownPendingTimerDelay = // NOLINT + absl::Seconds(15); +constexpr absl::Duration kProcessNetworkChangeTimerDelay = absl::Seconds(1); + +// Cooldown period after a successful incoming share before we allow the "Device +// nearby is sharing" notification to appear again. +constexpr absl::Duration kFastInitiationScannerCooldown = absl::Seconds(8); + +// The maximum number of certificate downloads that can be performed during a +// discovery session. +constexpr size_t kMaxCertificateDownloadsDuringDiscovery = 3u; +// The time between certificate downloads during a discovery session. The +// download is only attempted if there are discovered, contact-based +// advertisements that cannot decrypt any currently stored public certificates. +constexpr absl::Duration kCertificateDownloadDuringDiscoveryPeriod = + absl::Seconds(10); + +constexpr absl::string_view kConnectionListenerName = "nearby-share-service"; +constexpr absl::string_view kScreenStateListenerName = "nearby-share-service"; +constexpr absl::string_view kProfileRelativePath = "Google/Nearby/Sharing"; + +// Wraps a call to OnTransferUpdate() to filter any updates after receiving a +// final status. +class TransferUpdateDecorator : public TransferUpdateCallback { + public: + using Callback = + std::function; + + explicit TransferUpdateDecorator(Callback callback) + : callback_(std::move(callback)) {} + TransferUpdateDecorator(const TransferUpdateDecorator&) = delete; + TransferUpdateDecorator& operator=(const TransferUpdateDecorator&) = delete; + ~TransferUpdateDecorator() override = default; + + void OnTransferUpdate(const ShareTarget& share_target, + const TransferMetadata& transfer_metadata) override { + if (got_final_status_) { + // If we already got a final status, we can ignore any subsequent final + // statuses caused by race conditions. + NL_VLOG(1) + << __func__ << ": Transfer update decorator swallowed " + << "status update because a final status was already received: " + << share_target.id << ": " + << TransferMetadata::StatusToString(transfer_metadata.status()); + return; + } + got_final_status_ = transfer_metadata.is_final_status(); + callback_(share_target, transfer_metadata); + } + + private: + bool got_final_status_ = false; + Callback callback_; +}; + +} // namespace + +NearbySharingServiceImpl::NearbySharingServiceImpl( + Context* context, SharingPlatform& sharing_platform, + NearbySharingDecoder* decoder, + nearby::network::HttpClientFactory* http_client_factory, + std::unique_ptr nearby_connections_manager, + nearby::analytics::EventLogger* event_logger) + : context_(context), + device_info_(sharing_platform.GetDeviceInfo()), + preference_manager_(sharing_platform.GetPreferenceManager()), + account_manager_(sharing_platform.GetAccountManager()), + decoder_(decoder), + nearby_connections_manager_(std::move(nearby_connections_manager)), + nearby_share_client_factory_( + std::make_unique( + device_info_.GetOsType(), account_manager_, http_client_factory, + &nearby_share_http_notifier_, event_logger)), + profile_info_provider_( + std::make_unique( + device_info_, account_manager_)), + local_device_data_manager_( + NearbyShareLocalDeviceDataManagerImpl::Factory::Create( + context_, preference_manager_, account_manager_, device_info_, + nearby_share_client_factory_.get(), + profile_info_provider_.get())), + contact_manager_(NearbyShareContactManagerImpl::Factory::Create( + context_, preference_manager_, account_manager_, + nearby_share_client_factory_.get(), + local_device_data_manager_.get())), + nearby_fast_initiation_( + NearbyFastInitiationImpl::Factory::Create(context_)), + analytics_recorder_( + std::make_unique(event_logger)), + settings_(std::make_unique( + context_, context_->GetClock(), device_info_, preference_manager_, + local_device_data_manager_.get(), event_logger)), + service_extension_(std::make_unique( + context_, settings_.get())) { + NL_DCHECK(decoder_); + NL_DCHECK(nearby_connections_manager_); + + service_thread_ = context_->CreateSequencedTaskRunner(); + + certificate_download_during_discovery_timer_ = context_->CreateTimer(); + on_network_changed_delay_timer_ = context_->CreateTimer(); + mutual_acceptance_timeout_alarm_ = context_->CreateTimer(); + rotate_background_advertisement_timer_ = context_->CreateTimer(); + fast_initiation_scanner_cooldown_timer_ = context_->CreateTimer(); + + is_shutting_down_ = std::make_unique(false); + std::filesystem::path path = device_info_.GetAppDataPath(); + + std::filesystem::path full_database_path = + path / std::string(kProfileRelativePath); + certificate_manager_ = NearbyShareCertificateManagerImpl::Factory::Create( + context_, sharing_platform, local_device_data_manager_.get(), + contact_manager_.get(), full_database_path.string(), + nearby_share_client_factory_.get()), + + certificate_manager_->AddObserver(this); + context_->GetConnectivityManager()->RegisterConnectionListener( + kConnectionListenerName, + [this](nearby::ConnectivityManager::ConnectionType type, + bool is_lan_connected) { + OnNetworkChanged(type); + OnLanConnectedChanged(is_lan_connected); + }); + + screen_unlock_time_ = context_->GetClock()->Now(); + is_screen_locked_ = device_info_.IsScreenLocked(); + device_info_.RegisterScreenLockedListener( + kScreenStateListenerName, + [&](nearby::api::DeviceInfo::ScreenStatus screen_status) { + OnLockStateChanged(screen_status == + nearby::api::DeviceInfo::ScreenStatus::kLocked); + }); + + account_manager_.AddObserver(this); + settings_->AddSettingsObserver(this); + nearby_fast_initiation_->AddObserver(this); + // Setup saving path. + std::string custom_save_path = settings_->GetCustomSavePath(); + NL_LOG(INFO) << __func__ << ": Set custom save path: " << custom_save_path; + nearby_connections_manager_->SetCustomSavePath(custom_save_path); + + if (settings_->GetEnabled()) { + local_device_data_manager_->Start(); + contact_manager_->Start(); + certificate_manager_->Start(); + } + + update_file_paths_in_progress_ = false; + + SetupBluetoothAdapter(); +} + +NearbySharingServiceImpl::~NearbySharingServiceImpl() = default; + +void NearbySharingServiceImpl::Shutdown( + std::function status_codes_callback) { + RunOnNearbySharingServiceThread( + "api_shutdown", + [&, status_codes_callback = std::move(status_codes_callback)]() { + *is_shutting_down_ = true; + for (auto* observer : observers_.GetObservers()) { + observer->OnShutdown(); + } + + observers_.Clear(); + + StopAdvertising(); + if (IsBackgroundScanningFeatureEnabled()) { + StopFastInitiationScanning(); + } + StopFastInitiationAdvertising(); + StopScanning(); + nearby_connections_manager_->Shutdown(); + + Cleanup(); + + certificate_manager_->RemoveObserver(this); + account_manager_.RemoveObserver(this); + context_->GetConnectivityManager()->UnregisterConnectionListener( + kConnectionListenerName); + context_->GetBluetoothAdapter().RemoveObserver(this); + nearby_fast_initiation_->RemoveObserver(this); + + on_network_changed_delay_timer_->Stop(); + + foreground_receive_callbacks_.Clear(); + background_receive_callbacks_.Clear(); + + device_info_.UnregisterScreenLockedListener( + kScreenStateListenerName); + + settings_->RemoveSettingsObserver(this); + + if (settings_->GetEnabled()) { + local_device_data_manager_->Stop(); + contact_manager_->Stop(); + certificate_manager_->Stop(); + } + + is_shutting_down_ = nullptr; + std::move(status_codes_callback)(StatusCodes::kOk); + }); +} + +void NearbySharingServiceImpl::Cleanup() { + SetInHighVisibility(false); + + endpoint_discovery_events_ = {}; + + ClearOutgoingShareTargetInfoMap(); + incoming_share_target_info_map_.clear(); + discovered_advertisements_to_retry_map_.clear(); + discovered_advertisements_retried_set_.clear(); + + foreground_send_transfer_callbacks_.Clear(); + background_send_transfer_callbacks_.Clear(); + foreground_send_discovery_callbacks_.Clear(); + background_send_discovery_callbacks_.Clear(); + + last_incoming_metadata_.reset(); + last_outgoing_metadata_.reset(); + attachment_info_map_.clear(); + locally_cancelled_share_target_ids_.clear(); + + mutual_acceptance_timeout_alarm_->Stop(); + disconnection_timeout_alarms_.clear(); + + is_scanning_ = false; + is_transferring_ = false; + is_receiving_files_ = false; + is_sending_files_ = false; + is_connecting_ = false; + advertising_power_level_ = PowerLevel::kUnknown; + + certificate_download_during_discovery_timer_->Stop(); + rotate_background_advertisement_timer_->Stop(); +} + +void NearbySharingServiceImpl::AddObserver( + NearbySharingService::Observer* observer) { + observers_.AddObserver(observer); +} + +void NearbySharingServiceImpl::RemoveObserver( + NearbySharingService::Observer* observer) { + observers_.RemoveObserver(observer); +} + +bool NearbySharingServiceImpl::HasObserver( + NearbySharingService::Observer* observer) { + return observers_.HasObserver(observer); +} + +void NearbySharingServiceImpl::RegisterSendSurface( + TransferUpdateCallback* transfer_callback, + ShareTargetDiscoveredCallback* discovery_callback, SendSurfaceState state, + std::function status_codes_callback) { + RunOnNearbySharingServiceThread( + "api_register_send_surface", + [&, transfer_callback, discovery_callback, state, + status_codes_callback = std::move(status_codes_callback)]() { + NL_DCHECK(transfer_callback); + NL_DCHECK(discovery_callback); + NL_DCHECK_NE(static_cast(state), + static_cast(SendSurfaceState::kUnknown)); + NL_LOG(INFO) << __func__ + << ": RegisterSendSurface is called with state: " + << (state == SendSurfaceState::kForeground ? "Foreground" + : "Background") + << ", transfer_callback: " << transfer_callback; + + if (foreground_send_transfer_callbacks_.HasObserver( + transfer_callback) || + background_send_transfer_callbacks_.HasObserver( + transfer_callback)) { + NL_VLOG(1) + << __func__ + << ": RegisterSendSurface failed. Already registered for a " + "different state."; + std::move(status_codes_callback)(StatusCodes::kError); + return; + } + + if (state == SendSurfaceState::kForeground) { + // Only check this error case for foreground senders + if (!HasAvailableConnectionMediums()) { + NL_VLOG(1) << __func__ << ": No available connection medium."; + std::move(status_codes_callback)( + StatusCodes::kNoAvailableConnectionMedium); + return; + } + + foreground_send_transfer_callbacks_.AddObserver(transfer_callback); + foreground_send_discovery_callbacks_.AddObserver(discovery_callback); + } else { + background_send_transfer_callbacks_.AddObserver(transfer_callback); + background_send_discovery_callbacks_.AddObserver(discovery_callback); + } + + if (is_receiving_files_) { + InternalUnregisterSendSurface(transfer_callback, discovery_callback); + NL_VLOG(1) + << __func__ + << ": Ignore registering (and unregistering if registered) send " + "surface because we're currently receiving files."; + std::move(status_codes_callback)( + StatusCodes::kTransferAlreadyInProgress); + return; + } + + // If the share sheet to be registered is a foreground surface, let it + // catch up with most recent transfer metadata immediately. + if (state == SendSurfaceState::kForeground && + last_outgoing_metadata_.has_value()) { + // When a new share sheet is registered, we want to immediately show + // the in-progress bar. + discovery_callback->OnShareTargetDiscovered( + last_outgoing_metadata_->first); + transfer_callback->OnTransferUpdate(last_outgoing_metadata_->first, + last_outgoing_metadata_->second); + } + + // Sync down data from Nearby server when the sending flow starts, + // making our best effort to have fresh contact and certificate data. + // There is no need to wait for these calls to finish. The periodic + // server requests will typically be sufficient, but we don't want the + // user to be blocked for hours waiting for a periodic sync. + if (state == SendSurfaceState::kForeground && + !last_outgoing_metadata_) { + NL_VLOG(1) << __func__ + << ": Downloading local device data, contacts, and " + "certificates from " + << "Nearby server at start of sending flow."; + local_device_data_manager_->DownloadDeviceData(); + contact_manager_->DownloadContacts(); + certificate_manager_->DownloadPublicCertificates(); + } + + // Let newly registered send surface catch up with discovered share + // targets from current scanning session. + if (is_scanning_) { + for (const auto& item : outgoing_share_target_map_) { + discovery_callback->OnShareTargetDiscovered(item.second); + } + } + + // Set Share Start time for Foreground Send Surfaces + if (state == SendSurfaceState::kForeground) { + share_foreground_send_surface_start_timestamp_ = + context_->GetClock()->Now(); + } + + NL_VLOG(1) << __func__ + << ": A SendSurface has been registered for state: " + << SendSurfaceStateToString(state); + + NL_VLOG(1) + << "RegisterSendSurface: foreground_send_transfer_callbacks_:" + << foreground_send_transfer_callbacks_.size() + << ", foreground_send_discovery_callbacks_:" + << foreground_send_discovery_callbacks_.size() + << ", background_send_transfer_callbacks_:" + << background_send_transfer_callbacks_.size() + << ", background_send_discovery_callbacks_" + << background_send_discovery_callbacks_.size(); + + InvalidateSendSurfaceState(); + std::move(status_codes_callback)(StatusCodes::kOk); + }); +} + +void NearbySharingServiceImpl::UnregisterSendSurface( + TransferUpdateCallback* transfer_callback, + ShareTargetDiscoveredCallback* discovery_callback, + std::function status_codes_callback) { + RunOnNearbySharingServiceThread( + "api_unregister_send_surface", + [&, transfer_callback, discovery_callback, + status_codes_callback = std::move(status_codes_callback)]() { + StatusCodes status_codes = InternalUnregisterSendSurface( + transfer_callback, discovery_callback); + + NL_VLOG(1) + << "UnregisterSendSurface: foreground_send_transfer_callbacks_:" + << foreground_send_transfer_callbacks_.size() + << ", foreground_send_discovery_callbacks_:" + << foreground_send_discovery_callbacks_.size() + << ", background_send_transfer_callbacks_:" + << background_send_transfer_callbacks_.size() + << ", background_send_discovery_callbacks_" + << background_send_discovery_callbacks_.size(); + + std::move(status_codes_callback)(status_codes); + }); +} + +void NearbySharingServiceImpl::RegisterReceiveSurface( + TransferUpdateCallback* transfer_callback, ReceiveSurfaceState state, + std::function status_codes_callback) { + RunOnNearbySharingServiceThread( + "api_register_receive_surface", + [&, transfer_callback, state, + status_codes_callback = std::move(status_codes_callback)]() { + NL_DCHECK(transfer_callback); + NL_DCHECK_NE(static_cast(state), + static_cast(ReceiveSurfaceState::kUnknown)); + + NL_LOG(INFO) << __func__ + << ": RegisterReceiveSurface is called with state: " + << (state == ReceiveSurfaceState::kForeground + ? "Foreground" + : "Background") + << ", transfer_callback: " << transfer_callback; + + // Check available mediums. + if (!HasAvailableConnectionMediums()) { + NL_VLOG(1) << __func__ << ": No available connection medium."; + std::move(status_codes_callback)( + StatusCodes::kNoAvailableConnectionMedium); + return; + } + + // We specifically allow re-registering without error, so it is clear to + // caller that the transfer_callback is currently registered. + if (GetReceiveCallbacksFromState(state).HasObserver( + transfer_callback)) { + NL_VLOG(1) << __func__ + << ": transfer callback already registered, ignoring"; + std::move(status_codes_callback)(StatusCodes::kOk); + return; + } else if (foreground_receive_callbacks_.HasObserver( + transfer_callback) || + background_receive_callbacks_.HasObserver( + transfer_callback)) { + NL_LOG(ERROR) << __func__ + << ": transfer callback already registered but for a " + "different state."; + std::move(status_codes_callback)(StatusCodes::kError); + return; + } + + // If the receive surface to be registered is a foreground surface, let + // it catch up with most recent transfer metadata immediately. + if (state == ReceiveSurfaceState::kForeground && + last_incoming_metadata_) { + transfer_callback->OnTransferUpdate(last_incoming_metadata_->first, + last_incoming_metadata_->second); + } + + GetReceiveCallbacksFromState(state).AddObserver(transfer_callback); + + NL_VLOG(1) << __func__ << ": A ReceiveSurface(" + << ReceiveSurfaceStateToString(state) + << ") has been registered"; + + if (state == ReceiveSurfaceState::kForeground) { + if (!IsBluetoothPresent()) { + NL_LOG(ERROR) << __func__ << ": Bluetooth is not present."; + } else if (!IsBluetoothPowered()) { + NL_LOG(WARNING) << __func__ << ": Bluetooth is not powered."; + } else { + NL_VLOG(1) << __func__ << ": This device's MAC address is: " + << nearby::device::CanonicalizeBluetoothAddress( + *context_->GetBluetoothAdapter().GetAddress()); + } + } + + NL_VLOG(1) << "RegisterReceiveSurface: foreground_receive_callbacks_:" + << foreground_receive_callbacks_.size() + << ", background_receive_callbacks_:" + << background_receive_callbacks_.size(); + + InvalidateReceiveSurfaceState(); + std::move(status_codes_callback)(StatusCodes::kOk); + }); +} + +void NearbySharingServiceImpl::UnregisterReceiveSurface( + TransferUpdateCallback* transfer_callback, + std::function status_codes_callback) { + RunOnNearbySharingServiceThread( + "api_unregister_receive_surface", + [&, transfer_callback, + status_codes_callback = std::move(status_codes_callback)]() { + StatusCodes status_codes = + InternalUnregisterReceiveSurface(transfer_callback); + NL_VLOG(1) << "UnregisterReceiveSurface: foreground_receive_callbacks_:" + << foreground_receive_callbacks_.size() + << ", background_receive_callbacks_:" + << background_receive_callbacks_.size(); + std::move(status_codes_callback)(status_codes); + return; + }); +} + +void NearbySharingServiceImpl::ClearForegroundReceiveSurfaces( + std::function status_codes_callback) { + RunOnNearbySharingServiceThread( + "api_clear_foreground_receive_surfaces", + [&, status_codes_callback = std::move(status_codes_callback)]() { + std::vector fg_receivers; + for (auto& callback : foreground_receive_callbacks_.GetObservers()) + fg_receivers.push_back(callback); + + StatusCodes status = StatusCodes::kOk; + for (TransferUpdateCallback* callback : fg_receivers) { + if (InternalUnregisterReceiveSurface(callback) != StatusCodes::kOk) + status = StatusCodes::kError; + } + std::move(status_codes_callback)(status); + }); +} + +bool NearbySharingServiceImpl::IsInHighVisibility() const { + return in_high_visibility_; +} + +bool NearbySharingServiceImpl::IsTransferring() const { + return is_transferring_; +} + +bool NearbySharingServiceImpl::IsReceivingFile() const { + return is_receiving_files_; +} + +bool NearbySharingServiceImpl::IsSendingFile() const { + return is_sending_files_; +} + +bool NearbySharingServiceImpl::IsScanning() const { return is_scanning_; } + +bool NearbySharingServiceImpl::IsConnecting() const { return is_connecting_; } + +std::string NearbySharingServiceImpl::GetQrCodeUrl() const { + return service_extension_->GetQrCodeUrl(); +} + +void NearbySharingServiceImpl::SendAttachments( + const ShareTarget& share_target, + std::vector> attachments, + std::function status_codes_callback) { + ShareTarget share_target_copy = share_target; + for (std::unique_ptr& attachment : attachments) { + attachment->MoveToShareTarget(share_target_copy); + } + + RunOnNearbySharingServiceThread( + "api_send_attachments", + [&, share_target, share_target_copy = std::move(share_target_copy), + status_codes_callback = std::move(status_codes_callback)]() { + if (!is_scanning_) { + NL_LOG(WARNING) << __func__ + << ": Failed to send attachments. Not scanning."; + std::move(status_codes_callback)(StatusCodes::kError); + return; + } + + // |is_scanning_| means at least one send transfer callback. + NL_DCHECK(!foreground_send_transfer_callbacks_.empty() || + !background_send_transfer_callbacks_.empty()); + // |is_scanning_| and |is_transferring_| are mutually exclusive. + NL_DCHECK(!is_transferring_); + + ShareTargetInfo* info = GetShareTargetInfo(share_target); + if (!info || !info->endpoint_id()) { + NL_LOG(WARNING) + << __func__ + << ": Failed to send attachments. Unknown ShareTarget."; + std::move(status_codes_callback)(StatusCodes::kError); + return; + } + + // Set session ID. + info->set_session_id(analytics_recorder_->GenerateNextId()); + + if (!share_target_copy.has_attachments()) { + NL_LOG(WARNING) << __func__ << ": No attachments to send."; + std::move(status_codes_callback)(StatusCodes::kError); + return; + } + + // For sending advertisement from scanner, the request advertisement + // should always be visible to everyone. + std::optional> endpoint_info = + CreateEndpointInfo(local_device_data_manager_->GetDeviceName()); + if (!endpoint_info) { + NL_LOG(WARNING) << __func__ + << ": Could not create local endpoint info."; + std::move(status_codes_callback)(StatusCodes::kError); + return; + } + + info->set_transfer_update_callback( + std::make_unique( + [&](const ShareTarget& share_target, + const TransferMetadata& transfer_metadata) { + OnOutgoingTransferUpdate(share_target, transfer_metadata); + })); + + // Log analytics event of sending start. + analytics_recorder_->NewSendStart( + info->session_id(), + /*transfer_position=*/GetConnectedShareTargetPos(share_target), + /*concurrent_connections=*/GetConnectedShareTargetCount(), + share_target); + + send_attachments_timestamp_ = context_->GetClock()->Now(); + OnTransferStarted(/*is_incoming=*/false); + is_connecting_ = true; + InvalidateSendSurfaceState(); + + // Send process initialized successfully, from now on status updated + // will be sent out via OnOutgoingTransferUpdate(). + info->transfer_update_callback()->OnTransferUpdate( + share_target_copy, + TransferMetadataBuilder() + .set_status(TransferMetadata::Status::kConnecting) + .build()); + + CreatePayloads(std::move(share_target_copy), + [this, endpoint_info = std::move(*endpoint_info)]( + ShareTarget share_target, bool success) { + // Log analytics event of describing attachments. + analytics_recorder_->NewDescribeAttachments( + share_target.GetAttachments()); + + OnCreatePayloads(std::move(endpoint_info), + share_target, success); + }); + + std::move(status_codes_callback)(StatusCodes::kOk); + }); +} + +void NearbySharingServiceImpl::Accept( + const ShareTarget& share_target, + std::function status_codes_callback) { + RunOnNearbySharingServiceThread( + "api_accept", + [&, share_target, + status_codes_callback = std::move(status_codes_callback)]() { + // Log analytics event of responding to introduction. + analytics_recorder_->NewRespondToIntroduction( + ResponseToIntroduction::ACCEPT_INTRODUCTION, receiving_session_id_); + + ShareTargetInfo* info = GetShareTargetInfo(share_target); + if (!info || !info->connection()) { + NL_LOG(WARNING) << __func__ + << ": Accept invoked for unknown share target"; + std::move(status_codes_callback)(StatusCodes::kOutOfOrderApiCall); + return; + } + + std::optional> metadata = + share_target.is_incoming ? last_incoming_metadata_ + : last_outgoing_metadata_; + if (!ReadyToAccept(share_target, + metadata.has_value() + ? metadata->second.status() + : TransferMetadata::Status::kUnknown)) { + NL_LOG(WARNING) << __func__ << ": out of order API call."; + status_codes_callback(StatusCodes::kOutOfOrderApiCall); + return; + } + + is_waiting_to_record_accept_to_transfer_start_metric_ = + share_target.is_incoming; + if (share_target.is_incoming) { + incoming_share_accepted_timestamp_ = context_->GetClock()->Now(); + ReceivePayloads(share_target, std::move(status_codes_callback)); + return; + } + + std::move(status_codes_callback)(SendPayloads(share_target)); + }); +} + +void NearbySharingServiceImpl::Reject( + const ShareTarget& share_target, + std::function status_codes_callback) { + RunOnNearbySharingServiceThread( + "api_reject", + [&, share_target, + status_codes_callback = std::move(status_codes_callback)]() { + // Log analytics event of responding to introduction. + analytics_recorder_->NewRespondToIntroduction( + ResponseToIntroduction::REJECT_INTRODUCTION, receiving_session_id_); + + ShareTargetInfo* info = GetShareTargetInfo(share_target); + if (!info || !info->connection()) { + NL_LOG(WARNING) << __func__ + << ": Reject invoked for unknown share target"; + std::move(status_codes_callback)(StatusCodes::kOutOfOrderApiCall); + return; + } + + NearbyConnection* connection = info->connection(); + + RunOnNearbySharingServiceThreadDelayed( + "incoming_rejection_delay", kIncomingRejectionDelay, + [&, share_target]() { CloseConnection(share_target); }); + + connection->SetDisconnectionListener([&, share_target]() { + RunOnNearbySharingServiceThread( + "disconnection_listener", + [&, share_target]() { UnregisterShareTarget(share_target); }); + }); + + WriteResponseFrame( + *connection, + nearby::sharing::service::proto::ConnectionResponseFrame::REJECT); + NL_VLOG(1) << __func__ + << ": Successfully wrote a rejection response frame"; + + if (info->transfer_update_callback()) { + info->transfer_update_callback()->OnTransferUpdate( + share_target, TransferMetadataBuilder() + .set_status(TransferMetadata::Status::kRejected) + .build()); + } + + std::move(status_codes_callback)(StatusCodes::kOk); + }); +} + +void NearbySharingServiceImpl::Cancel( + const ShareTarget& share_target, + std::function status_codes_callback) { + RunOnAnyThread("api_cancel", [&, share_target, + status_codes_callback = + std::move(status_codes_callback)]() { + NL_LOG(INFO) << __func__ << ": User canceled transfer"; + if (locally_cancelled_share_target_ids_.contains(share_target.id)) { + NL_LOG(WARNING) << __func__ << ": Cancel is called again."; + status_codes_callback(StatusCodes::kOutOfOrderApiCall); + return; + } + locally_cancelled_share_target_ids_.insert(share_target.id); + DoCancel(share_target, std::move(status_codes_callback), + /*is_initiator_of_cancellation=*/true); + }); +} + +// Note: |share_target| is intentionally passed by value. A share target +// reference could likely be invalidated by the owner during the multistep +// cancellation process. +void NearbySharingServiceImpl::DoCancel( + ShareTarget share_target, + std::function status_codes_callback, + bool is_initiator_of_cancellation) { + ShareTargetInfo* info = GetShareTargetInfo(share_target); + if (!info || !info->endpoint_id()) { + NL_LOG(ERROR) << __func__ + << ": Cancel invoked for unknown share target, returning " + "kOutOfOrderApiCall"; + // Make sure to clean up files just in case. + if (!update_file_paths_in_progress_) { + UpdateFilePath(share_target); + } + RemoveIncomingPayloads(share_target); + std::move(status_codes_callback)(StatusCodes::kOutOfOrderApiCall); + return; + } + + // For metrics. + all_cancelled_share_target_ids_.insert(share_target.id); + + // Cancel all ongoing payload transfers before invoking the transfer update + // callback. Invoking the transfer update callback first could result in + // payload cleanup before we have a chance to cancel the payload via Nearby + // Connections, and the payload tracker might not receive the expected + // cancellation signals. Also, note that there might not be any ongoing + // payload transfer, for example, if a connection has not been established + // yet. + for (int64_t attachment_id : share_target.GetAttachmentIds()) { + std::optional payload_id = GetAttachmentPayloadId(attachment_id); + if (payload_id) { + nearby_connections_manager_->Cancel(*payload_id); + } + } + + // Inform the user that the transfer has been cancelled before disconnecting + // because subsequent disconnections might be interpreted as failure. The + // TransferUpdateDecorator will ignore subsequent statuses in favor of this + // cancelled status. Note that the transfer update callback might have already + // been invoked as a result of the payload cancellations above, but again, + // superfluous status updates are handled gracefully by the + // TransferUpdateDecorator. + if (info->transfer_update_callback()) { + info->transfer_update_callback()->OnTransferUpdate( + share_target, TransferMetadataBuilder() + .set_status(TransferMetadata::Status::kCancelled) + .build()); + } + + // If a connection exists, close the connection. Note: The initiator of a + // cancellation waits for a short delay before closing the connection, + // allowing for final processing by the other device. Otherwise, disconnect + // from endpoint id directly. Note: A share attempt can be cancelled by the + // user before a connection is fully established, in which case, + // info->connection() will be null. + if (info->connection()) { + NL_LOG(INFO) << "Disconnect fully established endpoint id:" + << *info->endpoint_id(); + if (is_initiator_of_cancellation) { + info->connection()->SetDisconnectionListener( + [&, share_target, info]() { + info->set_connection(nullptr); + RunOnNearbySharingServiceThread( + "api_unregister_share_target", [&, share_target]() { + NL_LOG(INFO) + << "Unregister share target in disconnection listener."; + UnregisterShareTarget(std::move(share_target)); + }); + }); + + RunOnNearbySharingServiceThreadDelayed( + "initiator_cancel_delay", kInitiatorCancelDelay, [&, share_target]() { + NL_LOG(INFO) << "Close connection after cancellation delay."; + CloseConnection(share_target); + }); + + WriteCancelFrame(*info->connection()); + } else { + info->connection()->Close(); + } + } else { + NL_LOG(INFO) << "Disconnect endpoint id:" << *info->endpoint_id(); + nearby_connections_manager_->Disconnect(*info->endpoint_id()); + UnregisterShareTarget(share_target); + } + + std::move(status_codes_callback)(StatusCodes::kOk); +} + +bool NearbySharingServiceImpl::DidLocalUserCancelTransfer( + const ShareTarget& share_target) { + return absl::c_linear_search(locally_cancelled_share_target_ids_, + share_target.id); +} + +void NearbySharingServiceImpl::Open( + const ShareTarget& share_target, + std::function status_codes_callback) { + RunOnAnyThread( + "api_open", [&, share_target, + status_codes_callback = std::move(status_codes_callback)]() { + NL_LOG(INFO) << __func__ << ": Open is called for share_target: " + << share_target.ToString(); + + // Log analytics event of opening received attachments. + ShareTargetInfo* info = GetShareTargetInfo(share_target); + analytics_recorder_->NewOpenReceivedAttachments( + share_target.GetAttachments(), + info != nullptr ? info->session_id() : 0); + + status_codes_callback(service_extension_->Open(share_target)); + }); +} + +void NearbySharingServiceImpl::OpenUrl(const ::nearby::network::Url& url) { + RunOnAnyThread("api_open_url", + [&, url]() { service_extension_->OpenUrl(url); }); +} + +void NearbySharingServiceImpl::CopyText(absl::string_view text) { + RunOnAnyThread("api_copy_text", [&, text = std::string(text)]() { + service_extension_->CopyText(text); + }); +} + +void NearbySharingServiceImpl::JoinWifiNetwork(absl::string_view ssid, + absl::string_view password) { + RunOnAnyThread("api_join_wifi_network", [&, ssid = std::string(ssid), + password = std::string(password)]() { + service_extension_->JoinWifiNetwork(ssid, password); + }); +} + +void NearbySharingServiceImpl::SetArcTransferCleanupCallback( + std::function callback) { + // In the case where multiple Nearby Share sessions are started, successive + // Nearby Share bubbles shown will prevent the user from sharing while the + // initial bubble is still active. For the successive bubble(s), we want to + // make sure only the original cleanup callback is valid. + // Also in the following case: + // 1. CrOS starts a receive transfer. + // 2. ARC starts a send transfer and |arc_transfer_cleanup_callback_| is set + // erroneously if |is_transferring_| check is missing. + // As multiple transfers cannot occur at the same time, a "Can't Share" error + // will occur. When the transfer in [1] finishes and another ARC Nearby Share + // session starts, the |arc_transfer_cleanup_callback_| can't be set if a + // value is already set to ensure all clean up is performed. Hence, check if + // not |is_transferring_| before setting |arc_transfer_cleanup_callback_|. + if (!is_transferring_ && arc_transfer_cleanup_callback_ == nullptr) { + arc_transfer_cleanup_callback_ = std::move(callback); + } +} + +NearbyShareSettings* NearbySharingServiceImpl::GetSettings() { + return settings_.get(); +} + +NearbyShareHttpNotifier* NearbySharingServiceImpl::GetHttpNotifier() { + return &nearby_share_http_notifier_; +} + +NearbyShareLocalDeviceDataManager* +NearbySharingServiceImpl::GetLocalDeviceDataManager() { + return local_device_data_manager_.get(); +} + +NearbyShareContactManager* NearbySharingServiceImpl::GetContactManager() { + return contact_manager_.get(); +} + +NearbyShareCertificateManager* +NearbySharingServiceImpl::GetCertificateManager() { + return certificate_manager_.get(); +} + +AccountManager* NearbySharingServiceImpl::GetAccountManager() { + return &account_manager_; +} + +void NearbySharingServiceImpl::OnIncomingConnection( + absl::string_view endpoint_id, absl::Span endpoint_info, + NearbyConnection* connection) { + NL_DCHECK(connection); + + // Sync down data from Nearby server when the receiving flow starts, making + // our best effort to have fresh contact and certificate data. There is no + // need to wait for these calls to finish. The periodic server requests will + // typically be sufficient, but we don't want the user to be blocked for + // hours waiting for a periodic sync. + NL_VLOG(1) + << __func__ + << ": Downloading local device data, contacts, and certificates from " + << "Nearby server at start of receiving flow."; + local_device_data_manager_->DownloadDeviceData(); + contact_manager_->DownloadContacts(); + certificate_manager_->DownloadPublicCertificates(); + + ShareTarget placeholder_share_target; + placeholder_share_target.is_incoming = true; + ShareTargetInfo& share_target_info = + GetOrCreateShareTargetInfo(placeholder_share_target, endpoint_id); + share_target_info.set_connection(connection); + + // Set receiving session id. + receiving_session_id_ = analytics_recorder_->GenerateNextId(); + + connection->SetDisconnectionListener([this, placeholder_share_target]() { + RunOnNearbySharingServiceThread( + "disconnection_listener", [&, placeholder_share_target]() { + RefreshUIOnDisconnection(placeholder_share_target); + }); + }); + + std::unique_ptr advertisement = + decoder_->DecodeAdvertisement(endpoint_info); + OnIncomingAdvertisementDecoded(endpoint_id, + std::move(placeholder_share_target), + std::move(advertisement)); +} + +NearbySharingService::StatusCodes +NearbySharingServiceImpl::InternalUnregisterSendSurface( + TransferUpdateCallback* transfer_callback, + ShareTargetDiscoveredCallback* discovery_callback) { + NL_DCHECK(transfer_callback); + NL_DCHECK(discovery_callback); + NL_LOG(INFO) << __func__ << ": UnregisterSendSurface is called" + << ", transfer_callback: " << transfer_callback; + + if (!foreground_send_transfer_callbacks_.HasObserver(transfer_callback) && + !background_send_transfer_callbacks_.HasObserver(transfer_callback)) { + NL_VLOG(1) + << __func__ + << ": unregisterSendSurface failed. Unknown TransferUpdateCallback"; + return StatusCodes::kError; + } + + if (!foreground_send_transfer_callbacks_.empty() && last_outgoing_metadata_ && + last_outgoing_metadata_->second.is_final_status()) { + // We already saw the final status in the foreground + // Nullify it so the next time the user opens sharing, it starts the UI from + // the beginning + last_outgoing_metadata_.reset(); + } + + SendSurfaceState state = SendSurfaceState::kUnknown; + if (foreground_send_transfer_callbacks_.HasObserver(transfer_callback)) { + foreground_send_transfer_callbacks_.RemoveObserver(transfer_callback); + foreground_send_discovery_callbacks_.RemoveObserver(discovery_callback); + state = SendSurfaceState::kForeground; + } else { + background_send_transfer_callbacks_.RemoveObserver(transfer_callback); + background_send_discovery_callbacks_.RemoveObserver(discovery_callback); + state = SendSurfaceState::kBackground; + } + + // Displays the most recent payload status processed by foreground surfaces on + // background surfaces. + if (foreground_send_transfer_callbacks_.empty() && last_outgoing_metadata_) { + for (auto& background_transfer_callback : + background_send_transfer_callbacks_.GetObservers()) { + background_transfer_callback->OnTransferUpdate( + last_outgoing_metadata_->first, last_outgoing_metadata_->second); + } + } + + NL_VLOG(1) << __func__ << ": A SendSurface has been unregistered: " + << SendSurfaceStateToString(state); + InvalidateSurfaceState(); + return StatusCodes::kOk; +} + +NearbySharingService::StatusCodes +NearbySharingServiceImpl::InternalUnregisterReceiveSurface( + TransferUpdateCallback* transfer_callback) { + NL_DCHECK(transfer_callback); + + NL_LOG(INFO) << __func__ << ": UnregisterReceiveSurface is called" + << ", transfer_callback: " << transfer_callback; + + bool is_foreground = + foreground_receive_callbacks_.HasObserver(transfer_callback); + bool is_background = + background_receive_callbacks_.HasObserver(transfer_callback); + if (!is_foreground && !is_background) { + NL_VLOG(1) << __func__ + << ": Unknown transfer callback was un-registered, ignoring."; + // We intentionally allow this be successful so the caller can be sure + // they are not registered anymore. + return StatusCodes::kOk; + } + + if (!foreground_receive_callbacks_.empty() && last_incoming_metadata_ && + last_incoming_metadata_->second.is_final_status()) { + // We already saw the final status in the foreground. + // Nullify it so the next time the user opens sharing, it starts the UI from + // the beginning + last_incoming_metadata_.reset(); + } + + if (is_foreground) { + foreground_receive_callbacks_.RemoveObserver(transfer_callback); + } else { + background_receive_callbacks_.RemoveObserver(transfer_callback); + } + + // Displays the most recent payload status processed by foreground surfaces on + // background surface. + if (foreground_receive_callbacks_.empty() && last_incoming_metadata_) { + for (auto& background_callback : + background_receive_callbacks_.GetObservers()) { + background_callback->OnTransferUpdate(last_incoming_metadata_->first, + last_incoming_metadata_->second); + } + } + + NL_VLOG(1) << __func__ << ": A ReceiveSurface(" + << (is_foreground ? "foreground" : "background") + << ") has been unregistered"; + InvalidateSurfaceState(); + return StatusCodes::kOk; +} + +std::string NearbySharingServiceImpl::Dump() const { + std::stringstream sstream; + + // Dump Nearby Sharing Service state + sstream << std::boolalpha; + sstream << "Nearby Sharing Service State" << std::endl; + sstream << " IsScanning: " << IsScanning() << std::endl; + sstream << " IsConnecting: " << IsConnecting() << std::endl; + sstream << " IsTransferring: " << IsTransferring() << std::endl; + sstream << " IsSendingFile: " << IsSendingFile() << std::endl; + sstream << " IsReceivingFile: " << IsReceivingFile() << std::endl; + + sstream << " IsScreenLocked: " << device_info_.IsScreenLocked() + << std::endl; + sstream << " IsBluetoothPresent: " << IsBluetoothPresent() << std::endl; + sstream << " IsBluetoothPowered: " << IsBluetoothPowered() << std::endl; + sstream << " IsExtendedAdvertisingSupported: " + << IsExtendedAdvertisingSupported() << std::endl; + sstream << " UpdateTrack: " + << NearbyFlags::GetInstance().GetStringFlag( + sharing::config_package_nearby::nearby_sharing_feature:: + kUpdateTrack) + << std::endl; + sstream << std::noboolalpha; + sstream << std::endl; + + // Dump scheduled tasks + sstream << "Nearby Tasks/Certificates State" << std::endl; + sstream << " Download & upload contacts: " + << ConvertToReadableSchedule( + preference_manager_, + prefs::kNearbySharingSchedulerContactDownloadAndUploadName) + << std::endl; + sstream << " Download device data: " + << ConvertToReadableSchedule( + preference_manager_, + prefs::kNearbySharingSchedulerDownloadDeviceDataName) + << std::endl; + sstream << " Download public certificates: " + << ConvertToReadableSchedule( + preference_manager_, + prefs::kNearbySharingSchedulerDownloadPublicCertificatesName) + << std::endl; + sstream << " Upload contacts periodically: " + << ConvertToReadableSchedule( + preference_manager_, + prefs::kNearbySharingSchedulerPeriodicContactUploadName) + << std::endl; + sstream + << " Upload local device certificates: " + << ConvertToReadableSchedule( + preference_manager_, + prefs::kNearbySharingSchedulerUploadLocalDeviceCertificatesName) + << std::endl; + sstream << " Upload device name: " + << ConvertToReadableSchedule( + preference_manager_, + prefs::kNearbySharingSchedulerUploadDeviceNameName) + << std::endl; + sstream << " Private certificates expiration: " + << ConvertToReadableSchedule( + preference_manager_, + prefs::kNearbySharingSchedulerPrivateCertificateExpirationName) + << std::endl; + sstream << " Public certificates expiration: " + << ConvertToReadableSchedule( + preference_manager_, + prefs::kNearbySharingSchedulerPublicCertificateExpirationName) + << std::endl; + + // Dump certificates information. + if (NearbyFlags::GetInstance().GetBoolFlag( + sharing::config_package_nearby::nearby_sharing_feature:: + kEnableCertificatesDump)) { + sstream << std::endl; + sstream << certificate_manager_->Dump(); + } + + sstream << std::endl; + sstream << nearby_connections_manager_->Dump(); + return sstream.str(); +} + +// Private methods for NearbyShareSettings::Observer. +void NearbySharingServiceImpl::OnSettingChanged(absl::string_view key, + const Data& data) { + if (key == prefs::kNearbySharingEnabledName) { + bool enabled = data.value.as_bool; + OnEnabledChanged(enabled); + } else if (key == prefs::kNearbySharingFastInitiationNotificationStateName) { + FastInitiationNotificationState state = + static_cast(data.value.as_int64); + OnFastInitiationNotificationStateChanged(state); + } else if (key == prefs::kNearbySharingDataUsageName) { + DataUsage data_usage = static_cast(data.value.as_int64); + OnDataUsageChanged(data_usage); + } else if (key == prefs::kNearbySharingCustomSavePath) { + absl::string_view custom_save_path = data.value.as_string; + OnCustomSavePathChanged(custom_save_path); + } else if (key == prefs::kNearbySharingBackgroundVisibilityName) { + DeviceVisibility visibility = + static_cast(data.value.as_int64); + OnVisibilityChanged(visibility); + } else if (key == prefs::kNearbySharingOnboardingCompleteName) { + bool is_complete = data.value.as_bool; + OnIsOnboardingCompleteChanged(is_complete); + } else if (key == prefs::kNearbySharingIsReceivingName) { + bool is_receiving = data.value.as_bool; + OnIsReceivingChanged(is_receiving); + } +} + +void NearbySharingServiceImpl::OnEnabledChanged(bool enabled) { + RunOnNearbySharingServiceThread("on_enabled_changed", [&, enabled]() { + if (enabled) { + NL_VLOG(1) << __func__ << ": Nearby sharing enabled!"; + local_device_data_manager_->Start(); + contact_manager_->Start(); + certificate_manager_->Start(); + } else { + NL_VLOG(1) << __func__ << ": Nearby sharing disabled!"; + StopAdvertising(); + StopScanning(); + nearby_connections_manager_->Shutdown(); + local_device_data_manager_->Stop(); + contact_manager_->Stop(); + certificate_manager_->Stop(); + } + + InvalidateSurfaceState(); + }); +} + +void NearbySharingServiceImpl::OnFastInitiationNotificationStateChanged( + FastInitiationNotificationState state) { + RunOnNearbySharingServiceThread( + "on_fast_initiation_notification_state_changed", [&, state]() { + if (!IsBackgroundScanningFeatureEnabled()) { + return; + } + + NL_VLOG(1) << __func__ << ": Fast initiation Notification state: " + << static_cast(state); + // Runs through a series of checks to determine if background scanning + // should be started or stopped. + InvalidateReceiveSurfaceState(); + }); +} + +void NearbySharingServiceImpl::OnIsFastInitiationHardwareSupportedChanged( + bool is_supported) {} + +void NearbySharingServiceImpl::OnDataUsageChanged(DataUsage data_usage) { + RunOnNearbySharingServiceThread("on_data_usage_changed", [&, data_usage]() { + NL_LOG(INFO) << __func__ << ": Nearby sharing data usage changed to " + << DataUsage_Name(data_usage); + StopAdvertisingAndInvalidateSurfaceState(); + }); +} + +void NearbySharingServiceImpl::OnCustomSavePathChanged( + absl::string_view custom_save_path) { + RunOnNearbySharingServiceThread( + "on_custom_save_path_changed", + [&, custom_save_path = std::string(custom_save_path)]() { + NL_LOG(INFO) << __func__ + << ": Nearby sharing custom save path changed to " + << custom_save_path; + nearby_connections_manager_->SetCustomSavePath(custom_save_path); + }); +} + +void NearbySharingServiceImpl::OnVisibilityChanged( + DeviceVisibility visibility) { + RunOnNearbySharingServiceThread("on_visibility_changed", [&, visibility]() { + NL_LOG(INFO) << __func__ << ": Nearby sharing visibility changed to " + << DeviceVisibility_Name(visibility); + StopAdvertisingAndInvalidateSurfaceState(); + }); +} + +void NearbySharingServiceImpl::OnIsOnboardingCompleteChanged(bool is_complete) { + // Log the event to analytics when is_complete is true. + if (is_complete) { + analytics_recorder_->NewAcceptAgreements(); + } +} + +void NearbySharingServiceImpl::OnIsReceivingChanged(bool is_receiving) { + RunOnNearbySharingServiceThread( + "on_is_receiving_changed", [&, is_receiving]() { + NL_LOG(INFO) << __func__ << ": Nearby sharing receiving changed to " + << is_receiving; + InvalidateSurfaceState(); + }); +} + +// NearbyShareCertificateManager::Observer: +void NearbySharingServiceImpl::OnPublicCertificatesDownloaded() { + if (!is_scanning_ || discovered_advertisements_to_retry_map_.empty()) { + return; + } + + NL_LOG(INFO) << __func__ + << ": Public certificates downloaded while scanning. " + << "Retrying decryption with " + << discovered_advertisements_to_retry_map_.size() + << " previously discovered advertisements."; + const auto map_copy = discovered_advertisements_to_retry_map_; + discovered_advertisements_to_retry_map_.clear(); + for (const auto& id_info_pair : map_copy) { + discovered_advertisements_retried_set_.insert(id_info_pair.first); + OnEndpointDiscovered(id_info_pair.first, id_info_pair.second); + } +} + +void NearbySharingServiceImpl::OnPrivateCertificatesChanged() { + RunOnNearbySharingServiceThread("on-private-certificates-changed", [&]() { + StopAdvertisingAndInvalidateSurfaceState(); + }); +} + +void NearbySharingServiceImpl::OnLoginSucceeded(absl::string_view account_id) { + RunOnNearbySharingServiceThread( + "on_login_succeeded", [&, account_id = std::string(account_id)]() { + NL_LOG(INFO) << __func__ << ": Account login."; + + ResetAllSettings(/*logout=*/false); + }); +} + +void NearbySharingServiceImpl::OnLogoutSucceeded(absl::string_view account_id) { + RunOnNearbySharingServiceThread( + "on_logout_succeeded", [&, account_id = std::string(account_id)]() { + NL_LOG(INFO) << __func__ << ": Account logout."; + + // Reset all settings. + ResetAllSettings(/*logout=*/true); + }); +} + +// NearbyConnectionsManager::DiscoveryListener: +void NearbySharingServiceImpl::OnEndpointDiscovered( + absl::string_view endpoint_id, absl::Span endpoint_info) { + // The calling thread may already be completed when calling lambda. We + // make a local copy of calling parameter to avoid possible memory + // issue. + std::vector endpoint_info_copy{endpoint_info.begin(), + endpoint_info.end()}; + RunOnNearbySharingServiceThread( + "on_endpoint_discovered", + [&, endpoint_id = std::string(endpoint_id), + endpoint_info_copy = std::move(endpoint_info_copy)]() { + AddEndpointDiscoveryEvent([&, endpoint_id, endpoint_info_copy]() { + HandleEndpointDiscovered(endpoint_id, endpoint_info_copy); + }); + }); +} + +void NearbySharingServiceImpl::OnEndpointLost(absl::string_view endpoint_id) { + RunOnNearbySharingServiceThread( + "on_endpoint_lost", [&, endpoint_id = std::string(endpoint_id)]() { + AddEndpointDiscoveryEvent( + [&, endpoint_id]() { HandleEndpointLost(endpoint_id); }); + }); +} + +void NearbySharingServiceImpl::OnLockStateChanged(bool locked) { + RunOnNearbySharingServiceThread("on_lock_state_changed", [&, locked]() { + NL_VLOG(1) << __func__ << ": Screen lock state changed. (" << locked << ")"; + is_screen_locked_ = locked; + InvalidateSurfaceState(); + }); +} + +void NearbySharingServiceImpl::AdapterPresentChanged( + sharing::api::BluetoothAdapter* adapter, bool present) { + RunOnNearbySharingServiceThread("bt_adapter_present_changed", [&, present]() { + NL_VLOG(1) << __func__ << ": Bluetooth adapter present state changed. (" + << present << ")"; + for (auto& observer : observers_.GetObservers()) { + observer->OnBluetoothStatusChanged(); + } + InvalidateSurfaceState(); + }); +} + +void NearbySharingServiceImpl::AdapterPoweredChanged( + sharing::api::BluetoothAdapter* adapter, bool powered) { + RunOnNearbySharingServiceThread("bt_adapter_power_changed", [&, powered]() { + NL_VLOG(1) << __func__ << ": Bluetooth adapter power state changed. (" + << powered << ")"; + for (auto& observer : observers_.GetObservers()) { + observer->OnBluetoothStatusChanged(); + } + InvalidateSurfaceState(); + }); +} + +void NearbySharingServiceImpl::AdapterPresentChanged( + sharing::api::WifiAdapter* adapter, bool present) { + RunOnNearbySharingServiceThread( + "wifi_adapter_present_changed", [&, present]() { + NL_VLOG(1) << __func__ << ": Wifi adapter present state changed. (" + << present << ")"; + for (auto& observer : observers_.GetObservers()) { + observer->OnWifiStatusChanged(); + } + InvalidateSurfaceState(); + }); +} + +void NearbySharingServiceImpl::AdapterPoweredChanged( + sharing::api::WifiAdapter* adapter, bool powered) { + RunOnNearbySharingServiceThread("wifi_adapter_power_changed", [&, powered]() { + NL_VLOG(1) << __func__ << ": Wifi adapter power state changed. (" << powered + << ")"; + for (auto& observer : observers_.GetObservers()) { + observer->OnWifiStatusChanged(); + } + InvalidateSurfaceState(); + }); +} + +void NearbySharingServiceImpl::HardwareErrorReported( + NearbyFastInitiation* fast_init) { + RunOnNearbySharingServiceThread("hardware_error_reported", [&]() { + NL_VLOG(1) << __func__ << ": Hardware error reported, need to restart PC."; + for (auto& observer : observers_.GetObservers()) { + observer->OnIrrecoverableHardwareErrorReported(); + } + InvalidateSurfaceState(); + }); +} + +void NearbySharingServiceImpl::SetupBluetoothAdapter() { + NL_VLOG(1) << __func__ << ": Setup bluetooth adapter."; + context_->GetBluetoothAdapter().AddObserver(this); + InvalidateSurfaceState(); +} + +ObserverList& +NearbySharingServiceImpl::GetReceiveCallbacksFromState( + ReceiveSurfaceState state) { + switch (state) { + case ReceiveSurfaceState::kForeground: + return foreground_receive_callbacks_; + case ReceiveSurfaceState::kBackground: + return background_receive_callbacks_; + case ReceiveSurfaceState::kUnknown: + return foreground_receive_callbacks_; + } +} + +bool NearbySharingServiceImpl::IsVisibleInBackground( + DeviceVisibility visibility) { + return visibility == DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS || + visibility == DeviceVisibility::DEVICE_VISIBILITY_SELECTED_CONTACTS || + visibility == DeviceVisibility::DEVICE_VISIBILITY_EVERYONE || + visibility == DeviceVisibility::DEVICE_VISIBILITY_SELF_SHARE; +} + +std::optional> +NearbySharingServiceImpl::CreateEndpointInfo( + const std::optional& device_name) const { + std::vector salt; + std::vector encrypted_key; + + if (account_manager_.GetCurrentAccount().has_value()) { + // If the user already signed in, setup contacts certificate for everyone + // mode to show correct user icon on remote device. + DeviceVisibility visibility = settings_->GetVisibility(); + if (visibility == proto::DEVICE_VISIBILITY_EVERYONE) { + // Make sure using all contacts certificate for everyone mode + visibility = proto::DEVICE_VISIBILITY_ALL_CONTACTS; + } + + std::optional encrypted_metadata_key = + certificate_manager_->EncryptPrivateCertificateMetadataKey(visibility); + if (encrypted_metadata_key.has_value()) { + salt = encrypted_metadata_key->salt(); + encrypted_key = encrypted_metadata_key->encrypted_key(); + } else { + NL_LOG(WARNING) << __func__ + << ": Failed to encrypt private certificate metadata key " + << "for advertisement."; + } + } + + // Generate random metadata key for non-login user or failed to generate + // metadata keys for login user. + + if (salt.empty() || encrypted_key.empty()) { + salt = GenerateRandomBytes(sharing::Advertisement::kSaltSize); + encrypted_key = GenerateRandomBytes( + sharing::Advertisement::kMetadataEncryptionKeyHashByteSize); + } + + ShareTargetType device_type = + static_cast(device_info_.GetDeviceType()); + + std::unique_ptr advertisement = Advertisement::NewInstance( + std::move(salt), std::move(encrypted_key), device_type, device_name); + if (advertisement) { + return advertisement->ToEndpointInfo(); + } else { + return std::nullopt; + } +} + +void NearbySharingServiceImpl::StartFastInitiationAdvertising() { + NL_VLOG(1) << __func__ << ": Starting fast initiation advertising."; + + if (nearby_fast_initiation_->IsAdvertising()) { + return; + } + + nearby_fast_initiation_->StartAdvertising( + NearbyFastInitiation::FastInitType::kSilent, + [&]() { OnStartFastInitiationAdvertising(); }, + [&]() { OnStartFastInitiationAdvertisingError(); }); + NL_VLOG(1) << __func__ << ": Fast initiation advertising in kSilent mode."; + + // Log analytics event of sending fast initiation. + analytics_recorder_->NewSendFastInitialization(); +} + +void NearbySharingServiceImpl::OnStartFastInitiationAdvertising() { + NL_VLOG(1) << __func__ << ": Started fast initiation advertising."; +} + +void NearbySharingServiceImpl::OnStartFastInitiationAdvertisingError() { + NL_LOG(ERROR) << __func__ << ": Failed to start fast initiation advertising."; +} + +void NearbySharingServiceImpl::StopFastInitiationAdvertising() { + NL_VLOG(1) << __func__ << ": Stopping fast initiation advertising."; + + if (!nearby_fast_initiation_->IsAdvertising()) { + return; + } + + nearby_fast_initiation_->StopAdvertising( + [&]() { OnStopFastInitiationAdvertising(); }); +} + +void NearbySharingServiceImpl::OnStopFastInitiationAdvertising() { + NL_VLOG(1) << __func__ << ": Stopped fast initiation advertising"; +} + +// Processes endpoint discovered/lost events. We queue up the events to ensure +// each discovered or lost event is fully handled before the next is run. For +// example, we don't want to start processing an endpoint-lost event before +// the corresponding endpoint-discovered event is finished. This is especially +// important because of the asynchronous steps required to process an +// endpoint-discovered event. +void NearbySharingServiceImpl::AddEndpointDiscoveryEvent( + std::function event) { + endpoint_discovery_events_.push(std::move(event)); + if (endpoint_discovery_events_.size() == 1u) { + auto discovery_event = std::move(endpoint_discovery_events_.front()); + discovery_event(); + } +} + +void NearbySharingServiceImpl::HandleEndpointDiscovered( + absl::string_view endpoint_id, absl::Span endpoint_info) { + NL_VLOG(1) << __func__ << ": endpoint_id=" << endpoint_id + << ", endpoint_info=" << nearby::utils::HexEncode(endpoint_info); + if (!is_scanning_) { + NL_VLOG(1) + << __func__ + << ": Ignoring discovered endpoint because we're no longer scanning"; + FinishEndpointDiscoveryEvent(); + return; + } + + std::unique_ptr advertisement = + decoder_->DecodeAdvertisement(endpoint_info); + OnOutgoingAdvertisementDecoded(endpoint_id, endpoint_info, + std::move(advertisement)); +} + +void NearbySharingServiceImpl::HandleEndpointLost( + absl::string_view endpoint_id) { + NL_VLOG(1) << __func__ << ": endpoint_id=" << endpoint_id; + + if (!is_scanning_) { + NL_VLOG(1) << __func__ + << ": Ignoring lost endpoint because we're no longer scanning"; + FinishEndpointDiscoveryEvent(); + return; + } + + discovered_advertisements_to_retry_map_.erase(endpoint_id); + discovered_advertisements_retried_set_.erase(endpoint_id); + RemoveOutgoingShareTargetWithEndpointId(endpoint_id); + FinishEndpointDiscoveryEvent(); +} + +void NearbySharingServiceImpl::FinishEndpointDiscoveryEvent() { + NL_DCHECK(!endpoint_discovery_events_.empty()); + NL_DCHECK(endpoint_discovery_events_.front() == nullptr); + endpoint_discovery_events_.pop(); + + // Handle the next queued up endpoint discovered/lost event. + if (!endpoint_discovery_events_.empty()) { + NL_DCHECK(endpoint_discovery_events_.front() != nullptr); + auto discovery_event = std::move(endpoint_discovery_events_.front()); + discovery_event(); + } +} + +void NearbySharingServiceImpl::OnOutgoingAdvertisementDecoded( + absl::string_view endpoint_id, absl::Span endpoint_info, + std::unique_ptr advertisement) { + if (!advertisement) { + NL_LOG(WARNING) << __func__ + << ": Failed to parse discovered advertisement."; + FinishEndpointDiscoveryEvent(); + return; + } + + // Now we will report endpoints met before in NearbyConnectionsManager. + // Check outgoingShareTargetInfoMap first and pass the same shareTarget if we + // found one. + + // Looking for the ShareTarget based on endpoint id. + if (outgoing_share_target_map_.find(endpoint_id) != + outgoing_share_target_map_.end()) { + FinishEndpointDiscoveryEvent(); + return; + } + + // Once we get the advertisement, the first thing to do is decrypt the + // certificate. + NearbyShareEncryptedMetadataKey encrypted_metadata_key( + advertisement->salt(), advertisement->encrypted_metadata_key()); + + std::string endpoint_id_copy = std::string(endpoint_id); + std::vector endpoint_info_copy{endpoint_info.begin(), + endpoint_info.end()}; + GetCertificateManager()->GetDecryptedPublicCertificate( + std::move(encrypted_metadata_key), + [this, endpoint_id_copy, endpoint_info_copy, + advertisement_copy = + *advertisement](std::optional + decrypted_public_certificate) { + std::unique_ptr advertisement = + Advertisement::NewInstance( + advertisement_copy.salt(), + advertisement_copy.encrypted_metadata_key(), + advertisement_copy.device_type(), + advertisement_copy.device_name()); + OnOutgoingDecryptedCertificate(endpoint_id_copy, endpoint_info_copy, + std::move(advertisement), + decrypted_public_certificate); + }); +} + +void NearbySharingServiceImpl::OnOutgoingDecryptedCertificate( + absl::string_view endpoint_id, absl::Span endpoint_info, + std::unique_ptr advertisement, + std::optional certificate) { + // Check again for this endpoint id, to avoid race conditions. + if (outgoing_share_target_map_.find(endpoint_id) != + outgoing_share_target_map_.end()) { + FinishEndpointDiscoveryEvent(); + return; + } + + // The certificate provides the device name, in order to create a ShareTarget + // to represent this remote device. + std::optional share_target = CreateShareTarget( + endpoint_id, std::move(advertisement), std::move(certificate), + /*is_incoming=*/false); + if (!share_target.has_value()) { + if (discovered_advertisements_retried_set_.contains(endpoint_id)) { + NL_LOG(INFO) + << __func__ + << ": Don't try to download public certificates again for endpoint=" + << endpoint_id; + FinishEndpointDiscoveryEvent(); + return; + } + + NL_LOG(INFO) + << __func__ << ": Failed to convert discovered advertisement to share " + << "target. Ignoring endpoint until next certificate download."; + std::vector endpoint_info_data(endpoint_info.begin(), + endpoint_info.end()); + + discovered_advertisements_to_retry_map_[endpoint_id] = endpoint_info_data; + FinishEndpointDiscoveryEvent(); + return; + } + + // Update the endpoint id for the share target. + NL_LOG(INFO) << __func__ + << ": An endpoint has been discovered, with an advertisement " + "containing a valid share target."; + + // Log analytics event of discovering share target. + analytics_recorder_->NewDiscoverShareTarget( + *share_target, scanning_session_id_, + absl::ToInt64Milliseconds(context_->GetClock()->Now() - + scanning_start_timestamp_), + /*flow_id=*/1, /*referrer_package=*/std::nullopt, + share_foreground_send_surface_start_timestamp_ == absl::InfinitePast() + ? -1 + : absl::ToInt64Milliseconds( + context_->GetClock()->Now() - + share_foreground_send_surface_start_timestamp_)); + + // Notifies the user that we discovered a device. + NL_VLOG(1) << __func__ << ": There are " + << (foreground_send_discovery_callbacks_.size() + + background_send_discovery_callbacks_.size()) + << " discovery callbacks be called."; + + for (ShareTargetDiscoveredCallback* discovery_callback : + foreground_send_discovery_callbacks_.GetObservers()) { + discovery_callback->OnShareTargetDiscovered(*share_target); + } + for (ShareTargetDiscoveredCallback* discovery_callback : + background_send_discovery_callbacks_.GetObservers()) { + discovery_callback->OnShareTargetDiscovered(*share_target); + } + + NL_VLOG(1) << __func__ << ": Reported OnShareTargetDiscovered " + << (context_->GetClock()->Now() - scanning_start_timestamp_); + + FinishEndpointDiscoveryEvent(); +} + +void NearbySharingServiceImpl::ScheduleCertificateDownloadDuringDiscovery( + size_t attempt_count) { + if (attempt_count >= kMaxCertificateDownloadsDuringDiscovery) { + return; + } + + if (certificate_download_during_discovery_timer_->IsRunning()) { + certificate_download_during_discovery_timer_->Stop(); + } + + certificate_download_during_discovery_timer_->Start( + absl::ToInt64Milliseconds(kCertificateDownloadDuringDiscoveryPeriod), 0, + [&, attempt_count]() { + OnCertificateDownloadDuringDiscoveryTimerFired(attempt_count); + }); +} + +void NearbySharingServiceImpl::OnCertificateDownloadDuringDiscoveryTimerFired( + size_t attempt_count) { + if (!is_scanning_) { + return; + } + + if (!discovered_advertisements_to_retry_map_.empty()) { + NL_VLOG(1) << __func__ << ": Detected " + << discovered_advertisements_to_retry_map_.size() + << " discovered advertisements that could not decrypt any " + << "public certificates. Re-downloading certificates."; + certificate_manager_->DownloadPublicCertificates(); + ++attempt_count; + } + + ScheduleCertificateDownloadDuringDiscovery(attempt_count); +} + +bool NearbySharingServiceImpl::IsBluetoothPresent() const { + return context_->GetBluetoothAdapter().IsPresent(); +} + +bool NearbySharingServiceImpl::IsBluetoothPowered() const { + return context_->GetBluetoothAdapter().IsPowered(); +} + +bool NearbySharingServiceImpl::IsExtendedAdvertisingSupported() const { + return context_->GetBluetoothAdapter().IsExtendedAdvertisingSupported(); +} + +bool NearbySharingServiceImpl::IsLanConnected() const { + return context_->GetConnectivityManager()->IsLanConnected(); +} + +bool NearbySharingServiceImpl::IsWifiPresent() const { + return context_->GetWifiAdapter().IsPresent(); +} + +bool NearbySharingServiceImpl::IsWifiPowered() const { + return context_->GetWifiAdapter().IsPowered(); +} + +bool NearbySharingServiceImpl::HasAvailableConnectionMediums() { + // Check if Wi-Fi or Ethernet LAN is off. Advertisements won't work, so + // disable them, unless bluetooth is known to be enabled. Not all platforms + // have bluetooth, so Wi-Fi LAN is a platform-agnostic check. + bool is_wifi_lan_enabled = NearbyFlags::GetInstance().GetBoolFlag( + config_package_nearby::nearby_sharing_feature::kEnableMediumWifiLan); + + ConnectivityManager::ConnectionType connection_type = + context_->GetConnectivityManager()->GetConnectionType(); + + bool hasNetworkConnection = + connection_type == ConnectivityManager::ConnectionType::kWifi || + connection_type == ConnectivityManager::ConnectionType::kEthernet; + + return IsBluetoothPowered() || (is_wifi_lan_enabled && hasNetworkConnection); +} + +void NearbySharingServiceImpl::InvalidateSurfaceState() { + InvalidateSendSurfaceState(); + InvalidateReceiveSurfaceState(); +} + +void NearbySharingServiceImpl::InvalidateSendSurfaceState() { + InvalidateScanningState(); + InvalidateFastInitiationAdvertising(); +} + +void NearbySharingServiceImpl::InvalidateScanningState() { + // Stop scanning when screen is off. + if (is_screen_locked_) { + StopScanning(); + NL_VLOG(1) << __func__ + << ": Stopping discovery because the screen is locked."; + return; + } + + if (!HasAvailableConnectionMediums()) { + StopScanning(); + NL_VLOG(1) << __func__ + << ": Stopping scanning because both bluetooth and wifi LAN are " + "disabled."; + return; + } + + // Nearby Sharing is disabled. Don't advertise. + if (!settings_->GetEnabled()) { + StopScanning(); + NL_VLOG(1) << __func__ + << ": Stopping discovery because Nearby Sharing is disabled."; + return; + } + + if (is_transferring_ || is_connecting_) { + StopScanning(); + NL_VLOG(1) + << __func__ + << ": Stopping discovery because we're currently in the midst of a " + "transfer."; + return; + } + + if (foreground_send_transfer_callbacks_.empty()) { + StopScanning(); + NL_VLOG(1) << __func__ + << ": Stopping discovery because no scanning surface has been " + "registered."; + return; + } + + // Screen is on, Bluetooth is enabled, and Nearby Sharing is enabled! Start + // discovery. + StartScanning(); +} + +void NearbySharingServiceImpl::InvalidateFastInitiationAdvertising() { + // Screen is off. Do no work. + if (is_screen_locked_) { + StopFastInitiationAdvertising(); + NL_VLOG(1) << __func__ + << ": Stopping fast initiation advertising because the " + "screen is locked."; + return; + } + + if (!IsBluetoothPowered()) { + StopFastInitiationAdvertising(); + NL_VLOG(1) << __func__ + << ": Stopping fast initiation advertising because " + "bluetooth is disabled due to powered off."; + return; + } + + // Nearby Sharing is disabled. Don't advertise. + if (!settings_->GetEnabled()) { + StopFastInitiationAdvertising(); + NL_VLOG(1) << __func__ + << ": Stopping fast initiation advertising because Nearby " + "Sharing is disabled."; + return; + } + + if (is_transferring_ || is_connecting_) { + StopFastInitiationAdvertising(); + NL_VLOG(1) << __func__ + << ": Stopping fast initiation advertising because we're " + "currently in the midst of a " + "transfer."; + return; + } + + if (foreground_send_transfer_callbacks_.empty()) { + StopFastInitiationAdvertising(); + NL_VLOG(1) << __func__ + << ": Stopping fast initiation advertising because no send " + "surface is registered."; + return; + } + + StartFastInitiationAdvertising(); +} + +void NearbySharingServiceImpl::InvalidateReceiveSurfaceState() { + InvalidateAdvertisingState(); + if (IsBackgroundScanningFeatureEnabled()) { + InvalidateFastInitiationScanning(); + } +} + +void NearbySharingServiceImpl::InvalidateAdvertisingState() { + if (!settings_->GetIsReceiving()) { + StopAdvertising(); + NL_VLOG(1) << __func__ + << ": Stopping advertising because receiving is disabled."; + return; + } + + // Do not advertise on lock screen unless Self Share is enabled. + if (is_screen_locked_ && + !NearbyFlags::GetInstance().GetBoolFlag( + config_package_nearby::nearby_sharing_feature::kEnableSelfShareUi)) { + StopAdvertising(); + NL_VLOG(1) << __func__ + << ": Stopping advertising because the screen is locked."; + return; + } + + if (!HasAvailableConnectionMediums()) { + StopAdvertising(); + NL_VLOG(1) + << __func__ + << ": Stopping advertising because both bluetooth and wifi LAN are " + "disabled."; + return; + } + + // Nearby Sharing is disabled. Don't advertise. + if (!settings_->GetEnabled()) { + StopAdvertising(); + NL_VLOG(1) << __func__ + << ": Stopping advertising because Nearby Sharing is disabled."; + return; + } + + // We're scanning for other nearby devices. Don't advertise. + if (is_scanning_) { + StopAdvertising(); + NL_VLOG(1) + << __func__ + << ": Stopping advertising because we're scanning for other devices."; + return; + } + + if (is_transferring_) { + StopAdvertising(); + NL_VLOG(1) + << __func__ + << ": Stopping advertising because we're currently in the midst of " + "a transfer."; + return; + } + + if (foreground_receive_callbacks_.empty() && + background_receive_callbacks_.empty()) { + StopAdvertising(); + NL_VLOG(1) + << __func__ + << ": Stopping advertising because no receive surface is registered."; + return; + } + + if (!IsVisibleInBackground(settings_->GetVisibility()) && + foreground_receive_callbacks_.empty()) { + StopAdvertising(); + NL_VLOG(1) + << __func__ + << ": Stopping advertising because no high power receive surface " + "is registered and device is visible to NO_ONE."; + return; + } + + PowerLevel power_level; + if (!foreground_receive_callbacks_.empty()) { + power_level = PowerLevel::kHighPower; + } else { + power_level = PowerLevel::kLowPower; + } + + DataUsage data_usage = settings_->GetDataUsage(); + if (advertising_power_level_ != PowerLevel::kUnknown) { + if (power_level == advertising_power_level_) { + NL_VLOG(1) << __func__ + << ": Ignoring, already advertising with power level " + << PowerLevelToString(advertising_power_level_) + << " and data usage preference " + << static_cast(data_usage); + return; + } + + StopAdvertising(); + NL_VLOG(1) << __func__ << ": Restart advertising with power level " + << PowerLevelToString(power_level) + << " and data usage preference " << static_cast(data_usage); + } + + std::optional device_name; + if (settings_->GetVisibility() == + DeviceVisibility::DEVICE_VISIBILITY_EVERYONE) { + device_name = local_device_data_manager_->GetDeviceName(); + } + + // Starts advertising through Nearby Connections. Caller is expected to ensure + // |listener| remains valid until StopAdvertising is called. + std::optional> endpoint_info = + CreateEndpointInfo(device_name); + if (!endpoint_info) { + NL_VLOG(1) << __func__ + << ": Unable to advertise since could not parse the " + "endpoint info from the advertisement."; + return; + } + bool used_device_name = device_name.has_value(); + if (used_device_name) { + for (auto& observer : observers_.GetObservers()) { + observer->OnHighVisibilityChangeRequested(); + } + } + + advertising_session_id_ = analytics_recorder_->GenerateNextId(); + + nearby_connections_manager_->StartAdvertising( + *endpoint_info, + /*listener=*/this, power_level, data_usage, + [&, used_device_name, data_usage](Status status) { + // Log analytics event of advertising start. + analytics_recorder_->NewAdvertiseDevicePresenceStart( + advertising_session_id_, + used_device_name ? DeviceVisibility::DEVICE_VISIBILITY_EVERYONE + : settings_->GetVisibility(), + status == Status::kSuccess ? SessionStatus::SUCCEEDED_SESSION_STATUS + : SessionStatus::FAILED_SESSION_STATUS, + data_usage, std::nullopt); + + OnStartAdvertisingResult(used_device_name, status); + }); + + advertising_power_level_ = power_level; + NL_VLOG(1) << __func__ + << ": StartAdvertising requested over Nearby Connections: " + << " power level: " << PowerLevelToString(power_level) + << " visibility: " + << DeviceVisibility_Name(settings_->GetVisibility()) + << " data usage: " << DataUsage_Name(data_usage) + << " advertise device name?: " + << (device_name.has_value() ? "yes" : "no"); + + ScheduleRotateBackgroundAdvertisementTimer(); +} + +void NearbySharingServiceImpl::StopAdvertising() { + if (advertising_power_level_ == PowerLevel::kUnknown) { + NL_VLOG(1) << __func__ << ": Not currently advertising, ignoring."; + return; + } + + nearby_connections_manager_->StopAdvertising([&](Status status) { + // Log analytics event of advertising end. + analytics_recorder_->NewAdvertiseDevicePresenceEnd(advertising_session_id_); + OnStopAdvertisingResult(status); + }); + + NL_VLOG(1) << __func__ << ": Stop advertising requested"; + + // Set power level to unknown immediately instead of waiting for the callback. + // In the case of restarting advertising (e.g. turning off high visibility + // with contact-based enabled), StartAdvertising will be called + // immediately after StopAdvertising and will fail if the power level + // indicates already advertising. + advertising_power_level_ = PowerLevel::kUnknown; +} + +void NearbySharingServiceImpl::StartScanning() { + NL_DCHECK(settings_->GetEnabled()); + NL_DCHECK(!is_screen_locked_); + NL_DCHECK(HasAvailableConnectionMediums()); + NL_DCHECK(!foreground_send_transfer_callbacks_.empty()); + + if (is_scanning_) { + NL_VLOG(1) << __func__ << ": We're currently scanning, ignoring."; + return; + } + + scanning_start_timestamp_ = context_->GetClock()->Now(); + share_foreground_send_surface_start_timestamp_ = absl::InfinitePast(); + is_scanning_ = true; + InvalidateReceiveSurfaceState(); + + ClearOutgoingShareTargetInfoMap(); + discovered_advertisements_to_retry_map_.clear(); + discovered_advertisements_retried_set_.clear(); + + scanning_session_id_ = analytics_recorder_->GenerateNextId(); + + nearby_connections_manager_->StartDiscovery( + /*listener=*/this, settings_->GetDataUsage(), [&](Status status) { + // Log analytics event of starting discovery. + analytics::AnalyticsInformation analytics_information; + analytics_information.send_surface_state = + foreground_send_discovery_callbacks_.empty() + ? analytics::SendSurfaceState::kBackground + : analytics::SendSurfaceState::kForeground; + analytics_recorder_->NewScanForShareTargetsStart( + scanning_session_id_, + status == Status::kSuccess ? SessionStatus::SUCCEEDED_SESSION_STATUS + : SessionStatus::FAILED_SESSION_STATUS, + analytics_information, + /*flow_id=*/1, /*referrer_package=*/std::nullopt); + OnStartDiscoveryResult(status); + }); + + InvalidateSendSurfaceState(); + NL_VLOG(1) << __func__ << ": Scanning has started"; +} + +NearbySharingService::StatusCodes NearbySharingServiceImpl::StopScanning() { + if (!is_scanning_) { + NL_VLOG(1) << __func__ << ": Not currently scanning, ignoring."; + return StatusCodes::kStatusAlreadyStopped; + } + + // Log analytics event of scanning end. + analytics_recorder_->NewScanForShareTargetsEnd(scanning_session_id_); + + nearby_connections_manager_->StopDiscovery(); + is_scanning_ = false; + + certificate_download_during_discovery_timer_->Stop(); + discovered_advertisements_to_retry_map_.clear(); + discovered_advertisements_retried_set_.clear(); + + // Note: We don't know if we stopped scanning in preparation to send a file, + // or we stopped because the user left the page. We'll invalidate after a + // short delay. + + RunOnNearbySharingServiceThreadDelayed("invalidate_delay", kInvalidateDelay, + [&]() { InvalidateSurfaceState(); }); + + NL_VLOG(1) << __func__ << ": Scanning has stopped."; + return StatusCodes::kOk; +} + +void NearbySharingServiceImpl::StopAdvertisingAndInvalidateSurfaceState() { + if (advertising_power_level_ != PowerLevel::kUnknown) StopAdvertising(); + InvalidateSurfaceState(); +} + +void NearbySharingServiceImpl::InvalidateFastInitiationScanning() { + if (!IsBackgroundScanningFeatureEnabled()) return; + + bool is_hardware_offloading_supported = + IsBluetoothPresent() && nearby_fast_initiation_->IsScanOffloadSupported(); + + // Hardware offloading support is computed when the bluetooth adapter becomes + // available. We set the hardware supported state on |settings_| to notify the + // UI of state changes. InvalidateFastInitiationScanning gets triggered on + // adapter change events. + settings_->SetIsFastInitiationHardwareSupported( + is_hardware_offloading_supported); + + if (fast_initiation_scanner_cooldown_timer_->IsRunning()) { + NL_VLOG(1) << __func__ + << ": Stopping background scanning due to post-transfer " + "cooldown period"; + StopFastInitiationScanning(); + return; + } + + if (settings_->GetFastInitiationNotificationState() != + FastInitiationNotificationState::ENABLED_FAST_INIT) { + NL_VLOG(1) << __func__ + << ": Stopping background scanning; fast initiation " + "notification is disabled"; + StopFastInitiationScanning(); + return; + } + + if (!settings_->GetEnabled()) { + NL_VLOG(1) << __func__ + << ": Stopping background scanning because Nearby Sharing " + "is disabled"; + StopFastInitiationScanning(); + return; + } + + // Screen is off. Do no work. + if (is_screen_locked_) { + NL_VLOG(1) + << __func__ + << ": Stopping background scanning because the screen is locked."; + StopFastInitiationScanning(); + return; + } + + if (!IsBluetoothPowered()) { + NL_VLOG(1) + << __func__ + << ": Stopping background scanning because bluetooth is powered down."; + StopFastInitiationScanning(); + return; + } + + // We're scanning for other nearby devices. Don't background scan. + if (is_scanning_) { + NL_VLOG(1) << __func__ + << ": Stopping background scanning because we're scanning " + "for other devices."; + StopFastInitiationScanning(); + return; + } + + if (is_transferring_) { + NL_VLOG(1) << __func__ + << ": Stopping background scanning because we're currently " + "in the midst of a transfer."; + StopFastInitiationScanning(); + return; + } + + if (advertising_power_level_ == PowerLevel::kHighPower) { + NL_VLOG(1) << __func__ + << ": Stopping background scanning because we're already " + "in high visibility mode."; + StopFastInitiationScanning(); + return; + } + + if (!is_hardware_offloading_supported) { + NL_VLOG(1) << __func__ + << ": Stopping background scanning because hardware " + "support is not available or not ready."; + StopFastInitiationScanning(); + return; + } + + StartFastInitiationScanning(); +} + +void NearbySharingServiceImpl::StartFastInitiationScanning() { + NL_VLOG(1) << __func__ << ": Starting background scanning."; + + if (nearby_fast_initiation_->IsScanning()) { + return; + } + + nearby_fast_initiation_->StartScanning( + [&]() { OnFastInitiationDevicesDetected(); }, + [&]() { OnFastInitiationDevicesNotDetected(); }, + [&]() { StopFastInitiationScanning(); }); +} + +void NearbySharingServiceImpl::OnFastInitiationDevicesDetected() { + NL_VLOG(1) << __func__; + + for (auto& observer : observers_.GetObservers()) { + observer->OnFastInitiationDevicesDetected(); + } +} + +void NearbySharingServiceImpl::OnFastInitiationDevicesNotDetected() { + NL_VLOG(1) << __func__; + for (auto& observer : observers_.GetObservers()) { + observer->OnFastInitiationDevicesNotDetected(); + } +} + +void NearbySharingServiceImpl::StopFastInitiationScanning() { + NL_VLOG(1) << __func__ << ": Stop fast initiation scanning."; + if (!nearby_fast_initiation_->IsScanning()) { + return; + } + + nearby_fast_initiation_->StopScanning([&]() { + NL_VLOG(1) << __func__ << ": Stopped fast initiation scanning."; + }); + + for (auto& observer : observers_.GetObservers()) { + observer->OnFastInitiationScanningStopped(); + } + NL_VLOG(1) << __func__ << ": Stopped background scanning."; +} + +void NearbySharingServiceImpl::ScheduleRotateBackgroundAdvertisementTimer() { + absl::BitGen bitgen; + uint64_t delayRangeMilliseconds = + absl::ToInt64Milliseconds(kBackgroundAdvertisementRotationDelayMax - + kBackgroundAdvertisementRotationDelayMin); + uint64_t bias = absl::Uniform(bitgen, 0u, delayRangeMilliseconds); + uint64_t delayMilliseconds = + bias + + absl::ToInt64Milliseconds(kBackgroundAdvertisementRotationDelayMin); + if (rotate_background_advertisement_timer_->IsRunning()) { + rotate_background_advertisement_timer_->Stop(); + } + rotate_background_advertisement_timer_->Start(delayMilliseconds, 0, [&]() { + OnRotateBackgroundAdvertisementTimerFired(); + }); +} + +void NearbySharingServiceImpl::OnRotateBackgroundAdvertisementTimerFired() { + NL_LOG(INFO) << __func__ << ": Rotate background advertisement timer fired."; + + RunOnNearbySharingServiceThread( + "on-rotate-background-advertisement-timer-fired", [&]() { + if (!foreground_receive_callbacks_.empty()) { + rotate_background_advertisement_timer_->Stop(); + ScheduleRotateBackgroundAdvertisementTimer(); + } else { + StopAdvertising(); + InvalidateSurfaceState(); + } + }); +} + +void NearbySharingServiceImpl::RemoveOutgoingShareTargetWithEndpointId( + absl::string_view endpoint_id) { + auto it = outgoing_share_target_map_.find(endpoint_id); + if (it == outgoing_share_target_map_.end()) { + return; + } + + NL_VLOG(1) << __func__ << ": Removing (endpoint_id=" << it->first + << ", share_target.id=" << it->second.id + << ") from outgoing share target map"; + ShareTarget share_target = std::move(it->second); + outgoing_share_target_map_.erase(it); + + auto info_it = outgoing_share_target_info_map_.find(share_target.id); + if (info_it != outgoing_share_target_info_map_.end()) { + outgoing_share_target_info_map_.erase(info_it); + } else { + NL_LOG(WARNING) << __func__ << ": share_target.id=" << it->second.id + << " not found in outgoing share target info map."; + return; + } + + for (ShareTargetDiscoveredCallback* discovery_callback : + foreground_send_discovery_callbacks_.GetObservers()) { + if (discovery_callback != nullptr) { + discovery_callback->OnShareTargetLost(share_target); + } else { + NL_LOG(WARNING) << __func__ + << "Foreground Discovery Callback is not exist"; + } + } + for (ShareTargetDiscoveredCallback* discovery_callback : + background_send_discovery_callbacks_.GetObservers()) { + if (discovery_callback != nullptr) { + discovery_callback->OnShareTargetLost(share_target); + } else { + NL_LOG(WARNING) << __func__ + << "Background Discovery Callback is not exist"; + } + } + + NL_VLOG(1) << __func__ << ": Reported OnShareTargetLost"; +} + +void NearbySharingServiceImpl::OnTransferComplete() { + bool was_sending_files = is_sending_files_; + is_receiving_files_ = false; + is_transferring_ = false; + is_sending_files_ = false; + + // Cleanup ARC after send transfer completes since reading from file + // descriptor(s) are done at this point even though there could be Nearby + // Connection frames cached that are not yet sent to the remote device. + if (was_sending_files && arc_transfer_cleanup_callback_) { + arc_transfer_cleanup_callback_(); + } + + NL_VLOG(1) << __func__ << ": NearbySharing state change transfer finished"; + // Files transfer is done! Receivers can immediately cancel, but senders + // should add a short delay to ensure the final in-flight packet(s) make + // it to the remote device. + RunOnNearbySharingServiceThreadDelayed( + "transfer_done_delay", + was_sending_files ? kInvalidateSurfaceStateDelayAfterTransferDone + : absl::Milliseconds(1), + [&]() { InvalidateSurfaceState(); }); +} + +void NearbySharingServiceImpl::OnTransferStarted(bool is_incoming) { + is_transferring_ = true; + if (is_incoming) { + is_receiving_files_ = true; + } else { + is_sending_files_ = true; + } + InvalidateSurfaceState(); +} + +void NearbySharingServiceImpl::ReceivePayloads( + ShareTarget share_target, + std::function status_codes_callback) { + mutual_acceptance_timeout_alarm_->Stop(); + + std::filesystem::path download_path = + std::filesystem::u8path(settings_->GetCustomSavePath()); + + // Register payload path for all valid file payloads. + absl::flat_hash_map valid_file_payloads; + for (auto& file : share_target.file_attachments) { + std::optional payload_id = GetAttachmentPayloadId(file.id()); + if (!payload_id) { + NL_LOG(WARNING) + << __func__ + << ": Failed to register payload path for attachment id - " + << file.id(); + continue; + } + + std::filesystem::path file_path = + download_path / std::filesystem::u8path(file.file_name().cbegin(), + file.file_name().cend()); + valid_file_payloads.emplace(file.id(), std::move(file_path)); + } + + auto aggregated_success = std::make_unique(true); + + if (valid_file_payloads.empty()) { + OnPayloadPathsRegistered(share_target, std::move(aggregated_success), + std::move(status_codes_callback)); + return; + } + + path_registration_status_.share_target = share_target; + path_registration_status_.expected_count = valid_file_payloads.size(); + path_registration_status_.current_count = 0; + path_registration_status_.status_codes_callback = + std::move(status_codes_callback); + path_registration_status_.status = true; + + for (const auto& payload : valid_file_payloads) { + std::optional payload_id = GetAttachmentPayloadId(payload.first); + NL_DCHECK(payload_id); + + file_handler_.GetUniquePath( + payload.second, + [&, attachment_id = payload.first, + payload_id = *payload_id](std::filesystem::path unique_path) { + OnUniquePathFetched( + attachment_id, payload_id, + [&](Status status) { OnPayloadPathRegistered(status); }, + unique_path); + }); + } +} + +NearbySharingService::StatusCodes NearbySharingServiceImpl::SendPayloads( + const ShareTarget& share_target) { + NL_VLOG(1) << __func__ << ": Preparing to send payloads to " + << share_target.id; + ShareTargetInfo* info = GetShareTargetInfo(share_target); + if (!info || !info->connection()) { + NL_LOG(WARNING) << __func__ + << ": Failed to send payload due to missing connection."; + return StatusCodes::kOutOfOrderApiCall; + } + if (!info->transfer_update_callback()) { + NL_LOG(WARNING) + << __func__ + << ": Failed to send payload due to missing transfer update " + "callback. Disconnecting."; + AbortAndCloseConnectionIfNecessary( + TransferMetadata::Status::kMissingTransferUpdateCallback, share_target); + return StatusCodes::kOutOfOrderApiCall; + } + + // Log analytics event of sending attachment start. + analytics_recorder_->NewSendAttachmentsStart( + info->session_id(), share_target.GetAttachments(), + /*transfer_position=*/GetConnectedShareTargetPos(share_target), + /*concurrent_connections=*/GetConnectedShareTargetCount()); + + info->transfer_update_callback()->OnTransferUpdate( + share_target, + TransferMetadataBuilder() + .set_token(info->token()) + .set_status(TransferMetadata::Status::kAwaitingRemoteAcceptance) + .build()); + + if (!info->endpoint_id()) { + NL_LOG(WARNING) << __func__ + << ": Failed to send payload due to missing endpoint id."; + AbortAndCloseConnectionIfNecessary( + TransferMetadata::Status::kMissingEndpointId, share_target); + return StatusCodes::kOutOfOrderApiCall; + } + + ReceiveConnectionResponse(share_target); + return StatusCodes::kOk; +} + +void NearbySharingServiceImpl::OnUniquePathFetched( + int64_t attachment_id, int64_t payload_id, + std::function callback, std::filesystem::path file_path) { + attachment_info_map_[attachment_id].file_path = file_path; + nearby_connections_manager_->RegisterPayloadPath(payload_id, file_path, + std::move(callback)); +} + +void NearbySharingServiceImpl::OnPayloadPathRegistered(Status status) { + if (status != Status::kSuccess) { + path_registration_status_.status = false; + } + + path_registration_status_.current_count += 1; + if (path_registration_status_.current_count == + path_registration_status_.expected_count) { + OnPayloadPathsRegistered( + path_registration_status_.share_target, + std::make_unique(path_registration_status_.status), + std::move(path_registration_status_.status_codes_callback)); + } +} + +void NearbySharingServiceImpl::OnPayloadPathsRegistered( + const ShareTarget& share_target, std::unique_ptr aggregated_success, + std::function status_codes_callback) { + NL_DCHECK(aggregated_success); + if (!*aggregated_success) { + NL_LOG(WARNING) + << __func__ + << ": Not all payload paths could be registered successfully."; + std::move(status_codes_callback)(StatusCodes::kError); + return; + } + + ShareTargetInfo* info = GetShareTargetInfo(share_target); + if (!info || !info->connection()) { + NL_LOG(WARNING) << __func__ << ": Accept invoked for unknown share target"; + std::move(status_codes_callback)(StatusCodes::kOutOfOrderApiCall); + return; + } + NearbyConnection* connection = info->connection(); + + if (!info->transfer_update_callback()) { + NL_LOG(WARNING) << __func__ + << ": Accept invoked for share target without transfer " + "update callback. Disconnecting."; + AbortAndCloseConnectionIfNecessary( + TransferMetadata::Status::kMissingTransferUpdateCallback, share_target); + std::move(status_codes_callback)(StatusCodes::kOutOfOrderApiCall); + return; + } + + // Log analytics event of starting to receive payloads. + analytics_recorder_->NewReceiveAttachmentsStart( + receiving_session_id_, share_target.GetAttachments()); + + info->set_payload_tracker(std::make_shared( + context_, share_target, attachment_info_map_, + [&](ShareTarget share_target, TransferMetadata transfer_metadata) { + OnPayloadTransferUpdate(share_target, transfer_metadata); + })); + + // Register status listener for all payloads. + for (int64_t attachment_id : share_target.GetAttachmentIds()) { + std::optional payload_id = GetAttachmentPayloadId(attachment_id); + if (!payload_id) { + NL_LOG(WARNING) << __func__ + << ": Failed to retrieve payload for attachment id - " + << attachment_id; + continue; + } + + NL_VLOG(1) << __func__ << ": Started listening for progress on payload - " + << *payload_id; + + nearby_connections_manager_->RegisterPayloadStatusListener( + *payload_id, info->payload_tracker()); + + NL_VLOG(1) << __func__ << ": Accepted incoming files from share target - " + << share_target.id; + } + + WriteResponseFrame( + *connection, + nearby::sharing::service::proto::ConnectionResponseFrame::ACCEPT); + NL_VLOG(1) << __func__ << ": Successfully wrote response frame"; + + info->transfer_update_callback()->OnTransferUpdate( + share_target, + TransferMetadataBuilder() + .set_status(TransferMetadata::Status::kAwaitingRemoteAcceptance) + .set_token(info->token()) + .build()); + + std::optional endpoint_id = info->endpoint_id(); + if (endpoint_id.has_value()) { + if (share_target.GetTotalAttachmentsSize() >= + kAttachmentsSizeThresholdOverHighQualityMedium) { + // Upgrade bandwidth regardless of advertising visibility because either + // the system or the user has verified the sender's identity; the + // stable identifiers potentially exposed by performing a bandwidth + // upgrade are no longer a concern. + NL_LOG(INFO) << __func__ << ": Upgrade bandwidth when receiving accept."; + nearby_connections_manager_->UpgradeBandwidth(*endpoint_id); + } + } else { + NL_LOG(WARNING) << __func__ + << ": Failed to initiate bandwidth upgrade. No endpoint_id " + "found for target - " + << share_target.id; + std::move(status_codes_callback)(StatusCodes::kOutOfOrderApiCall); + return; + } + + std::move(status_codes_callback)(StatusCodes::kOk); +} + +void NearbySharingServiceImpl::OnOutgoingConnection( + const ShareTarget& share_target, absl::Time connect_start_time, + NearbyConnection* connection) { + OutgoingShareTargetInfo* info = GetOutgoingShareTargetInfo(share_target); + bool success = info && info->endpoint_id() && connection; + + if (!success) { + NL_LOG(WARNING) << __func__ + << ": Failed to initiate connection to share target " + << share_target.id; + TransferMetadata::Status transfer_status = + TransferMetadata::Status::kFailedToInitiateOutgoingConnection; + if (info != nullptr && + info->connection_layer_status() == Status::kTimeout) { + transfer_status = TransferMetadata::Status::kTimedOut; + info->set_connection_layer_status(Status::kUnknown); + } + AbortAndCloseConnectionIfNecessary(transfer_status, share_target); + return; + } + + info->set_connection(connection); + + // Log analytics event of establishing connection. + analytics_recorder_->NewEstablishConnection( + info->session_id(), EstablishConnectionStatus::CONNECTION_STATUS_SUCCESS, + share_target, + /*transfer_position=*/GetConnectedShareTargetPos(share_target), + /*concurrent_connections=*/GetConnectedShareTargetCount(), + info->connection_start_time().has_value() + ? absl::ToInt64Milliseconds((context_->GetClock()->Now() - + *(info->connection_start_time()))) + : 0, + std::nullopt); + + connection->SetDisconnectionListener([&, share_target]() { + RunOnNearbySharingServiceThread( + "disconnection_listener", [&, share_target]() { + OnOutgoingConnectionDisconnected(share_target); + }); + }); + + std::optional four_digit_token = TokenToFourDigitString( + nearby_connections_manager_->GetRawAuthenticationToken( + *info->endpoint_id())); + + RunPairedKeyVerification( + share_target, *info->endpoint_id(), + [&, share_target, four_digit_token = std::move(four_digit_token)]( + PairedKeyVerificationRunner::PairedKeyVerificationResult result, + OSType remote_os_type) { + OnOutgoingConnectionKeyVerificationDone(share_target, four_digit_token, + result, remote_os_type); + }); +} + +void NearbySharingServiceImpl::SendIntroduction( + const ShareTarget& share_target, + std::optional four_digit_token) { + // We successfully connected! Now lets build up Payloads for all the files we + // want to send them. We won't send any just yet, but we'll send the Payload + // IDs in our introduction frame so that they know what to expect if they + // accept. + NL_VLOG(1) << __func__ << ": Preparing to send introduction to " + << share_target.id; + + ShareTargetInfo* info = GetShareTargetInfo(share_target); + if (!info || !info->connection()) { + NL_LOG(WARNING) << __func__ << ": No NearbyConnection tied to " + << share_target.id; + return; + } + + // Log analytics event of sending introduction. + analytics_recorder_->NewSendIntroduction( + info->session_id(), share_target, + /*transfer_position=*/GetConnectedShareTargetPos(share_target), + /*concurrent_connections=*/GetConnectedShareTargetCount(), + info->os_type()); + + NearbyConnection* connection = info->connection(); + + if (!info->transfer_update_callback()) { + NL_LOG(WARNING) << __func__ + << ": No transfer update callback, disconnecting."; + AbortAndCloseConnectionIfNecessary( + TransferMetadata::Status::kMissingTransferUpdateCallback, share_target); + return; + } + + if (foreground_send_transfer_callbacks_.empty() && + background_send_transfer_callbacks_.empty()) { + NL_LOG(WARNING) << __func__ << ": No transfer callbacks, disconnecting."; + connection->Close(); + return; + } + + // Build the introduction. + auto introduction = + std::make_unique(); + introduction->set_start_transfer(true); + NL_VLOG(1) << __func__ << ": Sending attachments to " << share_target.id; + + // Write introduction of file payloads. + for (const auto& file : share_target.file_attachments) { + std::optional payload_id = GetAttachmentPayloadId(file.id()); + if (!payload_id) { + NL_VLOG(1) << __func__ << ": Skipping unknown file attachment"; + continue; + } + auto* file_metadata = introduction->add_file_metadata(); + file_metadata->set_id(file.id()); + file_metadata->set_name(absl::StrCat(file.file_name())); + file_metadata->set_payload_id(*payload_id); + file_metadata->set_type(file.type()); + file_metadata->set_mime_type(absl::StrCat(file.mime_type())); + file_metadata->set_size(file.size()); + } + + // Write introduction of text payloads. + for (const auto& text : share_target.text_attachments) { + std::optional payload_id = GetAttachmentPayloadId(text.id()); + if (!payload_id) { + NL_VLOG(1) << __func__ << ": Skipping unknown text attachment"; + continue; + } + auto* text_metadata = introduction->add_text_metadata(); + text_metadata->set_id(text.id()); + text_metadata->set_text_title(std::string(text.text_title())); + text_metadata->set_type(text.type()); + text_metadata->set_size(text.size()); + text_metadata->set_payload_id(*payload_id); + } + + // Write introduction of Wi-Fi credentials payloads. + for (const auto& wifi_credentials : + share_target.wifi_credentials_attachments) { + std::optional payload_id = + GetAttachmentPayloadId(wifi_credentials.id()); + if (!payload_id) { + NL_VLOG(1) << __func__ + << ": Skipping unknown WiFi credentials attachment"; + continue; + } + auto* wifi_credentials_metadata = + introduction->add_wifi_credentials_metadata(); + wifi_credentials_metadata->set_id(wifi_credentials.id()); + wifi_credentials_metadata->set_ssid(std::string(wifi_credentials.ssid())); + wifi_credentials_metadata->set_security_type( + wifi_credentials.security_type()); + wifi_credentials_metadata->set_payload_id(*payload_id); + } + + if (introduction->file_metadata_size() == 0 && + introduction->text_metadata_size() == 0 && + introduction->wifi_credentials_metadata_size() == 0) { + NL_LOG(WARNING) << __func__ + << ": No payloads tied to transfer, disconnecting."; + AbortAndCloseConnectionIfNecessary( + TransferMetadata::Status::kMissingPayloads, share_target); + return; + } + + // Write the introduction to the remote device. + nearby::sharing::service::proto::Frame frame; + frame.set_version(nearby::sharing::service::proto::Frame::V1); + nearby::sharing::service::proto::V1Frame* v1_frame = frame.mutable_v1(); + v1_frame->set_type(nearby::sharing::service::proto::V1Frame::INTRODUCTION); + v1_frame->set_allocated_introduction(introduction.release()); + + std::vector data(frame.ByteSizeLong()); + frame.SerializeToArray(data.data(), frame.ByteSizeLong()); + connection->Write(std::move(data)); + + // We've successfully written the introduction, so we now have to wait for the + // remote side to accept. + NL_VLOG(1) << __func__ << ": Successfully wrote the introduction frame"; + + mutual_acceptance_timeout_alarm_->Stop(); + mutual_acceptance_timeout_alarm_->Start( + absl::ToInt64Milliseconds(kReadResponseFrameTimeout), 0, + [&, share_target]() { OnOutgoingMutualAcceptanceTimeout(share_target); }); + + info->transfer_update_callback()->OnTransferUpdate( + share_target, + TransferMetadataBuilder() + .set_status(TransferMetadata::Status::kAwaitingLocalConfirmation) + .set_token(four_digit_token) + .build()); +} + +void NearbySharingServiceImpl::CreatePayloads( + ShareTarget share_target, std::function callback) { + OutgoingShareTargetInfo* info = GetOutgoingShareTargetInfo(share_target); + if (!info || !share_target.has_attachments()) { + std::move(callback)(std::move(share_target), /*success=*/false); + return; + } + + if (!info->file_payloads().empty() || !info->text_payloads().empty() || + !info->wifi_credentials_payloads().empty()) { + // We may have already created the payloads in the case of retry, so we can + // skip this step. + std::move(callback)(std::move(share_target), /*success=*/false); + return; + } + + info->set_text_payloads(CreateTextPayloads(share_target.text_attachments)); + info->set_wifi_credentials_payloads( + CreateWifiCredentialsPayloads(share_target.wifi_credentials_attachments)); + if (share_target.file_attachments.empty()) { + std::move(callback)(std::move(share_target), /*success=*/true); + return; + } + + std::vector file_paths; + for (const FileAttachment& attachment : share_target.file_attachments) { + if (!attachment.file_path()) { + NL_LOG(WARNING) << __func__ << ": Got file attachment without path"; + std::move(callback)(std::move(share_target), /*success=*/false); + return; + } + file_paths.push_back(*attachment.file_path()); + } + + file_handler_.OpenFiles( + std::move(file_paths), + [&, share_target = std::move(share_target), + callback = std::move(callback)]( + std::vector file_infos) { + OnOpenFiles(std::move(share_target), std::move(callback), + std::move(file_infos)); + }); +} + +void NearbySharingServiceImpl::OnCreatePayloads( + std::vector endpoint_info, ShareTarget share_target, + bool success) { + OutgoingShareTargetInfo* info = GetOutgoingShareTargetInfo(share_target); + bool has_payloads = info && (!info->text_payloads().empty() || + !info->file_payloads().empty()); + if (!success || !has_payloads || !info->endpoint_id()) { + NL_LOG(WARNING) << __func__ + << ": Failed to send file to remote ShareTarget. Failed to " + "create payloads."; + if (info && info->transfer_update_callback()) { + info->transfer_update_callback()->OnTransferUpdate( + share_target, + TransferMetadataBuilder() + .set_status(TransferMetadata::Status::kMediaUnavailable) + .build()); + } + return; + } + + std::optional> bluetooth_mac_address = + GetBluetoothMacAddressForShareTarget(share_target); + + // For metrics. + all_cancelled_share_target_ids_.clear(); + + info->set_connection_start_time(context_->GetClock()->Now()); + + nearby_connections_manager_->Connect( + std::move(endpoint_info), *info->endpoint_id(), + std::move(bluetooth_mac_address), settings_->GetDataUsage(), + GetTransportType(share_target), + [&, share_target, info](NearbyConnection* connection, Status status) { + // Log analytics event of new connection. + info->set_connection_layer_status(status); + if (connection == nullptr) { + analytics_recorder_->NewEstablishConnection( + info->session_id(), + EstablishConnectionStatus::CONNECTION_STATUS_FAILURE, + share_target, + /*transfer_position=*/GetConnectedShareTargetPos(share_target), + /*concurrent_connections=*/GetConnectedShareTargetCount(), + info->connection_start_time().has_value() + ? absl::ToInt64Milliseconds(context_->GetClock()->Now() - + *(info->connection_start_time())) + : 0, + std::nullopt); + } + + OnOutgoingConnection(share_target, context_->GetClock()->Now(), + connection); + }); +} + +void NearbySharingServiceImpl::OnOpenFiles( + ShareTarget share_target, std::function callback, + std::vector files) { + OutgoingShareTargetInfo* info = GetOutgoingShareTargetInfo(share_target); + if (!info || files.size() != share_target.file_attachments.size()) { + std::move(callback)(std::move(share_target), /*success=*/false); + return; + } + + std::vector payloads; + payloads.reserve(files.size()); + + for (size_t i = 0; i < files.size(); ++i) { + FileAttachment& attachment = share_target.file_attachments[i]; + attachment.set_size(files[i].size); + InputFile input_file; + input_file.path = files[i].file_path; + Payload payload(input_file, attachment.parent_folder()); + payload.content.file_payload.size = files[i].size; + SetAttachmentPayloadId(attachment, payload.id); + payloads.push_back(std::move(payload)); + } + + info->set_file_payloads(std::move(payloads)); + std::move(callback)(std::move(share_target), /*success=*/true); +} + +std::vector NearbySharingServiceImpl::CreateTextPayloads( + const std::vector& attachments) { + std::vector payloads; + payloads.reserve(attachments.size()); + for (const TextAttachment& attachment : attachments) { + absl::string_view body = attachment.text_body(); + std::vector bytes(body.begin(), body.end()); + + Payload payload{bytes}; + SetAttachmentPayloadId(attachment, payload.id); + payloads.push_back(std::move(payload)); + } + return payloads; +} + +std::vector NearbySharingServiceImpl::CreateWifiCredentialsPayloads( + const std::vector& attachments) { + std::vector payloads; + payloads.reserve(attachments.size()); + for (const WifiCredentialsAttachment& attachment : attachments) { + nearby::sharing::service::proto::WifiCredentials wifi_credentials; + wifi_credentials.set_password(attachment.password()); + wifi_credentials.set_hidden_ssid(attachment.is_hidden()); + + std::vector bytes(wifi_credentials.ByteSizeLong()); + wifi_credentials.SerializeToArray(bytes.data(), + wifi_credentials.ByteSizeLong()); + + Payload payload{bytes}; + SetAttachmentPayloadId(attachment, payload.id); + payloads.push_back(std::move(payload)); + } + return payloads; +} + +void NearbySharingServiceImpl::WriteResponseFrame( + NearbyConnection& connection, + nearby::sharing::service::proto::ConnectionResponseFrame::Status + response_status) { + nearby::sharing::service::proto::Frame frame; + frame.set_version(nearby::sharing::service::proto::Frame::V1); + nearby::sharing::service::proto::V1Frame* v1_frame = frame.mutable_v1(); + v1_frame->set_type(nearby::sharing::service::proto::V1Frame::RESPONSE); + v1_frame->mutable_connection_response()->set_status(response_status); + + std::vector data(frame.ByteSizeLong()); + frame.SerializeToArray(data.data(), frame.ByteSizeLong()); + + connection.Write(std::move(data)); +} + +void NearbySharingServiceImpl::WriteCancelFrame(NearbyConnection& connection) { + NL_LOG(INFO) << __func__ << ": Writing cancel frame."; + + nearby::sharing::service::proto::Frame frame; + frame.set_version(nearby::sharing::service::proto::Frame::V1); + nearby::sharing::service::proto::V1Frame* v1_frame = frame.mutable_v1(); + v1_frame->set_type(nearby::sharing::service::proto::V1Frame::CANCEL); + + std::vector data(frame.ByteSizeLong()); + frame.SerializeToArray(data.data(), frame.ByteSizeLong()); + + connection.Write(std::move(data)); +} + +void NearbySharingServiceImpl::WriteProgressUpdateFrame( + NearbyConnection& connection, std::optional start_transfer, + std::optional progress) { + NL_LOG(INFO) << __func__ << ": Writing progress update frame. start_transfer=" + << (start_transfer.has_value() ? *start_transfer : false) + << ", progress=" << (progress.has_value() ? *progress : 0.0); + nearby::sharing::service::proto::Frame frame; + frame.set_version(nearby::sharing::service::proto::Frame::V1); + nearby::sharing::service::proto::V1Frame* v1_frame = frame.mutable_v1(); + v1_frame->set_type(nearby::sharing::service::proto::V1Frame::PROGRESS_UPDATE); + nearby::sharing::service::proto::ProgressUpdateFrame* progress_frame = + v1_frame->mutable_progress_update(); + if (start_transfer.has_value()) { + progress_frame->set_start_transfer(*start_transfer); + } + if (progress.has_value()) { + progress_frame->set_progress(*progress); + } + + std::vector data(frame.ByteSizeLong()); + frame.SerializeToArray(data.data(), frame.ByteSizeLong()); + + connection.Write(std::move(data)); +} + +void NearbySharingServiceImpl::Fail(const ShareTarget& share_target, + TransferMetadata::Status status) { + ShareTargetInfo* info = GetShareTargetInfo(share_target); + if (!info || !info->connection()) { + NL_LOG(WARNING) << __func__ << ": Fail invoked for unknown share target."; + return; + } + NearbyConnection* connection = info->connection(); + + RunOnNearbySharingServiceThreadDelayed( + "incoming_rejection_delay", kIncomingRejectionDelay, + [&, share_target]() { CloseConnection(share_target); }); + + connection->SetDisconnectionListener([&, share_target]() { + RunOnNearbySharingServiceThread( + "disconnection_listener", + [&, share_target]() { RefreshUIOnDisconnection(share_target); }); + }); + + // Send response to remote device. + nearby::sharing::service::proto::ConnectionResponseFrame::Status + response_status; + switch (status) { + case TransferMetadata::Status::kNotEnoughSpace: + response_status = nearby::sharing::service::proto:: + ConnectionResponseFrame::NOT_ENOUGH_SPACE; + break; + + case TransferMetadata::Status::kUnsupportedAttachmentType: + response_status = nearby::sharing::service::proto:: + ConnectionResponseFrame::UNSUPPORTED_ATTACHMENT_TYPE; + break; + + case TransferMetadata::Status::kTimedOut: + response_status = + nearby::sharing::service::proto::ConnectionResponseFrame::TIMED_OUT; + break; + + default: + response_status = + nearby::sharing::service::proto::ConnectionResponseFrame::UNKNOWN; + break; + } + + WriteResponseFrame(*connection, response_status); + + if (info->transfer_update_callback()) { + info->transfer_update_callback()->OnTransferUpdate( + share_target, TransferMetadataBuilder().set_status(status).build()); + } +} + +void NearbySharingServiceImpl::OnIncomingAdvertisementDecoded( + absl::string_view endpoint_id, ShareTarget placeholder_share_target, + std::unique_ptr advertisement) { + NearbyConnection* connection = GetConnection(placeholder_share_target); + if (!connection) { + NL_LOG(WARNING) << __func__ << ": Invalid connection for endpoint id - " + << endpoint_id; + return; + } + + if (!advertisement) { + NL_LOG(WARNING) << __func__ + << ": Failed to parse incoming connection from endpoint - " + << endpoint_id << ", disconnecting."; + AbortAndCloseConnectionIfNecessary( + TransferMetadata::Status::kDecodeAdvertisementFailed, + placeholder_share_target); + return; + } + + NearbyShareEncryptedMetadataKey encrypted_metadata_key( + advertisement->salt(), advertisement->encrypted_metadata_key()); + + // Because we cannot apply std::move on Advertisement in lambda, copy to pass + // data to lambda. + GetCertificateManager()->GetDecryptedPublicCertificate( + std::move(encrypted_metadata_key), + [this, endpoint_id, advertisement_copy = *advertisement, + placeholder_share_target = std::move(placeholder_share_target)]( + std::optional + decrypted_public_certificate) { + std::unique_ptr advertisement = + Advertisement::NewInstance( + advertisement_copy.salt(), + advertisement_copy.encrypted_metadata_key(), + advertisement_copy.device_type(), + advertisement_copy.device_name()); + OnIncomingDecryptedCertificate(endpoint_id, std::move(advertisement), + std::move(placeholder_share_target), + decrypted_public_certificate); + }); +} + +void NearbySharingServiceImpl::OnIncomingTransferUpdate( + const ShareTarget& share_target, const TransferMetadata& metadata) { + // kInProgress status is logged extensively elsewhere so avoid the spam. + if (metadata.status() != TransferMetadata::Status::kInProgress) { + NL_VLOG(1) << __func__ << ": Nearby Share service: " + << "Incoming transfer update for share target with ID " + << share_target.id << ": " + << TransferMetadata::StatusToString(metadata.status()); + } + if (metadata.status() != TransferMetadata::Status::kCancelled && + metadata.status() != TransferMetadata::Status::kRejected) { + last_incoming_metadata_ = + std::make_pair(share_target, TransferMetadataBuilder::Clone(metadata) + .set_is_original(false) + .build()); + } else { + last_incoming_metadata_ = std::nullopt; + } + + if (metadata.is_final_status()) { + // Log analytics event of receiving attachment end. + int64_t received_bytes = + share_target.GetTotalAttachmentsSize() * metadata.progress() / 100; + AttachmentTransmissionStatus transmission_status = + ConvertToTransmissionStatus(metadata.status()); + + analytics_recorder_->NewReceiveAttachmentsEnd( + receiving_session_id_, received_bytes, transmission_status, + /* referrer_package=*/std::nullopt); + + OnTransferComplete(); + if (metadata.status() != TransferMetadata::Status::kComplete) { + // For any type of failure, lets make sure any pending files get cleaned + // up. + RemoveIncomingPayloads(share_target); + } + } else if (metadata.status() == + TransferMetadata::Status::kAwaitingLocalConfirmation) { + OnTransferStarted(/*is_incoming=*/true); + } + + ObserverList& transfer_callbacks = + foreground_receive_callbacks_.empty() ? background_receive_callbacks_ + : foreground_receive_callbacks_; + + for (TransferUpdateCallback* callback : transfer_callbacks.GetObservers()) { + callback->OnTransferUpdate(share_target, metadata); + } +} + +void NearbySharingServiceImpl::OnOutgoingTransferUpdate( + const ShareTarget& share_target, const TransferMetadata& metadata) { + // kInProgress status is logged extensively elsewhere so avoid the spam. + if (metadata.status() != TransferMetadata::Status::kInProgress) { + NL_VLOG(1) << __func__ << ": Nearby Share service: " + << "Outgoing transfer update for share target with ID " + << share_target.id << ": " + << TransferMetadata::StatusToString(metadata.status()); + } + + OutgoingShareTargetInfo* info = GetOutgoingShareTargetInfo(share_target); + if (metadata.is_final_status()) { + // Log analytics event of sending attachment end. + int64_t sent_bytes = + share_target.GetTotalAttachmentsSize() * metadata.progress() / 100; + AttachmentTransmissionStatus transmission_status = + ConvertToTransmissionStatus(metadata.status()); + + if (info == nullptr) { + // The situation may happen when user cancel connection during + // establishing connection. + NL_LOG(INFO) << "No share target info is created for share_target:" + << share_target.device_name; + } else { + analytics_recorder_->NewSendAttachmentsEnd( + info->session_id(), sent_bytes, share_target, transmission_status, + /*transfer_position=*/GetConnectedShareTargetPos(share_target), + /*concurrent_connections=*/GetConnectedShareTargetCount(), + /*duration_millis=*/info->connection_start_time().has_value() + ? absl::ToInt64Milliseconds(context_->GetClock()->Now() - + *(info->connection_start_time())) + : 0, + /*referrer_package=*/std::nullopt, + ConvertToConnectionLayerStatus(info->connection_layer_status()), + info->os_type()); + } + is_connecting_ = false; + OnTransferComplete(); + } else if (metadata.status() == TransferMetadata::Status::kMediaDownloading || + metadata.status() == + TransferMetadata::Status::kAwaitingLocalConfirmation) { + is_connecting_ = false; + OnTransferStarted(/*is_incoming=*/false); + } + + bool has_foreground_send_surface = + !foreground_send_transfer_callbacks_.empty(); + ObserverList& transfer_callbacks = + has_foreground_send_surface ? foreground_send_transfer_callbacks_ + : background_send_transfer_callbacks_; + if (info) { + // only call transfer update when having share target info. + for (TransferUpdateCallback* callback : transfer_callbacks.GetObservers()) { + callback->OnTransferUpdate(share_target, metadata); + } + + // check whether need to send next payload. + if (NearbyFlags::GetInstance().GetBoolFlag( + config_package_nearby::nearby_sharing_feature:: + kEnableTransferCancellationOptimization)) { + if (metadata.in_progress_attachment_transferred_bytes().has_value() && + metadata.in_progress_attachment_total_bytes().has_value() && + *metadata.in_progress_attachment_transferred_bytes() == + *metadata.in_progress_attachment_total_bytes()) { + std::optional payload = info->ExtractNextPayload(); + if (payload.has_value()) { + NL_LOG(INFO) << __func__ << ": Send payload " << payload->id; + nearby_connections_manager_->Send(*info->endpoint_id(), + std::make_unique(*payload), + info->payload_tracker()); + } else { + NL_LOG(WARNING) << __func__ << ": There is no paylaods to send."; + } + } + } + } + + if (has_foreground_send_surface && metadata.is_final_status()) { + last_outgoing_metadata_ = std::nullopt; + } else { + last_outgoing_metadata_ = + std::make_pair(share_target, TransferMetadataBuilder::Clone(metadata) + .set_is_original(false) + .build()); + } +} + +void NearbySharingServiceImpl::CloseConnection( + const ShareTarget& share_target) { + NearbyConnection* connection = GetConnection(share_target); + if (!connection) { + NL_LOG(WARNING) << __func__ << ": Invalid connection for target - " + << share_target.id; + return; + } + connection->Close(); +} + +void NearbySharingServiceImpl::OnIncomingDecryptedCertificate( + absl::string_view endpoint_id, std::unique_ptr advertisement, + ShareTarget placeholder_share_target, + std::optional certificate) { + NearbyConnection* connection = GetConnection(placeholder_share_target); + if (!connection) { + NL_VLOG(1) << __func__ << ": Invalid connection for endpoint id - " + << endpoint_id; + return; + } + + // Remove placeholder share target since we are creating the actual share + // target below. + incoming_share_target_info_map_.erase(placeholder_share_target.id); + + std::optional share_target = + CreateShareTarget(endpoint_id, std::move(advertisement), + std::move(certificate), /*is_incoming=*/true); + + if (!share_target) { + NL_LOG(WARNING) << __func__ + << ": Failed to convert advertisement to share target for " + "incoming connection, disconnecting"; + AbortAndCloseConnectionIfNecessary( + TransferMetadata::Status::kMissingShareTarget, + placeholder_share_target); + return; + } + + NL_VLOG(1) << __func__ << ": Received incoming connection from " + << share_target->id; + + ShareTargetInfo* share_target_info = GetShareTargetInfo(*share_target); + NL_DCHECK(share_target_info); + share_target_info->set_connection(connection); + + share_target_info->set_transfer_update_callback( + std::make_unique( + [&](const ShareTarget& share_target, + const TransferMetadata& transfer_metadata) { + OnIncomingTransferUpdate(share_target, transfer_metadata); + })); + + connection->SetDisconnectionListener([&, share_target = *share_target]() { + RunOnNearbySharingServiceThread( + "disconnection_listener", + [&, share_target]() { RefreshUIOnDisconnection(share_target); }); + }); + + std::optional four_digit_token = TokenToFourDigitString( + nearby_connections_manager_->GetRawAuthenticationToken(endpoint_id)); + + RunPairedKeyVerification( + *share_target, endpoint_id, + [&, share_target = *share_target, + four_digit_token = std::move(four_digit_token)]( + PairedKeyVerificationRunner::PairedKeyVerificationResult + verification_result, + OSType remote_os_type) { + OnIncomingConnectionKeyVerificationDone(share_target, four_digit_token, + verification_result, + remote_os_type); + }); +} + +void NearbySharingServiceImpl::RunPairedKeyVerification( + const ShareTarget& share_target, absl::string_view endpoint_id, + std::function + callback) { + std::optional> token = + nearby_connections_manager_->GetRawAuthenticationToken(endpoint_id); + if (!token) { + NL_VLOG(1) << __func__ + << ": Failed to read authentication token from endpoint - " + << endpoint_id; + std::move(callback)( + PairedKeyVerificationRunner::PairedKeyVerificationResult::kFail, + OSType::UNKNOWN_OS_TYPE); + return; + } + + ShareTargetInfo* share_target_info = GetShareTargetInfo(share_target); + NL_DCHECK(share_target_info); + + share_target_info->set_frames_reader(std::make_shared( + context_, decoder_, share_target_info->connection())); + + bool restrict_to_contacts = share_target.is_incoming && + settings_->GetVisibility() != + DeviceVisibility::DEVICE_VISIBILITY_EVERYONE; + bool self_share_feature_enabled = NearbyFlags::GetInstance().GetBoolFlag( + config_package_nearby::nearby_sharing_feature::kEnableSelfShare); + share_target_info->set_key_verification_runner( + std::make_shared( + context_->GetClock(), device_info_, GetSettings(), + self_share_feature_enabled, share_target, endpoint_id, *token, + share_target_info->connection(), share_target_info->certificate(), + GetCertificateManager(), restrict_to_contacts, + share_target_info->frames_reader(), kReadFramesTimeout)); + share_target_info->key_verification_runner()->Run(std::move(callback)); +} + +void NearbySharingServiceImpl::OnIncomingConnectionKeyVerificationDone( + ShareTarget share_target, std::optional four_digit_token, + PairedKeyVerificationRunner::PairedKeyVerificationResult result, + OSType share_target_os_type) { + ShareTargetInfo* info = GetShareTargetInfo(share_target); + if (!info || !info->connection() || !info->endpoint_id()) { + NL_VLOG(1) << __func__ << ": Invalid connection or endpoint id"; + return; + } + + info->set_os_type(share_target_os_type); + + switch (result) { + case PairedKeyVerificationRunner::PairedKeyVerificationResult::kFail: + NL_VLOG(1) << __func__ << ": Paired key handshake failed for target " + << share_target.id << ". Disconnecting."; + AbortAndCloseConnectionIfNecessary( + TransferMetadata::Status::kPairedKeyVerificationFailed, share_target); + return; + + case PairedKeyVerificationRunner::PairedKeyVerificationResult::kSuccess: + NL_VLOG(1) << __func__ << ": Paired key handshake succeeded for target - " + << share_target.id; + ReceiveIntroduction(share_target, /*four_digit_token=*/std::nullopt); + break; + + case PairedKeyVerificationRunner::PairedKeyVerificationResult::kUnable: + NL_VLOG(1) << __func__ + << ": Unable to verify paired key encryption when " + "receiving connection from target - " + << share_target.id; + if (four_digit_token) info->set_token(*four_digit_token); + + ReceiveIntroduction(share_target, std::move(four_digit_token)); + break; + + case PairedKeyVerificationRunner::PairedKeyVerificationResult::kUnknown: + NL_VLOG(1) << __func__ + << ": Unknown PairedKeyVerificationResult for target " + << share_target.id << ". Disconnecting."; + AbortAndCloseConnectionIfNecessary( + TransferMetadata::Status::kPairedKeyVerificationFailed, share_target); + break; + } +} + +void NearbySharingServiceImpl::OnOutgoingConnectionKeyVerificationDone( + const ShareTarget& share_target, + std::optional four_digit_token, + PairedKeyVerificationRunner::PairedKeyVerificationResult result, + OSType share_target_os_type) { + ShareTargetInfo* info = GetShareTargetInfo(share_target); + if (!info || !info->connection()) { + return; + } + + if (!info->transfer_update_callback()) { + NL_VLOG(1) << __func__ << ": No transfer update callback. Disconnecting."; + AbortAndCloseConnectionIfNecessary( + TransferMetadata::Status::kMissingTransferUpdateCallback, share_target); + return; + } + + info->set_os_type(share_target_os_type); + + switch (result) { + case PairedKeyVerificationRunner::PairedKeyVerificationResult::kFail: + NL_VLOG(1) << __func__ << ": Paired key handshake failed for target " + << share_target.id << ". Disconnecting."; + AbortAndCloseConnectionIfNecessary( + TransferMetadata::Status::kPairedKeyVerificationFailed, share_target); + return; + + case PairedKeyVerificationRunner::PairedKeyVerificationResult::kSuccess: + NL_VLOG(1) << __func__ << ": Paired key handshake succeeded for target - " + << share_target.id; + SendIntroduction(share_target, /*four_digit_token=*/std::nullopt); + SendPayloads(share_target); + return; + + case PairedKeyVerificationRunner::PairedKeyVerificationResult::kUnable: + NL_VLOG(1) << __func__ + << ": Unable to verify paired key encryption when " + "initiating connection to target - " + << share_target.id; + + if (four_digit_token) { + info->set_token(*four_digit_token); + } + + if (NearbyFlags::GetInstance().GetBoolFlag( + config_package_nearby::nearby_sharing_feature:: + kSenderSkipsConfirmation)) { + NL_VLOG(1) << __func__ + << ": Sender-side verification is disabled. Skipping " + "token comparison with " + << share_target.id; + SendIntroduction(share_target, /*four_digit_token=*/std::nullopt); + SendPayloads(share_target); + } else { + SendIntroduction(share_target, std::move(four_digit_token)); + } + return; + + case PairedKeyVerificationRunner::PairedKeyVerificationResult::kUnknown: + NL_VLOG(1) << __func__ + << ": Unknown PairedKeyVerificationResult for target " + << share_target.id << ". Disconnecting."; + AbortAndCloseConnectionIfNecessary( + TransferMetadata::Status::kPairedKeyVerificationFailed, share_target); + break; + } +} + +void NearbySharingServiceImpl::RefreshUIOnDisconnection( + ShareTarget share_target) { + ShareTargetInfo* info = GetShareTargetInfo(share_target); + if (info && info->transfer_update_callback()) { + info->transfer_update_callback()->OnTransferUpdate( + share_target, + TransferMetadataBuilder() + .set_status( + TransferMetadata::Status::kAwaitingRemoteAcceptanceFailed) + .build()); + } + + UnregisterShareTarget(share_target); +} + +void NearbySharingServiceImpl::ReceiveIntroduction( + ShareTarget share_target, std::optional four_digit_token) { + NL_LOG(INFO) << __func__ << ": Receiving introduction from " + << share_target.id; + ShareTargetInfo* info = GetShareTargetInfo(share_target); + NL_DCHECK(info && info->connection()); + + info->frames_reader()->ReadFrame( + nearby::sharing::service::proto::V1Frame::INTRODUCTION, + [&, share_target = std::move(share_target), + four_digit_token = std::move(four_digit_token)]( + std::optional frame) { + OnReceivedIntroduction(std::move(share_target), + std::move(four_digit_token), std::move(frame)); + }, + kReadFramesTimeout); +} + +void NearbySharingServiceImpl::OnReceivedIntroduction( + ShareTarget share_target, std::optional four_digit_token, + std::optional frame) { + ShareTargetInfo* info = GetShareTargetInfo(share_target); + if (!info || !info->connection()) { + NL_LOG(WARNING) + << __func__ + << ": Ignore received introduction, due to no connection established."; + return; + } + + if (!frame.has_value()) { + AbortAndCloseConnectionIfNecessary( + TransferMetadata::Status::kInvalidIntroductionFrame, share_target); + NL_LOG(WARNING) << __func__ << ": Invalid introduction frame"; + return; + } + + NL_LOG(INFO) << __func__ << ": Successfully read the introduction frame."; + + int64_t file_size_sum = 0; + + nearby::sharing::service::proto::IntroductionFrame introduction_frame = + std::move(frame->introduction()); + + for (const auto& file : introduction_frame.file_metadata()) { + if (file.size() <= 0) { + Fail(share_target, TransferMetadata::Status::kUnsupportedAttachmentType); + NL_LOG(WARNING) + << __func__ + << ": Ignore introduction, due to invalid attachment size"; + return; + } + + NL_VLOG(1) << __func__ << ": Found file attachment: id=" << file.id() + << ", type= " << file.type() << ", size=" << file.size() + << ", payload_id=" << file.payload_id() + << ", parent_folder=" << file.parent_folder() + << ", mime_type=" << file.mime_type(); + FileAttachment attachment(file.id(), file.size(), file.name(), + file.mime_type(), file.type(), + file.parent_folder()); + SetAttachmentPayloadId(attachment, file.payload_id()); + share_target.file_attachments.push_back(std::move(attachment)); + + file_size_sum += file.size(); + if (file_size_sum < 0) { + Fail(share_target, TransferMetadata::Status::kNotEnoughSpace); + NL_LOG(WARNING) << __func__ + << ": Ignoring introduction, total file size overflowed " + "64 bit integer."; + return; + } + } + + for (const auto& text : introduction_frame.text_metadata()) { + if (text.size() <= 0) { + Fail(share_target, TransferMetadata::Status::kUnsupportedAttachmentType); + NL_LOG(WARNING) + << __func__ + << ": Ignore introduction, due to invalid attachment size"; + return; + } + + NL_VLOG(1) << __func__ << ": Found text attachment: id=" << text.id() + << ", type= " << text.type() << ", size=" << text.size() + << ", payload_id=" << text.payload_id(); + TextAttachment attachment(text.id(), text.type(), text.text_title(), + text.size()); + SetAttachmentPayloadId(attachment, text.payload_id()); + share_target.text_attachments.push_back(std::move(attachment)); + } + + if (kSupportReceivingWifiCredentials) { + for (const auto& wifi_credentials : + introduction_frame.wifi_credentials_metadata()) { + NL_VLOG(1) << __func__ << ": Found WiFi credentials attachment: id=" + << wifi_credentials.id() + << ", ssid= " << wifi_credentials.ssid() + << ", payload_id=" << wifi_credentials.payload_id(); + WifiCredentialsAttachment attachment(wifi_credentials.id(), + wifi_credentials.ssid(), + wifi_credentials.security_type()); + SetAttachmentPayloadId(attachment, wifi_credentials.payload_id()); + share_target.wifi_credentials_attachments.push_back( + std::move(attachment)); + } + } + + if (!share_target.has_attachments()) { + NL_LOG(WARNING) << __func__ + << ": No attachment is found for this share target. It can " + "be result of unrecognizable attachment type"; + Fail(share_target, TransferMetadata::Status::kUnsupportedAttachmentType); + + NL_VLOG(1) << __func__ + << ": We don't support the attachments sent by the sender. " + "We have informed " + << share_target.id; + return; + } + + // Log analytics event of receiving introduction. + analytics_recorder_->NewReceiveIntroduction( + receiving_session_id_, share_target, /*referrer_package=*/std::nullopt, + info->os_type()); + + if (file_size_sum == 0) { + OnStorageCheckCompleted(std::move(share_target), + std::move(four_digit_token), + /*is_out_of_storage=*/false); + return; + } + + if (introduction_frame.has_start_transfer() && + introduction_frame.start_transfer()) { + if (info->endpoint_id().has_value() && + share_target.GetTotalAttachmentsSize() >= + kAttachmentsSizeThresholdOverHighQualityMedium) { + NL_LOG(INFO) + << __func__ + << ": Upgrade bandwidth when receiving an introduction frame."; + nearby_connections_manager_->UpgradeBandwidth(*info->endpoint_id()); + } + } + + std::filesystem::path download_path = + std::filesystem::u8path(settings_->GetCustomSavePath()); + + bool is_out_of_storage = + IsOutOfStorage(device_info_, download_path, file_size_sum); + + OnStorageCheckCompleted(std::move(share_target), std::move(four_digit_token), + is_out_of_storage); +} + +void NearbySharingServiceImpl::ReceiveConnectionResponse( + ShareTarget share_target) { + NL_VLOG(1) << __func__ << ": Receiving response frame from " + << share_target.id; + ShareTargetInfo* info = GetShareTargetInfo(share_target); + NL_DCHECK(info && info->connection()); + + info->frames_reader()->ReadFrame( + nearby::sharing::service::proto::V1Frame::RESPONSE, + [&, share_target = std::move(share_target)]( + std::optional frame) { + OnReceiveConnectionResponse(share_target, std::move(frame)); + }, + kReadResponseFrameTimeout); +} + +void NearbySharingServiceImpl::OnReceiveConnectionResponse( + ShareTarget share_target, + std::optional frame) { + OutgoingShareTargetInfo* info = GetOutgoingShareTargetInfo(share_target); + if (!info || !info->connection()) { + NL_LOG(WARNING) << __func__ + << ": Ignore received connection response, due to no " + "connection established."; + return; + } + + if (!info->transfer_update_callback()) { + NL_LOG(WARNING) << __func__ + << ": No transfer update callback. Disconnecting."; + AbortAndCloseConnectionIfNecessary( + TransferMetadata::Status::kMissingTransferUpdateCallback, share_target); + return; + } + + if (!frame) { + NL_LOG(WARNING) + << __func__ + << ": Failed to read a response from the remote device. Disconnecting."; + AbortAndCloseConnectionIfNecessary( + TransferMetadata::Status::kFailedToReadOutgoingConnectionResponse, + share_target); + return; + } + + mutual_acceptance_timeout_alarm_->Stop(); + + NL_VLOG(1) << __func__ + << ": Successfully read the connection response frame."; + + nearby::sharing::service::proto::ConnectionResponseFrame response = + std::move(frame->connection_response()); + switch (response.status()) { + case nearby::sharing::service::proto::ConnectionResponseFrame::ACCEPT: { + // Write progress update frame to remote machine. + WriteProgressUpdateFrame(*info->connection(), true, std::nullopt); + + info->frames_reader()->ReadFrame( + [&, share_target]( + std::optional frame) { + OnFrameRead(share_target, std::move(frame)); + }); + + info->transfer_update_callback()->OnTransferUpdate( + share_target, TransferMetadataBuilder() + .set_status(TransferMetadata::Status::kInProgress) + .build()); + + info->set_payload_tracker(std::make_unique( + context_, share_target, attachment_info_map_, + [&](ShareTarget share_target, TransferMetadata transfer_metadata) { + OnPayloadTransferUpdate(share_target, transfer_metadata); + })); + + if (NearbyFlags::GetInstance().GetBoolFlag( + config_package_nearby::nearby_sharing_feature:: + kEnableTransferCancellationOptimization)) { + std::optional payload = info->ExtractNextPayload(); + if (payload.has_value()) { + NL_LOG(INFO) << __func__ << ": Send payload " << payload->id; + + nearby_connections_manager_->Send(*info->endpoint_id(), + std::make_unique(*payload), + info->payload_tracker()); + } else { + NL_LOG(WARNING) << __func__ << ": There is no payloads to send."; + } + } else { + for (auto& payload : info->ExtractTextPayloads()) { + nearby_connections_manager_->Send(*info->endpoint_id(), + std::make_unique(payload), + info->payload_tracker()); + } + for (auto& payload : info->ExtractFilePayloads()) { + nearby_connections_manager_->Send(*info->endpoint_id(), + std::make_unique(payload), + info->payload_tracker()); + } + } + NL_VLOG(1) + << __func__ + << ": The connection was accepted. Payloads are now being sent."; + break; + } + case nearby::sharing::service::proto::ConnectionResponseFrame::REJECT: + AbortAndCloseConnectionIfNecessary(TransferMetadata::Status::kRejected, + share_target); + NL_VLOG(1) + << __func__ + << ": The connection was rejected. The connection has been closed."; + break; + case nearby::sharing::service::proto::ConnectionResponseFrame:: + NOT_ENOUGH_SPACE: + AbortAndCloseConnectionIfNecessary( + TransferMetadata::Status::kNotEnoughSpace, share_target); + NL_VLOG(1) << __func__ + << ": The connection was rejected because the remote device " + "does not have enough space for our attachments. The " + "connection has been closed."; + break; + case nearby::sharing::service::proto::ConnectionResponseFrame:: + UNSUPPORTED_ATTACHMENT_TYPE: + AbortAndCloseConnectionIfNecessary( + TransferMetadata::Status::kUnsupportedAttachmentType, share_target); + NL_VLOG(1) << __func__ + << ": The connection was rejected because the remote device " + "does not support the attachments we were sending. The " + "connection has been closed."; + break; + case nearby::sharing::service::proto::ConnectionResponseFrame::TIMED_OUT: + AbortAndCloseConnectionIfNecessary(TransferMetadata::Status::kTimedOut, + share_target); + NL_VLOG(1) << __func__ + << ": The connection was rejected because the remote device " + "timed out. The connection has been closed."; + break; + default: + AbortAndCloseConnectionIfNecessary(TransferMetadata::Status::kFailed, + share_target); + NL_VLOG(1) << __func__ + << ": The connection failed. The connection has been closed."; + break; + } +} + +void NearbySharingServiceImpl::OnStorageCheckCompleted( + ShareTarget share_target, std::optional four_digit_token, + bool is_out_of_storage) { + if (is_out_of_storage) { + Fail(share_target, TransferMetadata::Status::kNotEnoughSpace); + NL_LOG(WARNING) << __func__ + << ": Not enough space on the receiver. We have informed " + << share_target.id; + return; + } + + ShareTargetInfo* info = GetShareTargetInfo(share_target); + if (!info || !info->connection()) { + NL_LOG(WARNING) << __func__ << ": Invalid connection for share target - " + << share_target.id; + return; + } + NearbyConnection* connection = info->connection(); + + if (!info->transfer_update_callback()) { + NL_VLOG(1) << __func__ << ": No transfer update callback. Disconnecting."; + AbortAndCloseConnectionIfNecessary( + TransferMetadata::Status::kMissingTransferUpdateCallback, share_target); + return; + } + + mutual_acceptance_timeout_alarm_->Stop(); + mutual_acceptance_timeout_alarm_->Start( + absl::ToInt64Milliseconds(kReadResponseFrameTimeout), 0, + [&, share_target]() { OnIncomingMutualAcceptanceTimeout(share_target); }); + + bool is_self_share = + NearbyFlags::GetInstance().GetBoolFlag( + config_package_nearby::nearby_sharing_feature::kEnableSelfShare) && + !four_digit_token.has_value() && share_target.for_self_share; + bool is_self_share_auto_accept = ShouldSelfShareAutoAccept(share_target); + + if (!is_self_share_auto_accept) { + TransferMetadataBuilder transfer_metadata_builder; + transfer_metadata_builder.set_status( + TransferMetadata::Status::kAwaitingLocalConfirmation); + transfer_metadata_builder.set_token(four_digit_token); + transfer_metadata_builder.set_is_self_share(is_self_share); + + info->transfer_update_callback()->OnTransferUpdate( + share_target, transfer_metadata_builder.build()); + } else { + // Don't need to send kAwaitingLocalConfirmation for auto accept of Self + // share. + OnTransferStarted(/*is_incoming=*/true); + } + + if (!incoming_share_target_info_map_.count(share_target.id)) { + NL_VLOG(1) << __func__ << ": IncomingShareTarget not found, disconnecting " + << share_target.id; + AbortAndCloseConnectionIfNecessary( + TransferMetadata::Status::kMissingShareTarget, share_target); + return; + } + + connection->SetDisconnectionListener([&, share_target]() { + RunOnNearbySharingServiceThread( + "disconnection_listener", [&, share_target]() { + OnIncomingConnectionDisconnected(share_target); + }); + }); + + auto* frames_reader = info->frames_reader(); + if (!frames_reader) { + NL_LOG(WARNING) << __func__ + << ": Stopped reading further frames, due to no connection " + "established."; + return; + } + + if (is_self_share_auto_accept) { + NL_LOG(INFO) << __func__ << ": Auto-accepting self share."; + Accept(share_target, [&](StatusCodes status_codes) { + NL_LOG(INFO) << __func__ << ": Auto-accepting result: " + << static_cast(status_codes); + }); + } + + frames_reader->ReadFrame( + [&, share_target = std::move(share_target)]( + std::optional frame) { + OnFrameRead(std::move(share_target), std::move(frame)); + }); +} + +void NearbySharingServiceImpl::OnFrameRead( + ShareTarget share_target, + std::optional frame) { + if (!frame.has_value()) { + // This is the case when the connection has been closed since we wait + // indefinitely for incoming frames. + return; + } + + switch (frame->type()) { + case nearby::sharing::service::proto::V1Frame::CANCEL: + RunOnAnyThread("cancel_transfer", [&, share_target]() { + NL_LOG(INFO) << __func__ + << ": Read the cancel frame, closing connection"; + DoCancel( + share_target, [&](StatusCodes status_codes) {}, + /*is_initiator_of_cancellation=*/false); + }); + break; + + case nearby::sharing::service::proto::V1Frame::CERTIFICATE_INFO: + HandleCertificateInfoFrame(frame->certificate_info()); + break; + + case nearby::sharing::service::proto::V1Frame::PROGRESS_UPDATE: + HandleProgressUpdateFrame(share_target, frame->progress_update()); + break; + + default: + NL_LOG(ERROR) << __func__ << ": Discarding unknown frame of type"; + break; + } + + ShareTargetInfo* info = GetShareTargetInfo(share_target); + if (!info || !info->frames_reader()) { + NL_LOG(WARNING) << __func__ + << ": Stopped reading further frames, due to no connection " + "established."; + return; + } + + info->frames_reader()->ReadFrame( + [&, share_target = std::move(share_target)]( + std::optional frame) { + OnFrameRead(share_target, std::move(frame)); + }); +} + +void NearbySharingServiceImpl::HandleCertificateInfoFrame( + const nearby::sharing::service::proto::CertificateInfoFrame& + certificate_frame) { + if (NearbyFlags::GetInstance().GetBoolFlag( + config_package_nearby::nearby_sharing_feature::kEnableSelfShare)) { + return; + } +} + +void NearbySharingServiceImpl::HandleProgressUpdateFrame( + const ShareTarget& share_target, + const nearby::sharing::service::proto::ProgressUpdateFrame& + progress_update_frame) { + if (progress_update_frame.has_start_transfer() && + progress_update_frame.start_transfer()) { + ShareTargetInfo* info = GetShareTargetInfo(share_target); + + if (info != nullptr && info->endpoint_id().has_value() && + share_target.GetTotalAttachmentsSize() >= + kAttachmentsSizeThresholdOverHighQualityMedium) { + NL_LOG(INFO) + << __func__ + << ": Upgrade bandwidth when receiving progress update frame " + "for endpoint " + << (*info->endpoint_id()); + nearby_connections_manager_->UpgradeBandwidth(*info->endpoint_id()); + } + } + + if (progress_update_frame.has_progress()) { + NL_LOG(WARNING) << __func__ << ": Current progress for ShareTarget " + << share_target.id << " is " + << progress_update_frame.progress(); + } +} + +void NearbySharingServiceImpl::OnIncomingConnectionDisconnected( + const ShareTarget& share_target) { + ShareTargetInfo* info = GetShareTargetInfo(share_target); + if (info && info->transfer_update_callback()) { + info->transfer_update_callback()->OnTransferUpdate( + share_target, + TransferMetadataBuilder() + .set_status(TransferMetadata::Status::kUnexpectedDisconnection) + .build()); + } + UnregisterShareTarget(share_target); +} + +void NearbySharingServiceImpl::OnOutgoingConnectionDisconnected( + const ShareTarget& share_target) { + ShareTargetInfo* info = GetShareTargetInfo(share_target); + if (info && info->transfer_update_callback()) { + info->transfer_update_callback()->OnTransferUpdate( + share_target, + TransferMetadataBuilder() + .set_status(TransferMetadata::Status::kUnexpectedDisconnection) + .build()); + } + UnregisterShareTarget(share_target); +} + +void NearbySharingServiceImpl::OnIncomingMutualAcceptanceTimeout( + const ShareTarget& share_target) { + NL_DCHECK(share_target.is_incoming); + + NL_VLOG(1) + << __func__ + << ": Incoming mutual acceptance timed out, closing connection for " + << share_target.id; + + Fail(share_target, TransferMetadata::Status::kTimedOut); +} + +void NearbySharingServiceImpl::OnOutgoingMutualAcceptanceTimeout( + const ShareTarget& share_target) { + NL_DCHECK(!share_target.is_incoming); + + NL_VLOG(1) + << __func__ + << ": Outgoing mutual acceptance timed out, closing connection for " + << share_target.id; + + AbortAndCloseConnectionIfNecessary(TransferMetadata::Status::kTimedOut, + share_target); +} + +std::optional NearbySharingServiceImpl::CreateShareTarget( + absl::string_view endpoint_id, std::unique_ptr advertisement, + std::optional certificate, + bool is_incoming) { + NL_DCHECK(advertisement); + + if (!advertisement->device_name() && !certificate.has_value()) { + NL_VLOG(1) << __func__ + << ": Failed to retrieve public certificate for contact " + "only advertisement."; + return std::nullopt; + } + + std::optional device_name = + GetDeviceName(advertisement.get(), certificate); + if (!device_name.has_value()) { + NL_VLOG(1) << __func__ + << ": Failed to retrieve device name for advertisement."; + return std::nullopt; + } + + ShareTarget target; + target.type = advertisement->device_type(); + target.device_name = std::move(*device_name); + target.is_incoming = is_incoming; + target.device_id = GetDeviceId(endpoint_id, certificate); + if (NearbyFlags::GetInstance().GetBoolFlag( + config_package_nearby::nearby_sharing_feature::kEnableSelfShare)) { + target.for_self_share = certificate && certificate->for_self_share(); + } + + ShareTargetInfo& info = GetOrCreateShareTargetInfo(target, endpoint_id); + + if (certificate.has_value()) { + if (certificate->unencrypted_metadata().has_full_name()) + target.full_name = certificate->unencrypted_metadata().full_name(); + + if (certificate->unencrypted_metadata().has_icon_url()) { + absl::StatusOr<::nearby::network::Url> url = + ::nearby::network::Url::Create( + certificate->unencrypted_metadata().icon_url()); + if (url.ok()) { + target.image_url = url.value(); + } else { + target.image_url = std::nullopt; + } + } + + target.is_known = true; + info.set_certificate(std::move(*certificate)); + } + + return target; +} + +void NearbySharingServiceImpl::OnPayloadTransferUpdate( + ShareTarget share_target, TransferMetadata metadata) { + bool is_in_progress = + metadata.status() == TransferMetadata::Status::kInProgress; + + if (is_in_progress && share_target.is_incoming && + is_waiting_to_record_accept_to_transfer_start_metric_) { + is_waiting_to_record_accept_to_transfer_start_metric_ = false; + } + + // kInProgress status is logged extensively elsewhere so avoid the spam. + if (!is_in_progress) { + NL_VLOG(1) << __func__ << ": Nearby Share service: " + << "Payload transfer update for share target with ID " + << share_target.id << ": " + << TransferMetadata::StatusToString(metadata.status()); + } + + // Update file paths during progress. It may impact transfer speed. + // TODO: b/289290115 - Revisit UpdateFilePath to enhance transfer speed for + // MacOS. + if (update_file_paths_in_progress_ && share_target.is_incoming) { + UpdateFilePath(share_target); + } + + if (metadata.status() == TransferMetadata::Status::kComplete && + share_target.is_incoming) { + if (!OnIncomingPayloadsComplete(share_target)) { + metadata = TransferMetadataBuilder() + .set_status(TransferMetadata::Status::kIncompletePayloads) + .build(); + + // Reset file paths for file attachments. + for (auto& file : share_target.file_attachments) + file.set_file_path(std::nullopt); + + // Reset body of text attachments. + for (auto& text : share_target.text_attachments) + text.set_text_body(std::string()); + + // Reset password of Wi-Fi credentials attachments. + for (auto& wifi_credentials : share_target.wifi_credentials_attachments) { + wifi_credentials.set_password(std::string()); + wifi_credentials.set_is_hidden(false); + } + } + + if (IsBackgroundScanningFeatureEnabled()) { + fast_initiation_scanner_cooldown_timer_->Stop(); + fast_initiation_scanner_cooldown_timer_->Start( + absl::ToInt64Milliseconds(kFastInitiationScannerCooldown), 0, [&]() { + fast_initiation_scanner_cooldown_timer_->Stop(); + InvalidateFastInitiationScanning(); + }); + } + } else if (metadata.status() == TransferMetadata::Status::kCancelled && + share_target.is_incoming) { + NL_VLOG(1) << __func__ << ": Update file paths for cancelled transfer"; + if (!update_file_paths_in_progress_) { + UpdateFilePath(share_target); + } + } + + // Make sure to call this before calling Disconnect, or we risk losing some + // transfer updates in the receive case due to the Disconnect call cleaning up + // share targets. + ShareTargetInfo* info = GetShareTargetInfo(share_target); + if (info && info->transfer_update_callback()) + info->transfer_update_callback()->OnTransferUpdate(share_target, metadata); + + // Cancellation has its own disconnection strategy, possibly adding a delay + // before disconnection to provide the other party time to process the + // cancellation. + if (TransferMetadata::IsFinalStatus(metadata.status()) && + metadata.status() != TransferMetadata::Status::kCancelled) { + Disconnect(share_target, metadata); + } +} + +bool NearbySharingServiceImpl::OnIncomingPayloadsComplete( + ShareTarget& share_target) { + NL_DCHECK(share_target.is_incoming); + + ShareTargetInfo* info = GetShareTargetInfo(share_target); + if (!info || !info->connection()) { + NL_VLOG(1) << __func__ << ": Connection not found for target - " + << share_target.id; + + return false; + } + NearbyConnection* connection = info->connection(); + + connection->SetDisconnectionListener([&, share_target]() { + RunOnNearbySharingServiceThread( + "disconnection_listener", + [&, share_target]() { UnregisterShareTarget(share_target); }); + }); + + if (!update_file_paths_in_progress_) { + UpdateFilePath(share_target); + } + + for (auto& text : share_target.text_attachments) { + AttachmentInfo& attachment_info = attachment_info_map_[text.id()]; + std::optional payload_id = attachment_info.payload_id; + if (!payload_id) { + NL_LOG(WARNING) << __func__ << ": No payload id found for text - " + << text.id(); + return false; + } + + Payload* incoming_payload = + nearby_connections_manager_->GetIncomingPayload(*payload_id); + if (!incoming_payload || !incoming_payload->content.is_bytes()) { + NL_LOG(WARNING) << __func__ << ": No payload found for text - " + << text.id(); + return false; + } + + std::vector bytes = incoming_payload->content.bytes_payload.bytes; + if (bytes.empty()) { + NL_LOG(WARNING) + << __func__ + << ": Incoming bytes is empty for text payload with payload_id - " + << *payload_id; + return false; + } + + std::string text_body(bytes.begin(), bytes.end()); + text.set_text_body(text_body); + + attachment_info.text_body = std::move(text_body); + } + + for (auto& wifi_credentials_attachment : + share_target.wifi_credentials_attachments) { + AttachmentInfo& attachment_info = + attachment_info_map_[wifi_credentials_attachment.id()]; + std::optional payload_id = attachment_info.payload_id; + if (!payload_id) { + NL_LOG(WARNING) << __func__ + << ": No payload id found for WiFi credentials - " + << wifi_credentials_attachment.id(); + return false; + } + + Payload* incoming_payload = + nearby_connections_manager_->GetIncomingPayload(*payload_id); + if (!incoming_payload || !incoming_payload->content.is_bytes()) { + NL_LOG(WARNING) << __func__ + << ": No payload found for WiFi credentials - " + << wifi_credentials_attachment.id(); + return false; + } + + std::vector bytes = incoming_payload->content.bytes_payload.bytes; + if (bytes.empty()) { + NL_LOG(WARNING) << __func__ + << ": Incoming bytes is empty for WiFi credentials " + "payload with payload_id - " + << *payload_id; + return false; + } + + auto wifi_credentials = + std::make_unique(); + if (!wifi_credentials->ParseFromArray(bytes.data(), bytes.size())) { + NL_LOG(WARNING) << __func__ + << ": Incoming bytes is invalid for WiFi credentials " + "payload with payload_id - " + << *payload_id; + return false; + } + + wifi_credentials_attachment.set_password(wifi_credentials->password()); + wifi_credentials_attachment.set_is_hidden(wifi_credentials->hidden_ssid()); + } + + return true; +} + +void NearbySharingServiceImpl::UpdateFilePath(ShareTarget& share_target) { + for (auto& file : share_target.file_attachments) { + // Skip file if it already has file_path set. + if (file.file_path().has_value()) { + continue; + } + AttachmentInfo& attachment_info = attachment_info_map_[file.id()]; + std::optional payload_id = attachment_info.payload_id; + if (!payload_id) { + NL_LOG(WARNING) << __func__ << ": No payload id found for file - " + << file.id(); + continue; + } + + Payload* incoming_payload = + nearby_connections_manager_->GetIncomingPayload(*payload_id); + if (!incoming_payload || !incoming_payload->content.is_file()) { + NL_LOG(WARNING) << __func__ << ": No payload found for file - " + << file.id(); + continue; + } + + auto file_path = incoming_payload->content.file_payload.file.path; + NL_VLOG(1) << __func__ << ": Updated file_path=" + << GetCompatibleU8String(file_path.u8string()); + file.set_file_path(file_path); + } +} + +void NearbySharingServiceImpl::RemoveIncomingPayloads( + ShareTarget share_target) { + if (!share_target.is_incoming) { + return; + } + + NL_LOG(INFO) << __func__ << ": Cleaning up payloads due to transfer failure"; + nearby_connections_manager_->ClearIncomingPayloads(); + std::vector files_for_deletion; + for (const auto& file : share_target.file_attachments) { + if (!file.file_path().has_value()) continue; + auto file_path = *file.file_path(); + NL_VLOG(1) << __func__ + << ": file_path=" << GetCompatibleU8String(file_path.u8string()); + if (attachment_info_map_.find(file.id()) == attachment_info_map_.end()) { + continue; + } + files_for_deletion.push_back(file_path); + } + file_handler_.DeleteFilesFromDisk(std::move(files_for_deletion), []() {}); +} + +void NearbySharingServiceImpl::Disconnect(const ShareTarget& share_target, + TransferMetadata metadata) { + ShareTargetInfo* share_target_info = GetShareTargetInfo(share_target); + if (!share_target_info) { + NL_LOG(WARNING) + << __func__ + << ": Failed to disconnect. No share target info found for target - " + << share_target.id; + return; + } + + std::optional endpoint_id = share_target_info->endpoint_id(); + if (!endpoint_id.has_value()) { + NL_LOG(WARNING) + << __func__ + << ": Failed to disconnect. No endpoint id found for share target - " + << share_target.id; + return; + } + + // Failed to send or receive. No point in continuing, so disconnect + // immediately. + if (metadata.status() != TransferMetadata::Status::kComplete) { + if (share_target_info->connection()) { + share_target_info->connection()->Close(); + } else { + nearby_connections_manager_->Disconnect(*endpoint_id); + } + return; + } + + // Files received successfully. Receivers can immediately cancel. + if (share_target.is_incoming) { + if (share_target_info->connection()) { + share_target_info->connection()->Close(); + } else { + nearby_connections_manager_->Disconnect(*endpoint_id); + } + return; + } + + // Disconnect after a timeout to make sure any pending payloads are sent. + // + // We assign endpoint_id = *endpoint_id here since endpoint_id is + // std::optional so the lambda will capture the string by value. + // For absl::string_view, please make it sure you wrap the string_view object + // with std::string() so that it captures the string by value correctly. + auto timer = context_->CreateTimer(); + timer->Start(absl::ToInt64Milliseconds(kOutgoingDisconnectionDelay), 0, + [&, endpoint_id = *endpoint_id]() { + OnDisconnectingConnectionTimeout(endpoint_id); + }); + + disconnection_timeout_alarms_[*endpoint_id] = std::move(timer); + + // Stop the disconnection timeout if the connection has been closed already. + // + // We assign endpoint_id = *endpoint_id here since endpoint_id is + // std::optional so the lambda will capture the string by value. + // For absl::string_view, please make it sure you wrap the string_view object + // with std::string() so that it captures the string by value correctly. + if (share_target_info->connection()) { + share_target_info->connection()->SetDisconnectionListener( + [&, share_target, share_target_info, endpoint_id = *endpoint_id]() { + share_target_info->set_connection(nullptr); + RunOnNearbySharingServiceThread( + "disconnection_listener", [&, share_target, endpoint_id]() { + OnDisconnectingConnectionDisconnected(share_target, + endpoint_id); + }); + }); + } +} + +void NearbySharingServiceImpl::OnDisconnectingConnectionTimeout( + absl::string_view endpoint_id) { + RunOnNearbySharingServiceThread( + "on_disconnecting_connection_timeout", + [&, endpoint_id = std::string(endpoint_id)]() { + disconnection_timeout_alarms_.erase(endpoint_id); + }); + nearby_connections_manager_->Disconnect(endpoint_id); +} + +void NearbySharingServiceImpl::OnDisconnectingConnectionDisconnected( + const ShareTarget& share_target, absl::string_view endpoint_id) { + disconnection_timeout_alarms_.erase(endpoint_id); + UnregisterShareTarget(share_target); +} + +ShareTargetInfo& NearbySharingServiceImpl::GetOrCreateShareTargetInfo( + const ShareTarget& share_target, absl::string_view endpoint_id) { + if (share_target.is_incoming) { + auto& info = incoming_share_target_info_map_[share_target.id]; + info.set_endpoint_id(std::string(endpoint_id)); + return info; + } else { + // We need to explicitly remove any previous share target for + // |endpoint_id| if one exists, notifying observers that a share target is + // lost. + const auto it = outgoing_share_target_map_.find(endpoint_id); + if (it != outgoing_share_target_map_.end() && + it->second.id != share_target.id) { + RemoveOutgoingShareTargetWithEndpointId(endpoint_id); + } + + NL_VLOG(1) << __func__ << ": Adding (endpoint_id=" << endpoint_id + << ", share_target_id=" << share_target.id + << ") to outgoing share target map"; + outgoing_share_target_map_.insert_or_assign(endpoint_id, share_target); + auto& info = outgoing_share_target_info_map_[share_target.id]; + info.set_endpoint_id(std::string(endpoint_id)); + info.set_connection_layer_status(Status::kUnknown); + return info; + } +} + +ShareTargetInfo* NearbySharingServiceImpl::GetShareTargetInfo( + const ShareTarget& share_target) { + if (share_target.is_incoming) + return GetIncomingShareTargetInfo(share_target); + else + return GetOutgoingShareTargetInfo(share_target); +} + +IncomingShareTargetInfo* NearbySharingServiceImpl::GetIncomingShareTargetInfo( + const ShareTarget& share_target) { + auto it = incoming_share_target_info_map_.find(share_target.id); + if (it == incoming_share_target_info_map_.end()) { + return nullptr; + } + + return &it->second; +} + +OutgoingShareTargetInfo* NearbySharingServiceImpl::GetOutgoingShareTargetInfo( + const ShareTarget& share_target) { + auto it = outgoing_share_target_info_map_.find(share_target.id); + if (it == outgoing_share_target_info_map_.end()) { + return nullptr; + } + + return &it->second; +} + +NearbyConnection* NearbySharingServiceImpl::GetConnection( + const ShareTarget& share_target) { + ShareTargetInfo* share_target_info = GetShareTargetInfo(share_target); + return share_target_info ? share_target_info->connection() : nullptr; +} + +std::optional> +NearbySharingServiceImpl::GetBluetoothMacAddressForShareTarget( + const ShareTarget& share_target) { + ShareTargetInfo* info = GetShareTargetInfo(share_target); + if (!info) { + NL_LOG(ERROR) << __func__ << ": No ShareTargetInfo found for " + << "share target id: " << share_target.id; + return std::nullopt; + } + + const std::optional& certificate = + info->certificate(); + if (!certificate) { + NL_LOG(ERROR) << __func__ << ": No decrypted public certificate found for " + << "share target id: " << share_target.id; + return std::nullopt; + } + + return GetBluetoothMacAddressFromCertificate(*certificate); +} + +void NearbySharingServiceImpl::ClearOutgoingShareTargetInfoMap() { + NL_VLOG(1) << __func__ << ": Clearing outgoing share target map."; + while (!outgoing_share_target_map_.empty()) { + RemoveOutgoingShareTargetWithEndpointId( + /*endpoint_id=*/outgoing_share_target_map_.begin()->first); + } + NL_DCHECK(outgoing_share_target_map_.empty()); + NL_DCHECK(outgoing_share_target_info_map_.empty()); +} + +void NearbySharingServiceImpl::SetAttachmentPayloadId( + const Attachment& attachment, int64_t payload_id) { + attachment_info_map_[attachment.id()].payload_id = payload_id; +} + +std::optional NearbySharingServiceImpl::GetAttachmentPayloadId( + int64_t attachment_id) { + auto it = attachment_info_map_.find(attachment_id); + if (it == attachment_info_map_.end()) return std::nullopt; + + return it->second.payload_id; +} + +void NearbySharingServiceImpl::UnregisterShareTarget( + const ShareTarget& share_target) { + NL_VLOG(1) << __func__ << ": Unregistering share target - " + << share_target.id; + + // For metrics. + all_cancelled_share_target_ids_.erase(share_target.id); + + if (share_target.is_incoming) { + if (last_incoming_metadata_ && + last_incoming_metadata_->first.id == share_target.id) { + last_incoming_metadata_.reset(); + } + + // Clear legacy incoming payloads to release resources. + nearby_connections_manager_->ClearIncomingPayloads(); + incoming_share_target_info_map_.erase(share_target.id); + } else { + if (last_outgoing_metadata_ && + last_outgoing_metadata_->first.id == share_target.id) { + last_outgoing_metadata_.reset(); + } + // Find the endpoint id that matches the given share target. + std::optional endpoint_id; + auto it = outgoing_share_target_info_map_.find(share_target.id); + if (it != outgoing_share_target_info_map_.end()) + endpoint_id = it->second.endpoint_id(); + + if (endpoint_id.has_value()) { + RemoveOutgoingShareTargetWithEndpointId(*endpoint_id); + mutual_acceptance_timeout_alarm_->Stop(); + return; + } + + // Be careful not to clear out the share target info map if a new session + // was started during the cancellation delay. + if (!is_scanning_ && !is_transferring_) { + ClearOutgoingShareTargetInfoMap(); + } + + NL_VLOG(1) << __func__ << ": Unregister share target: " << share_target.id; + } + mutual_acceptance_timeout_alarm_->Stop(); +} + +void NearbySharingServiceImpl::OnStartAdvertisingResult(bool used_device_name, + Status status) { + if (status == Status::kSuccess) { + NL_VLOG(1) << __func__ + << ": StartAdvertising over Nearby Connections was successful."; + SetInHighVisibility(used_device_name); + } else { + NL_LOG(ERROR) << __func__ + << ": StartAdvertising over Nearby Connections failed: " + << NearbyConnectionsManager::ConnectionsStatusToString( + status); + SetInHighVisibility(false); + for (auto& observer : observers_.GetObservers()) { + observer->OnStartAdvertisingFailure(); + } + } +} + +void NearbySharingServiceImpl::OnStopAdvertisingResult(Status status) { + if (status == Status::kSuccess) { + NL_VLOG(1) << __func__ + << ": StopAdvertising over Nearby Connections was successful."; + } else { + NL_LOG(ERROR) << __func__ + << ": StopAdvertising over Nearby Connections failed: " + << NearbyConnectionsManager::ConnectionsStatusToString( + status); + } + + // The |advertising_power_level_| is set in |StopAdvertising| instead of + // here at the callback because when restarting advertising, + // |StartAdvertising| is called immediately after |StopAdvertising| without + // waiting for the callback. Nearby Connections queues the requests and + // completes them in order, so waiting for Stop to complete is unnecessary, + // but Start will fail if the |advertising_power_level_| indicates we are + // already advertising. + SetInHighVisibility(false); +} + +void NearbySharingServiceImpl::OnStartDiscoveryResult(Status status) { + bool success = status == Status::kSuccess; + if (success) { + NL_VLOG(1) << __func__ + << ": StartDiscovery over Nearby Connections was successful."; + + // Periodically download certificates if there are discovered, contact-based + // advertisements that cannot decrypt any currently stored certificates. + ScheduleCertificateDownloadDuringDiscovery(/*attempt_count=*/0); + } else { + NL_LOG(ERROR) << __func__ + << ": StartDiscovery over Nearby Connections failed: " + << NearbyConnectionsManager::ConnectionsStatusToString( + status); + } + for (auto& observer : observers_.GetObservers()) { + observer->OnStartDiscoveryResult(success); + } +} + +void NearbySharingServiceImpl::SetInHighVisibility( + bool new_in_high_visibility) { + if (in_high_visibility_ == new_in_high_visibility) { + return; + } + + in_high_visibility_ = new_in_high_visibility; + for (auto& observer : observers_.GetObservers()) { + observer->OnHighVisibilityChanged(in_high_visibility_); + } +} + +void NearbySharingServiceImpl::AbortAndCloseConnectionIfNecessary( + TransferMetadata::Status status, const ShareTarget& share_target) { + RunOnNearbySharingServiceThread( + "abort_and_close_connection_if_necessary", [&, status, share_target]() { + TransferMetadata metadata = + TransferMetadataBuilder().set_status(status).build(); + ShareTargetInfo* info = GetShareTargetInfo(share_target); + + if (info == nullptr) { + NL_LOG(WARNING) << ": Share target " << share_target.id << " lost"; + return; + } + + // First invoke the appropriate transfer callback with the final + // |status|. + if (info && info->transfer_update_callback()) { + info->transfer_update_callback()->OnTransferUpdate(share_target, + metadata); + } else if (share_target.is_incoming) { + OnIncomingTransferUpdate(share_target, metadata); + } else { + OnOutgoingTransferUpdate(share_target, metadata); + } + + // Close connection if necessary. + if (info && info->connection()) { + // Ensure that the disconnect listener is set to UnregisterShareTarget + // because the other listeners also try to record a final status + // metric. + info->connection()->SetDisconnectionListener([&, share_target]() { + RunOnNearbySharingServiceThread( + "disconnection_listener", + [&, share_target]() { UnregisterShareTarget(share_target); }); + }); + + info->connection()->Close(); + } + }); +} + +void NearbySharingServiceImpl::OnNetworkChanged( + nearby::ConnectivityManager::ConnectionType type) { + on_network_changed_delay_timer_->Stop(); + on_network_changed_delay_timer_->Start( + absl::ToInt64Milliseconds(kProcessNetworkChangeTimerDelay), 0, [&]() { + RunOnNearbySharingServiceThread("on-network-changed", [&]() { + StopAdvertisingAndInvalidateSurfaceState(); + }); + }); +} + +void NearbySharingServiceImpl::OnLanConnectedChanged(bool connected) { + RunOnNearbySharingServiceThread("lan_connection_changed", [&, connected]() { + NL_VLOG(1) << __func__ + << ": LAN Connection state changed. (Connected: " << connected + << ")"; + for (auto& observer : observers_.GetObservers()) { + observer->OnLanStatusChanged(); + } + }); +} + +void NearbySharingServiceImpl::ResetAllSettings(bool logout) { + NL_LOG(INFO) << __func__ << ": Reset all settings!"; + + // Stop all services. + StopAdvertising(); + StopScanning(); + nearby_connections_manager_->Shutdown(); + local_device_data_manager_->Stop(); + contact_manager_->Stop(); + certificate_manager_->Stop(); + + // Reset preferences for logout. + if (logout) { + settings_->RemoveSettingsObserver(this); + // Visibility has a impact on the UI. So let's set this one first + // so that the UI can be as responsive as possible. + DeviceVisibility visibility = settings_->GetVisibility(); + bool is_temporarily_visible = settings_->GetIsTemporarilyVisible(); + // When logged out the visibility can either be "everyone" or "hidden". If + // the visibility wasn't already "always everyone", change it to "hidden". + if (visibility == DeviceVisibility::DEVICE_VISIBILITY_EVERYONE && + !is_temporarily_visible) { + settings_->SetIsReceiving(true); + } else { + settings_->SetIsReceiving(false); + settings_->SetVisibility(DeviceVisibility::DEVICE_VISIBILITY_HIDDEN); + } + + // There is no valid fallback visibility when logged out until we support + // "off" as an option. + settings_->SetFallbackVisibility( + DeviceVisibility::DEVICE_VISIBILITY_EVERYONE); + prefs::RegisterNearbySharingPrefs(preference_manager_, + /*skip_persistent_ones=*/true); + + settings_->AddSettingsObserver(this); + certificate_manager_->ClearPublicCertificates([&](bool result) { + NL_LOG(INFO) << "Clear public certificates. result: " << result; + }); + } else { + // should clear scheduled task to make it works immediately + settings_->RemoveSettingsObserver(this); + prefs::ResetSchedulers(preference_manager_); + settings_->AddSettingsObserver(this); + settings_->OnLocalDeviceDataChanged(/*did_device_name_change=*/true, + /*did_full_name_change=*/false, + /*did_icon_url_change=*/false); + // Set default visibility to kAllContacts if logged-in and onboarding. + if (!settings_->IsOnboardingComplete()) { + NL_LOG(INFO) << __func__ + << ": Set visibility to kAllContacts since user is " + "logged-in during onboarding"; + settings_->SetVisibility( + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + } + } + + // Start services again. + local_device_data_manager_->Start(); + contact_manager_->Start(); + certificate_manager_->Start(); + + InvalidateSurfaceState(); +} + +bool NearbySharingServiceImpl::ShouldSelfShareAutoAccept( + const ShareTarget& share_target) const { + // Auto-accept self shares when not in high-visibility mode. + if (NearbyFlags::GetInstance().GetBoolFlag( + config_package_nearby::nearby_sharing_feature::kEnableSelfShare) && + share_target.for_self_share) { + return true; + } + + return false; +} + +bool NearbySharingServiceImpl::ReadyToAccept( + const ShareTarget& share_target, TransferMetadata::Status status) const { + if (status == TransferMetadata::Status::kAwaitingLocalConfirmation) { + return true; + } + + if (ShouldSelfShareAutoAccept(share_target) && + status == TransferMetadata::Status::kUnknown) { + return true; + } + + return false; +} + +void NearbySharingServiceImpl::RunOnNearbySharingServiceThread( + absl::string_view task_name, std::function task) { + if (is_shutting_down_ == nullptr || *is_shutting_down_) { + NL_LOG(WARNING) << __func__ << ": Skip the task " << task_name + << " due to service is shutting down."; + return; + } + + NL_LOG(INFO) << __func__ << ": Scheduled to run task " << task_name + << " on API thread."; + + service_thread_->PostTask( + [&, is_shutting_down = std::weak_ptr(is_shutting_down_), + task_name = std::string(task_name), task = std::move(task)]() { + std::shared_ptr is_shutting = is_shutting_down.lock(); + if (is_shutting == nullptr || *is_shutting) { + NL_LOG(WARNING) << __func__ << ": Give up the task " << task_name + << " due to service is shutting down."; + return; + } + + NL_LOG(INFO) << __func__ << ": Started to run task " << task_name + << " on API thread."; + task(); + + NL_LOG(INFO) << __func__ << ": Completed to run task " << task_name + << " on API thread."; + }); +} + +void NearbySharingServiceImpl::RunOnNearbySharingServiceThreadDelayed( + absl::string_view task_name, absl::Duration delay, + std::function task) { + if (is_shutting_down_ == nullptr || *is_shutting_down_) { + NL_LOG(WARNING) << __func__ << ": Skip the delayed task " << task_name + << " due to service is shutting down."; + return; + } + + NL_LOG(INFO) << __func__ << ": Scheduled to run delayed task " << task_name + << " on API thread."; + service_thread_->PostDelayedTask( + delay, [&, is_shutting_down = std::weak_ptr(is_shutting_down_), + task_name = std::string(task_name), task = std::move(task)]() { + std::shared_ptr is_shutting = is_shutting_down.lock(); + if (is_shutting == nullptr || *is_shutting) { + NL_LOG(WARNING) << __func__ << ": Give up the delayed task " + << task_name << " due to service is shutting down."; + return; + } + + NL_LOG(INFO) << __func__ << ": Started to run delayed task " + << task_name << " on API thread."; + task(); + + NL_LOG(INFO) << __func__ << ": Completed to run delayed task " + << task_name << " on API thread."; + }); +} + +void NearbySharingServiceImpl::RunOnAnyThread(absl::string_view task_name, + std::function task) { + if (is_shutting_down_ == nullptr || *is_shutting_down_) { + NL_LOG(WARNING) << __func__ << ": Skip the task " << task_name + << " due to service is shutting down."; + return; + } + + NL_LOG(INFO) << __func__ << ": Scheduled to run task " << task_name + << " on API thread."; + context_->GetTaskRunner()->PostTask( + [&, is_shutting_down = std::weak_ptr(is_shutting_down_), + task_name = std::string(task_name), task = std::move(task)]() { + std::shared_ptr is_shutting = is_shutting_down.lock(); + if (is_shutting == nullptr || *is_shutting) { + NL_LOG(WARNING) << __func__ << ": Give up the delayed task " + << task_name << " due to service is shutting down."; + return; + } + + NL_LOG(INFO) << __func__ << ": Started to run task " << task_name + << " on API thread."; + task(); + + NL_LOG(INFO) << __func__ << ": Completed to run task " << task_name + << " on API thread."; + }); +} + +int NearbySharingServiceImpl::GetConnectedShareTargetPos( + const ShareTarget& target) { + // Returns 1 before group sharing is enabled. + return 1; +} + +int NearbySharingServiceImpl::GetConnectedShareTargetCount() { + // Returns 1 before group sharing is enabled. + return 1; +} + +::location::nearby::proto::sharing::SharingUseCase +NearbySharingServiceImpl::GetSenderUseCase() { + // Returns unknown before group sharing is enabled. + return ::location::nearby::proto::sharing::SharingUseCase::USE_CASE_UNKNOWN; +} + +TransportType NearbySharingServiceImpl::GetTransportType( + const ShareTarget& share_target) const { + if (share_target.GetTotalAttachmentsSize() > + kAttachmentsSizeThresholdOverHighQualityMedium) { + NL_LOG(INFO) << __func__ << ": Transport type is kHighQuality"; + return TransportType::kHighQuality; + } + + if (share_target.file_attachments.empty()) { + NL_LOG(INFO) << __func__ << ": Transport type is kNonDisruptive"; + return TransportType::kNonDisruptive; + } + + NL_LOG(INFO) << __func__ << ": Transport type is kAny"; + return TransportType::kAny; +} + +void NearbySharingServiceImpl::UpdateFilePathsInProgress( + bool update_file_paths) { + update_file_paths_in_progress_ = update_file_paths; + NL_LOG(INFO) << __func__ + << ": Update file paths in progress: " << update_file_paths; +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/nearby_sharing_service_impl.h b/sharing/nearby_sharing_service_impl.h new file mode 100644 index 00000000..18abba9a --- /dev/null +++ b/sharing/nearby_sharing_service_impl.h @@ -0,0 +1,684 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_NEARBY_SHARING_SERVICE_IMPL_H_ +#define THIRD_PARTY_NEARBY_SHARING_NEARBY_SHARING_SERVICE_IMPL_H_ + +#include +#include + +#include // NOLINT(build/c++17) +#include +#include +#include +#include +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" +#include "absl/strings/string_view.h" +#include "absl/time/time.h" +#include "absl/types/span.h" +#include "internal/analytics/event_logger.h" +#include "internal/base/observer_list.h" +#include "internal/network/http_client_factory.h" +#include "internal/network/url.h" +#include "internal/platform/device_info.h" +#include "internal/platform/implementation/account_manager.h" +#include "internal/platform/task_runner.h" +#include "internal/platform/timer.h" +#include "proto/sharing_enums.pb.h" +#include "sharing/advertisement.h" +#include "sharing/analytics/analytics_recorder.h" +#include "sharing/attachment.h" +#include "sharing/attachment_info.h" +#include "sharing/certificates/nearby_share_certificate_manager.h" +#include "sharing/certificates/nearby_share_decrypted_public_certificate.h" +#include "sharing/certificates/nearby_share_private_certificate.h" +#include "sharing/client/nearby_share_client.h" +#include "sharing/client/nearby_share_client_impl.h" +#include "sharing/client/nearby_share_http_notifier.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/common/nearby_share_profile_info_provider.h" +#include "sharing/fast_initiation/nearby_fast_initiation.h" +#include "sharing/incoming_share_target_info.h" +#include "sharing/internal/api/bluetooth_adapter.h" +#include "sharing/internal/api/preference_manager.h" +#include "sharing/internal/api/sharing_platform.h" +#include "sharing/internal/api/wifi_adapter.h" +#include "sharing/internal/public/connectivity_manager.h" +#include "sharing/internal/public/context.h" +#include "sharing/local_device_data/nearby_share_local_device_data_manager.h" +#include "sharing/nearby_connection.h" +#include "sharing/nearby_connections_manager.h" +#include "sharing/nearby_connections_types.h" +#include "sharing/nearby_file_handler.h" +#include "sharing/nearby_sharing_decoder.h" +#include "sharing/nearby_sharing_service.h" +#include "sharing/nearby_sharing_service_extension.h" +#include "sharing/nearby_sharing_settings.h" +#include "sharing/outgoing_share_target_info.h" +#include "sharing/paired_key_verification_runner.h" +#include "sharing/proto/enums.pb.h" +#include "sharing/proto/wire_format.pb.h" +#include "sharing/share_target.h" +#include "sharing/share_target_discovered_callback.h" +#include "sharing/share_target_info.h" +#include "sharing/text_attachment.h" +#include "sharing/transfer_metadata.h" +#include "sharing/transfer_update_callback.h" +#include "sharing/wifi_credentials_attachment.h" + +namespace nearby { +namespace sharing { + +class NearbyShareContactManager; + +namespace NearbySharingServiceUnitTests { +class NearbySharingServiceImplTest_CreateShareTarget_Test; +}; + +// All methods should be called from the same sequence that created the service. +class NearbySharingServiceImpl + : public NearbySharingService, + public NearbyShareSettings::Observer, + public NearbyShareCertificateManager::Observer, + public ::nearby::AccountManager::Observer, + public NearbyFastInitiation::Observer, + public sharing::api::BluetoothAdapter::Observer, + public sharing::api::WifiAdapter::Observer, + public NearbyConnectionsManager::IncomingConnectionListener, + public NearbyConnectionsManager::DiscoveryListener { + FRIEND_TEST(NearbySharingServiceUnitTests::NearbySharingServiceImplTest, + CreateShareTarget); + + public: + NearbySharingServiceImpl( + Context* context, nearby::sharing::api::SharingPlatform& sharing_platform, + NearbySharingDecoder* decoder, + nearby::network::HttpClientFactory* http_client_factory, + std::unique_ptr nearby_connections_manager, + nearby::analytics::EventLogger* event_logger = nullptr); + ~NearbySharingServiceImpl() override; + + // NearbySharingService + void AddObserver(NearbySharingService::Observer* observer) override; + void RemoveObserver(NearbySharingService::Observer* observer) override; + bool HasObserver(NearbySharingService::Observer* observer) override; + void Shutdown( + std::function status_codes_callback) override; + void RegisterSendSurface( + TransferUpdateCallback* transfer_callback, + ShareTargetDiscoveredCallback* discovery_callback, SendSurfaceState state, + std::function status_codes_callback) override; + void UnregisterSendSurface( + TransferUpdateCallback* transfer_callback, + ShareTargetDiscoveredCallback* discovery_callback, + std::function status_codes_callback) override; + void RegisterReceiveSurface( + TransferUpdateCallback* transfer_callback, ReceiveSurfaceState state, + std::function status_codes_callback) override; + void UnregisterReceiveSurface( + TransferUpdateCallback* transfer_callback, + std::function status_codes_callback) override; + void ClearForegroundReceiveSurfaces( + std::function status_codes_callback) override; + bool IsInHighVisibility() const override; + bool IsTransferring() const override; + bool IsReceivingFile() const override; + bool IsSendingFile() const override; + bool IsScanning() const override; + bool IsConnecting() const override; + bool IsBluetoothPresent() const override; + bool IsBluetoothPowered() const override; + bool IsExtendedAdvertisingSupported() const override; + bool IsLanConnected() const override; + bool IsWifiPresent() const override; + bool IsWifiPowered() const override; + std::string GetQrCodeUrl() const override; + void SendAttachments( + const ShareTarget& share_target, + std::vector> attachments, + std::function status_codes_callback) override; + void Accept(const ShareTarget& share_target, + std::function + status_codes_callback) override; + void Reject(const ShareTarget& share_target, + std::function + status_codes_callback) override; + void Cancel(const ShareTarget& share_target, + std::function + status_codes_callback) override; + bool DidLocalUserCancelTransfer(const ShareTarget& share_target) override; + void Open(const ShareTarget& share_target, + std::function status_codes_callback) + override; + void OpenUrl(const ::nearby::network::Url& url) override; + void CopyText(absl::string_view text) override; + void JoinWifiNetwork(absl::string_view ssid, + absl::string_view password) override; + void SetArcTransferCleanupCallback(std::function callback) override; + NearbyShareSettings* GetSettings() override; + NearbyShareHttpNotifier* GetHttpNotifier() override; + NearbyShareLocalDeviceDataManager* GetLocalDeviceDataManager() override; + NearbyShareContactManager* GetContactManager() override; + NearbyShareCertificateManager* GetCertificateManager() override; + AccountManager* GetAccountManager() override; + + // NearbyConnectionsManager::IncomingConnectionListener: + void OnIncomingConnection(absl::string_view endpoint_id, + absl::Span endpoint_info, + NearbyConnection* connection) override; + + std::string Dump() const override; + + void UpdateFilePathsInProgress(bool update) override; + + private: + // Internal implementation of methods to avoid using recursive mutex. + StatusCodes InternalUnregisterSendSurface( + TransferUpdateCallback* transfer_callback, + ShareTargetDiscoveredCallback* discovery_callback); + StatusCodes InternalUnregisterReceiveSurface( + TransferUpdateCallback* transfer_callback); + + // NearbyShareSettings::Observer: + void OnSettingChanged(absl::string_view key, const Data& data) override; + void OnIsFastInitiationHardwareSupportedChanged(bool is_supported) override; + + void OnEnabledChanged(bool enabled); + void OnFastInitiationNotificationStateChanged( + proto::FastInitiationNotificationState state); + void OnDeviceNameChanged(absl::string_view device_name); + void OnDataUsageChanged(proto::DataUsage data_usage); + void OnCustomSavePathChanged(absl::string_view custom_save_path); + void OnVisibilityChanged(proto::DeviceVisibility visibility); + void OnAllowedContactsChanged(absl::Span allowed_contacts); + void OnIsOnboardingCompleteChanged(bool is_complete); + void OnIsReceivingChanged(bool is_receiving); + + // NearbyShareCertificateManager::Observer: + void OnPublicCertificatesDownloaded() override; + void OnPrivateCertificatesChanged() override; + + // AccountManager::Observer: + void OnLoginSucceeded(absl::string_view account_id) override; + void OnLogoutSucceeded(absl::string_view account_id) override; + + // NearbyConnectionsManager::DiscoveryListener: + void OnEndpointDiscovered(absl::string_view endpoint_id, + absl::Span endpoint_info) override; + void OnEndpointLost(absl::string_view endpoint_id) override; + + // Handle the state changes of screen lock. + void OnLockStateChanged(bool locked); + + // Handle the state changes of bluetooth adapter. + void AdapterPresentChanged(sharing::api::BluetoothAdapter* adapter, + bool present) override; + void AdapterPoweredChanged(sharing::api::BluetoothAdapter* adapter, + bool powered) override; + + // Handle the state changes of Wi-Fi adapter. + void AdapterPresentChanged(sharing::api::WifiAdapter* adapter, + bool present) override; + void AdapterPoweredChanged(sharing::api::WifiAdapter* adapter, + bool powered) override; + + // Handle the hardware error reported that requires PC restart. + void HardwareErrorReported(NearbyFastInitiation* fast_init) override; + + void SetupBluetoothAdapter(); + + ObserverList& GetReceiveCallbacksFromState( + ReceiveSurfaceState state); + bool IsVisibleInBackground(proto::DeviceVisibility visibility); + std::optional> CreateEndpointInfo( + const std::optional& device_name) const; + void StartFastInitiationAdvertising(); + void OnStartFastInitiationAdvertising(); + void OnStartFastInitiationAdvertisingError(); + void StopFastInitiationAdvertising(); + void OnStopFastInitiationAdvertising(); + + // Processes endpoint discovered/lost events. We queue up the events to ensure + // each discovered or lost event is fully handled before the next is run. For + // example, we don't want to start processing an endpoint-lost event before + // the corresponding endpoint-discovered event is finished. This is especially + // important because of the asynchronous steps required to process an + // endpoint-discovered event. + void AddEndpointDiscoveryEvent(std::function event); + void HandleEndpointDiscovered(absl::string_view endpoint_id, + absl::Span endpoint_info); + void HandleEndpointLost(absl::string_view endpoint_id); + void FinishEndpointDiscoveryEvent(); + void OnOutgoingAdvertisementDecoded( + absl::string_view endpoint_id, absl::Span endpoint_info, + std::unique_ptr advertisement); + void OnOutgoingDecryptedCertificate( + absl::string_view endpoint_id, absl::Span endpoint_info, + std::unique_ptr advertisement, + std::optional certificate); + void ScheduleCertificateDownloadDuringDiscovery(size_t attempt_count); + void OnCertificateDownloadDuringDiscoveryTimerFired(size_t attempt_count); + + bool HasAvailableConnectionMediums(); + void InvalidateSurfaceState(); + void InvalidateSendSurfaceState(); + void InvalidateScanningState(); + void InvalidateFastInitiationAdvertising(); + void InvalidateReceiveSurfaceState(); + void InvalidateAdvertisingState(); + void StopAdvertising(); + void StartScanning(); + StatusCodes StopScanning(); + void StopAdvertisingAndInvalidateSurfaceState(); + + void InvalidateFastInitiationScanning(); + void StartFastInitiationScanning(); + void OnFastInitiationDevicesDetected(); + void OnFastInitiationDevicesNotDetected(); + void StopFastInitiationScanning(); + + void ScheduleRotateBackgroundAdvertisementTimer(); + void OnRotateBackgroundAdvertisementTimerFired(); + void RemoveOutgoingShareTargetWithEndpointId(absl::string_view endpoint_id); + + void OnTransferComplete(); + void OnTransferStarted(bool is_incoming); + + void ReceivePayloads( + ShareTarget share_target, + std::function status_codes_callback); + StatusCodes SendPayloads(const ShareTarget& share_target); + void OnUniquePathFetched(int64_t attachment_id, int64_t payload_id, + std::function callback, + std::filesystem::path path); + void OnPayloadPathRegistered(Status status); + void OnPayloadPathsRegistered( + const ShareTarget& share_target, std::unique_ptr aggregated_success, + std::function status_codes_callback); + + void OnOutgoingConnection(const ShareTarget& share_target, + absl::Time connect_start_time, + NearbyConnection* connection); + void SendIntroduction(const ShareTarget& share_target, + std::optional four_digit_token); + + void CreatePayloads(ShareTarget share_target, + std::function callback); + void OnCreatePayloads(std::vector endpoint_info, + ShareTarget share_target, bool success); + void OnOpenFiles(ShareTarget share_target, + std::function callback, + std::vector files); + std::vector CreateTextPayloads( + const std::vector& attachments); + std::vector CreateWifiCredentialsPayloads( + const std::vector& attachments); + + void WriteResponseFrame( + NearbyConnection& connection, + nearby::sharing::service::proto::ConnectionResponseFrame::Status + response_status); + void WriteCancelFrame(NearbyConnection& connection); + void WriteProgressUpdateFrame(NearbyConnection& connection, + std::optional start_transfer, + std::optional progress); + void Fail(const ShareTarget& share_target, TransferMetadata::Status status); + void OnIncomingAdvertisementDecoded( + absl::string_view endpoint_id, ShareTarget placeholder_share_target, + std::unique_ptr advertisement); + void OnIncomingTransferUpdate(const ShareTarget& share_target, + const TransferMetadata& metadata); + void OnOutgoingTransferUpdate(const ShareTarget& share_target, + const TransferMetadata& metadata); + void CloseConnection(const ShareTarget& share_target); + void OnIncomingDecryptedCertificate( + absl::string_view endpoint_id, + std::unique_ptr advertisement, + ShareTarget placeholder_share_target, + std::optional certificate); + void RunPairedKeyVerification( + const ShareTarget& share_target, absl::string_view endpoint_id, + std::function< + void(PairedKeyVerificationRunner::PairedKeyVerificationResult, + ::location::nearby::proto::sharing::OSType)> + callback); + void OnIncomingConnectionKeyVerificationDone( + ShareTarget share_target, std::optional four_digit_token, + PairedKeyVerificationRunner::PairedKeyVerificationResult result, + ::location::nearby::proto::sharing::OSType share_target_os_type); + void OnOutgoingConnectionKeyVerificationDone( + const ShareTarget& share_target, + std::optional four_digit_token, + PairedKeyVerificationRunner::PairedKeyVerificationResult result, + ::location::nearby::proto::sharing::OSType share_target_os_type); + void RefreshUIOnDisconnection(ShareTarget share_target); + void ReceiveIntroduction(ShareTarget share_target, + std::optional four_digit_token); + void OnReceivedIntroduction( + ShareTarget share_target, std::optional four_digit_token, + std::optional frame); + void ReceiveConnectionResponse(ShareTarget share_target); + void OnReceiveConnectionResponse( + ShareTarget share_target, + std::optional frame); + void OnStorageCheckCompleted(ShareTarget share_target, + std::optional four_digit_token, + bool is_out_of_storage); + void OnFrameRead( + ShareTarget share_target, + std::optional frame); + void HandleCertificateInfoFrame( + const nearby::sharing::service::proto::CertificateInfoFrame& + certificate_frame); + void HandleProgressUpdateFrame( + const ShareTarget& share_target, + const nearby::sharing::service::proto::ProgressUpdateFrame& + progress_update_frame); + + void OnIncomingConnectionDisconnected(const ShareTarget& share_target); + void OnOutgoingConnectionDisconnected(const ShareTarget& share_target); + + void OnIncomingMutualAcceptanceTimeout(const ShareTarget& share_target); + void OnOutgoingMutualAcceptanceTimeout(const ShareTarget& share_target); + + void Cleanup(); + + std::optional CreateShareTarget( + absl::string_view endpoint_id, + std::unique_ptr advertisement, + std::optional certificate, + bool is_incoming); + + void OnPayloadTransferUpdate(ShareTarget share_target, + TransferMetadata metadata); + bool OnIncomingPayloadsComplete(ShareTarget& share_target); + void RemoveIncomingPayloads(ShareTarget share_target); + void Disconnect(const ShareTarget& share_target, TransferMetadata metadata); + void OnDisconnectingConnectionTimeout(absl::string_view endpoint_id); + void OnDisconnectingConnectionDisconnected(const ShareTarget& share_target, + absl::string_view endpoint_id); + + ShareTargetInfo& GetOrCreateShareTargetInfo(const ShareTarget& share_target, + absl::string_view endpoint_id); + + ShareTargetInfo* GetShareTargetInfo(const ShareTarget& share_target); + IncomingShareTargetInfo* GetIncomingShareTargetInfo( + const ShareTarget& share_target); + OutgoingShareTargetInfo* GetOutgoingShareTargetInfo( + const ShareTarget& share_target); + + NearbyConnection* GetConnection(const ShareTarget& share_target); + std::optional> GetBluetoothMacAddressForShareTarget( + const ShareTarget& share_target); + + void ClearOutgoingShareTargetInfoMap(); + void SetAttachmentPayloadId(const Attachment& attachment, int64_t payload_id); + std::optional GetAttachmentPayloadId(int64_t attachment_id); + void UnregisterShareTarget(const ShareTarget& share_target); + + void OnStartAdvertisingResult(bool used_device_name, Status status); + void OnStopAdvertisingResult(Status status); + void OnStartDiscoveryResult(Status status); + void SetInHighVisibility(bool in_high_visibility); + + // Note: |share_target| is intentionally passed by value. A share target + // reference could likely be invalidated by the owner during the multistep + // cancellation process. + void DoCancel( + ShareTarget share_target, + std::function status_codes_callback, + bool is_initiator_of_cancellation); + + void AbortAndCloseConnectionIfNecessary(TransferMetadata::Status status, + const ShareTarget& share_target); + + // Monitor connectivity changes. + void OnNetworkChanged(nearby::ConnectivityManager::ConnectionType type); + void OnLanConnectedChanged(bool connected); + + // Resets all settings of the nearby sharing service. + // Resets user preferences to a valid logged out state when |logout| is true. + // This will clear all preferences, but preserve onboarding state and revert + // visibility to a state that is valid when logged out. For example: + // `contacts` -> `off`. + void ResetAllSettings(bool logout); + + // Checks whether SDK should auto-accept remote attachments. + bool ShouldSelfShareAutoAccept(const ShareTarget& share_target) const; + + // Checks whether we should accept transfer. + bool ReadyToAccept(const ShareTarget& share_target, + TransferMetadata::Status status) const; + + // Runs API/task on the service thread to avoid UI block. + void RunOnNearbySharingServiceThread(absl::string_view task_name, + std::function task); + + // Runs API/task on the service thread with delayed time. + void RunOnNearbySharingServiceThreadDelayed(absl::string_view task_name, + absl::Duration delay, + std::function task); + + // Runs API/task on a random thread. + void RunOnAnyThread(absl::string_view task_name, std::function task); + + // Returns a 1-based position.It is used by group share feature. + int GetConnectedShareTargetPos(const ShareTarget& target); + + // Returns the share target count. It is used by group share feature. + int GetConnectedShareTargetCount(); + + // Returns use case of sender. It is used by group share feature. + ::location::nearby::proto::sharing::SharingUseCase GetSenderUseCase(); + + // Calculates transport type on share target. + TransportType GetTransportType(const ShareTarget& share_target) const; + + // Update file path for the file attachment. + void UpdateFilePath(ShareTarget& share_target); + + Context* const context_; + nearby::DeviceInfo& device_info_; + nearby::sharing::api::PreferenceManager& preference_manager_; + AccountManager& account_manager_; + NearbySharingDecoder* const decoder_; + + std::unique_ptr nearby_connections_manager_; + // Scanner which is non-null when we are performing a background scan for + // remote devices that are attempting to share. + NearbyShareHttpNotifier nearby_share_http_notifier_; + std::unique_ptr nearby_share_client_factory_; + std::unique_ptr profile_info_provider_; + std::unique_ptr local_device_data_manager_; + std::unique_ptr contact_manager_; + std::unique_ptr certificate_manager_; + std::unique_ptr nearby_fast_initiation_; + + // Used to create analytics events. + std::unique_ptr analytics_recorder_; + + // Used to maintain the settings of nearby sharing. + std::unique_ptr settings_; + + // Accesses the extension methods to Nearby Sharing service. + std::unique_ptr service_extension_; + NearbyFileHandler file_handler_; + bool is_screen_locked_ = false; + std::unique_ptr rotate_background_advertisement_timer_; + std::unique_ptr certificate_download_during_discovery_timer_; + std::unique_ptr process_shutdown_pending_timer_; + + // A list of service observers. + ObserverList observers_; + // A list of foreground receivers. + ObserverList foreground_receive_callbacks_; + // A list of background receivers. + ObserverList background_receive_callbacks_; + // A list of foreground receivers for transfer updates on the send surface. + ObserverList foreground_send_transfer_callbacks_; + // A list of foreground receivers for discovered device updates on the send + // surface. + ObserverList + foreground_send_discovery_callbacks_; + // A list of background receivers for transfer updates on the send surface. + ObserverList background_send_transfer_callbacks_; + // A list of background receivers for discovered device updates on the send + // surface. + ObserverList + background_send_discovery_callbacks_; + + // Registers the most recent TransferMetadata and ShareTarget used for + // transitioning notifications between foreground surfaces and background + // surfaces. Empty if no metadata is available. + std::optional> + last_incoming_metadata_; + // The most recent outgoing TransferMetadata and ShareTarget. + std::optional> + last_outgoing_metadata_; + // A map of ShareTarget id to IncomingShareTargetInfo. This lets us know which + // Nearby Connections endpoint and public certificate are related to the + // incoming share target. + absl::flat_hash_map + incoming_share_target_info_map_; + // A map of endpoint id to ShareTarget, where each ShareTarget entry + // directly corresponds to a OutgoingShareTargetInfo entry in + // outgoing_share_target_info_map_; + absl::flat_hash_map outgoing_share_target_map_; + // A map of ShareTarget id to OutgoingShareTargetInfo. This lets us know which + // endpoint and public certificate are related to the outgoing share target. + absl::flat_hash_map + outgoing_share_target_info_map_; + // For metrics. The IDs of ShareTargets that are cancelled while trying to + // establish an outgoing connection. + absl::flat_hash_set all_cancelled_share_target_ids_; + // The IDs of ShareTargets that we cancelled the transfer to. + absl::flat_hash_set locally_cancelled_share_target_ids_; + // A map from endpoint ID to endpoint info from discovered, contact-based + // advertisements that could not decrypt any available public certificates. + // During discovery, if certificates are downloaded, we revisit this map and + // retry certificate decryption. + absl::flat_hash_map> + discovered_advertisements_to_retry_map_; + // If the discovered advertisements are retried when public certificates + // downloaded, we put it in to the retry set. The retried endpoints will not + // cause new download of public certificates. The purpose is to reduce the + // unnecessary backend API call. + absl::flat_hash_set discovered_advertisements_retried_set_; + + // A mapping of Attachment ID to additional AttachmentInfo related to the + // Attachment. + absl::flat_hash_map attachment_info_map_; + + // This alarm is used to disconnect the sharing connection if both sides do + // not press accept within the timeout. + std::unique_ptr mutual_acceptance_timeout_alarm_; + + // A map of ShareTarget id to disconnection timeout callback. Used to only + // disconnect after a timeout to keep sending any pending payloads. + absl::flat_hash_map> + disconnection_timeout_alarms_; + + // The current advertising power level. PowerLevel::kUnknown while not + // advertising. + PowerLevel advertising_power_level_ = PowerLevel::kUnknown; + // True if we are currently scanning for remote devices. + bool is_scanning_ = false; + // True if we're currently sending or receiving a file. + bool is_transferring_ = false; + // True if we're currently receiving a file. + bool is_receiving_files_ = false; + // True if we're currently sending a file. + bool is_sending_files_ = false; + // True if we're currently attempting to connect to a remote device. + bool is_connecting_ = false; + // The time scanning began. + absl::Time scanning_start_timestamp_; + // True when we are advertising with a device name visible to everyone. + bool in_high_visibility_ = false; + // The time attachments are sent after a share target is selected. This is + // used to time the process from selecting a share target to writing the + // introduction frame (last frame before receiver gets notified). + absl::Time send_attachments_timestamp_; + // Whether an incoming share has been accepted, and we are waiting to log the + // time from acceptance to the start of payload transfer. + bool is_waiting_to_record_accept_to_transfer_start_metric_ = false; + // Time at which an incoming transfer was accepted. This is used to calculate + // the time between an incoming share being accepted and the first payload + // byte being processed. + absl::Time incoming_share_accepted_timestamp_; + std::unique_ptr clear_recent_nearby_process_shutdown_count_timer_; + + // Used to debounce OnNetworkChanged processing. + std::unique_ptr on_network_changed_delay_timer_; + + // Used to prevent the "Device nearby is sharing" notification from appearing + // immediately after a completed share. + std::unique_ptr fast_initiation_scanner_cooldown_timer_; + + // A queue of endpoint-discovered and endpoint-lost events that ensures the + // events are processed sequentially, in the order received from Nearby + // Connections. An event is processed either immediately, if there are no + // other events in the queue, or as soon as the previous event processing + // finishes. When processing finishes, the event is removed from the queue. + std::queue> endpoint_discovery_events_; + + // Called when cleanup for ARC is needed as part of the transfer. + std::function arc_transfer_cleanup_callback_; + + // Used to run nearby sharing service APIs. + std::unique_ptr service_thread_ = nullptr; + + // Shouldn't schedule new task after shutting down, and skip task if the + // object is null. + std::shared_ptr is_shutting_down_ = nullptr; + + // Tracks the path registration. + struct PathRegistrationStatus { + ShareTarget share_target; + uint32_t expected_count; + uint32_t current_count; + std::function status_codes_callback; + bool status; + }; + + PathRegistrationStatus path_registration_status_; + + // Used to identify current scanning session. + int64_t scanning_session_id_ = 0; + + // Used to identify current advertising session. + int64_t advertising_session_id_ = 0; + + // Used to identify current receiving session. + int64_t receiving_session_id_ = 0; + + // Used to track the time of screen unlock. + absl::Time screen_unlock_time_; + + // Whether to update the file paths in transfer progress. + bool update_file_paths_in_progress_ = false; + + // Used to track the time when share sheet activity starts + absl::Time share_foreground_send_surface_start_timestamp_; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_NEARBY_SHARING_SERVICE_IMPL_H_ diff --git a/sharing/nearby_sharing_service_impl_test.cc b/sharing/nearby_sharing_service_impl_test.cc new file mode 100644 index 00000000..0fe95d61 --- /dev/null +++ b/sharing/nearby_sharing_service_impl_test.cc @@ -0,0 +1,4755 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/nearby_sharing_service_impl.h" + +#include +#include + +#include +#include +#include // NOLINT(build/c++17) +#include +#include +#include +#include +#include +#include +#include + +#include "base/casts.h" +#include "gmock/gmock.h" +#include "protobuf-matchers/protocol-buffer-matchers.h" +#include "gtest/gtest.h" +#include "absl/memory/memory.h" +#include "absl/status/status.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "absl/synchronization/notification.h" +#include "absl/time/clock.h" +#include "absl/time/time.h" +#include "absl/types/span.h" +#include "internal/account/account_manager_impl.h" +#include "internal/flags/nearby_flags.h" +#include "internal/network/http_client_factory.h" +#include "internal/test/fake_account_manager.h" +#include "internal/test/fake_device_info.h" +#include "internal/test/fake_http_client_factory.h" +#include "internal/test/fake_task_runner.h" +#include "sharing/advertisement.h" +#include "sharing/attachment.h" +#include "sharing/certificates/fake_nearby_share_certificate_manager.h" +#include "sharing/certificates/nearby_share_certificate_manager_impl.h" +#include "sharing/certificates/nearby_share_decrypted_public_certificate.h" +#include "sharing/certificates/nearby_share_encrypted_metadata_key.h" +#include "sharing/certificates/test_util.h" +#include "sharing/common/compatible_u8_string.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/common/nearby_share_prefs.h" +#include "sharing/constants.h" +#include "sharing/contacts/fake_nearby_share_contact_manager.h" +#include "sharing/contacts/nearby_share_contact_manager_impl.h" +#include "sharing/fake_nearby_connection.h" +#include "sharing/fake_nearby_connections_manager.h" +#include "sharing/fast_initiation/fake_nearby_fast_initiation.h" +#include "sharing/fast_initiation/nearby_fast_initiation_impl.h" +#include "sharing/file_attachment.h" +#include "sharing/flags/nearby_sharing_feature_flags.h" +#include "sharing/internal/api/mock_sharing_platform.h" +#include "sharing/internal/api/preference_manager.h" +#include "sharing/internal/public/connectivity_manager.h" +#include "sharing/internal/test/fake_bluetooth_adapter.h" +#include "sharing/internal/test/fake_connectivity_manager.h" +#include "sharing/internal/test/fake_context.h" +#include "sharing/internal/test/fake_preference_manager.h" +#include "sharing/local_device_data/fake_nearby_share_local_device_data_manager.h" +#include "sharing/local_device_data/nearby_share_local_device_data_manager_impl.h" +#include "sharing/nearby_connections_manager.h" +#include "sharing/nearby_connections_types.h" +#include "sharing/nearby_sharing_decoder.h" +#include "sharing/nearby_sharing_service.h" +#include "sharing/nearby_sharing_settings.h" +#include "sharing/proto/enums.pb.h" +#include "sharing/proto/rpc_resources.pb.h" +#include "sharing/proto/wire_format.pb.h" +#include "sharing/share_target.h" +#include "sharing/share_target_discovered_callback.h" +#include "sharing/text_attachment.h" +#include "sharing/transfer_metadata.h" +#include "sharing/transfer_update_callback.h" +#include "google/protobuf/repeated_ptr_field.h" + +namespace nearby { +namespace sharing { +namespace { + +using ConnectionType = ::nearby::ConnectivityManager::ConnectionType; +using SendSurfaceState = + ::nearby::sharing::NearbySharingService::SendSurfaceState; +using ::nearby::sharing::api::PreferenceManager; +using ::nearby::sharing::proto::DataUsage; +using ::nearby::sharing::proto::DeviceVisibility; +using ::nearby::sharing::proto::FastInitiationNotificationState; +using ::nearby::sharing::service::proto::ConnectionResponseFrame; +using ::nearby::sharing::service::proto::FileMetadata; +using ::nearby::sharing::service::proto::Frame; +using ::nearby::sharing::service::proto::IntroductionFrame; +using ::nearby::sharing::service::proto::PairedKeyResultFrame; +using ::nearby::sharing::service::proto::TextMetadata; +using ::nearby::sharing::service::proto::V1Frame; +using ::testing::InSequence; +using ::testing::NiceMock; +using ::testing::ReturnRef; + +class MockTransferUpdateCallback : public TransferUpdateCallback { + public: + ~MockTransferUpdateCallback() override = default; + + MOCK_METHOD(void, OnTransferUpdate, + (const ShareTarget& shareTarget, + const TransferMetadata& transferMetadata), + (override)); +}; + +class MockShareTargetDiscoveredCallback : public ShareTargetDiscoveredCallback { + public: + ~MockShareTargetDiscoveredCallback() override = default; + + MOCK_METHOD(void, OnShareTargetDiscovered, (const ShareTarget& share_target), + (override)); + MOCK_METHOD(void, OnShareTargetLost, (const ShareTarget& share_target), + (override)); +}; + +class MockNearbySharingDecoder : public NearbySharingDecoder { + public: + ~MockNearbySharingDecoder() override = default; + + MOCK_METHOD(std::unique_ptr, DecodeAdvertisement, + (absl::Span data), (override)); + MOCK_METHOD(std::unique_ptr, DecodeFrame, + (absl::Span data), (override)); +}; + +class MockAccountObserver : public ::nearby::AccountManager::Observer { + public: + ~MockAccountObserver() override = default; + + MOCK_METHOD(void, OnLoginSucceeded, (absl::string_view account_id), + (override)); + + MOCK_METHOD(void, OnLogoutSucceeded, (absl::string_view account_id), + (override)); +}; + +} // namespace + +namespace NearbySharingServiceUnitTests { + +constexpr absl::Duration kDelta = absl::Milliseconds(100); + +// Used to wait for absl::Notification to finish. +constexpr absl::Duration kWaitTimeout = absl::Milliseconds(500); +constexpr absl::Duration kTaskWaitTimeout = absl::Seconds(2); + +constexpr char kServiceId[] = "NearbySharing"; +constexpr char kDeviceName[] = "test_device_name"; +constexpr ShareTargetType kDeviceType = ShareTargetType::kPhone; +constexpr char kEndpointId[] = "test_endpoint_id"; +constexpr char kTextPayload[] = "Test text payload"; +constexpr char kFourDigitToken[] = "1953"; +constexpr absl::string_view kTestAccountId = "test_account"; + +constexpr int64_t kFreeDiskSpace = 10000; + +const std::vector& GetValidV1EndpointInfo() { + static std::vector* valid_v1_endpoint_info = + new std::vector({0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 10, 100, 101, + 118, 105, 99, 101, 78, 97, 109, 101}); + return *valid_v1_endpoint_info; +} + +const std::vector& GetToken() { + static std::vector* token = new std::vector({0, 1, 2}); + return *token; +} + +const std::vector& GetPrivateCertificateHashAuthToken() { + static std::vector* private_certificate_hash_auth_token = + new std::vector({0x8b, 0xcb, 0xa2, 0xf8, 0xe4, 0x06}); + return *private_certificate_hash_auth_token; +} + +const std::vector& GetIncomingConnectionSignedData() { + static std::vector* incoming_connection_signed_data = + new std::vector( + {0x30, 0x45, 0x02, 0x20, 0x4f, 0x83, 0x72, 0xbd, 0x02, 0x70, 0xd9, + 0xda, 0x62, 0x83, 0x5d, 0xb2, 0xdc, 0x6e, 0x3f, 0xa6, 0xa8, 0xa1, + 0x4f, 0x5f, 0xd3, 0xe3, 0xd9, 0x1a, 0x5d, 0x2d, 0x61, 0xd2, 0x6c, + 0xdd, 0x8d, 0xa5, 0x02, 0x21, 0x00, 0xd4, 0xe1, 0x1d, 0x14, 0xcb, + 0x58, 0xf7, 0x02, 0xd5, 0xab, 0x48, 0xe2, 0x2f, 0xcb, 0xc0, 0x53, + 0x41, 0x06, 0x50, 0x65, 0x95, 0x19, 0xa9, 0x22, 0x92, 0x00, 0x42, + 0x01, 0x26, 0x25, 0xcb, 0x8c}); + return *incoming_connection_signed_data; +} + +const std::vector& GetOutgoingConnectionSignedData() { + static std::vector* outgoing_connection_signed_data = + new std::vector( + {0x30, 0x45, 0x02, 0x21, 0x00, 0xf9, 0xc9, 0xa8, 0x89, 0x96, 0x6e, + 0x5c, 0xea, 0x0a, 0x60, 0x37, 0x3a, 0x84, 0x7d, 0xf5, 0x31, 0x82, + 0x74, 0xb9, 0xde, 0x3f, 0x64, 0x1b, 0xff, 0x4f, 0x54, 0x31, 0x1f, + 0x9e, 0x63, 0x68, 0xca, 0x02, 0x20, 0x52, 0x43, 0x46, 0xa7, 0x6f, + 0xcb, 0x96, 0x50, 0x86, 0xfd, 0x6f, 0x9f, 0x7e, 0x50, 0xa7, 0xa0, + 0x9b, 0xdf, 0xae, 0x79, 0x42, 0x47, 0xd9, 0x60, 0x71, 0x91, 0x7a, + 0xbb, 0x81, 0x9b, 0x0d, 0x2e}); + return *outgoing_connection_signed_data; +} + +constexpr int kFilePayloadId = 111; +constexpr int kPayloadSize = 1000000; + +const std::vector& GetValidIntroductionFramePayloadIds() { + static std::vector* valid_introduction_frame_payload_ids = + new std::vector({1, 2, 3, kFilePayloadId}); + return *valid_introduction_frame_payload_ids; +} + +constexpr size_t kMaxCertificateDownloadsDuringDiscovery = 3u; +constexpr absl::Duration kCertificateDownloadDuringDiscoveryPeriod = + absl::Seconds(10); + +std::unique_ptr GetFilePayload(int64_t payload_id) { + std::filesystem::path path = + std::filesystem::temp_directory_path() / absl::StrCat(payload_id); + InputFile input_file{path}; + return std::make_unique(input_file); +} + +std::unique_ptr GetTextPayload(int64_t payload_id, + absl::string_view text) { + return std::make_unique( + std::vector(text.begin(), text.end())); +} + +std::unique_ptr GetValidIntroductionFrame() { + IntroductionFrame* introduction_frame = + IntroductionFrame::default_instance().New(); + auto text_metadatas = introduction_frame->mutable_text_metadata(); + introduction_frame->set_start_transfer(true); + + for (int i = 1; i <= 3; ++i) { + nearby::sharing::service::proto::TextMetadata* text_metadata = + nearby::sharing::service::proto::TextMetadata::default_instance().New(); + + text_metadata->set_text_title(absl::StrCat("title ", i)); + text_metadata->set_type( + static_cast(i)); + text_metadata->set_payload_id(i); + text_metadata->set_size(kPayloadSize); + text_metadata->set_id(i); + + text_metadatas->AddAllocated(text_metadata); + } + + auto file_metadatas = introduction_frame->mutable_file_metadata(); + + nearby::sharing::service::proto::FileMetadata* file_metadata = + nearby::sharing::service::proto::FileMetadata::default_instance().New(); + file_metadata->set_name("unit_test_nearby_share_name_\x80"); + file_metadata->set_type(nearby::sharing::service::proto::FileMetadata::VIDEO); + file_metadata->set_payload_id(kFilePayloadId); + file_metadata->set_size(kPayloadSize); + file_metadata->set_mime_type("mime type"); + file_metadata->set_id(100); + file_metadatas->AddAllocated(file_metadata); + + V1Frame* v1_frame = V1Frame::default_instance().New(); + v1_frame->set_type(V1Frame::INTRODUCTION); + v1_frame->set_allocated_introduction(introduction_frame); + + Frame* frame = Frame::default_instance().New(); + frame->set_version(Frame::V1); + frame->set_allocated_v1(std::move(v1_frame)); + return std::unique_ptr(frame); +} + +std::unique_ptr GetEmptyIntroductionFrame() { + V1Frame* v1_frame = V1Frame::default_instance().New(); + v1_frame->set_type(V1Frame::INTRODUCTION); + v1_frame->set_allocated_introduction( + IntroductionFrame::default_instance().New()); + + Frame* frame = Frame::default_instance().New(); + frame->set_version(Frame::V1); + frame->set_allocated_v1(v1_frame); + return std::unique_ptr(frame); +} + +std::unique_ptr GetConnectionResponseFrame( + ConnectionResponseFrame::Status status) { + V1Frame* v1_frame = V1Frame::default_instance().New(); + v1_frame->set_type(V1Frame::RESPONSE); + ConnectionResponseFrame* response_frame = + ConnectionResponseFrame::default_instance().New(); + response_frame->set_status(status); + v1_frame->set_allocated_connection_response(response_frame); + + Frame* frame = Frame::default_instance().New(); + frame->set_version(Frame::V1); + frame->set_allocated_v1(v1_frame); + return std::unique_ptr(frame); +} + +std::unique_ptr GetCancelFrame() { + V1Frame* v1_frame = V1Frame::default_instance().New(); + v1_frame->set_type(V1Frame::CANCEL); + + Frame* frame = Frame::default_instance().New(); + frame->set_version(Frame::V1); + frame->set_allocated_v1(v1_frame); + return std::unique_ptr(frame); +} + +std::vector> CreateTextAttachments( + std::vector texts) { + std::vector> attachments; + for (auto& text : texts) { + attachments.push_back(std::make_unique( + service::proto::TextMetadata::TEXT, std::move(text), + /*text_title=*/std::nullopt, + /*mime_type=*/std::nullopt)); + } + return attachments; +} + +std::vector> CreateFileAttachments( + std::vector file_paths) { + std::vector> attachments; + for (auto& file_path : file_paths) { + attachments.push_back( + std::make_unique(std::move(file_path))); + } + return attachments; +} + +class NearbySharingServiceImplTest : public testing::Test { + public: + NearbySharingServiceImplTest() = default; + ~NearbySharingServiceImplTest() override = default; + + void SetUp() override { + ON_CALL(mock_sharing_platform_, GetDeviceInfo) + .WillByDefault(ReturnRef(fake_device_info_)); + ON_CALL(mock_sharing_platform_, GetPreferenceManager) + .WillByDefault(ReturnRef(preference_manager_)); + ON_CALL(mock_sharing_platform_, GetAccountManager) + .WillByDefault(ReturnRef(fake_account_manager_)); + NearbyShareLocalDeviceDataManagerImpl::Factory::SetFactoryForTesting( + &local_device_data_manager_factory_); + NearbyShareContactManagerImpl::Factory::SetFactoryForTesting( + &contact_manager_factory_); + NearbyShareCertificateManagerImpl::Factory::SetFactoryForTesting( + &certificate_manager_factory_); + AccountManagerImpl::Factory::SetFactoryForTesting([]() { + return std::make_unique(); + }); + nearby_fast_initiation_factory_ = + std::make_unique(); + NearbyFastInitiationImpl::Factory::SetFactoryForTesting( + nearby_fast_initiation_factory_.get()); + + NearbyFlags::GetInstance().OverrideBoolFlagValue( + config_package_nearby::nearby_sharing_feature:: + kEnableBackgroundScanning, + true); + NearbyFlags::GetInstance().OverrideBoolFlagValue( + config_package_nearby::nearby_sharing_feature::kEnableSelfShare, true); + NearbyFlags::GetInstance().OverrideBoolFlagValue( + config_package_nearby::nearby_sharing_feature::kEnableMediumWifiLan, + true); + + prefs::RegisterNearbySharingPrefs(preference_manager_); + SetBluetoothIsPresent(true); + SetBluetoothIsPowered(true); + SetScreenLocked(false); + SetConnectionType(ConnectionType::kWifi); + + service_ = CreateService(); + } + + void TearDown() override { + if (service_) Shutdown(); + + NearbyFlags::GetInstance().OverrideBoolFlagValue( + config_package_nearby::nearby_sharing_feature:: + kEnableBackgroundScanning, + true); + NearbyFlags::GetInstance().OverrideBoolFlagValue( + config_package_nearby::nearby_sharing_feature::kEnableSelfShare, true); + NearbyFlags::GetInstance().OverrideBoolFlagValue( + config_package_nearby::nearby_sharing_feature::kEnableMediumWifiLan, + true); + NearbyShareLocalDeviceDataManagerImpl::Factory::SetFactoryForTesting( + nullptr); + NearbyShareContactManagerImpl::Factory::SetFactoryForTesting(nullptr); + NearbyShareCertificateManagerImpl::Factory::SetFactoryForTesting(nullptr); + AccountManagerImpl::Factory::SetFactoryForTesting(nullptr); + nearby_fast_initiation_factory_.reset(); + } + + void SetConnectionType(ConnectionType type) { + FakeConnectivityManager* connectivity_manager = + down_cast( + fake_context_.GetConnectivityManager()); + connectivity_manager->SetConnectionType(type); + } + + std::unique_ptr CreateService() { + preference_manager_.SetBoolean(prefs::kNearbySharingEnabledName, true); + + fake_nearby_connections_manager_ = new FakeNearbyConnectionsManager(); + auto service = std::make_unique( + &fake_context_, mock_sharing_platform_, &fake_decoder_, + fake_http_client_.get(), + absl::WrapUnique(fake_nearby_connections_manager_)); + + return service; + } + + void SetVisibility(DeviceVisibility visibility) { + service_->GetSettings()->SetVisibility(visibility); + FlushTesting(); + } + + void SetIsEnabled(bool is_enabled) { + if (is_enabled) { + service_->GetSettings()->SetIsOnboardingComplete(is_enabled, []() {}); + } + service_->GetSettings()->SetEnabled(is_enabled); + FlushTesting(); + } + + void SetFastInitiationNotificationState( + FastInitiationNotificationState state) { + service_->GetSettings()->SetFastInitiationNotificationState(state); + FlushTesting(); + } + + void SetBluetoothIsPresent(bool present) { + FakeBluetoothAdapter& bluetooth_adapter = + down_cast(fake_context_.GetBluetoothAdapter()); + bluetooth_adapter.ReceivedAdapterPresentChangedFromOs(present); + FlushTesting(); + } + + void SetBluetoothIsPowered(bool powered) { + FakeBluetoothAdapter& bluetooth_adapter = + down_cast(fake_context_.GetBluetoothAdapter()); + bluetooth_adapter.ReceivedAdapterPoweredChangedFromOs(powered); + FlushTesting(); + } + + void FastForward(absl::Duration duration) { + fake_context_.fake_clock()->FastForward(duration); + FlushTesting(); + } + + void SetScreenLocked(bool locked) { + fake_device_info_.SetScreenLocked(locked); + FlushTesting(); + } + + void DisableSelfshareFeature() { + NearbyFlags::GetInstance().OverrideBoolFlagValue( + config_package_nearby::nearby_sharing_feature::kEnableSelfShare, false); + } + + NearbySharingService::StatusCodes RegisterSendSurface( + TransferUpdateCallback* transfer_callback, + ShareTargetDiscoveredCallback* discovery_callback, + SendSurfaceState state) { + NearbySharingService::StatusCodes result = + NearbySharingService::StatusCodes::kError; + absl::Notification notification; + service_->RegisterSendSurface( + transfer_callback, discovery_callback, state, + [&](NearbySharingService::StatusCodes status_codes) { + result = status_codes; + notification.Notify(); + }); + + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kTaskWaitTimeout)); + return result; + } + + NearbySharingService::StatusCodes UnregisterSendSurface( + TransferUpdateCallback* transfer_callback, + ShareTargetDiscoveredCallback* discovery_callback) { + NearbySharingService::StatusCodes result = + NearbySharingService::StatusCodes::kError; + absl::Notification notification; + service_->UnregisterSendSurface( + transfer_callback, discovery_callback, + [&](NearbySharingService::StatusCodes status_codes) { + result = status_codes; + notification.Notify(); + }); + + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kTaskWaitTimeout)); + return result; + } + + NearbySharingService::StatusCodes RegisterReceiveSurface( + TransferUpdateCallback* transfer_callback, + NearbySharingService::ReceiveSurfaceState state) { + NearbySharingService::StatusCodes result = + NearbySharingService::StatusCodes::kError; + absl::Notification notification; + service_->RegisterReceiveSurface( + transfer_callback, state, + [&](NearbySharingService::StatusCodes status_codes) { + result = status_codes; + notification.Notify(); + }); + + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kTaskWaitTimeout)); + return result; + } + + NearbySharingService::StatusCodes UnregisterReceiveSurface( + TransferUpdateCallback* transfer_callback) { + NearbySharingService::StatusCodes result = + NearbySharingService::StatusCodes::kError; + absl::Notification notification; + service_->UnregisterReceiveSurface( + transfer_callback, [&](NearbySharingService::StatusCodes status_codes) { + result = status_codes; + notification.Notify(); + }); + + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kTaskWaitTimeout)); + return result; + } + + NearbySharingService::StatusCodes SendAttachments( + const ShareTarget& share_target, + std::vector> attachments) { + NearbySharingService::StatusCodes result = + NearbySharingService::StatusCodes::kError; + absl::Notification notification; + service_->SendAttachments( + share_target, std::move(attachments), + [&](NearbySharingService::StatusCodes status_codes) { + result = status_codes; + notification.Notify(); + }); + + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kTaskWaitTimeout)); + return result; + } + + void Shutdown() { + NearbySharingService::StatusCodes result = + NearbySharingService::StatusCodes::kError; + absl::Notification notification; + service_->Shutdown([&](NearbySharingService::StatusCodes status_codes) { + result = status_codes; + notification.Notify(); + }); + + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kTaskWaitTimeout)); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + } + + void SetUpForegroundReceiveSurface( + NiceMock& callback) { + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kForeground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); + } + + void SetUpBackgroundReceiveSurface( + NiceMock& callback) { + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kBackground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); + } + + void ProcessLatestPublicCertificateDecryption(size_t expected_num_calls, + bool success, + bool for_self_share = false) { + // Ensure that all pending mojo messages are processed and the certificate + // manager state is as expected up to this point. + std::vector< + FakeNearbyShareCertificateManager::GetDecryptedPublicCertificateCall>& + calls = certificate_manager()->get_decrypted_public_certificate_calls(); + + ASSERT_FALSE(calls.empty()); + EXPECT_EQ(calls.size(), expected_num_calls); + EXPECT_EQ(GetNearbyShareTestEncryptedMetadataKey().salt(), + calls.back().encrypted_metadata_key.salt()); + EXPECT_EQ(GetNearbyShareTestEncryptedMetadataKey().encrypted_key(), + calls.back().encrypted_metadata_key.encrypted_key()); + + if (success) { + nearby::sharing::proto::PublicCertificate cert = + GetNearbyShareTestPublicCertificate( + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + cert.set_for_self_share(for_self_share); + std::move(calls.back().callback)( + NearbyShareDecryptedPublicCertificate::DecryptPublicCertificate( + cert, GetNearbyShareTestEncryptedMetadataKey())); + } else { + std::move(calls.back().callback)(std::nullopt); + } + FlushTesting(); + } + + void SetUpKeyVerification(bool is_incoming, + PairedKeyResultFrame::Status status) { + SetVisibility(DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + local_device_data_manager()->SetDeviceName(kDeviceName); + + std::string encryption_frame = "test_encryption_frame"; + std::vector encryption_bytes(encryption_frame.begin(), + encryption_frame.end()); + + EXPECT_CALL(fake_decoder_, DecodeFrame(testing::Eq(encryption_bytes))) + .WillOnce(testing::Invoke([=](absl::Span data) { + V1Frame* v1_frame = V1Frame::default_instance().New(); + v1_frame->set_type(V1Frame::PAIRED_KEY_ENCRYPTION); + nearby::sharing::service::proto::PairedKeyEncryptionFrame* + paired_key_encryption_frame = + nearby::sharing::service::proto::PairedKeyEncryptionFrame:: + default_instance() + .New(); + paired_key_encryption_frame->set_signed_data( + is_incoming + ? std::string(GetIncomingConnectionSignedData().begin(), + GetIncomingConnectionSignedData().end()) + : std::string(GetOutgoingConnectionSignedData().begin(), + GetOutgoingConnectionSignedData().end())); + paired_key_encryption_frame->set_secret_id_hash( + std::string(GetPrivateCertificateHashAuthToken().begin(), + GetPrivateCertificateHashAuthToken().end())); + v1_frame->set_allocated_paired_key_encryption( + paired_key_encryption_frame); + Frame* frame = Frame::default_instance().New(); + frame->set_version(Frame::V1); + frame->set_allocated_v1(v1_frame); + return std::unique_ptr(frame); + })); + + connection_.AppendReadableData(encryption_bytes); + FlushTesting(); + + std::string encryption_result = "test_encryption_result"; + std::vector result_bytes(encryption_result.begin(), + encryption_result.end()); + + EXPECT_CALL(fake_decoder_, DecodeFrame(testing::Eq(result_bytes))) + .WillOnce(testing::Invoke([=](absl::Span data) { + V1Frame* v1_frame = V1Frame::default_instance().New(); + v1_frame->set_type(V1Frame::PAIRED_KEY_RESULT); + PairedKeyResultFrame* paired_key_result_frame = + PairedKeyResultFrame::default_instance().New(); + paired_key_result_frame->set_status(status); + v1_frame->set_allocated_paired_key_result(paired_key_result_frame); + + Frame* frame = Frame::default_instance().New(); + frame->set_version(Frame::V1); + frame->set_allocated_v1(v1_frame); + return std::unique_ptr(frame); + })); + + connection_.AppendReadableData(result_bytes); + FlushTesting(); + } + + void SetUpAdvertisementDecoder(const std::vector& endpoint_info, + bool return_empty_advertisement, + bool return_empty_device_name, + size_t expected_number_of_calls) { + EXPECT_CALL(fake_decoder_, DecodeAdvertisement(testing::Eq(endpoint_info))) + .Times(expected_number_of_calls) + .WillRepeatedly(testing::Invoke([=](absl::Span data) { + if (return_empty_advertisement) { + connection_.AppendReadableData({}); + FlushTesting(); + return std::unique_ptr(nullptr); + } + + std::optional device_name; + if (!return_empty_device_name) device_name = kDeviceName; + + return Advertisement::NewInstance( + GetNearbyShareTestEncryptedMetadataKey().salt(), + GetNearbyShareTestEncryptedMetadataKey().encrypted_key(), + kDeviceType, device_name); + })); + } + + void SetUpIntroductionFrameDecoder(bool return_empty_introduction_frame) { + std::string intro = "introduction_frame"; + std::vector bytes(intro.begin(), intro.end()); + + EXPECT_CALL(fake_decoder_, DecodeFrame(testing::Eq(bytes))) + .WillOnce(testing::Invoke([=](absl::Span data) { + if (return_empty_introduction_frame) { + return GetEmptyIntroductionFrame(); + } + + return GetValidIntroductionFrame(); + })); + + connection_.AppendReadableData(bytes); + FlushTesting(); + } + + void SendConnectionResponse(ConnectionResponseFrame::Status status) { + std::string intro = "connection_result_frame"; + std::vector bytes(intro.begin(), intro.end()); + EXPECT_CALL(fake_decoder_, DecodeFrame(testing::Eq(bytes))) + .WillOnce(testing::Invoke([=](absl::Span data) { + return GetConnectionResponseFrame(status); + })); + connection_.AppendReadableData(bytes); + FlushTesting(); + } + + void SendCancel() { + std::string intro = "cancel_frame"; + std::vector bytes(intro.begin(), intro.end()); + EXPECT_CALL(fake_decoder_, DecodeFrame(testing::Eq(bytes))) + .WillOnce(testing::Invoke( + [=](absl::Span data) { return GetCancelFrame(); })); + connection_.AppendReadableData(bytes); + FlushTesting(); + } + + ShareTarget SetUpIncomingConnection( + NiceMock& callback, bool is_foreground = true, + bool for_self_share = false) { + fake_nearby_connections_manager_->SetRawAuthenticationToken(kEndpointId, + GetToken()); + SetUpAdvertisementDecoder(GetValidV1EndpointInfo(), + /*return_empty_advertisement=*/false, + /*return_empty_device_name=*/false, + /*expected_number_of_calls=*/1u); + + SetUpIntroductionFrameDecoder(/*return_empty_introduction_frame=*/false); + + ShareTarget share_target; + SetConnectionType(ConnectionType::kWifi); + absl::Notification notification; + + EXPECT_CALL(callback, OnTransferUpdate(testing::_, testing::_)) + .WillOnce(testing::Invoke([&](const ShareTarget& incoming_share_target, + const TransferMetadata& metadata) { + EXPECT_FALSE(metadata.is_final_status()); + TransferMetadata::Status expected_status; + + if (for_self_share && + NearbyFlags::GetInstance().GetBoolFlag( + config_package_nearby::nearby_sharing_feature:: + kEnableSelfShare)) { + expected_status = + TransferMetadata::Status::kAwaitingRemoteAcceptance; + } else { + expected_status = + TransferMetadata::Status::kAwaitingLocalConfirmation; + } + + EXPECT_EQ(metadata.status(), expected_status); + share_target = incoming_share_target; + notification.Notify(); + })); + + SetUpKeyVerification( + /*is_incoming=*/true, PairedKeyResultFrame::SUCCESS); + if (is_foreground) { + SetUpForegroundReceiveSurface(callback); + } else { + SetUpBackgroundReceiveSurface(callback); + } + service_->OnIncomingConnection(kEndpointId, GetValidV1EndpointInfo(), + &connection_); + ProcessLatestPublicCertificateDecryption(/*expected_num_calls=*/1, + /*success=*/true, for_self_share); + EXPECT_TRUE( + fake_nearby_connections_manager_->DidUpgradeBandwidth(kEndpointId)); + + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + return share_target; + } + + ShareTarget SetUpOutgoingShareTarget( + MockTransferUpdateCallback& transfer_callback, + MockShareTargetDiscoveredCallback& discovery_callback) { + SetUpKeyVerification( + /*is_incoming=*/false, PairedKeyResultFrame::SUCCESS); + + fake_nearby_connections_manager_->SetRawAuthenticationToken(kEndpointId, + GetToken()); + fake_nearby_connections_manager_->set_nearby_connection(&connection_); + + return DiscoverShareTarget(transfer_callback, discovery_callback); + } + + ShareTarget DiscoverShareTarget( + MockTransferUpdateCallback& transfer_callback, + MockShareTargetDiscoveredCallback& discovery_callback) { + SetConnectionType(ConnectionType::kWifi); + + // Ensure decoder parses a valid endpoint advertisement. + SetUpAdvertisementDecoder(GetValidV1EndpointInfo(), + /*return_empty_advertisement=*/false, + /*return_empty_device_name=*/false, + /*expected_number_of_calls=*/1u); + + // Start discovering, to ensure a discovery listener is registered. + EXPECT_EQ(RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kForeground), + NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsDiscovering()); + + ShareTarget discovered_target; + // Discover a new endpoint, with fields set up a valid certificate. + EXPECT_CALL(discovery_callback, OnShareTargetDiscovered) + .WillOnce([&discovered_target](ShareTarget share_target) { + discovered_target = share_target; + }); + + auto endpoint_info = std::make_unique( + GetValidV1EndpointInfo(), kServiceId); + fake_nearby_connections_manager_->OnEndpointFound(kEndpointId, + std::move(endpoint_info)); + FlushTesting(); + ProcessLatestPublicCertificateDecryption(/*expected_num_calls=*/1, + /*success=*/true); + return discovered_target; + } + + Frame GetWrittenFrame() { + EXPECT_TRUE( + FakeTaskRunner::WaitForRunningTasksWithTimeout(absl::Seconds(2))); + std::vector data = connection_.GetWrittenData(); + Frame frame; + frame.ParseFromArray(data.data(), data.size()); + return frame; + } + + bool ExpectPairedKeyEncryptionFrame() { + Frame frame = GetWrittenFrame(); + if (!frame.has_v1()) { + return false; + } + + if (!frame.v1().has_paired_key_encryption()) { + return false; + } + + return true; + } + + bool ExpectPairedKeyResultFrame() { + Frame frame = GetWrittenFrame(); + if (!frame.has_v1()) { + return false; + } + + if (!frame.v1().has_paired_key_result()) { + return false; + } + + return true; + } + + bool ExpectConnectionResponseFrame(ConnectionResponseFrame::Status status) { + Frame frame = GetWrittenFrame(); + if (!frame.has_v1()) { + return false; + } + + if (!frame.v1().has_connection_response()) { + return false; + } + + if (status != frame.v1().connection_response().status()) { + return false; + } + + return true; + } + + std::optional ExpectIntroductionFrame() { + Frame frame = GetWrittenFrame(); + if (!frame.has_v1()) { + return std::nullopt; + } + + if (!frame.v1().has_introduction()) { + return std::nullopt; + } + + return frame.v1().introduction(); + } + + bool ExpectCancelFrame() { + Frame frame = GetWrittenFrame(); + if (!frame.has_v1()) { + return false; + } + + if (frame.v1().type() != V1Frame::CANCEL) { + return false; + } + + return true; + } + + bool ExpectProgressUpdateFrame() { + Frame frame = GetWrittenFrame(); + if (!frame.has_v1()) { + return false; + } + + if (frame.v1().type() != V1Frame::PROGRESS_UPDATE) { + return false; + } + + return true; + } + + // Optionally, |new_share_target| is updated with the ShareTargets sent to + // OnTransferUpdate() calls. + void ExpectTransferUpdates( + MockTransferUpdateCallback& transfer_callback, const ShareTarget& target, + const std::vector& updates, + std::function callback, ShareTarget* new_share_target = nullptr) { + expect_transfer_updates_count_ = 0; + expect_transfer_updates_callback_ = std::move(callback); + auto& expectation = + EXPECT_CALL(transfer_callback, OnTransferUpdate).Times(updates.size()); + + for (TransferMetadata::Status status : updates) { + expectation.WillOnce( + testing::Invoke([=](const ShareTarget& share_target, + const TransferMetadata& metadata) { + EXPECT_EQ(share_target.id, target.id); + EXPECT_EQ(metadata.status(), status); + if (new_share_target) { + *new_share_target = share_target; + } + + ++expect_transfer_updates_count_; + if (expect_transfer_updates_count_ == updates.size()) { + expect_transfer_updates_callback_(); + } + })); + } + } + + // Returns the modified ShareTarget received from a TransferUpdate. + std::optional SetUpOutgoingConnectionUntilAccept( + MockTransferUpdateCallback& transfer_callback, + const ShareTarget& target) { + ShareTarget new_share_target; + ExpectTransferUpdates( + transfer_callback, target, + {TransferMetadata::Status::kConnecting, + TransferMetadata::Status::kAwaitingLocalConfirmation, + TransferMetadata::Status::kAwaitingRemoteAcceptance}, + [] {}, &new_share_target); + + absl::Notification send_notification; + NearbySharingServiceImpl::StatusCodes send_result; + service_->SendAttachments( + target, CreateTextAttachments({kTextPayload}), + [&](NearbySharingServiceImpl::StatusCodes status_codes) { + send_result = status_codes; + send_notification.Notify(); + }); + + EXPECT_TRUE( + send_notification.WaitForNotificationWithTimeout(kTaskWaitTimeout)); + EXPECT_EQ(send_result, NearbySharingServiceImpl::StatusCodes::kOk); + + // Verify data sent to the remote device so far. + if (!ExpectPairedKeyEncryptionFrame()) { + return std::nullopt; + } + + if (!ExpectPairedKeyResultFrame()) { + return std::nullopt; + } + + if (!ExpectIntroductionFrame().has_value()) { + return std::nullopt; + } + + return new_share_target; + } + + struct PayloadInfo { + int64_t payload_id; + std::weak_ptr listener; + }; + + PayloadInfo AcceptAndSendPayload( + MockTransferUpdateCallback& transfer_callback, + const ShareTarget& target) { + PayloadInfo info = {}; + fake_nearby_connections_manager_->set_send_payload_callback( + [&](std::unique_ptr payload, + std::weak_ptr + listener) { + ASSERT_TRUE(payload->content.is_bytes()); + std::vector bytes = payload->content.bytes_payload.bytes; + EXPECT_EQ(kTextPayload, std::string(bytes.begin(), bytes.end())); + info.payload_id = payload->id; + info.listener = listener; + }); + + // We're now waiting for the remote device to respond with the accept + // result. + ExpectTransferUpdates(transfer_callback, target, + {TransferMetadata::Status::kInProgress}, [] {}); + + // Kick off send process by accepting the transfer from the remote device. + SendConnectionResponse(ConnectionResponseFrame::ACCEPT); + return info; + } + + void FinishOutgoingTransfer(MockTransferUpdateCallback& transfer_callback, + const ShareTarget& target, + const PayloadInfo& info) { + // Simulate a successful transfer via Nearby Connections. + ExpectTransferUpdates(transfer_callback, target, + {TransferMetadata::Status::kComplete}, [] {}); + + auto payload_transfer_update = std::make_unique( + info.payload_id, PayloadStatus::kSuccess, + /*total_bytes=*/strlen(kTextPayload), + /*bytes_transferred=*/strlen(kTextPayload)); + if (auto listener = info.listener.lock()) { + listener->OnStatusUpdate(std::move(payload_transfer_update), + /*upgraded_medium=*/std::nullopt); + } + } + + std::unique_ptr GetCurrentAdvertisement() { + auto endpoint_info = + fake_nearby_connections_manager_->advertising_endpoint_info(); + if (!endpoint_info) return nullptr; + + return Advertisement::FromEndpointInfo(absl::MakeSpan( + *fake_nearby_connections_manager_->advertising_endpoint_info())); + } + + void FindEndpoint(absl::string_view endpoint_id) { + fake_nearby_connections_manager_->OnEndpointFound( + endpoint_id, std::make_unique( + GetValidV1EndpointInfo(), kServiceId)); + FlushTesting(); + } + + void LoseEndpoint(absl::string_view endpoint_id) { + fake_nearby_connections_manager_->OnEndpointLost(endpoint_id); + FlushTesting(); + } + + // This method sets up an incoming connection and performs the steps + // required to simulate a successful incoming transfer. + void SuccessfullyReceiveTransfer() { + for (int64_t payload_id : GetValidIntroductionFramePayloadIds()) { + fake_nearby_connections_manager_->SetPayloadPathStatus(payload_id, + Status::kSuccess); + } + + NiceMock callback; + ShareTarget share_target = SetUpIncomingConnection(callback); + + absl::Notification notification; + EXPECT_CALL(callback, OnTransferUpdate(testing::_, testing::_)) + .WillOnce(testing::Invoke( + [](const ShareTarget& share_target, TransferMetadata metadata) { + EXPECT_FALSE(metadata.is_final_status()); + EXPECT_EQ(metadata.status(), + TransferMetadata::Status::kAwaitingRemoteAcceptance); + })); + + service_->Accept( + share_target, [&](NearbySharingServiceImpl::StatusCodes status_code) { + EXPECT_EQ(status_code, NearbySharingServiceImpl::StatusCodes::kOk); + notification.Notify(); + }); + + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + + // Fail to accept again. + service_->Accept( + share_target, [&](NearbySharingServiceImpl::StatusCodes status_code) { + EXPECT_EQ(status_code, + NearbySharingServiceImpl::StatusCodes::kOutOfOrderApiCall); + }); + + fake_nearby_connections_manager_->SetIncomingPayload( + kFilePayloadId, GetFilePayload(kFilePayloadId)); + + for (int64_t id : GetValidIntroductionFramePayloadIds()) { + // Update file payload at the end. + if (id == kFilePayloadId) continue; + + fake_nearby_connections_manager_->SetIncomingPayload( + id, GetTextPayload(id, kTextPayload)); + + std::weak_ptr listener = + fake_nearby_connections_manager_->GetRegisteredPayloadStatusListener( + id); + + absl::Notification progress_notification; + + EXPECT_CALL(callback, OnTransferUpdate(testing::_, testing::_)) + .WillOnce(testing::Invoke([&](const ShareTarget& share_target, + TransferMetadata metadata) { + EXPECT_FALSE(metadata.is_final_status()); + EXPECT_EQ(metadata.status(), TransferMetadata::Status::kInProgress); + progress_notification.Notify(); + })); + + PayloadTransferUpdate payload = + PayloadTransferUpdate(id, PayloadStatus::kSuccess, + /*total_bytes=*/kPayloadSize, + /*bytes_transferred=*/kPayloadSize); + if (auto locked_listener = listener.lock()) { + locked_listener->OnStatusUpdate( + std::make_unique(payload), + /*upgraded_medium=*/std::nullopt); + } + + EXPECT_TRUE( + progress_notification.WaitForNotificationWithTimeout(kWaitTimeout)); + FastForward(kMinProgressUpdateFrequency); + } + + std::filesystem::path file_path; + absl::Notification success_notification; + EXPECT_CALL(callback, OnTransferUpdate(testing::_, testing::_)) + .WillOnce(testing::Invoke( + [&](const ShareTarget& share_target, TransferMetadata metadata) { + EXPECT_TRUE(metadata.is_final_status()); + EXPECT_EQ(metadata.status(), TransferMetadata::Status::kComplete); + + ASSERT_TRUE(share_target.has_attachments()); + EXPECT_EQ(1u, share_target.file_attachments.size()); + for (const FileAttachment& file : share_target.file_attachments) { + EXPECT_TRUE(file.file_path()); + file_path = *file.file_path(); + } + + EXPECT_EQ(3u, share_target.text_attachments.size()); + for (const TextAttachment& text : share_target.text_attachments) { + EXPECT_EQ(text.text_body(), kTextPayload); + } + + success_notification.Notify(); + })); + + std::weak_ptr listener = + fake_nearby_connections_manager_->GetRegisteredPayloadStatusListener( + kFilePayloadId); + + PayloadTransferUpdate payload = + PayloadTransferUpdate(kFilePayloadId, PayloadStatus::kSuccess, + /*total_bytes=*/kPayloadSize, + /*bytes_transferred=*/kPayloadSize); + if (auto locked_listener = listener.lock()) { + locked_listener->OnStatusUpdate( + std::make_unique(payload), + /*upgraded_medium=*/std::nullopt); + } + + EXPECT_TRUE( + success_notification.WaitForNotificationWithTimeout(kWaitTimeout)); + FlushTesting(); + EXPECT_FALSE(fake_nearby_connections_manager_->connection_endpoint_info( + kEndpointId)); + EXPECT_FALSE(fake_nearby_connections_manager_->has_incoming_payloads()); + + // To avoid UAF in OnIncomingTransferUpdate(). + UnregisterReceiveSurface(&callback); + + // Remove test file. + std::filesystem::remove(file_path); + } + + void FlushTesting() { + absl::SleepFor(absl::Milliseconds(200)); + FakeTaskRunner::WaitForRunningTasksWithTimeout(absl::Milliseconds(200)); + } + + void SetDiskSpace(size_t size) { + fake_device_info_.SetAvailableDiskSpaceInBytes( + std::filesystem::temp_directory_path(), size); + } + + void ResetDiskSpace() { + fake_device_info_.ResetDiskSpace(); + } + + protected: + FakeNearbyShareLocalDeviceDataManager* local_device_data_manager() { + EXPECT_EQ(local_device_data_manager_factory_.instances().size(), 1u); + return local_device_data_manager_factory_.instances().back(); + } + + FakeNearbyShareCertificateManager* certificate_manager() { + EXPECT_EQ(certificate_manager_factory_.instances().size(), 1u); + return certificate_manager_factory_.instances().back(); + } + + FakeAccountManager& account_manager() { + return fake_account_manager_; + } + + std::filesystem::path CreateTestFile(absl::string_view name, + const std::vector& content) { + std::filesystem::path path = std::filesystem::temp_directory_path() / name; + std::FILE* file = std::fopen(path.string().c_str(), "w+"); + std::fwrite(content.data(), 1, content.size(), file); + std::fclose(file); + return path; + } + + PreferenceManager& preference_manager() { return preference_manager_; } + + api::MockSharingPlatform mock_sharing_platform_; + nearby::FakePreferenceManager preference_manager_; + FakeAccountManager fake_account_manager_; + FakeContext fake_context_; + FakeDeviceInfo fake_device_info_; + FakeNearbyConnectionsManager* fake_nearby_connections_manager_ = nullptr; + FakeNearbyShareLocalDeviceDataManager::Factory + local_device_data_manager_factory_; + FakeNearbyShareContactManager::Factory contact_manager_factory_; + FakeNearbyShareCertificateManager::Factory certificate_manager_factory_; + std::unique_ptr + nearby_fast_initiation_factory_; + FakeNearbyConnection connection_; + MockNearbySharingDecoder fake_decoder_; + std::unique_ptr fake_http_client_ = + std::make_unique(); + std::unique_ptr service_; + int expect_transfer_updates_count_ = 0; + std::function expect_transfer_updates_callback_; +}; + +struct ValidSendSurfaceTestData { + bool bluetooth_enabled; + ConnectionType connection_type; +} kValidSendSurfaceTestData[] = { + // No network connection, only bluetooth available + {true, ConnectionType::kNone}, + // Wifi available + {true, ConnectionType::kWifi}, + // Ethernet available + {true, ConnectionType::kEthernet}, + // 3G available + {true, ConnectionType::k3G}}; + +class NearbySharingServiceImplValidSendTest + : public NearbySharingServiceImplTest, + public testing::WithParamInterface {}; + +struct InvalidSendSurfaceTestData { +} kInvalidSendSurfaceTestData[] = { + // Screen locked + {/*screen_locked=*/}, + // No network connection and no bluetooth + {/*screen_locked=*/}, + // 3G available and no bluetooth + {/*screen_locked=*/}, + // Wi-Fi available and no bluetooth (invalid until Wi-Fi LAN is supported) + {/*screen_locked=*/}, + // Ethernet available and no bluetooth (invalid until Wi-Fi LAN is + // supported) + {/*screen_locked=*/}}; + +using ResponseFrameStatus = ConnectionResponseFrame::Status; + +struct SendFailureTestData { + ResponseFrameStatus response_status; + TransferMetadata::Status expected_status; +} kSendFailureTestData[] = { + {service::proto::ConnectionResponseFrame::REJECT, + TransferMetadata::Status::kRejected}, + {service::proto::ConnectionResponseFrame::NOT_ENOUGH_SPACE, + TransferMetadata::Status::kNotEnoughSpace}, + {service::proto::ConnectionResponseFrame::UNSUPPORTED_ATTACHMENT_TYPE, + TransferMetadata::Status::kUnsupportedAttachmentType}, + {service::proto::ConnectionResponseFrame::TIMED_OUT, + TransferMetadata::Status::kTimedOut}, + {service::proto::ConnectionResponseFrame::UNKNOWN, + TransferMetadata::Status::kFailed}, +}; + +class NearbySharingServiceImplSendFailureTest + : public NearbySharingServiceImplTest, + public testing::WithParamInterface {}; + +class TestObserver : public NearbySharingService::Observer { + public: + explicit TestObserver(NearbySharingService* service) : service_(service) { + service_->AddObserver(this); + } + + void OnHighVisibilityChanged(bool in_high_visibility) override { + in_high_visibility_ = in_high_visibility; + } + + void OnStartAdvertisingFailure() override { + on_start_advertising_failure_called_ = true; + } + + void OnFastInitiationDevicesDetected() override { + devices_detected_called_ = true; + } + void OnFastInitiationDevicesNotDetected() override { + devices_not_detected_called_ = true; + } + void OnFastInitiationScanningStopped() override { + scanning_stopped_called_ = true; + } + + void OnShutdown() override { + shutdown_called_ = true; + service_->RemoveObserver(this); + } + + bool in_high_visibility_ = false; + bool shutdown_called_ = false; + bool on_start_advertising_failure_called_ = false; + bool devices_detected_called_ = false; + bool devices_not_detected_called_ = false; + bool scanning_stopped_called_ = false; + NearbySharingService* service_; +}; + +TEST_F(NearbySharingServiceImplTest, DisableNearbyShutdownConnections) { + SetConnectionType(ConnectionType::kWifi); + preference_manager().SetBoolean(prefs::kNearbySharingEnabledName, false); + FlushTesting(); + EXPECT_TRUE(fake_nearby_connections_manager_->is_shutdown()); +} + +TEST_F(NearbySharingServiceImplTest, StartFastInitiationAdvertising) { + FakeNearbyFastInitiation* fast_initiation = + nearby_fast_initiation_factory_->GetNearbyFastInitiation(); + SetConnectionType(ConnectionType::kWifi); + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + EXPECT_EQ(RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kForeground), + NearbySharingService::StatusCodes::kOk); + EXPECT_EQ(fast_initiation->StartAdvertisingCount(), 1); + + // Call RegisterSendSurface a second time and make sure StartAdvertising is + // not called again. + EXPECT_EQ(RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kForeground), + NearbySharingService::StatusCodes::kError); + EXPECT_EQ(fast_initiation->StartAdvertisingCount(), 1); +} + +TEST_F(NearbySharingServiceImplTest, StartFastInitiationAdvertisingError) { + SetConnectionType(ConnectionType::kWifi); + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + nearby_fast_initiation_factory_->GetNearbyFastInitiation() + ->SetStartAdvertisingError(true); + EXPECT_EQ(RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kForeground), + NearbySharingService::StatusCodes::kOk); +} + +TEST_F(NearbySharingServiceImplTest, + BackgroundStartFastInitiationAdvertisingError) { + FakeNearbyFastInitiation* fast_initiation = + nearby_fast_initiation_factory_->GetNearbyFastInitiation(); + SetConnectionType(ConnectionType::kWifi); + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + EXPECT_EQ(RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kBackground), + NearbySharingService::StatusCodes::kOk); + EXPECT_EQ(fast_initiation->StartAdvertisingCount(), 0); +} + +TEST_F(NearbySharingServiceImplTest, + StartFastInitiationAdvertising_BluetoothNotPresent) { + SetConnectionType(ConnectionType::kNone); + SetBluetoothIsPresent(false); + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + EXPECT_EQ(RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kForeground), + NearbySharingService::StatusCodes::kNoAvailableConnectionMedium); +} + +TEST_F(NearbySharingServiceImplTest, + StartFastInitiationAdvertising_BluetoothNotPowered) { + SetConnectionType(ConnectionType::kNone); + SetBluetoothIsPowered(false); + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + EXPECT_EQ(RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kForeground), + NearbySharingService::StatusCodes::kNoAvailableConnectionMedium); +} + +TEST_F(NearbySharingServiceImplTest, StopFastInitiationAdvertising) { + FakeNearbyFastInitiation* fast_initiation = + nearby_fast_initiation_factory_->GetNearbyFastInitiation(); + SetConnectionType(ConnectionType::kWifi); + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + EXPECT_EQ(RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kForeground), + NearbySharingService::StatusCodes::kOk); + EXPECT_EQ(fast_initiation->StartAdvertisingCount(), 1); + EXPECT_EQ(UnregisterSendSurface(&transfer_callback, &discovery_callback), + NearbySharingService::StatusCodes::kOk); + EXPECT_EQ(fast_initiation->StartAdvertisingCount(), + fast_initiation->StopAdvertisingCount()); +} + +TEST_F(NearbySharingServiceImplTest, + StopFastInitiationAdvertising_BluetoothBecomesNotPresent) { + FakeNearbyFastInitiation* fast_initiation = + nearby_fast_initiation_factory_->GetNearbyFastInitiation(); + SetConnectionType(ConnectionType::kNone); + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + EXPECT_EQ(RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kForeground), + NearbySharingService::StatusCodes::kOk); + SetBluetoothIsPresent(false); + EXPECT_EQ(fast_initiation->StartAdvertisingCount(), 1); + EXPECT_EQ(fast_initiation->StopAdvertisingCount(), 1); +} + +TEST_F(NearbySharingServiceImplTest, + StopFastInitiationAdvertising_BluetoothBecomesNotPowered) { + FakeNearbyFastInitiation* fast_initiation = + nearby_fast_initiation_factory_->GetNearbyFastInitiation(); + SetConnectionType(ConnectionType::kNone); + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + EXPECT_EQ(RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kForeground), + NearbySharingService::StatusCodes::kOk); + SetBluetoothIsPowered(false); + EXPECT_EQ(fast_initiation->StartAdvertisingCount(), 1); + EXPECT_EQ(fast_initiation->StopAdvertisingCount(), 1); +} + +TEST_F(NearbySharingServiceImplTest, FastInitiationScanning_StartAndStop) { + FakeNearbyFastInitiation* fast_initiation = + nearby_fast_initiation_factory_->GetNearbyFastInitiation(); + SetConnectionType(ConnectionType::kWifi); + + EXPECT_EQ(fast_initiation->StartScanningCount(), 1); + EXPECT_EQ(fast_initiation->StopScanningCount(), 0); + + // Trigger a call to StopFastInitiationScanning(). + SetBluetoothIsPowered(false); + EXPECT_EQ(fast_initiation->StartScanningCount(), 1); + EXPECT_EQ(fast_initiation->StopScanningCount(), 1); + + // Trigger a call to StartFastInitiationScanning(). + SetBluetoothIsPowered(true); + FakeTaskRunner::WaitForRunningTasksWithTimeout(kTaskWaitTimeout); + EXPECT_EQ(fast_initiation->StartScanningCount(), 2); + EXPECT_EQ(fast_initiation->StopScanningCount(), 1); +} + +TEST_F(NearbySharingServiceImplTest, + FastInitiationScanning_DisallowedBySettings) { + FakeNearbyFastInitiation* fast_initiation = + nearby_fast_initiation_factory_->GetNearbyFastInitiation(); + + EXPECT_EQ(fast_initiation->StartScanningCount(), 1); + EXPECT_EQ(fast_initiation->StopScanningCount(), 0); + + SetIsEnabled(false); + SetConnectionType(ConnectionType::kBluetooth); + + EXPECT_EQ(fast_initiation->StartScanningCount(), 1); + EXPECT_EQ(fast_initiation->StopScanningCount(), 1); +} + +TEST_F(NearbySharingServiceImplTest, + FastInitiationScanning_OnFastInitiationNotificationStateChanged) { + FakeNearbyFastInitiation* fast_initiation = + nearby_fast_initiation_factory_->GetNearbyFastInitiation(); + + // Fast init notifications are enabled by default so a scanner is created on + // initialization of the service. + EXPECT_EQ(fast_initiation->StartScanningCount(), 1); + EXPECT_EQ(fast_initiation->StopScanningCount(), 0); + + // The existing scanner is destroyed when fast init notifications are turned + // off. + SetFastInitiationNotificationState( + FastInitiationNotificationState::DISABLED_BY_USER_FAST_INIT); + EXPECT_EQ(fast_initiation->StartScanningCount(), 1); + EXPECT_EQ(fast_initiation->StopScanningCount(), 1); + + SetFastInitiationNotificationState( + FastInitiationNotificationState::ENABLED_FAST_INIT); + EXPECT_EQ(fast_initiation->StartScanningCount(), 2); + EXPECT_EQ(fast_initiation->StopScanningCount(), 1); +} + +TEST_F(NearbySharingServiceImplTest, FastInitiationScanning_NotifyObservers) { + FakeNearbyFastInitiation* fast_initiation = + nearby_fast_initiation_factory_->GetNearbyFastInitiation(); + SetConnectionType(ConnectionType::kBluetooth); + + TestObserver observer(service_.get()); + ASSERT_EQ(fast_initiation->StartScanningCount(), 1); + + fast_initiation->FireDevicesDetected(); + EXPECT_TRUE(observer.devices_detected_called_); + fast_initiation->FireDevicesNotDetected(); + EXPECT_TRUE(observer.devices_not_detected_called_); + + // Remove the observer before it goes out of scope. + service_->RemoveObserver(&observer); +} + +TEST_F(NearbySharingServiceImplTest, FastInitiationScanning_NoHardwareSupport) { + FakeNearbyFastInitiation* fast_initiation = + nearby_fast_initiation_factory_->GetNearbyFastInitiation(); + SetConnectionType(ConnectionType::kBluetooth); + + // Hardware support is enabled by default in these tests, so we expect that a + // scanner has been created. + EXPECT_EQ(fast_initiation->StartScanningCount(), 1); + EXPECT_EQ(fast_initiation->StopScanningCount(), 0); + + SetFastInitiationNotificationState( + FastInitiationNotificationState::DISABLED_BY_USER_FAST_INIT); + EXPECT_EQ(fast_initiation->StartScanningCount(), 1); + EXPECT_EQ(fast_initiation->StopScanningCount(), 1); + fast_initiation->SetScanOffloadSupported(false); + SetFastInitiationNotificationState( + FastInitiationNotificationState::ENABLED_FAST_INIT); + + // Make sure we stopped scanning and didn't restart. + EXPECT_EQ(fast_initiation->StartScanningCount(), 1); + EXPECT_EQ(fast_initiation->StopScanningCount(), 1); +} + +TEST_F(NearbySharingServiceImplTest, + FastInitiationScanning_PostTransferCooldown) { + FakeNearbyFastInitiation* fast_initiation = + nearby_fast_initiation_factory_->GetNearbyFastInitiation(); + SetConnectionType(ConnectionType::kBluetooth); + + // Make sure we started scanning once + EXPECT_EQ(fast_initiation->StartScanningCount(), 1); + EXPECT_EQ(fast_initiation->StopScanningCount(), 0); + + SuccessfullyReceiveTransfer(); + + // Make sure we stopped scanning and didn't restart... yet. + EXPECT_EQ(fast_initiation->StartScanningCount(), 1); + EXPECT_EQ(fast_initiation->StopScanningCount(), 1); + + // Fast-forward 10s to pass through the cooldown period. + FastForward(absl::Seconds(10)); + + // Make sure we restarted Fast Initiation scanning. + EXPECT_EQ(fast_initiation->StartScanningCount(), 2); + EXPECT_EQ(fast_initiation->StopScanningCount(), 1); +} + +TEST_F(NearbySharingServiceImplTest, + ForegroundRegisterSendSurfaceStartsDiscovering) { + SetConnectionType(ConnectionType::kWifi); + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + EXPECT_EQ(RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kForeground), + NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsDiscovering()); +} + +TEST_F(NearbySharingServiceImplTest, + ForegroundRegisterSendSurfaceTwiceKeepsDiscovering) { + SetConnectionType(ConnectionType::kWifi); + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + EXPECT_EQ(RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kForeground), + NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsDiscovering()); + + EXPECT_EQ(RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kForeground), + NearbySharingService::StatusCodes::kError); + EXPECT_TRUE(fake_nearby_connections_manager_->IsDiscovering()); +} + +TEST_F(NearbySharingServiceImplTest, + RegisterSendSurfaceAlreadyReceivingNotDiscovering) { + NiceMock callback; + ShareTarget share_target = SetUpIncomingConnection(callback); + EXPECT_FALSE(connection_.IsClosed()); + + MockTransferUpdateCallback send_callback; + MockShareTargetDiscoveredCallback discovery_callback; + EXPECT_EQ(RegisterSendSurface(&send_callback, &discovery_callback, + SendSurfaceState::kForeground), + NearbySharingService::StatusCodes::kTransferAlreadyInProgress); + EXPECT_FALSE(fake_nearby_connections_manager_->IsDiscovering()); + EXPECT_FALSE(fake_nearby_connections_manager_->is_shutdown()); + + // To avoid UAF in OnIncomingTransferUpdate(). + UnregisterReceiveSurface(&callback); +} + +TEST_F(NearbySharingServiceImplTest, + BackgroundRegisterSendSurfaceNotDiscovering) { + SetConnectionType(ConnectionType::kWifi); + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + EXPECT_EQ(RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kBackground), + NearbySharingService::StatusCodes::kOk); + EXPECT_FALSE(fake_nearby_connections_manager_->IsDiscovering()); + EXPECT_FALSE(fake_nearby_connections_manager_->is_shutdown()); +} + +TEST_F(NearbySharingServiceImplTest, + DifferentSurfaceRegisterSendSurfaceTwiceKeepsDiscovering) { + SetConnectionType(ConnectionType::kWifi); + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + EXPECT_EQ(RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kForeground), + NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsDiscovering()); + + EXPECT_EQ(RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kBackground), + NearbySharingService::StatusCodes::kError); + EXPECT_TRUE(fake_nearby_connections_manager_->IsDiscovering()); +} + +TEST_F(NearbySharingServiceImplTest, + RegisterSendSurfaceEndpointFoundDiscoveryCallbackNotified) { + SetConnectionType(ConnectionType::kWifi); + + // Ensure decoder parses a valid endpoint advertisement. + SetUpAdvertisementDecoder(GetValidV1EndpointInfo(), + /*return_empty_advertisement=*/false, + /*return_empty_device_name=*/false, + /*expected_number_of_calls=*/1u); + + // Start discovering, to ensure a discovery listener is registered. + MockTransferUpdateCallback transfer_callback; + NiceMock discovery_callback; + EXPECT_EQ(RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kForeground), + NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsDiscovering()); + + // Discover a new endpoint, with fields set up a valid certificate. + EXPECT_CALL(discovery_callback, OnShareTargetDiscovered) + .WillOnce([&](ShareTarget share_target) { + EXPECT_FALSE(share_target.is_incoming); + EXPECT_TRUE(share_target.is_known); + EXPECT_FALSE(share_target.has_attachments()); + EXPECT_EQ(share_target.device_name, kDeviceName); + EXPECT_EQ(share_target.type, kDeviceType); + EXPECT_TRUE(share_target.device_id); + EXPECT_NE(share_target.device_id, kEndpointId); + EXPECT_EQ(share_target.full_name, kTestMetadataFullName); + }); + fake_nearby_connections_manager_->OnEndpointFound( + kEndpointId, std::make_unique( + GetValidV1EndpointInfo(), kServiceId)); + FlushTesting(); + ProcessLatestPublicCertificateDecryption(/*expected_num_calls=*/1, + /*success=*/true); + // Register another send surface, which will automatically catch up + // discovered endpoints. + MockTransferUpdateCallback transfer_callback2; + NiceMock discovery_callback2; + EXPECT_CALL(discovery_callback2, OnShareTargetDiscovered) + .WillOnce([&](ShareTarget share_target) { + EXPECT_EQ(share_target.device_name, kDeviceName); + }); + + EXPECT_EQ(RegisterSendSurface(&transfer_callback2, &discovery_callback2, + SendSurfaceState::kForeground), + NearbySharingService::StatusCodes::kOk); + + // Shut down the service while the discovery callbacks are still in + // scope. OnShareTargetLost() will be invoked during shutdown. + Shutdown(); + service_.reset(); +} + +TEST_F(NearbySharingServiceImplTest, RegisterSendSurfaceEmptyCertificate) { + SetConnectionType(ConnectionType::kWifi); + + // Ensure decoder parses a valid endpoint advertisement. + SetUpAdvertisementDecoder(GetValidV1EndpointInfo(), + /*return_empty_advertisement=*/false, + /*return_empty_device_name=*/false, + /*expected_number_of_calls=*/1u); + + // Start discovering, to ensure a discovery listener is registered. + MockTransferUpdateCallback transfer_callback; + NiceMock discovery_callback; + EXPECT_EQ(RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kForeground), + NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsDiscovering()); + + // Discover a new endpoint, with fields set up a valid certificate. + EXPECT_CALL(discovery_callback, OnShareTargetDiscovered) + .WillOnce([](ShareTarget share_target) { + EXPECT_FALSE(share_target.is_incoming); + EXPECT_FALSE(share_target.is_known); + EXPECT_FALSE(share_target.has_attachments()); + EXPECT_EQ(share_target.device_name, kDeviceName); + EXPECT_FALSE(share_target.image_url); + EXPECT_EQ(share_target.type, kDeviceType); + EXPECT_TRUE(share_target.device_id); + EXPECT_EQ(share_target.device_id, kEndpointId); + EXPECT_FALSE(share_target.full_name); + }); + fake_nearby_connections_manager_->OnEndpointFound( + kEndpointId, std::make_unique( + GetValidV1EndpointInfo(), kServiceId)); + FlushTesting(); + ProcessLatestPublicCertificateDecryption(/*expected_num_calls=*/1, + /*success=*/false); + // Register another send surface, which will automatically catch up + // discovered endpoints. + MockTransferUpdateCallback transfer_callback2; + NiceMock discovery_callback2; + EXPECT_CALL(discovery_callback2, OnShareTargetDiscovered) + .WillOnce([](ShareTarget share_target) { + EXPECT_EQ(share_target.device_name, kDeviceName); + }); + + EXPECT_EQ(RegisterSendSurface(&transfer_callback2, &discovery_callback2, + SendSurfaceState::kForeground), + NearbySharingService::StatusCodes::kOk); + + // Shut down the service while the discovery callbacks are still in scope. + // OnShareTargetLost() will be invoked during shutdown. + Shutdown(); + service_.reset(); +} + +TEST_P(NearbySharingServiceImplValidSendTest, + RegisterSendSurfaceIsDiscovering) { + SetBluetoothIsPresent(GetParam().bluetooth_enabled); + SetConnectionType(GetParam().connection_type); + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + EXPECT_EQ(RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kForeground), + NearbySharingService::StatusCodes::kOk); + FlushTesting(); + EXPECT_TRUE(fake_nearby_connections_manager_->IsDiscovering()); +} + +INSTANTIATE_TEST_SUITE_P(NearbySharingServiceImplTest, + NearbySharingServiceImplValidSendTest, + testing::ValuesIn(kValidSendSurfaceTestData)); + +TEST_F(NearbySharingServiceImplTest, DisableFeatureSendSurfaceNotDiscovering) { + preference_manager().SetBoolean(prefs::kNearbySharingEnabledName, false); + FlushTesting(); + SetConnectionType(ConnectionType::kWifi); + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + EXPECT_EQ(RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kForeground), + NearbySharingService::StatusCodes::kOk); + EXPECT_FALSE(fake_nearby_connections_manager_->IsDiscovering()); + EXPECT_TRUE(fake_nearby_connections_manager_->is_shutdown()); +} + +TEST_F(NearbySharingServiceImplTest, + DisableFeatureSendSurfaceStopsDiscovering) { + SetConnectionType(ConnectionType::kWifi); + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + EXPECT_EQ(RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kForeground), + NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsDiscovering()); + + preference_manager().SetBoolean(prefs::kNearbySharingEnabledName, false); + FlushTesting(); + EXPECT_FALSE(fake_nearby_connections_manager_->IsDiscovering()); + EXPECT_TRUE(fake_nearby_connections_manager_->is_shutdown()); +} + +TEST_F(NearbySharingServiceImplTest, UnregisterSendSurfaceStopsDiscovering) { + SetConnectionType(ConnectionType::kWifi); + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + EXPECT_EQ(RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kForeground), + NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsDiscovering()); + + EXPECT_EQ(UnregisterSendSurface(&transfer_callback, &discovery_callback), + NearbySharingService::StatusCodes::kOk); + EXPECT_FALSE(fake_nearby_connections_manager_->IsDiscovering()); + EXPECT_FALSE(fake_nearby_connections_manager_->is_shutdown()); +} + +TEST_F(NearbySharingServiceImplTest, + UnregisterSendSurfaceDifferentCallbackKeepDiscovering) { + SetConnectionType(ConnectionType::kWifi); + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + EXPECT_EQ(RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kForeground), + NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsDiscovering()); + + MockTransferUpdateCallback transfer_callback2; + MockShareTargetDiscoveredCallback discovery_callback2; + EXPECT_EQ(UnregisterSendSurface(&transfer_callback2, &discovery_callback2), + NearbySharingService::StatusCodes::kError); + EXPECT_TRUE(fake_nearby_connections_manager_->IsDiscovering()); +} + +TEST_F(NearbySharingServiceImplTest, UnregisterSendSurfaceNeverRegistered) { + SetConnectionType(ConnectionType::kWifi); + + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + EXPECT_EQ(UnregisterSendSurface(&transfer_callback, &discovery_callback), + NearbySharingService::StatusCodes::kError); + EXPECT_FALSE(fake_nearby_connections_manager_->IsDiscovering()); +} + +TEST_F(NearbySharingServiceImplTest, + ForegroundRegisterReceiveSurfaceIsAdvertisingAllContacts) { + SetConnectionType(ConnectionType::kWifi); + SetVisibility(DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + ::nearby::AccountManager::Account account; + account.id = kTestAccountId; + account_manager().SetAccount(account); + local_device_data_manager()->SetDeviceName(kDeviceName); + MockTransferUpdateCallback callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kForeground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); + EXPECT_EQ(fake_nearby_connections_manager_->advertising_power_level(), + PowerLevel::kHighPower); + ASSERT_TRUE(fake_nearby_connections_manager_->advertising_endpoint_info()); + std::unique_ptr advertisement = GetCurrentAdvertisement(); + ASSERT_NE(advertisement, nullptr); + EXPECT_EQ(advertisement->device_name(), std::nullopt); + EXPECT_EQ(advertisement->device_type(), ShareTargetType::kLaptop); + auto& test_metadata_key = GetNearbyShareTestEncryptedMetadataKey(); + EXPECT_EQ(test_metadata_key.salt(), advertisement->salt()); + EXPECT_EQ(test_metadata_key.encrypted_key(), + advertisement->encrypted_metadata_key()); + account_manager().SetAccount(std::nullopt); +} + +TEST_F(NearbySharingServiceImplTest, + ForegroundRegisterReceiveSurfaceIsAdvertisingNoOne) { + SetConnectionType(ConnectionType::kWifi); + SetVisibility(DeviceVisibility::DEVICE_VISIBILITY_SELF_SHARE); + local_device_data_manager()->SetDeviceName(kDeviceName); + MockTransferUpdateCallback callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kForeground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); + EXPECT_EQ(PowerLevel::kHighPower, + fake_nearby_connections_manager_->advertising_power_level()); + ASSERT_TRUE(fake_nearby_connections_manager_->advertising_endpoint_info()); + std::unique_ptr advertisement = GetCurrentAdvertisement(); + ASSERT_NE(advertisement, nullptr); + EXPECT_EQ(advertisement->device_name(), std::nullopt); + EXPECT_EQ(ShareTargetType::kLaptop, advertisement->device_type()); + // Expecting random metadata key. + EXPECT_EQ(static_cast(sharing::Advertisement::kSaltSize), + advertisement->salt().size()); + EXPECT_EQ(static_cast( + sharing::Advertisement::kMetadataEncryptionKeyHashByteSize), + advertisement->encrypted_metadata_key().size()); +} + +TEST_F(NearbySharingServiceImplTest, + BackgroundRegisterReceiveSurfaceIsAdvertisingSelectedContacts) { + SetConnectionType(ConnectionType::kWifi); + SetVisibility(DeviceVisibility::DEVICE_VISIBILITY_SELECTED_CONTACTS); + ::nearby::AccountManager::Account account; + account.id = kTestAccountId; + account_manager().SetAccount(account); + preference_manager().SetInteger(prefs::kNearbySharingBackgroundVisibilityName, + static_cast(DeviceVisibility::DEVICE_VISIBILITY_SELECTED_CONTACTS)); + MockTransferUpdateCallback callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kBackground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); + EXPECT_EQ(PowerLevel::kLowPower, + fake_nearby_connections_manager_->advertising_power_level()); + ASSERT_TRUE(fake_nearby_connections_manager_->advertising_endpoint_info()); + std::unique_ptr advertisement = GetCurrentAdvertisement(); + ASSERT_NE(advertisement, nullptr); + EXPECT_FALSE(advertisement->device_name()); + EXPECT_EQ(ShareTargetType::kLaptop, advertisement->device_type()); + auto& test_metadata_key = GetNearbyShareTestEncryptedMetadataKey(); + EXPECT_EQ(test_metadata_key.salt(), advertisement->salt()); + EXPECT_EQ(test_metadata_key.encrypted_key(), + advertisement->encrypted_metadata_key()); + account_manager().SetAccount(std::nullopt); +} + +TEST_F(NearbySharingServiceImplTest, + RegisterReceiveSurfaceTwiceSameCallbackKeepAdvertising) { + SetConnectionType(ConnectionType::kWifi); + MockTransferUpdateCallback callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kForeground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); + + NearbySharingService::StatusCodes result2 = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kForeground); + EXPECT_EQ(result2, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); +} + +TEST_F(NearbySharingServiceImplTest, + RegisterReceiveSurfaceTwiceKeepAdvertising) { + SetConnectionType(ConnectionType::kWifi); + MockTransferUpdateCallback callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kForeground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); + + MockTransferUpdateCallback callback2; + NearbySharingService::StatusCodes result2 = RegisterReceiveSurface( + &callback2, NearbySharingService::ReceiveSurfaceState::kForeground); + EXPECT_EQ(result2, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); +} + +TEST_F(NearbySharingServiceImplTest, + DataUsageChangedRegisterReceiveSurfaceRestartsAdvertising) { + SetConnectionType(ConnectionType::kWifi); + preference_manager().SetInteger(prefs::kNearbySharingDataUsageName, + static_cast(DataUsage::OFFLINE_DATA_USAGE)); + FlushTesting(); + MockTransferUpdateCallback callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kForeground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); + EXPECT_EQ(DataUsage::OFFLINE_DATA_USAGE, + fake_nearby_connections_manager_->advertising_data_usage()); + + preference_manager().SetInteger(prefs::kNearbySharingDataUsageName, + static_cast(DataUsage::ONLINE_DATA_USAGE)); + FlushTesting(); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); + EXPECT_EQ(DataUsage::ONLINE_DATA_USAGE, + fake_nearby_connections_manager_->advertising_data_usage()); +} + +TEST_F( + NearbySharingServiceImplTest, + UnregisterForegroundReceiveSurfaceVisibilityAllContactsRestartAdvertising) { + SetConnectionType(ConnectionType::kWifi); + preference_manager().SetInteger(prefs::kNearbySharingBackgroundVisibilityName, + static_cast(DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS)); + FlushTesting(); + + // Register both foreground and background receive surfaces + MockTransferUpdateCallback background_transfer_callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &background_transfer_callback, + NearbySharingService::ReceiveSurfaceState::kBackground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); + + MockTransferUpdateCallback foreground_transfer_callback; + result = RegisterReceiveSurface( + &foreground_transfer_callback, + NearbySharingService::ReceiveSurfaceState::kForeground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); + + // Unregister the foreground surface. Advertising is stopped and restarted + // with low power. The service reports InHighVisibility until the + // StopAdvertising callback is called. + FakeNearbyConnectionsManager::ConnectionsCallback stop_advertising_callback = + fake_nearby_connections_manager_->GetStopAdvertisingCallback(); + FakeNearbyConnectionsManager::ConnectionsCallback start_advertising_callback = + fake_nearby_connections_manager_->GetStartAdvertisingCallback(); + result = UnregisterReceiveSurface(&foreground_transfer_callback); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); + EXPECT_FALSE(service_->IsInHighVisibility()); + + std::move(stop_advertising_callback)(ConnectionsStatus::kSuccess); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); + EXPECT_FALSE(service_->IsInHighVisibility()); + + std::move(start_advertising_callback)(ConnectionsStatus::kSuccess); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); + EXPECT_FALSE(service_->IsInHighVisibility()); +} + +TEST_F(NearbySharingServiceImplTest, + NoNetworkRegisterReceiveSurfaceIsAdvertising) { + MockTransferUpdateCallback callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kForeground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + // Succeeds since bluetooth is present. + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); +} + +TEST_F(NearbySharingServiceImplTest, + NoBluetoothNoNetworkRegisterForegroundReceiveSurfaceNotAdvertising) { + SetConnectionType(ConnectionType::kNone); + SetBluetoothIsPresent(false); + + MockTransferUpdateCallback callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kForeground); + EXPECT_EQ(result, + NearbySharingService::StatusCodes::kNoAvailableConnectionMedium); + EXPECT_FALSE(fake_nearby_connections_manager_->IsAdvertising()); + EXPECT_FALSE(fake_nearby_connections_manager_->is_shutdown()); +} + +TEST_F(NearbySharingServiceImplTest, + NoBluetoothNoNetworkRegisterBackgroundReceiveSurfaceWorks) { + SetConnectionType(ConnectionType::kNone); + SetBluetoothIsPresent(false); + + MockTransferUpdateCallback callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kBackground); + EXPECT_EQ(result, + NearbySharingService::StatusCodes::kNoAvailableConnectionMedium); + EXPECT_FALSE(fake_nearby_connections_manager_->IsAdvertising()); +} + +TEST_F(NearbySharingServiceImplTest, WifiRegisterReceiveSurfaceIsAdvertising) { + SetConnectionType(ConnectionType::kWifi); + MockTransferUpdateCallback callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kForeground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); +} + +TEST_F(NearbySharingServiceImplTest, + EthernetRegisterReceiveSurfaceIsAdvertising) { + SetConnectionType(ConnectionType::kEthernet); + MockTransferUpdateCallback callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kForeground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); +} + +TEST_F(NearbySharingServiceImplTest, + ThreeGRegisterReceiveSurfaceIsAdvertising) { + SetConnectionType(ConnectionType::k3G); + MockTransferUpdateCallback callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kForeground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + // Since bluetooth is on, connection still succeeds. + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); +} + +TEST_F(NearbySharingServiceImplTest, + NoBluetoothWifiReceiveSurfaceIsAdvertising) { + SetBluetoothIsPresent(false); + SetConnectionType(ConnectionType::kWifi); + MockTransferUpdateCallback callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kForeground); + + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); +} + +TEST_F(NearbySharingServiceImplTest, + NoBluetoothEthernetReceiveSurfaceIsAdvertising) { + SetBluetoothIsPresent(false); + SetConnectionType(ConnectionType::kEthernet); + MockTransferUpdateCallback callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kForeground); + + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); +} + +TEST_F(NearbySharingServiceImplTest, + NoBluetoothThreeGReceiveSurfaceNotAdvertising) { + SetBluetoothIsPresent(false); + SetConnectionType(ConnectionType::k3G); + MockTransferUpdateCallback callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kForeground); + EXPECT_EQ(result, + NearbySharingService::StatusCodes::kNoAvailableConnectionMedium); + EXPECT_FALSE(fake_nearby_connections_manager_->IsAdvertising()); + EXPECT_FALSE(fake_nearby_connections_manager_->is_shutdown()); +} + +TEST_F(NearbySharingServiceImplTest, + DisableFeatureReceiveSurfaceNotAdvertising) { + preference_manager().SetBoolean(prefs::kNearbySharingEnabledName, false); + FlushTesting(); + SetConnectionType(ConnectionType::kWifi); + MockTransferUpdateCallback callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kForeground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_FALSE(fake_nearby_connections_manager_->IsAdvertising()); + EXPECT_TRUE(fake_nearby_connections_manager_->is_shutdown()); +} + +TEST_F(NearbySharingServiceImplTest, + DisableFeatureReceiveSurfaceStopsAdvertising) { + SetConnectionType(ConnectionType::kWifi); + MockTransferUpdateCallback callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kForeground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); + + preference_manager().SetBoolean(prefs::kNearbySharingEnabledName, false); + FlushTesting(); + EXPECT_FALSE(fake_nearby_connections_manager_->IsAdvertising()); + EXPECT_TRUE(fake_nearby_connections_manager_->is_shutdown()); +} + +TEST_F(NearbySharingServiceImplTest, + ForegroundReceiveSurfaceNoOneVisibilityIsAdvertising) { + SetConnectionType(ConnectionType::kWifi); + preference_manager().SetInteger(prefs::kNearbySharingBackgroundVisibilityName, + static_cast(DeviceVisibility::DEVICE_VISIBILITY_SELF_SHARE)); + FlushTesting(); + MockTransferUpdateCallback callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kForeground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); +} + +TEST_F(NearbySharingServiceImplTest, + BackgroundReceiveSurfaceNoOneVisibilityNotAdvertising) { + SetConnectionType(ConnectionType::kWifi); + preference_manager().SetInteger(prefs::kNearbySharingBackgroundVisibilityName, + static_cast(DeviceVisibility::DEVICE_VISIBILITY_UNSPECIFIED)); + FlushTesting(); + MockTransferUpdateCallback callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kBackground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_FALSE(fake_nearby_connections_manager_->IsAdvertising()); + EXPECT_FALSE(fake_nearby_connections_manager_->is_shutdown()); +} + +TEST_F(NearbySharingServiceImplTest, + BackgroundReceiveSurfaceVisibilityToNoOneStopsAdvertising) { + SetConnectionType(ConnectionType::kWifi); + preference_manager().SetInteger(prefs::kNearbySharingBackgroundVisibilityName, + static_cast(DeviceVisibility::DEVICE_VISIBILITY_SELECTED_CONTACTS)); + FlushTesting(); + MockTransferUpdateCallback callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kBackground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); + + preference_manager().SetInteger(prefs::kNearbySharingBackgroundVisibilityName, + static_cast(DeviceVisibility::DEVICE_VISIBILITY_UNSPECIFIED)); + FlushTesting(); + EXPECT_FALSE(fake_nearby_connections_manager_->IsAdvertising()); + EXPECT_FALSE(fake_nearby_connections_manager_->is_shutdown()); +} + +TEST_F(NearbySharingServiceImplTest, + BackgroundReceiveSurfaceVisibilityToSelectedStartsAdvertising) { + SetConnectionType(ConnectionType::kWifi); + preference_manager().SetInteger(prefs::kNearbySharingBackgroundVisibilityName, + static_cast(DeviceVisibility::DEVICE_VISIBILITY_UNSPECIFIED)); + FlushTesting(); + MockTransferUpdateCallback callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kBackground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_FALSE(fake_nearby_connections_manager_->IsAdvertising()); + EXPECT_FALSE(fake_nearby_connections_manager_->is_shutdown()); + + preference_manager().SetInteger(prefs::kNearbySharingBackgroundVisibilityName, + static_cast(DeviceVisibility::DEVICE_VISIBILITY_SELECTED_CONTACTS)); + FlushTesting(); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); +} + +TEST_F(NearbySharingServiceImplTest, + ForegroundReceiveSurfaceSelectedContactsVisibilityIsAdvertising) { + SetConnectionType(ConnectionType::kWifi); + preference_manager().SetInteger(prefs::kNearbySharingBackgroundVisibilityName, + static_cast(DeviceVisibility::DEVICE_VISIBILITY_SELECTED_CONTACTS)); + FlushTesting(); + MockTransferUpdateCallback callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kForeground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); +} + +TEST_F(NearbySharingServiceImplTest, + BackgroundReceiveSurfaceSelectedContactsVisibilityIsAdvertising) { + SetConnectionType(ConnectionType::kWifi); + preference_manager().SetInteger(prefs::kNearbySharingBackgroundVisibilityName, + static_cast(DeviceVisibility::DEVICE_VISIBILITY_SELECTED_CONTACTS)); + FlushTesting(); + MockTransferUpdateCallback callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kBackground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); +} + +TEST_F(NearbySharingServiceImplTest, + ForegroundReceiveSurfaceAllContactsVisibilityIsAdvertising) { + SetConnectionType(ConnectionType::kWifi); + preference_manager().SetInteger(prefs::kNearbySharingBackgroundVisibilityName, + static_cast(DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS)); + FlushTesting(); + MockTransferUpdateCallback callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kForeground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); +} + +TEST_F(NearbySharingServiceImplTest, + BackgroundReceiveSurfaceAllContactsVisibilityNotAdvertising) { + SetConnectionType(ConnectionType::kWifi); + preference_manager().SetInteger(prefs::kNearbySharingBackgroundVisibilityName, + static_cast(DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS)); + FlushTesting(); + MockTransferUpdateCallback callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kBackground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); +} + +TEST_F(NearbySharingServiceImplTest, UnregisterReceiveSurfaceStopsAdvertising) { + SetConnectionType(ConnectionType::kWifi); + MockTransferUpdateCallback callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kForeground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); + + NearbySharingService::StatusCodes result2 = + UnregisterReceiveSurface(&callback); + EXPECT_EQ(result2, NearbySharingService::StatusCodes::kOk); + EXPECT_FALSE(fake_nearby_connections_manager_->IsAdvertising()); + EXPECT_FALSE(fake_nearby_connections_manager_->is_shutdown()); +} + +TEST_F(NearbySharingServiceImplTest, + UnregisterReceiveSurfaceDifferentCallbackKeepAdvertising) { + SetConnectionType(ConnectionType::kWifi); + MockTransferUpdateCallback callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kForeground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); + + MockTransferUpdateCallback callback2; + NearbySharingService::StatusCodes result2 = + UnregisterReceiveSurface(&callback2); + EXPECT_EQ(result2, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); +} + +TEST_F(NearbySharingServiceImplTest, UnregisterReceiveSurfaceNeverRegistered) { + SetConnectionType(ConnectionType::kWifi); + + MockTransferUpdateCallback callback; + NearbySharingService::StatusCodes result = + UnregisterReceiveSurface(&callback); + // This is no longer considered an error condition. + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_FALSE(fake_nearby_connections_manager_->IsAdvertising()); +} + +TEST_F(NearbySharingServiceImplTest, + IncomingConnectionClosedReadingIntroduction) { + fake_nearby_connections_manager_->SetRawAuthenticationToken(kEndpointId, + GetToken()); + SetUpAdvertisementDecoder(GetValidV1EndpointInfo(), + /*return_empty_advertisement=*/false, + /*return_empty_device_name=*/false, + /*expected_number_of_calls=*/1u); + + SetConnectionType(ConnectionType::kWifi); + NiceMock callback; + EXPECT_CALL(callback, OnTransferUpdate(testing::_, testing::_)).Times(0); + + SetUpKeyVerification(/*is_incoming=*/true, + service::proto::PairedKeyResultFrame::SUCCESS); + SetUpForegroundReceiveSurface(callback); + service_->OnIncomingConnection(kEndpointId, GetValidV1EndpointInfo(), + &connection_); + ProcessLatestPublicCertificateDecryption(/*expected_num_calls=*/1, + /*success=*/true); + + // To avoid UAF in OnIncomingTransferUpdate(). + UnregisterReceiveSurface(&callback); +} + +TEST_F(NearbySharingServiceImplTest, IncomingConnectionEmptyIntroductionFrame) { + fake_nearby_connections_manager_->SetRawAuthenticationToken(kEndpointId, + GetToken()); + SetUpAdvertisementDecoder(GetValidV1EndpointInfo(), + /*return_empty_advertisement=*/false, + /*return_empty_device_name=*/false, + /*expected_number_of_calls=*/1u); + SetUpIntroductionFrameDecoder(/*return_empty_introduction_frame=*/true); + + SetConnectionType(ConnectionType::kWifi); + NiceMock callback; + EXPECT_CALL(callback, OnTransferUpdate(testing::_, testing::_)) + .WillOnce(testing::Invoke( + [](const ShareTarget& share_target, TransferMetadata metadata) { + EXPECT_TRUE(metadata.is_final_status()); + EXPECT_EQ(metadata.status(), + TransferMetadata::Status::kUnsupportedAttachmentType); + EXPECT_TRUE(share_target.is_incoming); + EXPECT_TRUE(share_target.is_known); + EXPECT_FALSE(share_target.has_attachments()); + EXPECT_EQ(share_target.device_name, kDeviceName); + EXPECT_EQ(share_target.type, kDeviceType); + EXPECT_TRUE(share_target.device_id); + EXPECT_NE(share_target.device_id, kEndpointId); + EXPECT_EQ(share_target.full_name, kTestMetadataFullName); + })); + + SetUpKeyVerification(/*is_incoming=*/true, + service::proto::PairedKeyResultFrame::SUCCESS); + SetUpForegroundReceiveSurface(callback); + service_->OnIncomingConnection(kEndpointId, GetValidV1EndpointInfo(), + &connection_); + ProcessLatestPublicCertificateDecryption(/*expected_num_calls=*/1, + /*success=*/true); + // Check data written to connection_. + ASSERT_TRUE(ExpectPairedKeyEncryptionFrame()); + ASSERT_TRUE(ExpectPairedKeyResultFrame()); + ASSERT_TRUE(ExpectConnectionResponseFrame( + service::proto::ConnectionResponseFrame::UNSUPPORTED_ATTACHMENT_TYPE)); + + // To avoid UAF in OnIncomingTransferUpdate(). + UnregisterReceiveSurface(&callback); +} + +TEST_F(NearbySharingServiceImplTest, + IncomingConnectionValidIntroductionFrameInvalidCertificate) { + fake_nearby_connections_manager_->SetRawAuthenticationToken(kEndpointId, + GetToken()); + SetUpAdvertisementDecoder(GetValidV1EndpointInfo(), + /*return_empty_advertisement=*/false, + /*return_empty_device_name=*/false, + /*expected_number_of_calls=*/1u); + SetUpIntroductionFrameDecoder(/*return_empty_introduction_frame=*/false); + + SetConnectionType(ConnectionType::kWifi); + NiceMock callback; + + EXPECT_CALL(callback, OnTransferUpdate(testing::_, testing::_)) + .WillOnce(testing::Invoke( + [](const ShareTarget& share_target, TransferMetadata metadata) { + EXPECT_FALSE(metadata.is_final_status()); + EXPECT_EQ(TransferMetadata::Status::kAwaitingLocalConfirmation, + metadata.status()); + EXPECT_TRUE(share_target.is_incoming); + EXPECT_FALSE(share_target.is_known); + EXPECT_TRUE(share_target.has_attachments()); + EXPECT_EQ(share_target.text_attachments.size(), 3u); + EXPECT_EQ(share_target.file_attachments.size(), 1u); + EXPECT_EQ(share_target.device_name, kDeviceName); + EXPECT_FALSE(share_target.image_url); + EXPECT_EQ(share_target.type, kDeviceType); + EXPECT_EQ(share_target.device_id, kEndpointId); + EXPECT_FALSE(share_target.full_name); + })); + + SetUpKeyVerification(/*is_incoming=*/true, + service::proto::PairedKeyResultFrame::SUCCESS); + SetUpForegroundReceiveSurface(callback); + service_->OnIncomingConnection(kEndpointId, GetValidV1EndpointInfo(), + &connection_); + ProcessLatestPublicCertificateDecryption(/*expected_num_calls=*/1, + /*success=*/false); + EXPECT_FALSE(connection_.IsClosed()); + + // To avoid UAF in OnIncomingTransferUpdate(). + UnregisterReceiveSurface(&callback); +} + +TEST_F(NearbySharingServiceImplTest, IncomingConnectionTimedOut) { + NiceMock callback; + ShareTarget share_target = SetUpIncomingConnection(callback); + EXPECT_FALSE(connection_.IsClosed()); + + EXPECT_CALL(callback, OnTransferUpdate(testing::_, testing::_)) + .WillOnce(testing::Invoke( + [](const ShareTarget& share_target, TransferMetadata metadata) { + EXPECT_TRUE(metadata.is_final_status()); + EXPECT_EQ(metadata.status(), TransferMetadata::Status::kTimedOut); + })); + + FastForward(kReadResponseFrameTimeout); + + // Waits for delay to close connection. + FastForward(kIncomingRejectionDelay); + EXPECT_TRUE(connection_.IsClosed()); +} + +TEST_F(NearbySharingServiceImplTest, + IncomingConnectionClosedWaitingLocalConfirmation) { + NiceMock callback; + ShareTarget share_target = SetUpIncomingConnection(callback); + + EXPECT_CALL(callback, OnTransferUpdate(testing::_, testing::_)) + .WillOnce(testing::Invoke( + [](const ShareTarget& share_target, TransferMetadata metadata) { + EXPECT_TRUE(metadata.is_final_status()); + EXPECT_EQ(metadata.status(), + TransferMetadata::Status::kUnexpectedDisconnection); + })); + + connection_.Close(); + + // To avoid UAF in OnIncomingTransferUpdate(). + UnregisterReceiveSurface(&callback); +} + +TEST_F(NearbySharingServiceImplTest, IncomingConnectionOutOfStorage) { + SetDiskSpace(kFreeDiskSpace); + preference_manager().SetString( + prefs::kNearbySharingCustomSavePath, + GetCompatibleU8String(fake_device_info_.GetDownloadPath().u8string())); + fake_nearby_connections_manager_->SetRawAuthenticationToken(kEndpointId, + GetToken()); + SetUpAdvertisementDecoder(GetValidV1EndpointInfo(), + /*return_empty_advertisement=*/false, + /*return_empty_device_name=*/false, + /*expected_number_of_calls=*/1u); + + // Set a huge file size in introduction frame to go out of storage. + std::string intro = "introduction_frame"; + std::vector bytes(intro.begin(), intro.end()); + EXPECT_CALL(fake_decoder_, DecodeFrame(testing::Eq(bytes))) + .WillOnce(testing::Invoke([=](absl::Span data) { + IntroductionFrame* introduction_frame = + IntroductionFrame::default_instance().New(); + auto file_metadatas = introduction_frame->mutable_file_metadata(); + nearby::sharing::service::proto::FileMetadata* file_metadata = + nearby::sharing::service::proto::FileMetadata::default_instance() + .New(); + file_metadata->set_name("name"); + file_metadata->set_type( + nearby::sharing::service::proto::FileMetadata::AUDIO); + file_metadata->set_payload_id(1); + file_metadata->set_size(kFreeDiskSpace + 1); + file_metadata->set_mime_type("mime type"); + file_metadata->set_id(123); + file_metadatas->AddAllocated(file_metadata); + + V1Frame* v1_frame = V1Frame::default_instance().New(); + v1_frame->set_type(V1Frame::INTRODUCTION); + v1_frame->set_allocated_introduction(introduction_frame); + + Frame* frame = Frame::default_instance().New(); + frame->set_version(Frame::V1); + frame->set_allocated_v1(v1_frame); + return std::unique_ptr(frame); + })); + connection_.AppendReadableData(std::move(bytes)); + FlushTesting(); + + SetConnectionType(ConnectionType::kWifi); + NiceMock callback; + EXPECT_CALL(callback, OnTransferUpdate(testing::_, testing::_)) + .WillOnce(testing::Invoke([](const ShareTarget& share_target, + TransferMetadata metadata) { + EXPECT_TRUE(share_target.is_incoming); + EXPECT_TRUE(share_target.is_known); + EXPECT_TRUE(share_target.has_attachments()); + EXPECT_EQ(share_target.text_attachments.size(), 0u); + EXPECT_EQ(share_target.file_attachments.size(), 1u); + EXPECT_EQ(share_target.device_name, kDeviceName); + EXPECT_EQ(share_target.type, kDeviceType); + EXPECT_TRUE(share_target.device_id); + EXPECT_NE(share_target.device_id, kEndpointId); + EXPECT_EQ(share_target.full_name, kTestMetadataFullName); + EXPECT_EQ(metadata.status(), TransferMetadata::Status::kNotEnoughSpace); + })); + + SetUpKeyVerification( + /*is_incoming=*/true, PairedKeyResultFrame::SUCCESS); + SetUpForegroundReceiveSurface(callback); + service_->OnIncomingConnection(kEndpointId, GetValidV1EndpointInfo(), + &connection_); + ProcessLatestPublicCertificateDecryption(/*expected_num_calls=*/1, + /*success=*/true); + // To avoid UAF in OnIncomingTransferUpdate(). + UnregisterReceiveSurface(&callback); + ResetDiskSpace(); +} + +TEST_F(NearbySharingServiceImplTest, IncomingConnectionFileSizeOverflow) { + fake_nearby_connections_manager_->SetRawAuthenticationToken(kEndpointId, + GetToken()); + SetUpAdvertisementDecoder(GetValidV1EndpointInfo(), + /*return_empty_advertisement=*/false, + /*return_empty_device_name=*/false, + /*expected_number_of_calls=*/1u); + + // Set file size sum huge to check for overflow. + std::string intro = "introduction_frame"; + std::vector bytes(intro.begin(), intro.end()); + EXPECT_CALL(fake_decoder_, DecodeFrame(testing::Eq(bytes))) + .WillOnce(testing::Invoke([=](absl::Span data) { + IntroductionFrame* introduction_frame = + IntroductionFrame::default_instance().New(); + auto file_metadatas = introduction_frame->mutable_file_metadata(); + nearby::sharing::service::proto::FileMetadata* file_metadata = + nearby::sharing::service::proto::FileMetadata::default_instance() + .New(); + file_metadata->set_name("name_1"); + file_metadata->set_type( + nearby::sharing::service::proto::FileMetadata::AUDIO); + file_metadata->set_payload_id(1); + file_metadata->set_size(std::numeric_limits::max()); + file_metadata->set_mime_type("mime type"); + file_metadata->set_id(123); + file_metadatas->AddAllocated(file_metadata); + nearby::sharing::service::proto::FileMetadata* file2_metadata = + nearby::sharing::service::proto::FileMetadata::default_instance() + .New(); + file2_metadata->set_name("name_2"); + file2_metadata->set_type( + nearby::sharing::service::proto::FileMetadata::VIDEO); + file2_metadata->set_payload_id(2); + file2_metadata->set_size(100); + file2_metadata->set_mime_type("mime type"); + file2_metadata->set_id(124); + file_metadatas->AddAllocated(file2_metadata); + + V1Frame* v1_frame = V1Frame::default_instance().New(); + v1_frame->set_type(V1Frame::INTRODUCTION); + v1_frame->set_allocated_introduction(introduction_frame); + + Frame* frame = Frame::default_instance().New(); + frame->set_version(Frame::V1); + frame->set_allocated_v1(v1_frame); + return std::unique_ptr(frame); + })); + connection_.AppendReadableData(std::move(bytes)); + FlushTesting(); + + SetConnectionType(ConnectionType::kWifi); + NiceMock callback; + + EXPECT_CALL(callback, OnTransferUpdate(testing::_, testing::_)) + .WillOnce(testing::Invoke([](const ShareTarget& share_target, + TransferMetadata metadata) { + EXPECT_TRUE(share_target.is_incoming); + EXPECT_TRUE(share_target.is_known); + EXPECT_EQ(share_target.device_name, kDeviceName); + EXPECT_EQ(share_target.type, kDeviceType); + EXPECT_TRUE(share_target.device_id); + EXPECT_NE(share_target.device_id, kEndpointId); + EXPECT_EQ(share_target.full_name, kTestMetadataFullName); + EXPECT_EQ(metadata.status(), TransferMetadata::Status::kNotEnoughSpace); + })); + + SetUpKeyVerification( + /*is_incoming=*/true, PairedKeyResultFrame::SUCCESS); + SetUpForegroundReceiveSurface(callback); + service_->OnIncomingConnection(kEndpointId, GetValidV1EndpointInfo(), + &connection_); + ProcessLatestPublicCertificateDecryption(/*expected_num_calls=*/1, + /*success=*/true); + // To avoid UAF in OnIncomingTransferUpdate(). + UnregisterReceiveSurface(&callback); +} + +TEST_F(NearbySharingServiceImplTest, + IncomingConnectionValidIntroductionFrameValidCertificate) { + fake_nearby_connections_manager_->SetRawAuthenticationToken(kEndpointId, + GetToken()); + SetUpAdvertisementDecoder(GetValidV1EndpointInfo(), + /*return_empty_advertisement=*/false, + /*return_empty_device_name=*/false, + /*expected_number_of_calls=*/1u); + SetUpIntroductionFrameDecoder(/*return_empty_introduction_frame=*/false); + + SetConnectionType(ConnectionType::kWifi); + NiceMock callback; + absl::Notification notification; + EXPECT_CALL(callback, OnTransferUpdate(testing::_, testing::_)) + .WillOnce(testing::Invoke([¬ification](const ShareTarget& share_target, + TransferMetadata metadata) { + EXPECT_FALSE(metadata.is_final_status()); + EXPECT_EQ(TransferMetadata::Status::kAwaitingLocalConfirmation, + metadata.status()); + EXPECT_TRUE(share_target.is_incoming); + EXPECT_TRUE(share_target.is_known); + EXPECT_TRUE(share_target.has_attachments()); + EXPECT_EQ(share_target.text_attachments.size(), 3u); + EXPECT_EQ(share_target.file_attachments.size(), 1u); + EXPECT_EQ(share_target.device_name, kDeviceName); + EXPECT_EQ(share_target.type, kDeviceType); + EXPECT_TRUE(share_target.device_id); + EXPECT_NE(share_target.device_id, kEndpointId); + EXPECT_EQ(share_target.full_name, kTestMetadataFullName); + + EXPECT_FALSE(metadata.token().has_value()); + notification.Notify(); + })); + + SetUpKeyVerification(/*is_incoming=*/true, PairedKeyResultFrame::SUCCESS); + SetUpForegroundReceiveSurface(callback); + service_->OnIncomingConnection(kEndpointId, GetValidV1EndpointInfo(), + &connection_); + ProcessLatestPublicCertificateDecryption(/*expected_num_calls=*/1, + /*success=*/true); + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + + EXPECT_FALSE(connection_.IsClosed()); + + // To avoid UAF in OnIncomingTransferUpdate(). + UnregisterReceiveSurface(&callback); +} + +TEST_F(NearbySharingServiceImplTest, AcceptInvalidShareTarget) { + ShareTarget share_target; + absl::Notification notification; + service_->Accept( + share_target, [&](NearbySharingServiceImpl::StatusCodes status_code) { + EXPECT_EQ(status_code, + NearbySharingServiceImpl::StatusCodes::kOutOfOrderApiCall); + notification.Notify(); + }); + + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); +} + +TEST_F(NearbySharingServiceImplTest, + AcceptValidShareTargetRegisterPayloadError) { + fake_nearby_connections_manager_->SetPayloadPathStatus(kFilePayloadId, + Status::kError); + NiceMock callback; + ShareTarget share_target = SetUpIncomingConnection(callback); + + absl::Notification notification; + service_->Accept( + share_target, [&](NearbySharingServiceImpl::StatusCodes status_code) { + EXPECT_EQ(NearbySharingServiceImpl::StatusCodes::kError, status_code); + notification.Notify(); + }); + + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + + EXPECT_TRUE( + fake_nearby_connections_manager_->DidUpgradeBandwidth(kEndpointId)); + + // Check data written to connection_. + EXPECT_TRUE(ExpectPairedKeyEncryptionFrame()); + EXPECT_TRUE(ExpectPairedKeyResultFrame()); + + EXPECT_FALSE(connection_.IsClosed()); + + { + std::optional path = + fake_nearby_connections_manager_->GetRegisteredPayloadPath( + kFilePayloadId); + EXPECT_TRUE(path.has_value()); + std::filesystem::remove(*path); + } + + // To avoid UAF in OnIncomingTransferUpdate(). + UnregisterReceiveSurface(&callback); +} + +TEST_F(NearbySharingServiceImplTest, AcceptValidShareTarget) { + for (int64_t payload_id : GetValidIntroductionFramePayloadIds()) { + fake_nearby_connections_manager_->SetPayloadPathStatus(payload_id, + Status::kSuccess); + } + + NiceMock callback; + ShareTarget share_target = SetUpIncomingConnection(callback); + + absl::Notification notification; + EXPECT_CALL(callback, OnTransferUpdate(testing::_, testing::_)) + .WillOnce(testing::Invoke( + [](const ShareTarget& share_target, TransferMetadata metadata) { + EXPECT_FALSE(metadata.is_final_status()); + EXPECT_EQ(metadata.status(), + TransferMetadata::Status::kAwaitingRemoteAcceptance); + })); + + service_->Accept( + share_target, [&](NearbySharingServiceImpl::StatusCodes status_code) { + EXPECT_EQ(status_code, NearbySharingServiceImpl::StatusCodes::kOk); + notification.Notify(); + }); + + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + + EXPECT_TRUE( + fake_nearby_connections_manager_->DidUpgradeBandwidth(kEndpointId)); + + // Check data written to connection_. + EXPECT_TRUE(ExpectPairedKeyEncryptionFrame()); + EXPECT_TRUE(ExpectPairedKeyResultFrame()); + EXPECT_TRUE(ExpectConnectionResponseFrame( + service::proto::ConnectionResponseFrame::ACCEPT)); + + EXPECT_FALSE(connection_.IsClosed()); + + // To avoid UAF in OnIncomingTransferUpdate(). + UnregisterReceiveSurface(&callback); +} + +TEST_F(NearbySharingServiceImplTest, AcceptValidShareTargetPayloadSuccessful) { + SuccessfullyReceiveTransfer(); +} + +TEST_F(NearbySharingServiceImplTest, + AcceptValidShareTargetPayloadSuccessfulIncomingPayloadNotFound) { + for (int64_t payload_id : GetValidIntroductionFramePayloadIds()) { + fake_nearby_connections_manager_->SetPayloadPathStatus(payload_id, + Status::kSuccess); + } + + NiceMock callback; + ShareTarget share_target = SetUpIncomingConnection(callback); + + absl::Notification notification; + EXPECT_CALL(callback, OnTransferUpdate(testing::_, testing::_)) + .WillOnce(testing::Invoke( + [](const ShareTarget& share_target, TransferMetadata metadata) { + EXPECT_FALSE(metadata.is_final_status()); + EXPECT_EQ(metadata.status(), + TransferMetadata::Status::kAwaitingRemoteAcceptance); + })); + + service_->Accept( + share_target, [&](NearbySharingServiceImpl::StatusCodes status_code) { + EXPECT_EQ(status_code, NearbySharingServiceImpl::StatusCodes::kOk); + notification.Notify(); + }); + + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + + fake_nearby_connections_manager_->SetIncomingPayload( + kFilePayloadId, GetFilePayload(kFilePayloadId)); + + for (int64_t id : GetValidIntroductionFramePayloadIds()) { + // Update file payload at the end. + if (id == kFilePayloadId) continue; + + // Deliberately not calling SetIncomingPayload() for text payloads to check + // for failure condition. + + std::weak_ptr listener = + fake_nearby_connections_manager_->GetRegisteredPayloadStatusListener( + id); + ASSERT_FALSE(listener.expired()); + + absl::Notification progress_notification; + EXPECT_CALL(callback, OnTransferUpdate(testing::_, testing::_)) + .WillOnce(testing::Invoke([&](const ShareTarget& share_target, + TransferMetadata metadata) { + EXPECT_FALSE(metadata.is_final_status()); + EXPECT_EQ(metadata.status(), TransferMetadata::Status::kInProgress); + progress_notification.Notify(); + })); + + auto payload = std::make_unique( + id, PayloadStatus::kSuccess, + /*total_bytes=*/kPayloadSize, + /*bytes_transferred=*/kPayloadSize); + if (auto locked_listener = listener.lock()) { + locked_listener->OnStatusUpdate(std::move(payload), + /*upgraded_medium=*/std::nullopt); + } + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + FastForward(kMinProgressUpdateFrequency); + } + + absl::Notification success_notification; + EXPECT_CALL(callback, OnTransferUpdate(testing::_, testing::_)) + .WillOnce(testing::Invoke( + [&](const ShareTarget& share_target, TransferMetadata metadata) { + EXPECT_TRUE(metadata.is_final_status()); + EXPECT_EQ(metadata.status(), + TransferMetadata::Status::kIncompletePayloads); + ASSERT_TRUE(share_target.has_attachments()); + EXPECT_EQ(share_target.file_attachments.size(), 1u); + const FileAttachment& file = share_target.file_attachments[0]; + EXPECT_FALSE(file.file_path()); + success_notification.Notify(); + })); + + std::weak_ptr listener = + fake_nearby_connections_manager_->GetRegisteredPayloadStatusListener( + kFilePayloadId); + ASSERT_FALSE(listener.expired()); + + auto payload = std::make_unique( + kFilePayloadId, PayloadStatus::kSuccess, + /*total_bytes=*/kPayloadSize, + /*bytes_transferred=*/kPayloadSize); + if (auto locked_listener = listener.lock()) { + locked_listener->OnStatusUpdate(std::move(payload), + /*upgraded_medium=*/std::nullopt); + } + EXPECT_TRUE( + success_notification.WaitForNotificationWithTimeout(kWaitTimeout)); + + EXPECT_FALSE( + fake_nearby_connections_manager_->connection_endpoint_info(kEndpointId)); + EXPECT_FALSE(fake_nearby_connections_manager_->has_incoming_payloads()); + + // File deletion runs in a ThreadPool. + EXPECT_TRUE( + FakeTaskRunner::WaitForRunningTasksWithTimeout(absl::Milliseconds(200))); + + // To avoid UAF in OnIncomingTransferUpdate(). + UnregisterReceiveSurface(&callback); +} + +TEST_F(NearbySharingServiceImplTest, AcceptValidShareTargetPayloadFailed) { + for (int64_t payload_id : GetValidIntroductionFramePayloadIds()) { + fake_nearby_connections_manager_->SetPayloadPathStatus(payload_id, + Status::kSuccess); + } + + NiceMock callback; + ShareTarget share_target = SetUpIncomingConnection(callback); + + absl::Notification notification; + EXPECT_CALL(callback, OnTransferUpdate(testing::_, testing::_)) + .WillOnce(testing::Invoke( + [](const ShareTarget& share_target, TransferMetadata metadata) { + EXPECT_FALSE(metadata.is_final_status()); + EXPECT_EQ(metadata.status(), + TransferMetadata::Status::kAwaitingRemoteAcceptance); + })); + + service_->Accept( + share_target, [&](NearbySharingServiceImpl::StatusCodes status_code) { + EXPECT_EQ(status_code, NearbySharingServiceImpl::StatusCodes::kOk); + notification.Notify(); + }); + + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + + std::weak_ptr listener = + fake_nearby_connections_manager_->GetRegisteredPayloadStatusListener( + kFilePayloadId); + ASSERT_FALSE(listener.expired()); + + absl::Notification failure_notification; + EXPECT_CALL(callback, OnTransferUpdate(testing::_, testing::_)) + .WillOnce(testing::Invoke( + [&](const ShareTarget& share_target, TransferMetadata metadata) { + EXPECT_TRUE(metadata.is_final_status()); + EXPECT_EQ(metadata.status(), TransferMetadata::Status::kFailed); + ASSERT_TRUE(share_target.has_attachments()); + EXPECT_EQ(share_target.file_attachments.size(), 1u); + const FileAttachment& file = share_target.file_attachments[0]; + EXPECT_FALSE(file.file_path()); + failure_notification.Notify(); + })); + + auto payload = std::make_unique( + kFilePayloadId, PayloadStatus::kFailure, + /*total_bytes=*/kPayloadSize, + /*bytes_transferred=*/kPayloadSize); + if (auto locked_listener = listener.lock()) { + locked_listener->OnStatusUpdate(std::move(payload), + /*upgraded_medium=*/std::nullopt); + } + + EXPECT_TRUE( + failure_notification.WaitForNotificationWithTimeout(kWaitTimeout)); + + EXPECT_FALSE( + fake_nearby_connections_manager_->connection_endpoint_info(kEndpointId)); + EXPECT_FALSE(fake_nearby_connections_manager_->has_incoming_payloads()); + + // File deletion runs in a ThreadPool. + EXPECT_TRUE( + FakeTaskRunner::WaitForRunningTasksWithTimeout(absl::Milliseconds(200))); + + // To avoid UAF in OnIncomingTransferUpdate(). + UnregisterReceiveSurface(&callback); +} + +TEST_F(NearbySharingServiceImplTest, AcceptValidShareTargetPayloadCancelled) { + for (int64_t payload_id : GetValidIntroductionFramePayloadIds()) { + fake_nearby_connections_manager_->SetPayloadPathStatus(payload_id, + Status::kSuccess); + } + + NiceMock callback; + ShareTarget share_target = SetUpIncomingConnection(callback); + + absl::Notification notification; + EXPECT_CALL(callback, OnTransferUpdate(testing::_, testing::_)) + .WillOnce(testing::Invoke( + [](const ShareTarget& share_target, TransferMetadata metadata) { + EXPECT_FALSE(metadata.is_final_status()); + EXPECT_EQ(metadata.status(), + TransferMetadata::Status::kAwaitingRemoteAcceptance); + })); + + service_->Accept( + share_target, [&](NearbySharingServiceImpl::StatusCodes status_code) { + EXPECT_EQ(status_code, NearbySharingServiceImpl::StatusCodes::kOk); + notification.Notify(); + }); + + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + + std::weak_ptr listener = + fake_nearby_connections_manager_->GetRegisteredPayloadStatusListener( + kFilePayloadId); + ASSERT_FALSE(listener.expired()); + + absl::Notification failure_notification; + EXPECT_CALL(callback, OnTransferUpdate(testing::_, testing::_)) + .WillOnce(testing::Invoke( + [&](const ShareTarget& share_target, TransferMetadata metadata) { + EXPECT_TRUE(metadata.is_final_status()); + EXPECT_EQ(metadata.status(), TransferMetadata::Status::kCancelled); + ASSERT_TRUE(share_target.has_attachments()); + EXPECT_EQ(share_target.file_attachments.size(), 1u); + const FileAttachment& file = share_target.file_attachments[0]; + EXPECT_FALSE(file.file_path()); + failure_notification.Notify(); + })); + + auto payload = std::make_unique( + kFilePayloadId, PayloadStatus::kCanceled, + /*total_bytes=*/kPayloadSize, + /*bytes_transferred=*/kPayloadSize); + if (auto locked_listener = listener.lock()) { + locked_listener->OnStatusUpdate(std::move(payload), + /*upgraded_medium=*/std::nullopt); + } + EXPECT_TRUE( + failure_notification.WaitForNotificationWithTimeout(kWaitTimeout)); + + EXPECT_FALSE( + fake_nearby_connections_manager_->connection_endpoint_info(kEndpointId)); + EXPECT_FALSE(fake_nearby_connections_manager_->has_incoming_payloads()); + + // File deletion runs in a ThreadPool. + EXPECT_TRUE( + FakeTaskRunner::WaitForRunningTasksWithTimeout(absl::Milliseconds(200))); + + // To avoid UAF in OnIncomingTransferUpdate(). + UnregisterReceiveSurface(&callback); +} + +TEST_F(NearbySharingServiceImplTest, RejectInvalidShareTarget) { + ShareTarget share_target; + absl::Notification notification; + service_->Reject( + share_target, [&](NearbySharingServiceImpl::StatusCodes status_code) { + EXPECT_EQ(status_code, + NearbySharingServiceImpl::StatusCodes::kOutOfOrderApiCall); + notification.Notify(); + }); + + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); +} + +TEST_F(NearbySharingServiceImplTest, RejectValidShareTarget) { + NiceMock callback; + ShareTarget share_target = SetUpIncomingConnection(callback); + + absl::Notification notification; + EXPECT_CALL(callback, OnTransferUpdate(testing::_, testing::_)) + .WillOnce(testing::Invoke( + [](const ShareTarget& share_target, TransferMetadata metadata) { + EXPECT_TRUE(metadata.is_final_status()); + EXPECT_EQ(metadata.status(), TransferMetadata::Status::kRejected); + })); + + service_->Reject( + share_target, [&](NearbySharingServiceImpl::StatusCodes status_code) { + EXPECT_EQ(status_code, NearbySharingServiceImpl::StatusCodes::kOk); + notification.Notify(); + }); + + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + + // Check data written to connection_. + EXPECT_TRUE(ExpectPairedKeyEncryptionFrame()); + EXPECT_TRUE(ExpectPairedKeyResultFrame()); + EXPECT_TRUE(ExpectConnectionResponseFrame(ConnectionResponseFrame::REJECT)); + + FastForward(kIncomingRejectionDelay + kDelta); + EXPECT_TRUE(connection_.IsClosed()); + + // To avoid UAF in OnIncomingTransferUpdate(). + UnregisterReceiveSurface(&callback); +} + +TEST_F(NearbySharingServiceImplTest, + IncomingConnectionKeyVerificationRunnerStatusUnable) { + fake_nearby_connections_manager_->SetRawAuthenticationToken(kEndpointId, + GetToken()); + SetUpAdvertisementDecoder(GetValidV1EndpointInfo(), + /*return_empty_advertisement=*/false, + /*return_empty_device_name=*/false, + /*expected_number_of_calls=*/1u); + SetUpIntroductionFrameDecoder(/*return_empty_introduction_frame=*/false); + + SetConnectionType(ConnectionType::kWifi); + NiceMock callback; + absl::Notification notification; + EXPECT_CALL(callback, OnTransferUpdate(testing::_, testing::_)) + .WillOnce(testing::Invoke( + [&](const ShareTarget& share_target, TransferMetadata metadata) { + EXPECT_FALSE(metadata.is_final_status()); + EXPECT_EQ(metadata.status(), + TransferMetadata::Status::kAwaitingLocalConfirmation); + EXPECT_TRUE(share_target.is_incoming); + EXPECT_TRUE(share_target.is_known); + EXPECT_TRUE(share_target.has_attachments()); + EXPECT_EQ(share_target.text_attachments.size(), 3u); + EXPECT_EQ(share_target.file_attachments.size(), 1u); + EXPECT_EQ(share_target.device_name, kDeviceName); + EXPECT_EQ(share_target.type, kDeviceType); + EXPECT_TRUE(share_target.device_id); + EXPECT_NE(share_target.device_id, kEndpointId); + EXPECT_EQ(share_target.full_name, kTestMetadataFullName); + EXPECT_EQ(metadata.token(), kFourDigitToken); + notification.Notify(); + })); + + SetUpKeyVerification(/*is_incoming=*/true, PairedKeyResultFrame::UNABLE); + SetUpForegroundReceiveSurface(callback); + + service_->OnIncomingConnection(kEndpointId, GetValidV1EndpointInfo(), + &connection_); + ProcessLatestPublicCertificateDecryption(/*expected_num_calls=*/1, + /*success=*/true); + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + + EXPECT_TRUE( + fake_nearby_connections_manager_->DidUpgradeBandwidth(kEndpointId)); + + EXPECT_FALSE(connection_.IsClosed()); + + // To avoid UAF in OnIncomingTransferUpdate(). + UnregisterReceiveSurface(&callback); +} + +TEST_F(NearbySharingServiceImplTest, + IncomingConnectionKeyVerificationRunnerStatusUnableLowPower) { + fake_nearby_connections_manager_->SetRawAuthenticationToken(kEndpointId, + GetToken()); + SetUpAdvertisementDecoder(GetValidV1EndpointInfo(), + /*return_empty_advertisement=*/false, + /*return_empty_device_name=*/false, + /*expected_number_of_calls=*/1u); + SetUpIntroductionFrameDecoder(/*return_empty_introduction_frame=*/false); + + SetConnectionType(ConnectionType::kWifi); + NiceMock callback; + absl::Notification notification; + EXPECT_CALL(callback, OnTransferUpdate(testing::_, testing::_)) + .WillOnce(testing::Invoke( + [&](const ShareTarget& share_target, TransferMetadata metadata) { + EXPECT_FALSE(metadata.is_final_status()); + EXPECT_EQ(TransferMetadata::Status::kAwaitingLocalConfirmation, + metadata.status()); + EXPECT_TRUE(share_target.is_incoming); + EXPECT_TRUE(share_target.is_known); + EXPECT_TRUE(share_target.has_attachments()); + EXPECT_EQ(share_target.text_attachments.size(), 3u); + EXPECT_EQ(share_target.file_attachments.size(), 1u); + EXPECT_EQ(share_target.device_name, kDeviceName); + EXPECT_EQ(share_target.type, kDeviceType); + EXPECT_TRUE(share_target.device_id); + EXPECT_NE(share_target.device_id, kEndpointId); + EXPECT_EQ(share_target.full_name, kTestMetadataFullName); + + EXPECT_EQ(kFourDigitToken, metadata.token()); + notification.Notify(); + })); + + SetUpKeyVerification(/*is_incoming=*/true, PairedKeyResultFrame::UNABLE); + + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kBackground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); + + service_->OnIncomingConnection(kEndpointId, GetValidV1EndpointInfo(), + &connection_); + ProcessLatestPublicCertificateDecryption(/*expected_num_calls=*/1, + /*success=*/true); + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + + EXPECT_TRUE( + fake_nearby_connections_manager_->DidUpgradeBandwidth(kEndpointId)); + + EXPECT_FALSE(connection_.IsClosed()); + + // To avoid UAF in OnIncomingTransferUpdate(). + UnregisterReceiveSurface(&callback); +} + +TEST_F(NearbySharingServiceImplTest, + IncomingConnectionKeyVerificationRunnerStatusFail) { + fake_nearby_connections_manager_->SetRawAuthenticationToken(kEndpointId, + GetToken()); + SetUpAdvertisementDecoder(GetValidV1EndpointInfo(), + /*return_empty_advertisement=*/false, + /*return_empty_device_name=*/false, + /*expected_number_of_calls=*/1u); + + SetConnectionType(ConnectionType::kWifi); + NiceMock callback; + + SetUpKeyVerification(/*is_incoming=*/true, PairedKeyResultFrame::FAIL); + SetUpForegroundReceiveSurface(callback); + + // Ensures that introduction is never received for failed key verification. + std::string intro = "introduction_frame"; + std::vector bytes(intro.begin(), intro.end()); + EXPECT_CALL(fake_decoder_, DecodeFrame(testing::Eq(bytes))).Times(0); + connection_.AppendReadableData(bytes); + FlushTesting(); + + service_->OnIncomingConnection(kEndpointId, GetValidV1EndpointInfo(), + &connection_); + ProcessLatestPublicCertificateDecryption(/*expected_num_calls=*/1, + /*success=*/true); + + // Ensure that the messages sent by ProcessLatestPublicCertificateDecryption + // are processed prior to checking if connection is closed. + EXPECT_TRUE( + FakeTaskRunner::WaitForRunningTasksWithTimeout(absl::Milliseconds(200))); + EXPECT_TRUE(connection_.IsClosed()); + + // To avoid UAF in OnIncomingTransferUpdate(). + UnregisterReceiveSurface(&callback); +} + +TEST_F(NearbySharingServiceImplTest, + IncomingConnectionEmptyAuthTokenKeyVerificationRunnerStatusFail) { + SetUpAdvertisementDecoder(GetValidV1EndpointInfo(), + /*return_empty_advertisement=*/false, + /*return_empty_device_name=*/false, + /*expected_number_of_calls=*/1u); + + SetConnectionType(ConnectionType::kWifi); + NiceMock callback; + + SetUpForegroundReceiveSurface(callback); + + // Ensures that introduction is never received for empty auth token. + std::string intro = "introduction_frame"; + std::vector bytes(intro.begin(), intro.end()); + EXPECT_CALL(fake_decoder_, DecodeFrame(testing::Eq(bytes))).Times(0); + connection_.AppendReadableData(bytes); + FlushTesting(); + + service_->OnIncomingConnection(kEndpointId, GetValidV1EndpointInfo(), + &connection_); + ProcessLatestPublicCertificateDecryption(/*expected_num_calls=*/1, + /*success=*/true); + + EXPECT_TRUE(connection_.IsClosed()); + + // To avoid UAF in OnIncomingTransferUpdate(). + UnregisterReceiveSurface(&callback); +} + +TEST_F(NearbySharingServiceImplTest, RegisterReceiveSurfaceAlreadyReceiving) { + NiceMock callback; + ShareTarget share_target = SetUpIncomingConnection(callback); + EXPECT_FALSE(connection_.IsClosed()); + + EXPECT_EQ( + RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kForeground), + NearbySharingService::StatusCodes::kOk); + EXPECT_FALSE(fake_nearby_connections_manager_->IsDiscovering()); + EXPECT_FALSE(fake_nearby_connections_manager_->is_shutdown()); + + // To avoid UAF in OnIncomingTransferUpdate(). + UnregisterReceiveSurface(&callback); +} + +TEST_F(NearbySharingServiceImplTest, RegisterReceiveSurfaceWhileDiscovering) { + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + EXPECT_EQ(RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kForeground), + NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsDiscovering()); + + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &transfer_callback, + NearbySharingService::ReceiveSurfaceState::kForeground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); +} + +TEST_F(NearbySharingServiceImplTest, SendAttachmentsWithoutAttachments) { + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + ShareTarget target = + DiscoverShareTarget(transfer_callback, discovery_callback); + + EXPECT_EQ(SendAttachments(target, /*attachments=*/{}), + NearbySharingServiceImpl::StatusCodes::kError); + + UnregisterSendSurface(&transfer_callback, &discovery_callback); +} + +TEST_F(NearbySharingServiceImplTest, RegisterReceiveSurfaceWhileSending) { + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + ShareTarget target = + SetUpOutgoingShareTarget(transfer_callback, discovery_callback); + + absl::Notification notification; + ExpectTransferUpdates(transfer_callback, target, + {TransferMetadata::Status::kConnecting, + TransferMetadata::Status::kAwaitingLocalConfirmation, + TransferMetadata::Status::kAwaitingRemoteAcceptance}, + [&]() { notification.Notify(); }); + EXPECT_EQ(SendAttachments(target, CreateTextAttachments({kTextPayload})), + NearbySharingServiceImpl::StatusCodes::kOk); + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &transfer_callback, + NearbySharingService::ReceiveSurfaceState::kForeground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + + UnregisterSendSurface(&transfer_callback, &discovery_callback); +} + +TEST_F(NearbySharingServiceImplTest, SendTextAlreadySending) { + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + ShareTarget target = + SetUpOutgoingShareTarget(transfer_callback, discovery_callback); + + absl::Notification notification; + ExpectTransferUpdates(transfer_callback, target, + {TransferMetadata::Status::kConnecting, + TransferMetadata::Status::kAwaitingLocalConfirmation, + TransferMetadata::Status::kAwaitingRemoteAcceptance}, + [&]() { notification.Notify(); }); + EXPECT_EQ(SendAttachments(target, CreateTextAttachments({kTextPayload})), + NearbySharingServiceImpl::StatusCodes::kOk); + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + + // We're now in the sending state, try to send again should fail + EXPECT_EQ(SendAttachments(target, CreateTextAttachments({kTextPayload})), + NearbySharingServiceImpl::StatusCodes::kError); + + UnregisterSendSurface(&transfer_callback, &discovery_callback); +} + +TEST_F(NearbySharingServiceImplTest, SendTextWithoutScanning) { + ShareTarget target; + EXPECT_EQ(SendAttachments(target, CreateTextAttachments({kTextPayload})), + NearbySharingServiceImpl::StatusCodes::kError); +} + +TEST_F(NearbySharingServiceImplTest, SendTextUnknownTarget) { + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + DiscoverShareTarget(transfer_callback, discovery_callback); + + ShareTarget target; + EXPECT_EQ(SendAttachments(target, CreateTextAttachments({kTextPayload})), + NearbySharingServiceImpl::StatusCodes::kError); + UnregisterSendSurface(&transfer_callback, &discovery_callback); +} + +TEST_F(NearbySharingServiceImplTest, SendTextFailedCreateEndpointInfo) { + // Set name with too many characters. + local_device_data_manager()->SetDeviceName(std::string(300, 'a')); + + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + ShareTarget target = + DiscoverShareTarget(transfer_callback, discovery_callback); + + EXPECT_EQ(SendAttachments(target, CreateTextAttachments({kTextPayload})), + NearbySharingServiceImpl::StatusCodes::kError); + + UnregisterSendSurface(&transfer_callback, &discovery_callback); +} + +TEST_F(NearbySharingServiceImplTest, SendTextFailedToConnect) { + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + // Call DiscoverShareTarget() instead of SetUpOutgoingShareTarget() as we want + // to fail before key verification is done. + ShareTarget target = + DiscoverShareTarget(transfer_callback, discovery_callback); + + absl::Notification notification; + ExpectTransferUpdates( + transfer_callback, target, + {TransferMetadata::Status::kConnecting, + TransferMetadata::Status::kFailedToInitiateOutgoingConnection}, + [&]() { notification.Notify(); }); + + EXPECT_EQ(SendAttachments(target, CreateTextAttachments({kTextPayload})), + NearbySharingServiceImpl::StatusCodes::kOk); + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + + UnregisterSendSurface(&transfer_callback, &discovery_callback); +} + +TEST_F(NearbySharingServiceImplTest, SendTextFailedKeyVerification) { + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + ShareTarget target = + DiscoverShareTarget(transfer_callback, discovery_callback); + + absl::Notification notification; + ExpectTransferUpdates( + transfer_callback, target, + {TransferMetadata::Status::kConnecting, + TransferMetadata::Status::kPairedKeyVerificationFailed}, + [&]() { notification.Notify(); }); + + SetUpKeyVerification(/*is_incoming=*/false, PairedKeyResultFrame::FAIL); + fake_nearby_connections_manager_->SetRawAuthenticationToken(kEndpointId, + GetToken()); + fake_nearby_connections_manager_->set_nearby_connection(&connection_); + + EXPECT_EQ(SendAttachments(target, CreateTextAttachments({kTextPayload})), + NearbySharingServiceImpl::StatusCodes::kOk); + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + + UnregisterSendSurface(&transfer_callback, &discovery_callback); +} + +TEST_F(NearbySharingServiceImplTest, SendTextUnableToVerifyKey) { + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + ShareTarget target = + DiscoverShareTarget(transfer_callback, discovery_callback); + + absl::Notification notification; + ExpectTransferUpdates(transfer_callback, target, + {TransferMetadata::Status::kConnecting, + TransferMetadata::Status::kAwaitingLocalConfirmation, + TransferMetadata::Status::kAwaitingRemoteAcceptance}, + [&]() { notification.Notify(); }); + + SetUpKeyVerification(/*is_incoming=*/false, PairedKeyResultFrame::UNABLE); + fake_nearby_connections_manager_->SetRawAuthenticationToken(kEndpointId, + GetToken()); + fake_nearby_connections_manager_->set_nearby_connection(&connection_); + EXPECT_EQ(SendAttachments(target, CreateTextAttachments({kTextPayload})), + NearbySharingServiceImpl::StatusCodes::kOk); + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + + UnregisterSendSurface(&transfer_callback, &discovery_callback); +} + +INSTANTIATE_TEST_SUITE_P(NearbySharingServiceImplSendFailureTest, + NearbySharingServiceImplSendFailureTest, + testing::ValuesIn(kSendFailureTestData)); + +TEST_P(NearbySharingServiceImplSendFailureTest, SendTextRemoteFailure) { + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + ShareTarget target = + SetUpOutgoingShareTarget(transfer_callback, discovery_callback); + + absl::Notification notification; + ExpectTransferUpdates(transfer_callback, target, + {TransferMetadata::Status::kConnecting, + TransferMetadata::Status::kAwaitingLocalConfirmation, + TransferMetadata::Status::kAwaitingRemoteAcceptance}, + [&]() { notification.Notify(); }); + + EXPECT_EQ(SendAttachments(target, CreateTextAttachments({kTextPayload})), + NearbySharingServiceImpl::StatusCodes::kOk); + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + + // Verify data sent to the remote device so far. + EXPECT_TRUE(ExpectPairedKeyEncryptionFrame()); + EXPECT_TRUE(ExpectPairedKeyResultFrame()); + EXPECT_TRUE(ExpectIntroductionFrame().has_value()); + + // We're now waiting for the remote device to respond with the accept + absl::Notification reject_notification; + ExpectTransferUpdates(transfer_callback, target, {GetParam().expected_status}, + [&]() { reject_notification.Notify(); }); + + // Cancel the transfer by rejecting it. + SendConnectionResponse(GetParam().response_status); + EXPECT_TRUE(reject_notification.WaitForNotificationWithTimeout(kWaitTimeout)); + + EXPECT_TRUE(connection_.IsClosed()); + + UnregisterSendSurface(&transfer_callback, &discovery_callback); +} + +TEST_P(NearbySharingServiceImplSendFailureTest, SendFilesRemoteFailure) { + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + ShareTarget target = + SetUpOutgoingShareTarget(transfer_callback, discovery_callback); + + std::vector test_data = {'T', 'e', 's', 't'}; + std::filesystem::path path = CreateTestFile("text.txt", test_data); + + absl::Notification notification; + ExpectTransferUpdates(transfer_callback, target, + {TransferMetadata::Status::kConnecting, + TransferMetadata::Status::kAwaitingLocalConfirmation, + TransferMetadata::Status::kAwaitingRemoteAcceptance}, + [&]() { notification.Notify(); }); + + EXPECT_EQ(SendAttachments(target, CreateFileAttachments({path})), + NearbySharingServiceImpl::StatusCodes::kOk); + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + + // Verify data sent to the remote device so far. + EXPECT_TRUE(ExpectPairedKeyEncryptionFrame()); + EXPECT_TRUE(ExpectPairedKeyResultFrame()); + EXPECT_TRUE(ExpectIntroductionFrame().has_value()); + + // We're now waiting for the remote device to respond with the accept + absl::Notification reject_notification; + ExpectTransferUpdates(transfer_callback, target, {GetParam().expected_status}, + [&]() { reject_notification.Notify(); }); + + // Cancel the transfer by rejecting it. + SendConnectionResponse(GetParam().response_status); + EXPECT_TRUE(reject_notification.WaitForNotificationWithTimeout(kWaitTimeout)); + + EXPECT_TRUE(connection_.IsClosed()); + + UnregisterSendSurface(&transfer_callback, &discovery_callback); +} + +TEST_F(NearbySharingServiceImplTest, SendTextSuccess) { + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + AccountManager::Account account; + account.id = kTestAccountId; + account_manager().SetAccount(account); + ShareTarget target = + SetUpOutgoingShareTarget(transfer_callback, discovery_callback); + + absl::Notification notification; + ExpectTransferUpdates(transfer_callback, target, + {TransferMetadata::Status::kConnecting, + TransferMetadata::Status::kAwaitingLocalConfirmation, + TransferMetadata::Status::kAwaitingRemoteAcceptance}, + [&]() { notification.Notify(); }); + + EXPECT_EQ(SendAttachments(target, CreateTextAttachments({kTextPayload})), + NearbySharingServiceImpl::StatusCodes::kOk); + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + + // Verify data sent to the remote device so far. + EXPECT_TRUE(ExpectPairedKeyEncryptionFrame()); + EXPECT_TRUE(ExpectPairedKeyResultFrame()); + std::optional intro = ExpectIntroductionFrame(); + + ASSERT_TRUE(intro.has_value()); + ASSERT_EQ(intro->text_metadata_size(), 1); + auto meta = intro->text_metadata(0); + + EXPECT_EQ(meta.text_title(), kTextPayload); + EXPECT_EQ(static_cast(meta.size()), strlen(kTextPayload)); + EXPECT_EQ(meta.type(), TextMetadata::TEXT); + + ASSERT_TRUE( + fake_nearby_connections_manager_->connection_endpoint_info(kEndpointId)); + std::unique_ptr advertisement = + Advertisement::FromEndpointInfo(absl::Span( + *fake_nearby_connections_manager_->connection_endpoint_info( + kEndpointId))); + ASSERT_TRUE(advertisement); + EXPECT_EQ(advertisement->device_name(), kDeviceName); + EXPECT_EQ(advertisement->device_type(), ShareTargetType::kLaptop); + auto& test_metadata_key = GetNearbyShareTestEncryptedMetadataKey(); + EXPECT_EQ(advertisement->salt(), test_metadata_key.salt()); + EXPECT_EQ(advertisement->encrypted_metadata_key(), + test_metadata_key.encrypted_key()); + + PayloadInfo info = AcceptAndSendPayload(transfer_callback, target); + FinishOutgoingTransfer(transfer_callback, target, info); + + // We should not have called disconnect yet as we want to wait for 1 minute to + // make sure all outgoing packets have been sent properly. + EXPECT_TRUE( + fake_nearby_connections_manager_->connection_endpoint_info(kEndpointId)); + + // Forward time until we send the disconnect request to Nearby + FastForward(kOutgoingDisconnectionDelay); + + // Expect to be disconnected now. + EXPECT_FALSE( + fake_nearby_connections_manager_->connection_endpoint_info(kEndpointId)); + + UnregisterSendSurface(&transfer_callback, &discovery_callback); + account_manager().SetAccount(std::nullopt); +} + +TEST_F(NearbySharingServiceImplTest, SendTextSuccessClosedConnection) { + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + ShareTarget target = + SetUpOutgoingShareTarget(transfer_callback, discovery_callback); + SetUpOutgoingConnectionUntilAccept(transfer_callback, target); + PayloadInfo info = AcceptAndSendPayload(transfer_callback, target); + FinishOutgoingTransfer(transfer_callback, target, info); + + // We should not have called disconnect yet as we want to wait for 1 minute + // to make sure all outgoing packets have been sent properly. + EXPECT_TRUE( + fake_nearby_connections_manager_->connection_endpoint_info(kEndpointId)); + + // Call disconnect on the connection early before the timeout has passed. + connection_.Close(); + + // Expect that we haven't called disconnect again as the endpoint is already + // disconnected. + EXPECT_TRUE( + fake_nearby_connections_manager_->connection_endpoint_info(kEndpointId)); + + // Make sure the scheduled disconnect callback does nothing. + FastForward(kOutgoingDisconnectionDelay); + + UnregisterSendSurface(&transfer_callback, &discovery_callback); +} + +TEST_F(NearbySharingServiceImplTest, SendFilesSuccess) { + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + ShareTarget target = + SetUpOutgoingShareTarget(transfer_callback, discovery_callback); + + std::vector test_data = {'T', 'e', 's', 't'}; + std::string file_name = "test.txt"; + std::filesystem::path path = CreateTestFile(file_name, test_data); + + absl::Notification introduction_notification; + ExpectTransferUpdates(transfer_callback, target, + {TransferMetadata::Status::kConnecting, + TransferMetadata::Status::kAwaitingLocalConfirmation, + TransferMetadata::Status::kAwaitingRemoteAcceptance}, + [&]() { introduction_notification.Notify(); }); + + EXPECT_EQ(SendAttachments(target, CreateFileAttachments({path})), + NearbySharingServiceImpl::StatusCodes::kOk); + EXPECT_TRUE( + introduction_notification.WaitForNotificationWithTimeout(kWaitTimeout)); + + // Verify data sent to the remote device so far. + EXPECT_TRUE(ExpectPairedKeyEncryptionFrame()); + EXPECT_TRUE(ExpectPairedKeyResultFrame()); + auto intro = ExpectIntroductionFrame(); + ASSERT_TRUE(intro.has_value()); + ASSERT_EQ(intro->file_metadata_size(), 1); + auto meta = intro->file_metadata(0); + + EXPECT_EQ(meta.name(), file_name); + EXPECT_EQ(meta.mime_type(), "text/plain"); + EXPECT_EQ(test_data.size(), static_cast(meta.size())); + EXPECT_EQ(meta.type(), FileMetadata::UNKNOWN); + + // Expect the file payload to be sent in the end. + absl::Notification payload_notification; + fake_nearby_connections_manager_->set_send_payload_callback( + [&](std::unique_ptr payload, + std::weak_ptr + listener) { + ASSERT_TRUE(payload->content.is_file()); + std::filesystem::path file = payload->content.file_payload.file.path; + ASSERT_TRUE(std::filesystem::exists(file)); + + payload_notification.Notify(); + }); + + // We're now waiting for the remote device to respond with the accept + // result. + absl::Notification accept_notification; + ExpectTransferUpdates(transfer_callback, target, + {TransferMetadata::Status::kInProgress}, + [&]() { accept_notification.Notify(); }); + + // Kick off send process by accepting the transfer from the remote device. + SendConnectionResponse(ConnectionResponseFrame::ACCEPT); + + EXPECT_TRUE(accept_notification.WaitForNotificationWithTimeout(kWaitTimeout)); + EXPECT_TRUE( + payload_notification.WaitForNotificationWithTimeout(kWaitTimeout)); + + UnregisterSendSurface(&transfer_callback, &discovery_callback); +} + +TEST_F(NearbySharingServiceImplTest, CancelSenderInitiator) { + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + ShareTarget target = + SetUpOutgoingShareTarget(transfer_callback, discovery_callback); + std::optional outgoing_share_target = + SetUpOutgoingConnectionUntilAccept(transfer_callback, target); + ASSERT_TRUE(outgoing_share_target.has_value()); + target = *outgoing_share_target; + PayloadInfo info = AcceptAndSendPayload(transfer_callback, target); + + // After we stop scanning, we check back in after kInvalidateDelay + // milliseconds to make sure that we stopped in order to send a file and + // not because the user left the page. We have to fast-forward here. + // otherwise, we will hit this callback when trying to fast-forward by + // kInitiatorCancelDelay below. + FastForward(kInvalidateDelay); + + absl::Notification notification; + EXPECT_CALL(transfer_callback, OnTransferUpdate(testing::_, testing::_)) + .WillOnce(testing::Invoke( + [&](const ShareTarget& share_target, TransferMetadata metadata) { + EXPECT_EQ(share_target.id, target.id); + EXPECT_EQ(metadata.status(), TransferMetadata::Status::kCancelled); + })); + EXPECT_FALSE( + fake_nearby_connections_manager_->WasPayloadCanceled(info.payload_id)); + // The initiator of the cancellation explicitly calls Cancel(). + service_->Cancel( + target, [&](NearbySharingServiceImpl::StatusCodes status_code) { + EXPECT_EQ(status_code, NearbySharingServiceImpl::StatusCodes::kOk); + notification.Notify(); + }); + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + EXPECT_TRUE( + fake_nearby_connections_manager_->WasPayloadCanceled(info.payload_id)); + + // After the TransferMetadata::Status::kCancelled update, we expect other + // classes to unregister the send surface. + UnregisterSendSurface(&transfer_callback, &discovery_callback); + + // The initiator of the cancel should send a cancel frame to the other device, + // then wait a few seconds before disconnecting to allow for processing on the + // other device. + EXPECT_TRUE(ExpectProgressUpdateFrame()); + EXPECT_TRUE(ExpectCancelFrame()); + EXPECT_FALSE(connection_.IsClosed()); + FastForward(kInitiatorCancelDelay); + EXPECT_TRUE(connection_.IsClosed()); +} + +TEST_F(NearbySharingServiceImplTest, CancelSenderNoninitiator) { + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + ShareTarget target = + SetUpOutgoingShareTarget(transfer_callback, discovery_callback); + std::optional outgoing_share_target = + SetUpOutgoingConnectionUntilAccept(transfer_callback, target); + ASSERT_TRUE(outgoing_share_target.has_value()); + target = *outgoing_share_target; + PayloadInfo info = AcceptAndSendPayload(transfer_callback, target); + + absl::Notification notification; + EXPECT_CALL(transfer_callback, OnTransferUpdate(testing::_, testing::_)) + .WillOnce(testing::Invoke( + [&](const ShareTarget& share_target, TransferMetadata metadata) { + EXPECT_EQ(share_target.id, target.id); + EXPECT_EQ(metadata.status(), TransferMetadata::Status::kCancelled); + notification.Notify(); + })); + EXPECT_FALSE( + fake_nearby_connections_manager_->WasPayloadCanceled(info.payload_id)); + // The non-initiator of the cancellation processes a cancellation frame from + // the initiator. + SendCancel(); + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + EXPECT_TRUE( + fake_nearby_connections_manager_->WasPayloadCanceled(info.payload_id)); + + // The non-initiator should close the connection immediately + EXPECT_TRUE(connection_.IsClosed()); +} + +TEST_F(NearbySharingServiceImplTest, CancelReceiverInitiator) { + NiceMock transfer_callback; + ShareTarget target = SetUpIncomingConnection(transfer_callback); + ASSERT_TRUE(ExpectPairedKeyEncryptionFrame()); + ASSERT_TRUE(ExpectPairedKeyResultFrame()); + + absl::Notification notification; + EXPECT_CALL(transfer_callback, OnTransferUpdate(testing::_, testing::_)) + .WillOnce(testing::Invoke( + [&](const ShareTarget& share_target, TransferMetadata metadata) { + EXPECT_EQ(share_target.id, target.id); + EXPECT_EQ(metadata.status(), TransferMetadata::Status::kCancelled); + })); + EXPECT_FALSE( + fake_nearby_connections_manager_->WasPayloadCanceled(kFilePayloadId)); + // The initiator of the cancellation explicitly calls Cancel(). + service_->Cancel( + target, [&](NearbySharingServiceImpl::StatusCodes status_code) { + EXPECT_EQ(NearbySharingServiceImpl::StatusCodes::kOk, status_code); + notification.Notify(); + }); + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + EXPECT_TRUE( + fake_nearby_connections_manager_->WasPayloadCanceled(kFilePayloadId)); + + // After the TransferMetadata::Status::kCancelled update, we expect other + // classes to unregister the receive surface. + UnregisterReceiveSurface(&transfer_callback); + + // The initiator of the cancel should send a cancel frame to the other device, + // then wait a few seconds before disconnecting to allow for processing on the + // other device. + ASSERT_TRUE(ExpectCancelFrame()); + EXPECT_FALSE(connection_.IsClosed()); + FastForward(kInitiatorCancelDelay); + EXPECT_TRUE(connection_.IsClosed()); +} + +TEST_F(NearbySharingServiceImplTest, CancelReceiverNoninitiator) { + NiceMock transfer_callback; + ShareTarget target = SetUpIncomingConnection(transfer_callback); + ExpectPairedKeyEncryptionFrame(); + ExpectPairedKeyResultFrame(); + + absl::Notification notification; + EXPECT_CALL(transfer_callback, OnTransferUpdate(testing::_, testing::_)) + .WillOnce(testing::Invoke( + [&](const ShareTarget& share_target, TransferMetadata metadata) { + EXPECT_EQ(target.id, share_target.id); + EXPECT_EQ(TransferMetadata::Status::kCancelled, metadata.status()); + notification.Notify(); + })); + EXPECT_FALSE( + fake_nearby_connections_manager_->WasPayloadCanceled(kFilePayloadId)); + // The non-initiator of the cancellation processes a cancellation frame from + // the initiator. + SendCancel(); + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + EXPECT_TRUE( + fake_nearby_connections_manager_->WasPayloadCanceled(kFilePayloadId)); + + // The non-initiator should close the connection immediately + EXPECT_TRUE(connection_.IsClosed()); +} + +TEST_F(NearbySharingServiceImplTest, + RegisterForegroundReceiveSurfaceEntersHighVisibility) { + TestObserver observer(service_.get()); + NiceMock callback; + + SetConnectionType(ConnectionType::kWifi); + SetVisibility(DeviceVisibility::DEVICE_VISIBILITY_EVERYONE); + local_device_data_manager()->SetDeviceName(kDeviceName); + + // To start, we should not be in high visibility state. + EXPECT_FALSE(service_->IsInHighVisibility()); + EXPECT_FALSE(observer.on_start_advertising_failure_called_); + + // If we register a foreground surface we should end up in high visibility + // state. + SetUpForegroundReceiveSurface(callback); + + // At this point we should have a new high visibility state and the observer + // should have been called as well. + EXPECT_TRUE(service_->IsInHighVisibility()); + EXPECT_TRUE(observer.in_high_visibility_); + EXPECT_FALSE(observer.on_start_advertising_failure_called_); + + // If we unregister the foreground receive surface we should no longer be in + // high visibility and the observer should be notified. + EXPECT_EQ(UnregisterReceiveSurface(&callback), + NearbySharingService::StatusCodes::kOk); + EXPECT_FALSE(service_->IsInHighVisibility()); + EXPECT_FALSE(observer.in_high_visibility_); + + // Remove the observer before it goes out of scope. + service_->RemoveObserver(&observer); +} + +TEST_F(NearbySharingServiceImplTest, ShutdownCallsObservers) { + TestObserver observer(service_.get()); + EXPECT_FALSE(observer.shutdown_called_); + Shutdown(); + EXPECT_TRUE(observer.shutdown_called_); + // Prevent a double shutdown. + service_.reset(); +} + +TEST_F(NearbySharingServiceImplTest, RotateBackgroundAdvertisementPeriodic) { + certificate_manager()->set_next_salt({0x00, 0x01}); + SetVisibility(DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + NiceMock callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kBackground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); + auto endpoint_info_initial = + fake_nearby_connections_manager_->advertising_endpoint_info(); + + certificate_manager()->set_next_salt({0x00, 0x02}); + FastForward(absl::Seconds(870)); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); + auto endpoint_info_rotated = + fake_nearby_connections_manager_->advertising_endpoint_info(); + EXPECT_NE(endpoint_info_initial, endpoint_info_rotated); +} + +TEST_F(NearbySharingServiceImplTest, + RotateBackgroundAdvertisementPrivateCertificatesChange) { + certificate_manager()->set_next_salt({0x00, 0x01}); + SetVisibility(DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + NiceMock callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kBackground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); + auto endpoint_info_initial = + fake_nearby_connections_manager_->advertising_endpoint_info(); + + certificate_manager()->set_next_salt({0x00, 0x02}); + certificate_manager()->NotifyPrivateCertificatesChanged(); + FakeTaskRunner::WaitForRunningTasksWithTimeout(kTaskWaitTimeout); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); + auto endpoint_info_rotated = + fake_nearby_connections_manager_->advertising_endpoint_info(); + EXPECT_NE(endpoint_info_initial, endpoint_info_rotated); + UnregisterReceiveSurface(&callback); + FakeTaskRunner::WaitForRunningTasksWithTimeout(kTaskWaitTimeout); +} + +TEST_F(NearbySharingServiceImplTest, OrderedEndpointDiscoveryEvents) { + SetConnectionType(ConnectionType::kWifi); + + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + + // Start discovering, to ensure a discovery listener is registered. + EXPECT_EQ(RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kForeground), + NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsDiscovering()); + + // Ensure that the endpoint discovered and lost event are process + // sequentially. This is particularly important due to the asynchronous + // operations needed to handle endpoint discovery. + // + // Order of events: + // - Nearby Connections discovers endpoint 1 + // - Nearby Connections loses endpoint 1 + // - Nearby Share processes these two events in order. + // - Nearby Connections discovers endpoint 2 + // - Nearby Connections discovers endpoint 3 + // - Nearby Connections loses endpoint 3 + // - Nearby Connections loses endpoint 2 + // - Nearby Share processes these four events in order. + + // Expect the advertisement decoder to be invoked once for each discovery. + SetUpAdvertisementDecoder(GetValidV1EndpointInfo(), + /*return_empty_advertisement=*/false, + /*return_empty_device_name=*/false, + /*expected_number_of_calls=*/3u); + { + absl::Notification notification; + FindEndpoint(/*endpoint_id=*/"1"); + LoseEndpoint(/*endpoint_id=*/"1"); + InSequence s; + EXPECT_CALL(discovery_callback, OnShareTargetDiscovered); + EXPECT_CALL(discovery_callback, OnShareTargetLost) + .WillOnce([&](ShareTarget share_target) { notification.Notify(); }); + + // Needed for discovery processing. + ProcessLatestPublicCertificateDecryption(/*expected_num_calls=*/1, + /*success=*/true); + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + } + { + absl::Notification notification; + FindEndpoint(/*endpoint_id=*/"2"); + FindEndpoint(/*endpoint_id=*/"3"); + LoseEndpoint(/*endpoint_id=*/"3"); + LoseEndpoint(/*endpoint_id=*/"2"); + InSequence s; + EXPECT_CALL(discovery_callback, OnShareTargetDiscovered) + .WillOnce([](ShareTarget share_target) { + EXPECT_EQ(share_target.device_id, "2"); + }); + EXPECT_CALL(discovery_callback, OnShareTargetDiscovered) + .WillOnce([](ShareTarget share_target) { + EXPECT_EQ(share_target.device_id, "3"); + }); + EXPECT_CALL(discovery_callback, OnShareTargetLost) + .WillOnce([](ShareTarget share_target) { + EXPECT_EQ(share_target.device_id, "3"); + }); + EXPECT_CALL(discovery_callback, OnShareTargetLost) + .WillOnce([&](ShareTarget share_target) { + EXPECT_EQ(share_target.device_id, "2"); + notification.Notify(); + }); + + // Needed for discovery processing. Fail, then the ShareTarget device ID is + // set to the endpoint ID, which we use above to verify the correct endpoint + // ID processing order. + ProcessLatestPublicCertificateDecryption(/*expected_num_calls=*/2, + /*success=*/false); + ProcessLatestPublicCertificateDecryption(/*expected_num_calls=*/3, + /*success=*/false); + + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + } +} + +TEST_F(NearbySharingServiceImplTest, + RetryDiscoveredEndpointsNoDownloadIfDecryption) { + // Start discovery. + SetConnectionType(ConnectionType::kWifi); + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kForeground); + EXPECT_EQ(certificate_manager()->num_download_public_certificates_calls(), + 1u); + EXPECT_TRUE(fake_nearby_connections_manager_->IsDiscovering()); + SetUpAdvertisementDecoder(GetValidV1EndpointInfo(), + /*return_empty_advertisement=*/false, + /*return_empty_device_name=*/true, + /*expected_number_of_calls=*/1u); + // Order of events: + // - Discover endpoint 1 --> decrypts public certificate + // - Fire certificate download timer --> no download because no cached + // advertisements + { + absl::Notification notification; + FindEndpoint(/*endpoint_id=*/"1"); + EXPECT_CALL(discovery_callback, OnShareTargetDiscovered) + .WillOnce([&](ShareTarget share_target) { notification.Notify(); }); + ProcessLatestPublicCertificateDecryption(/*expected_num_calls=*/1, + /*success=*/true); + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + } + + FastForward(kCertificateDownloadDuringDiscoveryPeriod); + EXPECT_EQ(certificate_manager()->num_download_public_certificates_calls(), + 1u); + + EXPECT_CALL(discovery_callback, OnShareTargetLost); + Shutdown(); + service_.reset(); +} + +TEST_F(NearbySharingServiceImplTest, + RetryDiscoveredEndpointsDownloadCertsAndRetryDecryption) { + // Start discovery. + SetConnectionType(ConnectionType::kWifi); + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kForeground); + EXPECT_EQ(certificate_manager()->num_download_public_certificates_calls(), + 1u); + EXPECT_TRUE(fake_nearby_connections_manager_->IsDiscovering()); + SetUpAdvertisementDecoder(GetValidV1EndpointInfo(), + /*return_empty_advertisement=*/false, + /*return_empty_device_name=*/true, + /*expected_number_of_calls=*/6u); + // Order of events: + // - Discover endpoint 1 --> decrypts public certificate + // - Discover endpoint 2 --> cannot decrypt public certificate + // - Discover endpoint 3 --> decrypts public certificate + // - Discover endpoint 4 --> cannot decrypt public certificate + // - Lose endpoint 3 + // - Fire certificate download timer --> certificates downloaded + // - (Re)discover endpoints 2 and 4 + { + absl::Notification notification; + FindEndpoint(/*endpoint_id=*/"1"); + FindEndpoint(/*endpoint_id=*/"2"); + FindEndpoint(/*endpoint_id=*/"3"); + FindEndpoint(/*endpoint_id=*/"4"); + LoseEndpoint(/*endpoint_id=*/"3"); + ::testing::InSequence s; + EXPECT_CALL(discovery_callback, OnShareTargetDiscovered).Times(2); + EXPECT_CALL(discovery_callback, OnShareTargetLost) + .WillOnce([&](ShareTarget share_target) { notification.Notify(); }); + ProcessLatestPublicCertificateDecryption(/*expected_num_calls=*/1, + /*success=*/true); + ProcessLatestPublicCertificateDecryption(/*expected_num_calls=*/2, + /*success=*/false); + ProcessLatestPublicCertificateDecryption(/*expected_num_calls=*/3, + /*success=*/true); + ProcessLatestPublicCertificateDecryption(/*expected_num_calls=*/4, + /*success=*/false); + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + } + FastForward(kCertificateDownloadDuringDiscoveryPeriod); + EXPECT_EQ(certificate_manager()->num_download_public_certificates_calls(), + 2u); + certificate_manager()->NotifyPublicCertificatesDownloaded(); + FlushTesting(); + { + absl::Notification notification; + ::testing::InSequence s; + EXPECT_CALL(discovery_callback, OnShareTargetDiscovered); + EXPECT_CALL(discovery_callback, OnShareTargetDiscovered) + .WillOnce([&](ShareTarget share_target) { notification.Notify(); }); + ProcessLatestPublicCertificateDecryption(/*expected_num_calls=*/5, + /*success=*/true); + ProcessLatestPublicCertificateDecryption(/*expected_num_calls=*/6, + /*success=*/true); + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + } + EXPECT_CALL(discovery_callback, OnShareTargetLost).Times(3); + Shutdown(); + service_.reset(); +} + +TEST_F(NearbySharingServiceImplTest, + RetryDiscoveredEndpointsDiscoveryRestartClearsCache) { + // Start discovery. + SetConnectionType(ConnectionType::kWifi); + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kForeground); + EXPECT_EQ(certificate_manager()->num_download_public_certificates_calls(), + 1u); + EXPECT_TRUE(fake_nearby_connections_manager_->IsDiscovering()); + SetUpAdvertisementDecoder(GetValidV1EndpointInfo(), + /*return_empty_advertisement=*/false, + /*return_empty_device_name=*/true, + /*expected_number_of_calls=*/1u); + // Order of events: + // - Discover endpoint 1 --> cannot decrypt public certificate + // - Stop discovery + // - Certificate download timer not running; not discovering + // - Start discovery + // - Fire certificate download timer --> certificates not downloaded; cached + // advertisement map has been cleared + FindEndpoint(/*endpoint_id=*/"1"); + InSequence s; + EXPECT_CALL(discovery_callback, OnShareTargetDiscovered).Times(0); + EXPECT_CALL(discovery_callback, OnShareTargetLost).Times(0); + ProcessLatestPublicCertificateDecryption(/*expected_num_calls=*/1, + /*success=*/false); + UnregisterSendSurface(&transfer_callback, &discovery_callback); + FastForward(kCertificateDownloadDuringDiscoveryPeriod); + EXPECT_EQ(certificate_manager()->num_download_public_certificates_calls(), + 1u); + RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kForeground); + // Note: Certificate downloads are also requested in RegisterSendSurface; this + // is not related to the retry timer. + EXPECT_EQ(certificate_manager()->num_download_public_certificates_calls(), + 2u); + EXPECT_TRUE(fake_nearby_connections_manager_->IsDiscovering()); + FastForward(kCertificateDownloadDuringDiscoveryPeriod); + EXPECT_EQ(certificate_manager()->num_download_public_certificates_calls(), + 2u); + Shutdown(); + service_.reset(); +} + +TEST_F(NearbySharingServiceImplTest, + RetryDiscoveredEndpointsWhenCannotDecrypted) { + // Start discovery. + SetConnectionType(ConnectionType::kWifi); + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kForeground); + EXPECT_EQ(certificate_manager()->num_download_public_certificates_calls(), + 1u); + EXPECT_TRUE(fake_nearby_connections_manager_->IsDiscovering()); + SetUpAdvertisementDecoder(GetValidV1EndpointInfo(), + /*return_empty_advertisement=*/false, + /*return_empty_device_name=*/true, + /*expected_number_of_calls=*/3u); + FindEndpoint(/*endpoint_id=*/"1"); + + ProcessLatestPublicCertificateDecryption(/*expected_num_calls=*/1, + /*success=*/false); + EXPECT_EQ(certificate_manager()->num_download_public_certificates_calls(), + 1u); + // Because there is one pending advertisement to resolve, it will cause public + // certificates download. + FastForward(kCertificateDownloadDuringDiscoveryPeriod); + EXPECT_EQ(certificate_manager()->num_download_public_certificates_calls(), + 2u); + certificate_manager()->NotifyPublicCertificatesDownloaded(); + FlushTesting(); + // Don't download public certificates when it is tried. + FastForward(kCertificateDownloadDuringDiscoveryPeriod); + EXPECT_EQ(certificate_manager()->num_download_public_certificates_calls(), + 2u); + FlushTesting(); + // Don't download public certificates in case of the endpoint is discovered + // again. + FindEndpoint(/*endpoint_id=*/"1"); + ProcessLatestPublicCertificateDecryption(/*expected_num_calls=*/2, + /*success=*/false); + EXPECT_EQ(certificate_manager()->num_download_public_certificates_calls(), + 2u); + Shutdown(); + service_.reset(); +} + +TEST_F(NearbySharingServiceImplTest, RetryDiscoveredEndpointsDownloadLimit) { + // Start discovery. + SetConnectionType(ConnectionType::kWifi); + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kForeground); + EXPECT_EQ(certificate_manager()->num_download_public_certificates_calls(), + 1u); + EXPECT_TRUE(fake_nearby_connections_manager_->IsDiscovering()); + SetUpAdvertisementDecoder(GetValidV1EndpointInfo(), + /*return_empty_advertisement=*/false, + /*return_empty_device_name=*/true, + /*expected_number_of_calls=*/2u + + kMaxCertificateDownloadsDuringDiscovery); + // Order of events: + // - x3: + // - (Re)discover endpoint 1 --> cannot decrypt public certificate + // - Fire certificate download timer --> certificates downloaded + // - Rediscover endpoint 1 --> cannot decrypt public certificate + // - Fire certificate download timer --> no download; limit reached + // - Restart discovery which resets limit counter + for (size_t i = 1; i <= kMaxCertificateDownloadsDuringDiscovery; ++i) { + FindEndpoint(/*endpoint_id=*/absl::StrCat(i)); + } + + for (size_t i = 1; i <= kMaxCertificateDownloadsDuringDiscovery; ++i) { + SCOPED_TRACE(i); + ProcessLatestPublicCertificateDecryption(/*expected_num_calls=*/i, + /*success=*/false); + FastForward(kCertificateDownloadDuringDiscoveryPeriod); + EXPECT_EQ(certificate_manager()->num_download_public_certificates_calls(), + 1u + i); + certificate_manager()->NotifyPublicCertificatesDownloaded(); + FlushTesting(); + } + FastForward(kCertificateDownloadDuringDiscoveryPeriod); + EXPECT_EQ(certificate_manager()->num_download_public_certificates_calls(), + 1u + kMaxCertificateDownloadsDuringDiscovery); + UnregisterSendSurface(&transfer_callback, &discovery_callback); + RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kForeground); + // Note: Certificate downloads are also requested in RegisterSendSurface; this + // is not related to the retry timer. + EXPECT_EQ(certificate_manager()->num_download_public_certificates_calls(), + 2u + kMaxCertificateDownloadsDuringDiscovery); + FindEndpoint(/*endpoint_id=*/"1"); + ProcessLatestPublicCertificateDecryption( + /*expected_num_calls=*/1u + kMaxCertificateDownloadsDuringDiscovery, + /*success=*/false); + FastForward(kCertificateDownloadDuringDiscoveryPeriod); + EXPECT_EQ(certificate_manager()->num_download_public_certificates_calls(), + 3u + kMaxCertificateDownloadsDuringDiscovery); + + Shutdown(); + service_.reset(); +} + +TEST_F(NearbySharingServiceImplTest, OpenSharedTarget) { + ShareTarget share_target; + share_target.text_attachments = { + TextAttachment(TextMetadata::TEXT, "body", "title", "mime")}; + NearbySharingService::StatusCodes result; + absl::Notification notification; + service_->Open(share_target, + [&](NearbySharingService::StatusCodes status_code) { + result = status_code; + notification.Notify(); + }); + + ASSERT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); +} + +TEST_F(NearbySharingServiceImplTest, + ScreenLockedRegisterReceiveSurfaceNotAdvertising) { + SetScreenLocked(true); + SetConnectionType(ConnectionType::kWifi); + MockTransferUpdateCallback callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kForeground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_FALSE(fake_nearby_connections_manager_->IsAdvertising()); + EXPECT_FALSE(fake_nearby_connections_manager_->is_shutdown()); +} + +TEST_F(NearbySharingServiceImplTest, ScreenLocksDuringAdvertising) { + SetConnectionType(ConnectionType::kWifi); + MockTransferUpdateCallback callback; + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kForeground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); + EXPECT_FALSE(fake_nearby_connections_manager_->is_shutdown()); + + SetScreenLocked(true); + EXPECT_FALSE(fake_nearby_connections_manager_->IsAdvertising()); + EXPECT_FALSE(fake_nearby_connections_manager_->is_shutdown()); + + SetScreenLocked(false); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); + EXPECT_FALSE(fake_nearby_connections_manager_->is_shutdown()); +} + +TEST_F(NearbySharingServiceImplTest, ScreenLocksDuringDiscovery) { + SetConnectionType(ConnectionType::kWifi); + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + EXPECT_EQ(RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kForeground), + NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsDiscovering()); + + SetScreenLocked(true); + EXPECT_FALSE(fake_nearby_connections_manager_->IsDiscovering()); + SetScreenLocked(false); + EXPECT_TRUE(fake_nearby_connections_manager_->IsDiscovering()); +} + +TEST_F(NearbySharingServiceImplTest, CreateShareTarget) { + auto CreateShareTarget = + [](NearbySharingServiceImpl* service_, + std::unique_ptr advertisement, + std::optional certificate) + -> std::optional { + return service_->CreateShareTarget(kEndpointId, std::move(advertisement), + certificate, + /*is_incoming=*/true); + }; + + std::unique_ptr advertisement = Advertisement::NewInstance( + GetNearbyShareTestEncryptedMetadataKey().salt(), + GetNearbyShareTestEncryptedMetadataKey().encrypted_key(), kDeviceType, + kDeviceName); + + // Flip |for_self_share| to true to ensure the resulting ShareTarget picks + // this up. + nearby::sharing::proto::PublicCertificate certificate_proto = + GetNearbyShareTestPublicCertificate( + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + certificate_proto.set_for_self_share(true); + + std::optional certificate = + NearbyShareDecryptedPublicCertificate::DecryptPublicCertificate( + certificate_proto, GetNearbyShareTestEncryptedMetadataKey()); + ASSERT_TRUE(certificate.has_value()); + ASSERT_EQ(certificate_proto.for_self_share(), certificate->for_self_share()); + + std::optional share_target = + CreateShareTarget(service_.get(), std::move(advertisement), certificate); + + ASSERT_TRUE(share_target.has_value()); + EXPECT_EQ(kDeviceName, share_target->device_name); + EXPECT_EQ(kDeviceType, share_target->type); + EXPECT_EQ(certificate_proto.for_self_share(), share_target->for_self_share); + + // Test when |certificate| is null. + advertisement = Advertisement::NewInstance( + GetNearbyShareTestEncryptedMetadataKey().salt(), + GetNearbyShareTestEncryptedMetadataKey().encrypted_key(), kDeviceType, + kDeviceName); + share_target = CreateShareTarget(service_.get(), std::move(advertisement), + /*certificate=*/std::nullopt); + ASSERT_TRUE(share_target.has_value()); + EXPECT_EQ(kDeviceName, share_target->device_name); + EXPECT_EQ(kDeviceType, share_target->type); + EXPECT_FALSE(share_target->for_self_share); +} + +TEST_F(NearbySharingServiceImplTest, SelfShareAutoAccept) { + for (int64_t payload_id : GetValidIntroductionFramePayloadIds()) { + fake_nearby_connections_manager_->SetPayloadPathStatus(payload_id, + Status::kSuccess); + } + + // We create an incoming connection corresponding to a certificate where the + // |for_self_share| field is set to 'true'. This value will be propagated to + // the ShareTarget, which will be used as a signal for the service to + // automatically accept the transfer when Self Share is enabled. This is + // similar to other tests (see "AcceptValidShareTarget") but without the + // explicit call to service_->Accept(). + NiceMock callback; + ShareTarget share_target = SetUpIncomingConnection( + callback, /*is_foreground=*/false, /*for_self_share=*/true); + + // Should fail to call accept. + service_->Accept( + share_target, [&](NearbySharingServiceImpl::StatusCodes status_code) { + EXPECT_EQ(status_code, + NearbySharingServiceImpl::StatusCodes::kOutOfOrderApiCall); + }); + + // Check data written to connection_. + ExpectPairedKeyEncryptionFrame(); + ExpectPairedKeyResultFrame(); + ExpectConnectionResponseFrame(ConnectionResponseFrame::ACCEPT); + + EXPECT_FALSE(connection_.IsClosed()); + + // To avoid UAF in OnIncomingTransferUpdate(). + UnregisterReceiveSurface(&callback); + FlushTesting(); + Shutdown(); + service_.reset(); +} + +TEST_F(NearbySharingServiceImplTest, SelfShareNormalFlowWhenSelfshareDisabled) { + for (int64_t payload_id : GetValidIntroductionFramePayloadIds()) { + fake_nearby_connections_manager_->SetPayloadPathStatus(payload_id, + Status::kSuccess); + } + + DisableSelfshareFeature(); + + NiceMock callback; + ShareTarget share_target = SetUpIncomingConnection( + callback, /*is_foreground=*/false, /*for_self_share=*/true); + + absl::Notification notification; + EXPECT_CALL(callback, OnTransferUpdate(testing::_, testing::_)) + .WillOnce(testing::Invoke( + [&](const ShareTarget& share_target, TransferMetadata metadata) { + EXPECT_FALSE(metadata.is_final_status()); + EXPECT_EQ(metadata.status(), + TransferMetadata::Status::kAwaitingRemoteAcceptance); + notification.Notify(); + })); + + service_->Accept( + share_target, [&](NearbySharingServiceImpl::StatusCodes status_code) { + EXPECT_EQ(status_code, NearbySharingServiceImpl::StatusCodes::kOk); + }); + + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + + // To avoid UAF in OnIncomingTransferUpdate(). + UnregisterReceiveSurface(&callback); +} + +TEST_F(NearbySharingServiceImplTest, SelfShareNoAutoAcceptInForeground) { + for (int64_t payload_id : GetValidIntroductionFramePayloadIds()) { + fake_nearby_connections_manager_->SetPayloadPathStatus(payload_id, + Status::kSuccess); + } + + NiceMock callback; + ShareTarget share_target = SetUpIncomingConnection( + callback, /*is_foreground=*/true, /*for_self_share=*/true); + + EXPECT_CALL(callback, OnTransferUpdate(testing::_, testing::_)).Times(0); + + service_->Accept( + share_target, [&](NearbySharingServiceImpl::StatusCodes status_code) { + EXPECT_EQ(status_code, + NearbySharingServiceImpl::StatusCodes::kOutOfOrderApiCall); + }); + + // Check data written to connection_. + ExpectPairedKeyEncryptionFrame(); + ExpectPairedKeyResultFrame(); + ExpectConnectionResponseFrame(ConnectionResponseFrame::ACCEPT); + + EXPECT_FALSE(connection_.IsClosed()); + + // To avoid UAF in OnIncomingTransferUpdate(). + UnregisterReceiveSurface(&callback); +} + +TEST_F(NearbySharingServiceImplTest, ObserveAccountLoginAndLogout) { + MockAccountObserver account_observer; + absl::Notification notification; + service_->GetAccountManager()->AddObserver(&account_observer); + + EXPECT_CALL(account_observer, OnLoginSucceeded(kTestAccountId)).Times(1); + AccountManager::Account account; + account.id = kTestAccountId; + account_manager().SetAccount(account); + service_->GetAccountManager()->Login( + [&](AccountManager::Account account) { + EXPECT_EQ(account.id, kTestAccountId); + notification.Notify(); + }, + []() {}); + EXPECT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + EXPECT_CALL(account_observer, OnLogoutSucceeded(kTestAccountId)).Times(1); + absl::Notification logout_notification; + service_->GetAccountManager()->Logout([&](absl::Status status) { + EXPECT_TRUE(status.ok()); + logout_notification.Notify(); + }); + EXPECT_TRUE(logout_notification.WaitForNotificationWithTimeout(kWaitTimeout)); + service_->GetAccountManager()->RemoveObserver(&account_observer); + FakeTaskRunner::WaitForRunningTasksWithTimeout(kTaskWaitTimeout); +} + +TEST_F(NearbySharingServiceImplTest, LoginAndLogoutShouldResetSettings) { + SetConnectionType(ConnectionType::kWifi); + + // Used to check whether the setting is cleared after login. + service_->GetSettings()->SetIsAnalyticsEnabled(true); + + // Create account. + AccountManager::Account account; + account.id = kTestAccountId; + + // Login user. + absl::Notification login_notification; + account_manager().SetAccount(account); + service_->GetAccountManager()->Login( + [&](AccountManager::Account account) { + EXPECT_EQ(account.id, kTestAccountId); + login_notification.Notify(); + }, + []() {}); + ASSERT_TRUE(login_notification.WaitForNotificationWithTimeout(kWaitTimeout)); + FakeTaskRunner::WaitForRunningTasksWithTimeout(kTaskWaitTimeout); + EXPECT_TRUE(service_->GetSettings()->GetIsAnalyticsEnabled()); + ASSERT_TRUE(service_->GetAccountManager()->GetCurrentAccount().has_value()); + EXPECT_EQ(service_->GetAccountManager()->GetCurrentAccount()->id, + kTestAccountId); + + // Logout user. + absl::Notification logout_notification; + service_->GetAccountManager()->Logout([&](absl::Status status) { + EXPECT_TRUE(status.ok()); + logout_notification.Notify(); + }); + EXPECT_TRUE(logout_notification.WaitForNotificationWithTimeout(kWaitTimeout)); + absl::SleepFor(absl::Milliseconds(100)); + EXPECT_FALSE(service_->GetSettings()->GetIsAnalyticsEnabled()); + EXPECT_FALSE(service_->GetAccountManager()->GetCurrentAccount().has_value()); + FakeTaskRunner::WaitForRunningTasksWithTimeout(kTaskWaitTimeout); +} + +TEST_F(NearbySharingServiceImplTest, + VisibilityShouldSetAsAllContactsAfterLoginSuccessDuringOnBoarding) { + SetConnectionType(ConnectionType::kWifi); + + service_->GetSettings()->SetIsOnboardingComplete(false, []() {}); + service_->GetSettings()->SetVisibility( + DeviceVisibility::DEVICE_VISIBILITY_HIDDEN); + + EXPECT_EQ(service_->GetSettings()->GetVisibility(), + DeviceVisibility::DEVICE_VISIBILITY_HIDDEN); + + // Create account. + ::nearby::AccountManager::Account account; + account.id = kTestAccountId; + + // Login user. + absl::Notification login_notification; + account_manager().SetAccount(account); + service_->GetAccountManager()->Login( + [&](AccountManager::Account account) { + EXPECT_EQ(account.id, kTestAccountId); + login_notification.Notify(); + }, + []() {}); + ASSERT_TRUE(login_notification.WaitForNotificationWithTimeout(kWaitTimeout)); + FakeTaskRunner::WaitForRunningTasksWithTimeout(kTaskWaitTimeout); + + EXPECT_EQ(service_->GetSettings()->GetVisibility(), + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + FakeTaskRunner::WaitForRunningTasksWithTimeout(kTaskWaitTimeout); +} + +TEST_F(NearbySharingServiceImplTest, LogoutShouldNotResetOnboarding) { + SetConnectionType(ConnectionType::kWifi); + + // Used to check whether the setting is cleared after login. + service_->GetSettings()->SetIsOnboardingComplete(false, []() {}); + + // Create account. + ::nearby::AccountManager::Account account; + account.id = kTestAccountId; + + // Login user. + absl::Notification login_notification; + account_manager().SetAccount(account); + service_->GetAccountManager()->Login( + [&](AccountManager::Account account) { + EXPECT_EQ(account.id, kTestAccountId); + login_notification.Notify(); + }, + []() {}); + ASSERT_TRUE(login_notification.WaitForNotificationWithTimeout(kWaitTimeout)); + FakeTaskRunner::WaitForRunningTasksWithTimeout(kTaskWaitTimeout); + EXPECT_FALSE(service_->GetSettings()->IsOnboardingComplete()); + + // Logout user. + absl::Notification logout_notification; + service_->GetAccountManager()->Logout([&](absl::Status status) { + EXPECT_TRUE(status.ok()); + logout_notification.Notify(); + }); + EXPECT_TRUE(logout_notification.WaitForNotificationWithTimeout(kWaitTimeout)); + FakeTaskRunner::WaitForRunningTasksWithTimeout(kTaskWaitTimeout); + EXPECT_FALSE(service_->GetSettings()->IsOnboardingComplete()); + + // Complete onboarding. + service_->GetSettings()->SetIsOnboardingComplete(true, []() {}); + + // Login user. + absl::Notification login2_notification; + account_manager().SetAccount(account); + service_->GetAccountManager()->Login( + [&](AccountManager::Account account) { + EXPECT_EQ(account.id, kTestAccountId); + login2_notification.Notify(); + }, + []() {}); + ASSERT_TRUE(login2_notification.WaitForNotificationWithTimeout(kWaitTimeout)); + FakeTaskRunner::WaitForRunningTasksWithTimeout(kTaskWaitTimeout); + EXPECT_TRUE(service_->GetSettings()->IsOnboardingComplete()); + + // Logout user. + absl::Notification logout2_notification; + service_->GetAccountManager()->Logout([&](absl::Status status) { + EXPECT_TRUE(status.ok()); + logout2_notification.Notify(); + }); + EXPECT_TRUE( + logout2_notification.WaitForNotificationWithTimeout(kWaitTimeout)); + FakeTaskRunner::WaitForRunningTasksWithTimeout(kTaskWaitTimeout); + EXPECT_TRUE(service_->GetSettings()->IsOnboardingComplete()); + FakeTaskRunner::WaitForRunningTasksWithTimeout(kTaskWaitTimeout); +} + +TEST_F(NearbySharingServiceImplTest, LogoutShouldSetValidVisibility) { + SetConnectionType(ConnectionType::kWifi); + + // Create account. + AccountManager::Account account; + account.id = kTestAccountId; + + // Login user. + absl::Notification login_notification; + account_manager().SetAccount(account); + service_->GetAccountManager()->Login( + [&](AccountManager::Account account) { + EXPECT_EQ(account.id, kTestAccountId); + login_notification.Notify(); + }, + []() {}); + ASSERT_TRUE(login_notification.WaitForNotificationWithTimeout(kWaitTimeout)); + FakeTaskRunner::WaitForRunningTasksWithTimeout(kTaskWaitTimeout); + + // Set visibility. + service_->GetSettings()->SetIsReceiving(true); + service_->GetSettings()->SetVisibility( + DeviceVisibility::DEVICE_VISIBILITY_SELF_SHARE); + + // Logout user. + absl::Notification logout_notification; + service_->GetAccountManager()->Logout([&](absl::Status status) { + EXPECT_TRUE(status.ok()); + logout_notification.Notify(); + }); + EXPECT_TRUE(logout_notification.WaitForNotificationWithTimeout(kWaitTimeout)); + FakeTaskRunner::WaitForRunningTasksWithTimeout(kTaskWaitTimeout); + EXPECT_FALSE(service_->GetSettings()->GetIsReceiving()); + + // Login user. + absl::Notification login2_notification; + account_manager().SetAccount(account); + service_->GetAccountManager()->Login( + [&](AccountManager::Account account) { + EXPECT_EQ(account.id, kTestAccountId); + login2_notification.Notify(); + }, + []() {}); + ASSERT_TRUE(login2_notification.WaitForNotificationWithTimeout(kWaitTimeout)); + FakeTaskRunner::WaitForRunningTasksWithTimeout(kTaskWaitTimeout); + + // Set visibility. + service_->GetSettings()->SetIsReceiving(true); + service_->GetSettings()->SetVisibility( + DeviceVisibility::DEVICE_VISIBILITY_EVERYONE); + + // Logout user. + absl::Notification logout2_notification; + service_->GetAccountManager()->Logout([&](absl::Status status) { + EXPECT_TRUE(status.ok()); + logout2_notification.Notify(); + }); + EXPECT_TRUE( + logout2_notification.WaitForNotificationWithTimeout(kWaitTimeout)); + FakeTaskRunner::WaitForRunningTasksWithTimeout(kTaskWaitTimeout); + EXPECT_TRUE(service_->GetSettings()->GetIsReceiving()); + EXPECT_EQ(service_->GetSettings()->GetVisibility(), + DeviceVisibility::DEVICE_VISIBILITY_EVERYONE); + FakeTaskRunner::WaitForRunningTasksWithTimeout(kTaskWaitTimeout); +} + +TEST_F(NearbySharingServiceImplTest, LoginAndLogoutNoStopRunningSurfaces) { + absl::Notification notification; + SetConnectionType(ConnectionType::kWifi); + MockTransferUpdateCallback transfer_callback; + MockShareTargetDiscoveredCallback discovery_callback; + + EXPECT_EQ(RegisterSendSurface(&transfer_callback, &discovery_callback, + SendSurfaceState::kForeground), + NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsDiscovering()); + + // Set an account and use it to login. + AccountManager::Account account; + account.id = kTestAccountId; + account_manager().SetAccount(account); + service_->GetAccountManager()->Login( + [&](AccountManager::Account account) { + EXPECT_EQ(account.id, kTestAccountId); + notification.Notify(); + }, + []() {}); + ASSERT_TRUE(notification.WaitForNotificationWithTimeout(kWaitTimeout)); + EXPECT_TRUE(fake_nearby_connections_manager_->IsDiscovering()); + + // Logout user. + absl::Notification logout_notification; + service_->GetAccountManager()->Logout([&](absl::Status status) { + EXPECT_TRUE(status.ok()); + logout_notification.Notify(); + }); + EXPECT_TRUE(logout_notification.WaitForNotificationWithTimeout(kWaitTimeout)); + UnregisterSendSurface(&transfer_callback, &discovery_callback); + FakeTaskRunner::WaitForRunningTasksWithTimeout(kTaskWaitTimeout); +} + +TEST_F(NearbySharingServiceImplTest, + IsReceivingEnabledWithRegisterReceiveSurfaceForeground) { + MockTransferUpdateCallback callback; + service_->GetSettings()->SetIsReceiving(true); + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kForeground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); + UnregisterReceiveSurface(&callback); +} + +TEST_F(NearbySharingServiceImplTest, + IsReceivingDisabledWithRegisterReceiveSurfaceForeground) { + MockTransferUpdateCallback callback; + service_->GetSettings()->SetIsReceiving(false); + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kForeground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_FALSE(fake_nearby_connections_manager_->IsAdvertising()); + UnregisterReceiveSurface(&callback); +} + +TEST_F(NearbySharingServiceImplTest, + IsReceivingEnabledWithRegisterReceiveSurfaceBackground) { + MockTransferUpdateCallback callback; + service_->GetSettings()->SetVisibility( + DeviceVisibility::DEVICE_VISIBILITY_SELF_SHARE); + service_->GetSettings()->SetIsReceiving(true); + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kBackground); + FakeTaskRunner::WaitForRunningTasksWithTimeout(kTaskWaitTimeout); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_TRUE(fake_nearby_connections_manager_->IsAdvertising()); + UnregisterReceiveSurface(&callback); +} + +TEST_F(NearbySharingServiceImplTest, + IsReceivingDisabledWithRegisterReceiveSurfaceBackground) { + MockTransferUpdateCallback callback; + service_->GetSettings()->SetIsReceiving(false); + NearbySharingService::StatusCodes result = RegisterReceiveSurface( + &callback, NearbySharingService::ReceiveSurfaceState::kBackground); + EXPECT_EQ(result, NearbySharingService::StatusCodes::kOk); + EXPECT_FALSE(fake_nearby_connections_manager_->IsAdvertising()); + UnregisterReceiveSurface(&callback); +} + +} // namespace NearbySharingServiceUnitTests +} // namespace sharing +} // namespace nearby diff --git a/sharing/nearby_sharing_service_test.cc b/sharing/nearby_sharing_service_test.cc new file mode 100644 index 00000000..a0cdea64 --- /dev/null +++ b/sharing/nearby_sharing_service_test.cc @@ -0,0 +1,67 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/nearby_sharing_service.h" + +#include +#include + +#include "gtest/gtest.h" + +namespace nearby { +namespace sharing { +namespace { + +using ::nearby::sharing::NearbySharingService; +using StatusCodes = NearbySharingService::StatusCodes; + +struct StatusCodeToStringData { + StatusCodes status_code; + std::string expected_string_result; +}; + +std::vector GetTestData() { + static std::vector* kStatusCodeToStringData = + new std::vector({ + {StatusCodes::kOk, "kOk"}, + {StatusCodes::kError, "kError"}, + {StatusCodes::kOutOfOrderApiCall, "kOutOfOrderApiCall"}, + {StatusCodes::kStatusAlreadyStopped, "kStatusAlreadyStopped"}, + {StatusCodes::kTransferAlreadyInProgress, + "kTransferAlreadyInProgress"}, + {StatusCodes::kNoAvailableConnectionMedium, + "kNoAvailableConnectionMedium"}, + {StatusCodes::kIrrecoverableHardwareError, + "kIrrecoverableHardwareError"}, + // If entries are added, kMaxValue and + // NearbySharingService::StatusCodeToString should be updated. + {StatusCodes::kMaxValue, "kIrrecoverableHardwareError"}, + }); + + return *kStatusCodeToStringData; +} + +using StatusCodeToString = testing::TestWithParam; + +TEST_P(StatusCodeToString, ToStringResultMatches) { + EXPECT_EQ(GetParam().expected_string_result, + NearbySharingService::StatusCodeToString(GetParam().status_code)); +} + +INSTANTIATE_TEST_CASE_P(StatusCodeToString, StatusCodeToString, + testing::ValuesIn(GetTestData())); + +} // namespace +} // namespace sharing +} // namespace nearby diff --git a/sharing/nearby_sharing_settings.cc b/sharing/nearby_sharing_settings.cc new file mode 100644 index 00000000..78f86480 --- /dev/null +++ b/sharing/nearby_sharing_settings.cc @@ -0,0 +1,620 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/nearby_sharing_settings.h" + +#include +#include // NOLINT(build/c++17) +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "absl/time/time.h" +#include "absl/types/span.h" +#include "internal/analytics/event_logger.h" +#include "internal/flags/nearby_flags.h" +#include "internal/platform/clock.h" +#include "internal/platform/device_info.h" +#include "internal/platform/mutex_lock.h" +#include "proto/sharing_enums.pb.h" +#include "sharing/analytics/analytics_recorder.h" +#include "sharing/common/compatible_u8_string.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/common/nearby_share_prefs.h" +#include "sharing/flags/nearby_sharing_feature_flags.h" +#include "sharing/internal/api/preference_manager.h" +#include "sharing/internal/public/context.h" +#include "sharing/internal/public/logging.h" +#include "sharing/local_device_data/nearby_share_local_device_data_manager.h" +#include "sharing/proto/enums.pb.h" + +namespace nearby { +namespace sharing { +namespace { + +using ::location::nearby::proto::sharing::DesktopNotification; +using ::location::nearby::proto::sharing::DesktopTransferEventType; +using ::location::nearby::proto::sharing::ShowNotificationStatus; +using ::nearby::sharing::api::PreferenceManager; +using ::nearby::sharing::proto::DataUsage; +using ::nearby::sharing::proto::DeviceVisibility; +using ::nearby::sharing::proto::FastInitiationNotificationState; + +constexpr absl::string_view kPreferencesObserverName = + "nearby-sharing-settings"; +constexpr int kMaxVisibilityExpirationSeconds = + prefs::kDefaultMaxVisibilityExpirationSeconds; + +ShowNotificationStatus GetNotificationStatus( + FastInitiationNotificationState state) { + switch (state) { + case FastInitiationNotificationState::ENABLED_FAST_INIT: + return ShowNotificationStatus::SHOW; + case FastInitiationNotificationState::DISABLED_BY_USER_FAST_INIT: + case FastInitiationNotificationState::DISABLED_BY_FEATURE_FAST_INIT: + return ShowNotificationStatus::NOT_SHOW; + default: + return ShowNotificationStatus::UNKNOWN_SHOW_NOTIFICATION_STATUS; + } +} +} // namespace + +NearbyShareSettings::NearbyShareSettings( + Context* context, + nearby::Clock* clock, + nearby::DeviceInfo& device_info, + PreferenceManager& preference_manager, + NearbyShareLocalDeviceDataManager* local_device_data_manager, + nearby::analytics::EventLogger* event_logger) + : clock_(clock), + device_info_(device_info), + preference_manager_(preference_manager), + local_device_data_manager_(local_device_data_manager), + analytics_recorder_( + std::make_unique(event_logger)) { + is_desctructing_ = std::make_shared(false); + visibility_expiration_timer_ = context->CreateTimer(); + RestoreFallbackVisibility(); + preference_manager_.AddObserver( + kPreferencesObserverName, + [this, desctructing = + std::weak_ptr(is_desctructing_)](absl::string_view key) { + std::shared_ptr is_desctructing = desctructing.lock(); + if (is_desctructing == nullptr || *is_desctructing) { + NL_LOG(WARNING) << ": Ignore the preferences change callback."; + return; + } + + OnPreferenceChanged(key); + }); + local_device_data_manager_->AddObserver(this); +} + +NearbyShareSettings::~NearbyShareSettings() { + MutexLock lock(&mutex_); + is_desctructing_ = nullptr; + preference_manager_.RemoveObserver(kPreferencesObserverName); + local_device_data_manager_->RemoveObserver(this); + visibility_expiration_timer_->Stop(); +} + +bool NearbyShareSettings::GetEnabled() const { + MutexLock lock(&mutex_); + return preference_manager_.GetBoolean(prefs::kNearbySharingEnabledName, + false); +} + +FastInitiationNotificationState +NearbyShareSettings::GetFastInitiationNotificationState() const { + MutexLock lock(&mutex_); + return static_cast( + preference_manager_.GetInteger( + prefs::kNearbySharingFastInitiationNotificationStateName, + static_cast( + FastInitiationNotificationState::ENABLED_FAST_INIT))); +} + +void NearbyShareSettings::SetIsFastInitiationHardwareSupported( + bool is_supported) { + MutexLock lock(&mutex_); + + // If the new value is the same as the old value, don't notify observers. + if (is_fast_initiation_hardware_supported_ == is_supported) { + return; + } + + is_fast_initiation_hardware_supported_ = is_supported; + + for (Observer* observer : observers_set_.GetObservers()) { + observer->OnIsFastInitiationHardwareSupportedChanged(is_supported); + } +} + +std::string NearbyShareSettings::GetDeviceName() const { + return local_device_data_manager_->GetDeviceName(); +} + +DataUsage NearbyShareSettings::GetDataUsage() const { + MutexLock lock(&mutex_); + return static_cast( + preference_manager_.GetInteger(prefs::kNearbySharingDataUsageName, 0)); +} + +void NearbyShareSettings::StartVisibilityTimer( + absl::Duration expiration) const { + NL_LOG(INFO) << __func__ + << ": start visibility timer. expiration=" << expiration; + visibility_expiration_timer_->Start( + expiration / absl::Milliseconds(1), 0, [this]() { + NL_LOG(INFO) << __func__ << ": visibility timer expired."; + int visibility; + { + MutexLock lock(&mutex_); + visibility_expiration_timer_->Stop(); + visibility = preference_manager_.GetInteger( + prefs::kNearbySharingBackgroundFallbackVisibilityName, + static_cast(prefs::kDefaultFallbackVisibility)); + } + SetVisibility(DeviceVisibility(visibility)); + }); +} + +void NearbyShareSettings::RestoreFallbackVisibility() { + MutexLock lock(&mutex_); + + int64_t expiration_seconds = preference_manager_.GetInteger( + prefs::kNearbySharingBackgroundVisibilityExpirationSeconds, 0); + int64_t fallback_visibility = preference_manager_.GetInteger( + prefs::kNearbySharingBackgroundFallbackVisibilityName, + static_cast(prefs::kDefaultFallbackVisibility)); + fallback_visibility_ = static_cast(fallback_visibility); + + int64_t now_seconds = absl::ToUnixSeconds(clock_->Now()); + int64_t remaining_seconds = expiration_seconds - now_seconds; + int64_t diff = kMaxVisibilityExpirationSeconds - remaining_seconds; + NL_LOG(INFO) << __func__ << ": diff=" << diff << ", now=" << now_seconds + << ", expiration=" << expiration_seconds + << ", max=" << kMaxVisibilityExpirationSeconds; + if (remaining_seconds > 0 && + remaining_seconds <= kMaxVisibilityExpirationSeconds) { // Not expired + StartVisibilityTimer(absl::Seconds(remaining_seconds)); + } else if (expiration_seconds != 0) { // Expired. + NL_LOG(INFO) << __func__ + << ": timer is already expired. Restore fallback visibility."; + SetVisibility(static_cast(fallback_visibility)); + } else { + NL_LOG(INFO) << __func__ << ": No running fallback Visibility."; + } +} + +std::vector NearbyShareSettings::GetAllowedContacts() const { + MutexLock lock(&mutex_); + std::vector allowed_contacts = + preference_manager_.GetStringArray( + prefs::kNearbySharingAllowedContactsName, {}); + return allowed_contacts; +} + +bool NearbyShareSettings::IsOnboardingComplete() const { + MutexLock lock(&mutex_); + return preference_manager_.GetBoolean( + prefs::kNearbySharingOnboardingCompleteName, false); +} + +std::string NearbyShareSettings::GetCustomSavePath() const { + MutexLock lock(&mutex_); + return preference_manager_.GetString( + prefs::kNearbySharingCustomSavePath, + GetCompatibleU8String(device_info_.GetDownloadPath().u8string())); +} + +bool NearbyShareSettings::IsDisabledByPolicy() const { return !GetEnabled(); } + +void NearbyShareSettings::AddSettingsObserver(Observer* observer) { + MutexLock lock(&mutex_); + observers_set_.AddObserver(observer); +} + +void NearbyShareSettings::RemoveSettingsObserver(Observer* observer) { + MutexLock lock(&mutex_); + observers_set_.RemoveObserver(observer); +} + +void NearbyShareSettings::GetEnabled(std::function callback) { + std::move(callback)(GetEnabled()); +} + +void NearbyShareSettings::GetFastInitiationNotificationState( + std::function callback) { + std::move(callback)(GetFastInitiationNotificationState()); +} + +void NearbyShareSettings::GetIsFastInitiationHardwareSupported( + std::function callback) { + MutexLock lock(&mutex_); + std::move(callback)(is_fast_initiation_hardware_supported_); +} + +void NearbyShareSettings::SetEnabled(bool enabled) { + MutexLock lock(&mutex_); + + preference_manager_.SetBoolean(prefs::kNearbySharingEnabledName, enabled); + if (enabled && + GetVisibility() == DeviceVisibility::DEVICE_VISIBILITY_UNSPECIFIED) { + NL_LOG(ERROR) << "Nearby Share enabled with visibility unset. Setting " + "default visibility to kEveryone."; + SetVisibility(DeviceVisibility::DEVICE_VISIBILITY_EVERYONE); + } +} + +void NearbyShareSettings::SetFastInitiationNotificationState( + FastInitiationNotificationState state) { + MutexLock lock(&mutex_); + analytics_recorder_->NewToggleShowNotification( + GetNotificationStatus(GetFastInitiationNotificationState()), + GetNotificationStatus(state)); + + preference_manager_.SetInteger( + prefs::kNearbySharingFastInitiationNotificationStateName, + static_cast(state)); +} + +void NearbyShareSettings::IsOnboardingComplete( + std::function callback) { + std::move(callback)(IsOnboardingComplete()); +} + +void NearbyShareSettings::SetIsOnboardingComplete( + bool completed, std::function callback) { + MutexLock lock(&mutex_); + preference_manager_.SetBoolean(prefs::kNearbySharingOnboardingCompleteName, + completed); + std::move(callback)(); +} + +void NearbyShareSettings::GetDeviceName( + std::function callback) { + std::move(callback)(GetDeviceName()); +} + +void NearbyShareSettings::ValidateDeviceName( + absl::string_view device_name, + std::function callback) { + std::move(callback)( + local_device_data_manager_->ValidateDeviceName(device_name)); +} + +void NearbyShareSettings::SetDeviceName( + absl::string_view device_name, + std::function callback) { + analytics_recorder_->NewSetDeviceName(device_name.size()); + std::move(callback)(local_device_data_manager_->SetDeviceName(device_name)); +} + +void NearbyShareSettings::GetDataUsage( + std::function callback) { + std::move(callback)(GetDataUsage()); +} + +void NearbyShareSettings::SetDataUsage(DataUsage data_usage) { + MutexLock lock(&mutex_); + analytics_recorder_->NewSetDataUsage(GetDataUsage(), data_usage); + preference_manager_.SetInteger(prefs::kNearbySharingDataUsageName, + static_cast(data_usage)); +} + +void NearbyShareSettings::GetVisibility( + std::function callback) { + std::move(callback)(GetVisibility()); +} + +DeviceVisibility NearbyShareSettings::GetVisibility() const { + MutexLock lock(&mutex_); + DeviceVisibility visibility = + static_cast(preference_manager_.GetInteger( + prefs::kNearbySharingBackgroundVisibilityName, + static_cast(prefs::kDefaultVisibility))); + if (visibility == DeviceVisibility::DEVICE_VISIBILITY_SELECTED_CONTACTS) { + // Set the visibility to self share if it's only visible to selected + // contacts, as part of QuickShare rebrand work. + SetVisibility(DeviceVisibility::DEVICE_VISIBILITY_SELF_SHARE); + return DeviceVisibility::DEVICE_VISIBILITY_SELF_SHARE; + } + return visibility; +} + +void NearbyShareSettings::SetVisibility(DeviceVisibility visibility, + absl::Duration expiration) const { + MutexLock lock(&mutex_); + DeviceVisibility last_visibility = + static_cast(preference_manager_.GetInteger( + prefs::kNearbySharingBackgroundVisibilityName, + static_cast(prefs::kDefaultVisibility))); + analytics_recorder_->NewSetVisibility(last_visibility, visibility, + expiration / absl::Milliseconds(1)); + + NL_VLOG(1) << __func__ + << ": set visibility. visibility=" << static_cast(visibility) + << ", expiration=" << expiration; + if (visibility_expiration_timer_->IsRunning()) { + NL_VLOG(1) << __func__ + << ": temporary visibility timer is running. stopped."; + visibility_expiration_timer_->Stop(); + } + + absl::Time now = clock_->Now(); + if (expiration != absl::ZeroDuration()) { + NL_VLOG(1) << __func__ << ": temporary visibility timer starts."; + absl::Time fallback_visibility_timestamp = now + expiration; + preference_manager_.SetInteger( + prefs::kNearbySharingBackgroundVisibilityExpirationSeconds, + absl::ToUnixSeconds(fallback_visibility_timestamp)); + StartVisibilityTimer(expiration); + } else { + preference_manager_.SetInteger( + prefs::kNearbySharingBackgroundVisibilityExpirationSeconds, 0); + } + + last_visibility_timestamp_ = now; + last_visibility_ = last_visibility; + preference_manager_.SetInteger( + prefs::kNearbySharingBackgroundVisibilityName, + static_cast(visibility)); +} + +absl::Time NearbyShareSettings::GetLastVisibilityTimestamp() const { + MutexLock lock(&mutex_); + return last_visibility_timestamp_; +} + +proto::DeviceVisibility NearbyShareSettings::GetLastVisibility() const { + MutexLock lock(&mutex_); + return static_cast(last_visibility_); +} + +DeviceVisibility NearbyShareSettings::GetFallbackVisibility() const { + MutexLock lock(&mutex_); + NL_VLOG(1) << __func__ << ": get fallback visibility called."; + return fallback_visibility_.has_value() ? *fallback_visibility_ + : prefs::kDefaultFallbackVisibility; +} + +void NearbyShareSettings::SetFallbackVisibility( + DeviceVisibility visibility) const { + MutexLock lock(&mutex_); + NL_VLOG(1) << __func__ << ": set fallback visibility. visibility=" + << static_cast(visibility); + if (visibility == DeviceVisibility::DEVICE_VISIBILITY_EVERYONE) { + NL_VLOG(1) << __func__ << ": visibility is everyone. Skip."; + return; + } + + fallback_visibility_ = visibility; + preference_manager_.SetInteger( + prefs::kNearbySharingBackgroundFallbackVisibilityName, + static_cast(visibility)); +} + +bool NearbyShareSettings::GetIsTemporarilyVisible() const { + MutexLock lock(&mutex_); + return preference_manager_.GetBoolean( + prefs::kNearbySharingBackgroundTemporarilyVisibleName, false); +} + +void NearbyShareSettings::SetIsTemporarilyVisible( + bool is_temporarily_visible) const { + MutexLock lock(&mutex_); + preference_manager_.SetBoolean( + prefs::kNearbySharingBackgroundTemporarilyVisibleName, + is_temporarily_visible); +} + +void NearbyShareSettings::GetAllowedContacts( + std::function)> callback) { + std::move(callback)(GetAllowedContacts()); +} + +void NearbyShareSettings::SetAllowedContacts( + absl::Span allowed_contacts) { + MutexLock lock(&mutex_); + preference_manager_.SetStringArray(prefs::kNearbySharingAllowedContactsName, + allowed_contacts); +} + +void NearbyShareSettings::GetCustomSavePathAsync( + const std::function& callback) const { + callback(GetCustomSavePath()); +} + +void NearbyShareSettings::SetCustomSavePathAsync( + absl::string_view save_path, const std::function& callback) { + MutexLock lock(&mutex_); + preference_manager_.SetString(prefs::kNearbySharingCustomSavePath, + save_path); + callback(); +} + +void NearbyShareSettings::OnPreferenceChanged(absl::string_view key) { + MutexLock lock(&mutex_); + if (key == prefs::kNearbySharingEnabledName) { + NotifyAllObservers(key, Observer::Data(GetEnabled())); + + if (NearbyFlags::GetInstance().GetBoolFlag( + config_package_nearby::nearby_sharing_feature:: + kEnableBackgroundScanning)) { + ProcessFastInitiationNotificationParentPrefChanged(GetEnabled()); + } + } else if (key == prefs::kNearbySharingFastInitiationNotificationStateName) { + NotifyAllObservers(key, Observer::Data(static_cast( + GetFastInitiationNotificationState()))); + } else if (key == prefs::kNearbySharingBackgroundVisibilityName) { + NotifyAllObservers(key, + Observer::Data(static_cast(GetVisibility()))); + } else if (key == prefs::kNearbySharingDataUsageName) { + NotifyAllObservers(key, + Observer::Data(static_cast(GetDataUsage()))); + } else if (key == prefs::kNearbySharingAllowedContactsName) { + NotifyAllObservers(key, Observer::Data(GetAllowedContacts())); + } else if (key == prefs::kNearbySharingOnboardingCompleteName) { + NotifyAllObservers(key, Observer::Data(IsOnboardingComplete())); + } else if (key == prefs::kNearbySharingIsReceivingName) { + NotifyAllObservers(key, Observer::Data(GetIsReceiving())); + } else if (key == prefs::kNearbySharingCustomSavePath) { + NotifyAllObservers(key, Observer::Data(GetCustomSavePath())); + } else { + // Not a monitored key. + return; + } +} + +void NearbyShareSettings::OnLocalDeviceDataChanged(bool did_device_name_change, + bool did_full_name_change, + bool did_icon_url_change) { + MutexLock lock(&mutex_); + if (!did_device_name_change) return; + + std::string device_name = GetDeviceName(); + NotifyAllObservers(prefs::kNearbySharingDeviceNameName, + Observer::Data(device_name)); +} + +void NearbyShareSettings::NotifyAllObservers(absl::string_view key, + Observer::Data value) { + for (Observer* observer : observers_set_.GetObservers()) { + observer->OnSettingChanged(key, value); + } +} + +void NearbyShareSettings::ProcessFastInitiationNotificationParentPrefChanged( + bool enabled) { + // If onboarding is not yet complete the Nearby feature should not be able + // to affect the enabled state. + if (!IsOnboardingComplete()) { + return; + } + + // If the user explicitly disabled notifications, toggling the Nearby Share + // feature does not re-enable the notification sub-feature. + if (GetFastInitiationNotificationState() == + FastInitiationNotificationState::DISABLED_BY_USER_FAST_INIT) { + return; + } + SetFastInitiationNotificationState( + enabled ? FastInitiationNotificationState::ENABLED_FAST_INIT + : FastInitiationNotificationState::DISABLED_BY_FEATURE_FAST_INIT); +} + +bool NearbyShareSettings::GetIsReceiving() { + MutexLock lock(&mutex_); + return preference_manager_.GetBoolean(prefs::kNearbySharingIsReceivingName, + true); +} + +void NearbyShareSettings::SetIsReceiving(bool is_receiving) const { + MutexLock lock(&mutex_); + preference_manager_.SetBoolean(prefs::kNearbySharingIsReceivingName, + is_receiving); +} + +bool NearbyShareSettings::GetIsAnalyticsEnabled() { + MutexLock lock(&mutex_); + return preference_manager_.GetBoolean( + prefs::kNearbySharingIsAnalyticsEnabledName, true); +} + +void NearbyShareSettings::SetIsAnalyticsEnabled( + bool is_analytics_enabled) const { + MutexLock lock(&mutex_); + preference_manager_.SetBoolean(prefs::kNearbySharingIsAnalyticsEnabledName, + is_analytics_enabled); +} + +std::string NearbyShareSettings::Dump() const { + std::stringstream sstream; + sstream << "Nearby Share Settings" << std::endl; + sstream << " Device name: " << GetDeviceName() << std::endl; + sstream << " Visibility: " << DeviceVisibility_Name(GetVisibility()) + << std::endl; + sstream << " Enabled: " << std::boolalpha << GetEnabled() << std::noboolalpha + << std::endl; + sstream << " FastInitiationNotification: " + << FastInitiationNotificationState_Name( + GetFastInitiationNotificationState()) + << std::endl; + sstream << " DataUsage: " << DataUsage_Name(GetDataUsage()) << std::endl; + sstream << " Last Visibility: " << DeviceVisibility_Name(GetLastVisibility()) + << std::endl; + return sstream.str(); +} + +bool NearbyShareSettings::GetIsAllContactsEnabled() { + MutexLock lock(&mutex_); + return preference_manager_.GetBoolean( + prefs::kNearbySharingIsAllContactsEnabledName, true); +} + +void NearbyShareSettings::SetIsAllContactsEnabled( + bool is_all_contacts_enabled) const { + MutexLock lock(&mutex_); + preference_manager_.SetBoolean( + prefs::kNearbySharingIsAllContactsEnabledName, is_all_contacts_enabled); + + if (is_all_contacts_enabled) { + if (GetVisibility() == + DeviceVisibility::DEVICE_VISIBILITY_SELECTED_CONTACTS) { + SetVisibility(DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + } + } else { + if (GetVisibility() == DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS) { + SetVisibility(DeviceVisibility::DEVICE_VISIBILITY_SELECTED_CONTACTS); + } + } +} + +bool NearbyShareSettings::GetAutoAppStartEnabled() const { + MutexLock lock(&mutex_); + return preference_manager_.GetBoolean( + prefs::kNearbySharingAutoAppStartEnabledName, true); +} + +void NearbyShareSettings::SetAutoAppStartEnabled(bool is_auto_app_start) const { + MutexLock lock(&mutex_); + preference_manager_.SetBoolean(prefs::kNearbySharingAutoAppStartEnabledName, + is_auto_app_start); +} + +void NearbyShareSettings::SendDesktopNotification( + DesktopNotification event) const { + analytics_recorder_->NewSendDesktopNotification(event); +} + +void NearbyShareSettings::SendDesktopTransferEvent( + DesktopTransferEventType event) const { + analytics_recorder_->NewSendDesktopTransferEvent(event); +} + +bool NearbyShareSettings::is_fast_initiation_hardware_supported() { + MutexLock lock(&mutex_); + return is_fast_initiation_hardware_supported_; +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/nearby_sharing_settings.h b/sharing/nearby_sharing_settings.h new file mode 100644 index 00000000..cb0e200f --- /dev/null +++ b/sharing/nearby_sharing_settings.h @@ -0,0 +1,298 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_NEARBY_SHARING_SETTINGS_H_ +#define THIRD_PARTY_NEARBY_SHARING_NEARBY_SHARING_SETTINGS_H_ + +#include +#include +#include +#include +#include +#include + +#include "absl/base/thread_annotations.h" +#include "absl/strings/string_view.h" +#include "absl/time/time.h" +#include "absl/types/span.h" +#include "internal/analytics/event_logger.h" +#include "internal/base/observer_list.h" +#include "internal/platform/clock.h" +#include "internal/platform/device_info.h" +#include "internal/platform/mutex.h" +#include "proto/sharing_enums.pb.h" +#include "sharing/analytics/analytics_recorder.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/internal/api/preference_manager.h" +#include "sharing/internal/public/context.h" +#include "sharing/local_device_data/nearby_share_local_device_data_manager.h" +#include "sharing/proto/settings_observer_data.pb.h" + +namespace nearby { +namespace sharing { + +// Provides a type safe wrapper/abstraction over prefs for both C++ and +// Javascript (over mojo) to interact with Nearby user settings. This class +// always reads directly from prefs and relies on preference's memory cache. +// It is designed to be contained within the Nearby Sharing Service with an +// instance per user profile. This class also helps to keep some prefs +// logic out of |NearbyShareServiceImpl|. +// +// This class is also used to expose device properties that affect the settings +// UI, but cannot be added at load time because they need to be re-computed. See +// GetIsFastInitiationHardwareSupported() as an example. +// +// The mojo interface is intended to be exposed in settings, os_settings, and +// the nearby WebUI. +// +// NOTE: The pref-change registrar only notifies observers of pref value +// changes; observers are not notified if the pref value is set but does not +// change. This class inherits this behavior. +// +// NOTE: Because the observer interface is over mojo, setting a value directly +// will not synchronously trigger the observer event. Generally this is not a +// problem because these settings should only be changed by user interaction, +// but this is necessary to know when writing unit-tests. +class NearbyShareSettings + : nearby::sharing::NearbyShareLocalDeviceDataManager::Observer { + public: + class Observer { + public: + // LINT.IfChange(TaggedUnion) + // The C++ counterpart of message `Data` in + // third_party/nearby/sharing/proto/settings_observer_data.proto + struct Data { + proto::Tag tag; + + union Value { + bool as_bool; + int64_t as_int64; + std::string as_string; + std::vector as_string_array; + + explicit Value(std::nullptr_t = nullptr) {} + explicit Value(bool data) : as_bool(data) {} + explicit Value(int64_t data) : as_int64(data) {} + explicit Value(std::string data) : as_string(data) {} + explicit Value(std::vector data) : as_string_array(data) {} + + ~Value() {} + } value; + + ~Data() { + // Call the destructors of members who are not basic types. + if (tag == proto::Tag::TAG_STRING) { + this->value.as_string.~basic_string(); + } + if (tag == proto::Tag::TAG_STRING_ARRAY) { + this->value.as_string_array.~vector(); + } + } + + explicit Data(std::nullptr_t = nullptr) + : tag(proto::Tag::TAG_NULL), value() {} + + explicit Data(const bool data) : tag(proto::Tag::TAG_BOOL), value(data) {} + + explicit Data(const int64_t data) + : tag(proto::Tag::TAG_INT64), value(data) {} + + explicit Data(const std::string& data) + : tag(proto::Tag::TAG_STRING), value(data) {} + + explicit Data(const std::vector& data) + : tag(proto::Tag::TAG_STRING_ARRAY), value(data) {} + + explicit operator std::unique_ptr() const { + auto result = std::make_unique(); + result->set_tag(this->tag); + switch (this->tag) { + case proto::Tag::TAG_NULL: + break; + case proto::Tag::TAG_BOOL: + result->set_as_bool(this->value.as_bool); + break; + case proto::Tag::TAG_INT64: + result->set_as_int64(this->value.as_int64); + break; + case proto::Tag::TAG_STRING: + result->set_as_string(this->value.as_string); + break; + case proto::Tag::TAG_STRING_ARRAY: + for (const auto& value : this->value.as_string_array) { + result->add_as_string_array(value); + } + break; + default: + LOG(QFATAL) << "Invalid tag: " << this->tag; + break; + } + return result; + } + }; + // LINT.ThenChange( + // //depot/google3/third_party/nearby/sharing/proto/settings_observer_data.proto:TaggedUnion + // ) + + virtual ~Observer() = default; + + virtual void OnSettingChanged(absl::string_view key, const Data& data) {} + // Called when the fast initiation hardware offloading support state + // changes. + virtual void OnIsFastInitiationHardwareSupportedChanged( + bool is_supported) = 0; + }; + + NearbyShareSettings( + Context* context, + nearby::Clock* clock, + nearby::DeviceInfo& device_info, + nearby::sharing::api::PreferenceManager& preference_manager, + NearbyShareLocalDeviceDataManager* local_device_data_manager, + nearby::analytics::EventLogger* event_logger = nullptr); + ~NearbyShareSettings() override; + + // Internal synchronous getters for C++ clients + bool GetEnabled() const; + proto::FastInitiationNotificationState GetFastInitiationNotificationState() + const; + bool is_fast_initiation_hardware_supported(); + void SetIsFastInitiationHardwareSupported(bool is_supported); + std::string GetDeviceName() const; + proto::DataUsage GetDataUsage() const; + proto::DeviceVisibility GetVisibility() const; + // Gets the timestamp of last visibility change. Need the timestamp to decide + // whether need to send optional signature data during key pairing. + absl::Time GetLastVisibilityTimestamp() const; + proto::DeviceVisibility GetLastVisibility() const; + + proto::DeviceVisibility GetFallbackVisibility() const; + bool GetIsTemporarilyVisible() const; + void SetIsTemporarilyVisible(bool is_temporarily_visible) const; + std::vector GetAllowedContacts() const; + bool IsOnboardingComplete() const; + std::string GetCustomSavePath() const; + + // Returns true if the feature is disabled by policy. + bool IsDisabledByPolicy() const; + + // Asynchronous APIs exposed by NearbyShareSettings + void AddSettingsObserver(Observer* observer); + void RemoveSettingsObserver(Observer* observer); + void GetEnabled(std::function callback); + void GetFastInitiationNotificationState( + std::function callback); + void GetIsFastInitiationHardwareSupported(std::function callback); + void SetEnabled(bool enabled); + void SetFastInitiationNotificationState( + proto::FastInitiationNotificationState state); + void IsOnboardingComplete(std::function callback); + void SetIsOnboardingComplete(bool completed, std::function callback); + void GetDeviceName(std::function callback); + void ValidateDeviceName( + absl::string_view device_name, + std::function callback); + void SetDeviceName(absl::string_view device_name, + std::function callback); + void GetDataUsage(std::function callback); + void SetDataUsage(proto::DataUsage data_usage); + void GetVisibility(std::function callback); + void SetVisibility(proto::DeviceVisibility visibility, + absl::Duration expiration = absl::ZeroDuration()) const; + void SetFallbackVisibility(proto::DeviceVisibility visibility) const; + bool GetIsReceiving(); + void SetIsReceiving(bool is_receiving) const; + bool GetIsAnalyticsEnabled(); + void SetIsAnalyticsEnabled(bool is_analytics_enabled) const; + bool GetIsAllContactsEnabled(); + void SetIsAllContactsEnabled(bool is_all_contacts_enabled) const; + + void GetAllowedContacts( + std::function)> callback); + void SetAllowedContacts(absl::Span allowed_contacts); + + void GetCustomSavePathAsync( + const std::function& callback) const; + void SetCustomSavePathAsync(absl::string_view save_path, + const std::function& callback); + + bool GetAutoAppStartEnabled() const; + void SetAutoAppStartEnabled(bool is_auto_app_start) const; + + // NearbyShareLocalDeviceDataManager::Observer: + void OnLocalDeviceDataChanged(bool did_device_name_change, + bool did_full_name_change, + bool did_icon_url_change) override; + + void SendDesktopNotification( + ::location::nearby::proto::sharing::DesktopNotification event) const; + + void SendDesktopTransferEvent( + ::location::nearby::proto::sharing::DesktopTransferEventType event) const; + + std::string Dump() const; + + private: + void OnEnabledPrefChanged(); + void OnFastInitiationNotificationStatePrefChanged(); + void OnDataUsagePrefChanged(); + void OnVisibilityPrefChanged(); + void OnIsReceivingPrefChanged(); + void OnAllowedContactsPrefChanged(); + void OnIsOnboardingCompletePrefChanged(); + void OnCustomSavePathChanged(); + void OnPreferenceChanged(absl::string_view key); + + void NotifyAllObservers(absl::string_view key, Observer::Data value) + ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_); + + // If the Nearby Share parent feature is toggled on then Fast Initiation + // notifications should be re-enabled unless the user explicitly disabled the + // notification sub-feature. + void ProcessFastInitiationNotificationParentPrefChanged(bool enabled) + ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_); + + void StartVisibilityTimer(absl::Duration expiration) const + ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_); + + // Restore/Save fallback visibility + void RestoreFallbackVisibility(); + + // Make sure thread safe to access Nearby settings + mutable RecursiveMutex mutex_; + nearby::Clock* const clock_; + nearby::DeviceInfo& device_info_; + nearby::sharing::api::PreferenceManager& preference_manager_; + NearbyShareLocalDeviceDataManager* const local_device_data_manager_; + // Used to create analytics events. + std::unique_ptr analytics_recorder_; + + std::shared_ptr is_desctructing_ = nullptr; + bool is_fast_initiation_hardware_supported_ ABSL_GUARDED_BY(mutex_) = false; + ObserverList observers_set_ ABSL_GUARDED_BY(mutex_); + std::unique_ptr visibility_expiration_timer_ ABSL_GUARDED_BY(mutex_); + mutable std::optional fallback_visibility_ + ABSL_GUARDED_BY(mutex_); + + // Used to track the timestamp of visibility change. + mutable absl::Time last_visibility_timestamp_ ABSL_GUARDED_BY(mutex_) = + absl::InfinitePast(); + mutable proto::DeviceVisibility last_visibility_ ABSL_GUARDED_BY(mutex_) = + proto::DeviceVisibility::DEVICE_VISIBILITY_UNSPECIFIED; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_NEARBY_SHARING_SETTINGS_H_ diff --git a/sharing/nearby_sharing_settings_test.cc b/sharing/nearby_sharing_settings_test.cc new file mode 100644 index 00000000..f5c049ac --- /dev/null +++ b/sharing/nearby_sharing_settings_test.cc @@ -0,0 +1,576 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/nearby_sharing_settings.h" + +#include +#include // NOLINT(build/c++17) +#include +#include +#include + +#include "gtest/gtest.h" +#include "absl/base/thread_annotations.h" +#include "absl/strings/string_view.h" +#include "absl/synchronization/mutex.h" +#include "absl/synchronization/notification.h" +#include "absl/time/clock.h" +#include "absl/time/time.h" +#include "absl/types/span.h" +#include "internal/test/fake_device_info.h" +#include "internal/test/fake_task_runner.h" +#include "sharing/common/compatible_u8_string.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/common/nearby_share_prefs.h" +#include "sharing/internal/test/fake_context.h" +#include "sharing/internal/test/fake_preference_manager.h" +#include "sharing/local_device_data/fake_nearby_share_local_device_data_manager.h" +#include "sharing/proto/enums.pb.h" + +namespace nearby { +namespace sharing { +namespace { + +using ::location::nearby::proto::sharing::DesktopNotification; +using ::location::nearby::proto::sharing::DesktopTransferEventType; +using ::nearby::sharing::proto::DataUsage; +using ::nearby::sharing::proto::DeviceVisibility; +using ::nearby::sharing::proto::FastInitiationNotificationState; + +constexpr char kDefaultDeviceName[] = "Josh's Chromebook"; + +class FakeNearbyShareSettingsObserver : public NearbyShareSettings::Observer { + public: + void OnSettingChanged(absl::string_view key, const Data& data) override { + absl::MutexLock lock(&mutex_); + if (key == prefs::kNearbySharingEnabledName) { + enabled_ = data.value.as_bool; + } else if (key == + prefs::kNearbySharingFastInitiationNotificationStateName) { + fast_initiation_notification_state_ = + static_cast(data.value.as_int64); + } else if (key == prefs::kNearbySharingDataUsageName) { + data_usage_ = static_cast(data.value.as_int64); + } else if (key == prefs::kNearbySharingCustomSavePath) { + custom_save_path_ = data.value.as_string; + } else if (key == prefs::kNearbySharingBackgroundVisibilityName) { + visibility_ = static_cast(data.value.as_int64); + } else if (key == prefs::kNearbySharingOnboardingCompleteName) { + is_onboarding_complete_ = data.value.as_bool; + } else if (key == prefs::kNearbySharingIsReceivingName) { + is_receiving_ = data.value.as_bool; + } else if (key == prefs::kNearbySharingAllowedContactsName) { + allowed_contacts_.clear(); + for (auto& allowed_contact : data.value.as_string_array) { + allowed_contacts_.push_back(allowed_contact); + } + } else if (key == prefs::kNearbySharingDeviceNameName) { + device_name_ = data.value.as_string; + } + } + + void OnIsFastInitiationHardwareSupportedChanged(bool is_supported) override { + absl::MutexLock lock(&mutex_); + is_fast_initiation_notification_hardware_supported_ = is_supported; + } + + bool enabled() const { + absl::MutexLock lock(&mutex_); + return enabled_; + } + + void set_enabled(bool enabled) { + absl::MutexLock lock(&mutex_); + enabled_ = enabled; + } + + FastInitiationNotificationState fast_initiation_notification_state() const { + absl::MutexLock lock(&mutex_); + return fast_initiation_notification_state_; + } + + bool is_fast_initiation_notification_hardware_supported() const { + absl::MutexLock lock(&mutex_); + return is_fast_initiation_notification_hardware_supported_; + } + + bool is_onboarding_complete() const { + absl::MutexLock lock(&mutex_); + return is_onboarding_complete_; + } + + const std::string& device_name() const { + absl::MutexLock lock(&mutex_); + return device_name_; + } + + const std::string& custom_save_path() const { + absl::MutexLock lock(&mutex_); + return custom_save_path_; + } + + DataUsage data_usage() const { + absl::MutexLock lock(&mutex_); + return data_usage_; + } + + DeviceVisibility visibility() const { + absl::MutexLock lock(&mutex_); + return visibility_; + } + + const std::vector& allowed_contacts() { + absl::MutexLock lock(&mutex_); + return allowed_contacts_; + } + + private: + mutable absl::Mutex mutex_; + + bool enabled_ ABSL_GUARDED_BY(mutex_) = false; + FastInitiationNotificationState fast_initiation_notification_state_ + ABSL_GUARDED_BY(mutex_) = + FastInitiationNotificationState::ENABLED_FAST_INIT; + bool is_fast_initiation_notification_hardware_supported_ + ABSL_GUARDED_BY(mutex_) = false; + bool is_onboarding_complete_ ABSL_GUARDED_BY(mutex_) = false; + bool is_receiving_ ABSL_GUARDED_BY(mutex_) = false; + std::string device_name_ ABSL_GUARDED_BY(mutex_) = "uncalled"; + std::string custom_save_path_ ABSL_GUARDED_BY(mutex_); + DataUsage data_usage_ ABSL_GUARDED_BY(mutex_) = DataUsage::UNKNOWN_DATA_USAGE; + DeviceVisibility visibility_ ABSL_GUARDED_BY(mutex_) = + DeviceVisibility::DEVICE_VISIBILITY_UNSPECIFIED; + std::vector allowed_contacts_ ABSL_GUARDED_BY(mutex_); +}; + +class NearbyShareSettingsTest : public ::testing::Test { + public: + NearbyShareSettingsTest() + : local_device_data_manager_(kDefaultDeviceName) { + prefs::RegisterNearbySharingPrefs(preference_manager_); + nearby_share_settings_ = std::make_unique( + &context_, context_.GetClock(), fake_device_info_, preference_manager_, + &local_device_data_manager_); + + nearby_share_settings_->AddSettingsObserver(&observer_); + } + + ~NearbyShareSettingsTest() override = default; + + void TearDown() override { Flush(); } + + NearbyShareSettings* settings() { return nearby_share_settings_.get(); } + + void SetIsOnboardingComplete(bool is_complete) { + preference_manager_.SetBoolean( + prefs::kNearbySharingOnboardingCompleteName, is_complete); + } + + void SetVisibilityExpirationPreference(int expiration) { + preference_manager_.SetInteger( + prefs::kNearbySharingBackgroundVisibilityExpirationSeconds, expiration); + } + + void SetCustomSavePath(absl::string_view path) { + preference_manager_.SetString( + prefs::kNearbySharingCustomSavePath, path); + } + + // Waits for running tasks to complete. + void Flush() { + absl::SleepFor(absl::Seconds(1)); + FakeTaskRunner::WaitForRunningTasksWithTimeout(absl::Milliseconds(200)); + } + + void FastForward(absl::Duration duration) { + context_.fake_clock()->FastForward(duration); + } + + bool Contains(std::vector v, std::string val) { + if (std::find(v.begin(), v.end(), val) != v.end()) { + return true; + } + return false; + } + + protected: + nearby::FakeDeviceInfo fake_device_info_; + nearby::FakePreferenceManager preference_manager_; + FakeContext context_; + FakeNearbyShareLocalDeviceDataManager local_device_data_manager_; + FakeNearbyShareSettingsObserver observer_; + std::unique_ptr nearby_share_settings_; +}; + +TEST_F(NearbyShareSettingsTest, GetAndSetEnabled) { + EXPECT_EQ(observer_.enabled(), false); + settings()->SetIsOnboardingComplete(true, []() {}); + settings()->SetEnabled(true); + EXPECT_EQ(settings()->GetEnabled(), true); + Flush(); + EXPECT_EQ(observer_.enabled(), true); + + bool enabled = false; + settings()->GetEnabled([&enabled](bool result) { enabled = result; }); + EXPECT_EQ(enabled, true); + + settings()->SetEnabled(false); + EXPECT_EQ(settings()->GetEnabled(), false); + Flush(); + EXPECT_EQ(observer_.enabled(), false); + + settings()->GetEnabled([&enabled](bool result) { enabled = result; }); + EXPECT_EQ(enabled, false); + + // Verify that setting the value to false again value doesn't trigger an + // observer event. + observer_.set_enabled(true); + settings()->SetEnabled(false); + EXPECT_EQ(settings()->GetEnabled(), false); + Flush(); + // the observers' value should not have been updated. + EXPECT_EQ(observer_.enabled(), true); +} + +TEST_F(NearbyShareSettingsTest, GetAndSetFastInitiationNotificationState) { + // Fast init notifications are enabled by default. + EXPECT_EQ(observer_.fast_initiation_notification_state(), + FastInitiationNotificationState::ENABLED_FAST_INIT); + settings()->SetFastInitiationNotificationState( + FastInitiationNotificationState::DISABLED_BY_USER_FAST_INIT); + EXPECT_EQ(FastInitiationNotificationState::DISABLED_BY_USER_FAST_INIT, + settings()->GetFastInitiationNotificationState()); + Flush(); + EXPECT_EQ(observer_.fast_initiation_notification_state(), + FastInitiationNotificationState::DISABLED_BY_USER_FAST_INIT); + + FastInitiationNotificationState state = + FastInitiationNotificationState::ENABLED_FAST_INIT; + settings()->GetFastInitiationNotificationState( + [&state](FastInitiationNotificationState result) { state = result; }); + EXPECT_EQ(state, FastInitiationNotificationState::DISABLED_BY_USER_FAST_INIT); +} + +TEST_F(NearbyShareSettingsTest, + ParentFeatureChangesFastInitiationNotificationState) { + // Fast init notifications are enabled by default. + EXPECT_EQ(observer_.fast_initiation_notification_state(), + FastInitiationNotificationState::ENABLED_FAST_INIT); + settings()->SetIsOnboardingComplete(true, []() {}); + settings()->SetEnabled(true); + Flush(); + + // Simulate toggling the parent feature off. + settings()->SetEnabled(false); + Flush(); + EXPECT_FALSE(settings()->GetEnabled()); + EXPECT_EQ(observer_.fast_initiation_notification_state(), + FastInitiationNotificationState::DISABLED_BY_FEATURE_FAST_INIT); + + // Simulate toggling the parent feature on. + settings()->SetEnabled(true); + Flush(); + EXPECT_TRUE(settings()->GetEnabled()); + EXPECT_EQ(observer_.fast_initiation_notification_state(), + FastInitiationNotificationState::ENABLED_FAST_INIT); +} + +TEST_F(NearbyShareSettingsTest, + ParentFeatureChangesFastInitiationNotificationDisabledByUser) { + // Fast init notifications are enabled by default. + EXPECT_EQ(observer_.fast_initiation_notification_state(), + FastInitiationNotificationState::ENABLED_FAST_INIT); + + // Set explicitly disabled by user. + settings()->SetFastInitiationNotificationState( + FastInitiationNotificationState::DISABLED_BY_USER_FAST_INIT); + Flush(); + EXPECT_EQ(observer_.fast_initiation_notification_state(), + FastInitiationNotificationState::DISABLED_BY_USER_FAST_INIT); + + // Simulate toggling parent feature on. + settings()->SetIsOnboardingComplete(true, []() {}); + settings()->SetEnabled(true); + Flush(); + + // The disabled by user flag should persist if the parent feature is enabled. + EXPECT_EQ(observer_.fast_initiation_notification_state(), + FastInitiationNotificationState::DISABLED_BY_USER_FAST_INIT); +} + +TEST_F(NearbyShareSettingsTest, GetAndSetCustomSavePath) { + absl::Notification notification; + settings()->SetCustomSavePathAsync( + GetCompatibleU8String(std::filesystem::temp_directory_path().u8string()), + [&]() { notification.Notify(); }); + Flush(); + EXPECT_TRUE(notification.HasBeenNotified()); + settings()->GetCustomSavePathAsync([&](absl::string_view path) { + observer_.OnSettingChanged( + prefs::kNearbySharingCustomSavePath, + NearbyShareSettings::Observer::Data(std::string(path))); + }); + Flush(); + EXPECT_EQ( + observer_.custom_save_path(), + GetCompatibleU8String(std::filesystem::temp_directory_path().u8string())); +} + +TEST_F(NearbyShareSettingsTest, GetAndSetIsOnboardingComplete) { + EXPECT_FALSE(observer_.is_onboarding_complete()); + SetIsOnboardingComplete(true); + EXPECT_TRUE(settings()->IsOnboardingComplete()); + Flush(); + EXPECT_TRUE(observer_.is_onboarding_complete()); + + bool is_complete = false; + settings()->IsOnboardingComplete( + [&is_complete](bool result) { is_complete = result; }); + EXPECT_TRUE(is_complete); +} + +TEST_F(NearbyShareSettingsTest, GetAndSetIsFastInitiationHardwareSupported) { + EXPECT_FALSE(observer_.is_fast_initiation_notification_hardware_supported()); + settings()->SetIsFastInitiationHardwareSupported(true); + + Flush(); + EXPECT_TRUE(observer_.is_fast_initiation_notification_hardware_supported()); + + bool is_supported = false; + settings()->GetIsFastInitiationHardwareSupported( + [&is_supported](bool result) { is_supported = result; }); + EXPECT_TRUE(is_supported); +} + +TEST_F(NearbyShareSettingsTest, ValidateDeviceName) { + auto result = DeviceNameValidationResult::kValid; + local_device_data_manager_.set_next_validation_result( + DeviceNameValidationResult::kErrorEmpty); + settings()->ValidateDeviceName( + "", [&result](DeviceNameValidationResult res) { result = res; }); + EXPECT_EQ(result, DeviceNameValidationResult::kErrorEmpty); + + local_device_data_manager_.set_next_validation_result( + DeviceNameValidationResult::kValid); + settings()->ValidateDeviceName( + "this string is 32 bytes in UTF-8", + [&result](DeviceNameValidationResult res) { result = res; }); + EXPECT_EQ(result, DeviceNameValidationResult::kValid); +} + +TEST_F(NearbyShareSettingsTest, GetAndSetDeviceName) { + std::string name = "not_the_default"; + settings()->GetDeviceName( + [&name](absl::string_view result) { name = std::string(result); }); + EXPECT_EQ(kDefaultDeviceName, name); + + // When we get a validation error, setting the name should not succeed. + EXPECT_EQ(observer_.device_name(), "uncalled"); + auto result = DeviceNameValidationResult::kValid; + local_device_data_manager_.set_next_validation_result( + DeviceNameValidationResult::kErrorEmpty); + settings()->SetDeviceName( + "", [&result](DeviceNameValidationResult res) { result = res; }); + EXPECT_EQ(result, DeviceNameValidationResult::kErrorEmpty); + EXPECT_EQ(settings()->GetDeviceName(), kDefaultDeviceName); + + // When the name is valid, the setting should succeed. + EXPECT_EQ(observer_.device_name(), "uncalled"); + result = DeviceNameValidationResult::kValid; + local_device_data_manager_.set_next_validation_result( + DeviceNameValidationResult::kValid); + settings()->SetDeviceName( + "d", [&result](DeviceNameValidationResult res) { result = res; }); + EXPECT_EQ(result, DeviceNameValidationResult::kValid); + EXPECT_EQ(settings()->GetDeviceName(), "d"); + + Flush(); + EXPECT_EQ(observer_.device_name(), "d"); + + settings()->GetDeviceName( + [&name](absl::string_view result) { name = std::string(result); }); + EXPECT_EQ(name, "d"); +} + +TEST_F(NearbyShareSettingsTest, GetAndSetDataUsage) { + EXPECT_EQ(observer_.data_usage(), DataUsage::UNKNOWN_DATA_USAGE); + settings()->SetDataUsage(DataUsage::OFFLINE_DATA_USAGE); + EXPECT_EQ(settings()->GetDataUsage(), DataUsage::OFFLINE_DATA_USAGE); + Flush(); + EXPECT_EQ(observer_.data_usage(), DataUsage::OFFLINE_DATA_USAGE); + + DataUsage data_usage = DataUsage::UNKNOWN_DATA_USAGE; + settings()->GetDataUsage( + [&data_usage](DataUsage usage) { data_usage = usage; }); + EXPECT_EQ(data_usage, DataUsage::OFFLINE_DATA_USAGE); +} + +TEST_F(NearbyShareSettingsTest, GetAndSetVisibility) { + EXPECT_EQ(observer_.visibility(), + DeviceVisibility::DEVICE_VISIBILITY_UNSPECIFIED); + settings()->SetVisibility(DeviceVisibility::DEVICE_VISIBILITY_EVERYONE); + EXPECT_EQ(settings()->GetVisibility(), + DeviceVisibility::DEVICE_VISIBILITY_EVERYONE); + Flush(); + EXPECT_EQ(observer_.visibility(), + DeviceVisibility::DEVICE_VISIBILITY_EVERYONE); + + DeviceVisibility visibility = DeviceVisibility::DEVICE_VISIBILITY_UNSPECIFIED; + settings()->GetVisibility( + [&visibility](DeviceVisibility result) { visibility = result; }); + EXPECT_EQ(visibility, DeviceVisibility::DEVICE_VISIBILITY_EVERYONE); +} + +TEST_F(NearbyShareSettingsTest, GetAndSetVisibilityWithSelectedContacts) { + EXPECT_EQ(observer_.visibility(), + DeviceVisibility::DEVICE_VISIBILITY_UNSPECIFIED); + settings()->SetVisibility( + DeviceVisibility::DEVICE_VISIBILITY_SELECTED_CONTACTS); + EXPECT_EQ(settings()->GetVisibility(), + DeviceVisibility::DEVICE_VISIBILITY_SELF_SHARE); + Flush(); + EXPECT_EQ(observer_.visibility(), + DeviceVisibility::DEVICE_VISIBILITY_SELF_SHARE); +} + +TEST_F(NearbyShareSettingsTest, GetFallbackVisibility) { + EXPECT_EQ(observer_.visibility(), + DeviceVisibility::DEVICE_VISIBILITY_UNSPECIFIED); + DeviceVisibility visibility = settings()->GetFallbackVisibility(); + EXPECT_EQ(visibility, DeviceVisibility::DEVICE_VISIBILITY_HIDDEN); + settings()->SetFallbackVisibility( + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + settings()->SetVisibility(DeviceVisibility::DEVICE_VISIBILITY_EVERYONE); + EXPECT_EQ(settings()->GetVisibility(), + DeviceVisibility::DEVICE_VISIBILITY_EVERYONE); + EXPECT_EQ(settings()->GetFallbackVisibility(), + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + + settings()->SetFallbackVisibility( + DeviceVisibility::DEVICE_VISIBILITY_EVERYONE); + EXPECT_EQ(settings()->GetFallbackVisibility(), + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + Flush(); + EXPECT_EQ(observer_.visibility(), + DeviceVisibility::DEVICE_VISIBILITY_EVERYONE); + + settings()->SetVisibility(settings()->GetVisibility(), absl::Seconds(1)); + Flush(); + FastForward(absl::Seconds(1)); + Flush(); + visibility = DeviceVisibility::DEVICE_VISIBILITY_UNSPECIFIED; + settings()->GetVisibility( + [&visibility](DeviceVisibility result) { visibility = result; }); + EXPECT_EQ(visibility, DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + Flush(); +} + +TEST_F(NearbyShareSettingsTest, GetAndSetAllowedContacts) { + const std::string id1("1"); + + std::vector allowed_contacts; + + settings()->GetAllowedContacts( + [&allowed_contacts](absl::Span result) { + allowed_contacts.clear(); + for (auto& contact : result) { + allowed_contacts.push_back(contact); + } + }); + EXPECT_EQ(allowed_contacts.size(), 0u); + + settings()->SetAllowedContacts({id1}); + Flush(); + EXPECT_EQ(observer_.allowed_contacts().size(), 1u); + EXPECT_TRUE(Contains(observer_.allowed_contacts(), id1)); + + settings()->GetAllowedContacts( + [&allowed_contacts](absl::Span result) { + allowed_contacts.clear(); + for (auto& contact : result) { + allowed_contacts.push_back(contact); + } + }); + EXPECT_EQ(allowed_contacts.size(), 1u); + EXPECT_TRUE(Contains(observer_.allowed_contacts(), id1)); + + settings()->SetAllowedContacts({}); + Flush(); + EXPECT_EQ(observer_.allowed_contacts().size(), 0u); + + settings()->GetAllowedContacts( + [&allowed_contacts](absl::Span result) { + allowed_contacts.clear(); + for (auto& contact : result) { + allowed_contacts.push_back(contact); + } + }); + EXPECT_EQ(allowed_contacts.size(), 0u); +} + +TEST_F(NearbyShareSettingsTest, GetAndSetAutoAppStartEnabled) { + bool is_auto_app_start_enabled = settings()->GetAutoAppStartEnabled(); + EXPECT_TRUE(is_auto_app_start_enabled); + + settings()->SetAutoAppStartEnabled(false); + Flush(); + is_auto_app_start_enabled = settings()->GetAutoAppStartEnabled(); + EXPECT_FALSE(is_auto_app_start_enabled); +} + +TEST_F(NearbyShareSettingsTest, SendDesktopNotification) { + settings()->SendDesktopNotification( + DesktopNotification::DESKTOP_NOTIFICATION_UNKNOWN); + settings()->SendDesktopNotification( + DesktopNotification::DESKTOP_NOTIFICATION_CONNECTING); + settings()->SendDesktopNotification( + DesktopNotification::DESKTOP_NOTIFICATION_PROGRESS); + settings()->SendDesktopNotification( + DesktopNotification::DESKTOP_NOTIFICATION_ACCEPT); + settings()->SendDesktopNotification( + DesktopNotification::DESKTOP_NOTIFICATION_RECEIVED); + settings()->SendDesktopNotification( + DesktopNotification::DESKTOP_NOTIFICATION_ERROR); +} + +TEST_F(NearbyShareSettingsTest, ReceiveDesktopTransferEvent) { + settings()->SendDesktopTransferEvent( + DesktopTransferEventType::DESKTOP_TRANSFER_EVENT_TYPE_UNKNOWN); + settings()->SendDesktopTransferEvent( + DesktopTransferEventType::DESKTOP_TRANSFER_EVENT_RECEIVE_TYPE_ACCEPT); + settings()->SendDesktopTransferEvent( + DesktopTransferEventType::DESKTOP_TRANSFER_EVENT_RECEIVE_TYPE_PROGRESS); + settings()->SendDesktopTransferEvent( + DesktopTransferEventType::DESKTOP_TRANSFER_EVENT_RECEIVE_TYPE_RECEIVED); + settings()->SendDesktopTransferEvent( + DesktopTransferEventType::DESKTOP_TRANSFER_EVENT_RECEIVE_TYPE_ERROR); +} + +TEST_F(NearbyShareSettingsTest, SendDesktopTransferEvent) { + settings()->SendDesktopTransferEvent( + DesktopTransferEventType::DESKTOP_TRANSFER_EVENT_TYPE_UNKNOWN); + settings()->SendDesktopTransferEvent( + DesktopTransferEventType::DESKTOP_TRANSFER_EVENT_SEND_TYPE_START); + settings()->SendDesktopTransferEvent( + DesktopTransferEventType:: + DESKTOP_TRANSFER_EVENT_SEND_TYPE_SELECT_A_DEVICE); + settings()->SendDesktopTransferEvent( + DesktopTransferEventType::DESKTOP_TRANSFER_EVENT_SEND_TYPE_PROGRESS); + settings()->SendDesktopTransferEvent( + DesktopTransferEventType::DESKTOP_TRANSFER_EVENT_SEND_TYPE_SENT); +} + +} // namespace +} // namespace sharing +} // namespace nearby diff --git a/sharing/nearby_sharing_util.cc b/sharing/nearby_sharing_util.cc new file mode 100644 index 00000000..9f403405 --- /dev/null +++ b/sharing/nearby_sharing_util.cc @@ -0,0 +1,288 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/nearby_sharing_util.h" + +#include +#include +#include +#include +#include // NOLINT(build/c++17) +#include +#include +#include + +#include "absl/hash/hash.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" +#include "absl/strings/string_view.h" +#include "internal/flags/nearby_flags.h" +#include "internal/platform/device_info.h" +#include "proto/sharing_enums.pb.h" +#include "sharing/advertisement.h" +#include "sharing/certificates/nearby_share_decrypted_public_certificate.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/flags/nearby_sharing_feature_flags.h" +#include "sharing/internal/base/encode.h" +#include "sharing/internal/public/logging.h" +#include "sharing/nearby_connections_types.h" +#include "sharing/nearby_sharing_service.h" +#include "sharing/transfer_metadata.h" + +namespace nearby { +namespace sharing { +namespace { +using ::location::nearby::proto::sharing::AttachmentTransmissionStatus; +using ::location::nearby::proto::sharing::ConnectionLayerStatus; + +// Used to hash a token into a 4 digit string. +constexpr int kHashModulo = 9973; +constexpr int kHashBaseMultiplier = 31; +} // namespace + +bool IsBackgroundScanningFeatureEnabled() { + return NearbyFlags::GetInstance().GetBoolFlag( + sharing::config_package_nearby::nearby_sharing_feature:: + kEnableBackgroundScanning); +} + +std::string ReceiveSurfaceStateToString( + NearbySharingService::ReceiveSurfaceState state) { + switch (state) { + case NearbySharingService::ReceiveSurfaceState::kForeground: + return "FOREGROUND"; + case NearbySharingService::ReceiveSurfaceState::kBackground: + return "BACKGROUND"; + case NearbySharingService::ReceiveSurfaceState::kUnknown: + return "UNKNOWN"; + } +} + +std::string SendSurfaceStateToString( + NearbySharingService::SendSurfaceState state) { + switch (state) { + case NearbySharingService::SendSurfaceState::kForeground: + return "FOREGROUND"; + case NearbySharingService::SendSurfaceState::kBackground: + return "BACKGROUND"; + case NearbySharingService::SendSurfaceState::kUnknown: + return "UNKNOWN"; + } +} + +std::string PowerLevelToString(PowerLevel level) { + switch (level) { + case PowerLevel::kLowPower: + return "LOW_POWER"; + case PowerLevel::kMediumPower: + return "MEDIUM_POWER"; + case PowerLevel::kHighPower: + return "HIGH_POWER"; + case PowerLevel::kUnknown: + return "UNKNOWN"; + } +} + +std::optional> GetBluetoothMacAddressFromCertificate( + const NearbyShareDecryptedPublicCertificate& certificate) { + if (!certificate.unencrypted_metadata().has_bluetooth_mac_address()) { + NL_LOG(WARNING) << __func__ << ": Public certificate " + << nearby::utils::HexEncode(certificate.id()) + << " did not contain a Bluetooth mac address."; + return std::nullopt; + } + + std::string mac_address = + certificate.unencrypted_metadata().bluetooth_mac_address(); + if (mac_address.size() != 6) { + NL_LOG(ERROR) << __func__ << ": Invalid bluetooth mac address: '" + << mac_address << "'"; + return std::nullopt; + } + + return std::vector(mac_address.begin(), mac_address.end()); +} + +std::optional GetDeviceName( + const Advertisement* advertisement, + const std::optional& certificate) { + NL_DCHECK(advertisement); + + // Device name is always included when visible to everyone. + if (advertisement->device_name().has_value()) { + return advertisement->device_name(); + } + + // For contacts only advertisements, we can't do anything without the + // certificate. + if (!certificate.has_value() || + !certificate->unencrypted_metadata().has_device_name()) { + return std::nullopt; + } + + return certificate->unencrypted_metadata().device_name(); +} + +// Return the most stable device identifier with the following priority: +// 1. Hash of Bluetooth MAC address. +// 2. Certificate ID. +// 3. Endpoint ID. +std::string GetDeviceId( + absl::string_view endpoint_id, + const std::optional& certificate) { + if (!certificate.has_value()) { + return std::string(endpoint_id); + } + + std::optional> mac_address = + GetBluetoothMacAddressFromCertificate(*certificate); + if (mac_address.has_value()) { + return absl::StrCat(absl::Hash>{}(*mac_address)); + } + + if (!certificate->id().empty()) { + return std::string(certificate->id().begin(), certificate->id().end()); + } + + return std::string(endpoint_id); +} + +std::optional TokenToFourDigitString( + const std::optional>& bytes) { + if (!bytes.has_value()) { + return std::nullopt; + } + + int hash = 0; + int multiplier = 1; + for (uint8_t byte : *bytes) { + // Java bytes are signed two's complement so cast to use the correct sign. + hash = (hash + static_cast(byte) * multiplier) % kHashModulo; + multiplier = (multiplier * kHashBaseMultiplier) % kHashModulo; + } + + return absl::StrFormat("%04d", std::abs(hash)); +} + +bool IsOutOfStorage(DeviceInfo& device_info, std::filesystem::path file_path, + int64_t storage_required) { + std::optional available_storage = + device_info.GetAvailableDiskSpaceInBytes(file_path); + + if (!available_storage.has_value()) { + return false; + } + + return *available_storage <= storage_required; +} + +AttachmentTransmissionStatus ConvertToTransmissionStatus( + TransferMetadata::Status status) { + switch (status) { + case TransferMetadata::Status::kComplete: + return AttachmentTransmissionStatus:: + COMPLETE_ATTACHMENT_TRANSMISSION_STATUS; + case TransferMetadata::Status::kCancelled: + return AttachmentTransmissionStatus:: + CANCELED_ATTACHMENT_TRANSMISSION_STATUS; + case TransferMetadata::Status::kFailed: + return AttachmentTransmissionStatus:: + FAILED_ATTACHMENT_TRANSMISSION_STATUS; + case TransferMetadata::Status::kAwaitingRemoteAcceptanceFailed: + return AttachmentTransmissionStatus:: + AWAITING_REMOTE_ACCEPTANCE_FAILED_ATTACHMENT; + case TransferMetadata::Status::kFailedToInitiateOutgoingConnection: + return AttachmentTransmissionStatus::FAILED_NULL_CONNECTION_INIT_OUTGOING; + case TransferMetadata::Status::kFailedToReadOutgoingConnectionResponse: + return AttachmentTransmissionStatus::FAILED_UNKNOWN_REMOTE_RESPONSE; + case TransferMetadata::Status::kIncompletePayloads: + return AttachmentTransmissionStatus::FAILED_NO_PAYLOAD; + case TransferMetadata::Status::kInvalidIntroductionFrame: + return AttachmentTransmissionStatus::FAILED_WRITE_INTRODUCTION; + case TransferMetadata::Status::kMediaUnavailable: + return AttachmentTransmissionStatus::MEDIA_UNAVAILABLE_ATTACHMENT; + case TransferMetadata::Status::kMissingEndpointId: + return AttachmentTransmissionStatus::FAILED_NO_SHARE_TARGET_ENDPOINT; + case TransferMetadata::Status::kMissingPayloads: + return AttachmentTransmissionStatus::FAILED_NO_PAYLOAD; + case TransferMetadata::Status::kMissingTransferUpdateCallback: + return AttachmentTransmissionStatus::FAILED_NO_TRANSFER_UPDATE_CALLBACK; + case TransferMetadata::Status::kPairedKeyVerificationFailed: + return AttachmentTransmissionStatus::FAILED_PAIRED_KEYHANDSHAKE; + case TransferMetadata::Status::kRejected: + return AttachmentTransmissionStatus::REJECTED_ATTACHMENT; + case TransferMetadata::Status::kTimedOut: + return AttachmentTransmissionStatus::TIMED_OUT_ATTACHMENT; + case TransferMetadata::Status::kUnexpectedDisconnection: + return AttachmentTransmissionStatus::FAILED_NULL_CONNECTION_DISCONNECTED; + case TransferMetadata::Status::kUnsupportedAttachmentType: + return AttachmentTransmissionStatus:: + UNSUPPORTED_ATTACHMENT_TYPE_ATTACHMENT; + default: + return AttachmentTransmissionStatus:: + UNKNOWN_ATTACHMENT_TRANSMISSION_STATUS; + } +} + +ConnectionLayerStatus ConvertToConnectionLayerStatus(Status status) { + switch (status) { + case Status::kUnknown: + return ConnectionLayerStatus::CONNECTION_LAYER_STATUS_UNKNOWN; + case Status::kSuccess: + return ConnectionLayerStatus::CONNECTION_LAYER_STATUS_SUCCESS; + case Status::kError: + return ConnectionLayerStatus::CONNECTION_LAYER_STATUS_ERROR; + case Status::kOutOfOrderApiCall: + return ConnectionLayerStatus:: + CONNECTION_LAYER_STATUS_OUT_OF_ORDER_API_CALL; + case Status::kAlreadyHaveActiveStrategy: + return ConnectionLayerStatus:: + CONNECTION_LAYER_STATUS_ALREADY_HAVE_ACTIVE_STRATEGY; + case Status::kAlreadyAdvertising: + return ConnectionLayerStatus::CONNECTION_LAYER_STATUS_ALREADY_ADVERTISING; + case Status::kAlreadyDiscovering: + return ConnectionLayerStatus::CONNECTION_LAYER_STATUS_ALREADY_DISCOVERING; + case Status::kAlreadyListening: + return ConnectionLayerStatus::CONNECTION_LAYER_STATUS_ALREADY_LISTENING; + case Status::kEndpointIOError: + return ConnectionLayerStatus::CONNECTION_LAYER_STATUS_END_POINT_IO_ERROR; + case Status::kEndpointUnknown: + return ConnectionLayerStatus::CONNECTION_LAYER_STATUS_END_POINT_UNKNOWN; + case Status::kConnectionRejected: + return ConnectionLayerStatus::CONNECTION_LAYER_STATUS_CONNECTION_REJECTED; + case Status::kAlreadyConnectedToEndpoint: + return ConnectionLayerStatus:: + CONNECTION_LAYER_STATUS_ALREADY_CONNECTED_TO_END_POINT; + case Status::kNotConnectedToEndpoint: + return ConnectionLayerStatus:: + CONNECTION_LAYER_STATUS_NOT_CONNECTED_TO_END_POINT; + case Status::kBluetoothError: + return ConnectionLayerStatus::CONNECTION_LAYER_STATUS_BLUETOOTH_ERROR; + case Status::kBleError: + return ConnectionLayerStatus::CONNECTION_LAYER_STATUS_BLE_ERROR; + case Status::kWifiLanError: + return ConnectionLayerStatus::CONNECTION_LAYER_STATUS_WIFI_LAN_ERROR; + case Status::kPayloadUnknown: + return ConnectionLayerStatus::CONNECTION_LAYER_STATUS_PAYLOAD_UNKNOWN; + case Status::kReset: + return ConnectionLayerStatus::CONNECTION_LAYER_STATUS_RESET; + case Status::kTimeout: + return ConnectionLayerStatus::CONNECTION_LAYER_STATUS_TIMEOUT; + default: + return ConnectionLayerStatus::CONNECTION_LAYER_STATUS_UNKNOWN; + } +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/nearby_sharing_util.h b/sharing/nearby_sharing_util.h new file mode 100644 index 00000000..c3427e95 --- /dev/null +++ b/sharing/nearby_sharing_util.h @@ -0,0 +1,86 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_NEARBY_SHARING_UTIL_H_ +#define THIRD_PARTY_NEARBY_SHARING_NEARBY_SHARING_UTIL_H_ + +#include +#include // NOLINT(build/c++17) +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "internal/platform/device_info.h" +#include "proto/sharing_enums.pb.h" +#include "sharing/advertisement.h" +#include "sharing/certificates/nearby_share_decrypted_public_certificate.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/nearby_connections_types.h" +#include "sharing/nearby_sharing_service.h" +#include "sharing/transfer_metadata.h" + +namespace nearby { +namespace sharing { + +// Checks whether the background scanning feature is enabled or not. +bool IsBackgroundScanningFeatureEnabled(); + +// Checks whether having enough disk space for required storage. +// +// device_info - Nearby Share DeviceInfo +// file_path - The path is to store sharing contents. +// storage_required - required storage space. +bool IsOutOfStorage(nearby::DeviceInfo& device_info, + std::filesystem::path file_path, int64_t storage_required); + +// Decodes certificate to find MAC address encoded in it. +std::optional> GetBluetoothMacAddressFromCertificate( + const NearbyShareDecryptedPublicCertificate& certificate); + +// Returns device name based on arguments advertisement and certificate. +std::optional GetDeviceName( + const Advertisement* advertisement, + const std::optional& certificate); + +// Converts authentication token to four bytes digit string. +std::optional TokenToFourDigitString( + const std::optional>& bytes); + +std::string ReceiveSurfaceStateToString( + NearbySharingService::ReceiveSurfaceState state); + +std::string SendSurfaceStateToString( + NearbySharingService::SendSurfaceState state); + +std::string PowerLevelToString(PowerLevel level); + +// Return the most stable device identifier with the following priority: +// 1. Hash of Bluetooth MAC address. +// 2. Certificate ID. +// 3. Endpoint ID. +std::string GetDeviceId( + absl::string_view endpoint_id, + const std::optional& certificate); + +::location::nearby::proto::sharing::AttachmentTransmissionStatus +ConvertToTransmissionStatus(TransferMetadata::Status status); + +::location::nearby::proto::sharing::ConnectionLayerStatus +ConvertToConnectionLayerStatus(Status status); + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_NEARBY_SHARING_UTIL_H_ diff --git a/sharing/outgoing_share_target_info.cc b/sharing/outgoing_share_target_info.cc new file mode 100644 index 00000000..3e8a1ac2 --- /dev/null +++ b/sharing/outgoing_share_target_info.cc @@ -0,0 +1,68 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/outgoing_share_target_info.h" + +#include +#include +#include +#include + +#include "sharing/nearby_connections_types.h" + +namespace nearby { +namespace sharing { + +OutgoingShareTargetInfo::OutgoingShareTargetInfo() = default; + +OutgoingShareTargetInfo::OutgoingShareTargetInfo(OutgoingShareTargetInfo&&) = + default; + +OutgoingShareTargetInfo& OutgoingShareTargetInfo::operator=( + OutgoingShareTargetInfo&&) = default; + +OutgoingShareTargetInfo::~OutgoingShareTargetInfo() = default; + +std::vector OutgoingShareTargetInfo::ExtractTextPayloads() { + return std::move(text_payloads_); +} + +std::vector OutgoingShareTargetInfo::ExtractFilePayloads() { + return std::move(file_payloads_); +} + +std::optional OutgoingShareTargetInfo::ExtractNextPayload() { + if (!text_payloads_.empty()) { + Payload payload = text_payloads_.back(); + text_payloads_.pop_back(); + return payload; + } + + if (!file_payloads_.empty()) { + Payload payload = file_payloads_.back(); + file_payloads_.pop_back(); + return payload; + } + + if (!wifi_credentials_payloads_.empty()) { + Payload payload = wifi_credentials_payloads_.back(); + wifi_credentials_payloads_.pop_back(); + return payload; + } + + return std::nullopt; +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/outgoing_share_target_info.h b/sharing/outgoing_share_target_info.h new file mode 100644 index 00000000..1b6ee1f4 --- /dev/null +++ b/sharing/outgoing_share_target_info.h @@ -0,0 +1,87 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_OUTGOING_SHARE_TARGET_INFO_H_ +#define THIRD_PARTY_NEARBY_SHARING_OUTGOING_SHARE_TARGET_INFO_H_ + +#include +#include +#include +#include + +#include "sharing/nearby_connections_types.h" +#include "sharing/share_target_info.h" + +namespace nearby { +namespace sharing { + +// A description of the outgoing connection to a remote device. +class OutgoingShareTargetInfo : public ShareTargetInfo { + public: + OutgoingShareTargetInfo(); + OutgoingShareTargetInfo(OutgoingShareTargetInfo&&); + OutgoingShareTargetInfo& operator=(OutgoingShareTargetInfo&&); + ~OutgoingShareTargetInfo() override; + + const std::optional& obfuscated_gaia_id() const { + return obfuscated_gaia_id_; + } + + void set_obfuscated_gaia_id(std::string obfuscated_gaia_id) { + obfuscated_gaia_id_ = std::move(obfuscated_gaia_id); + } + + const std::vector& text_payloads() const { return text_payloads_; } + + void set_text_payloads(std::vector payloads) { + text_payloads_ = std::move(payloads); + } + + const std::vector& wifi_credentials_payloads() const { + return wifi_credentials_payloads_; + } + + void set_wifi_credentials_payloads(std::vector payloads) { + wifi_credentials_payloads_ = std::move(payloads); + } + + const std::vector& file_payloads() const { return file_payloads_; } + + void set_file_payloads(std::vector payloads) { + file_payloads_ = std::move(payloads); + } + + Status connection_layer_status() const { return connection_layer_status_; } + + void set_connection_layer_status(Status status) { + connection_layer_status_ = status; + } + + std::vector ExtractTextPayloads(); + std::vector ExtractFilePayloads(); + std::vector ExtractWifiCredentialsPayloads(); + std::optional ExtractNextPayload(); + + private: + std::optional obfuscated_gaia_id_; + std::vector text_payloads_; + std::vector file_payloads_; + std::vector wifi_credentials_payloads_; + Status connection_layer_status_; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_OUTGOING_SHARE_TARGET_INFO_H_ diff --git a/sharing/paired_key_verification_runner.cc b/sharing/paired_key_verification_runner.cc new file mode 100644 index 00000000..06f44378 --- /dev/null +++ b/sharing/paired_key_verification_runner.cc @@ -0,0 +1,503 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/paired_key_verification_runner.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "absl/time/time.h" +#include "internal/platform/clock.h" +#include "internal/platform/device_info.h" +#include "internal/platform/implementation/device_info.h" +#include "proto/sharing_enums.pb.h" +#include "sharing/certificates/common.h" +#include "sharing/certificates/constants.h" +#include "sharing/certificates/nearby_share_certificate_manager.h" +#include "sharing/certificates/nearby_share_decrypted_public_certificate.h" +#include "sharing/incoming_frames_reader.h" +#include "sharing/internal/public/logging.h" +#include "sharing/nearby_connection.h" +#include "sharing/nearby_sharing_settings.h" +#include "sharing/proto/enums.pb.h" +#include "sharing/proto/rpc_resources.pb.h" +#include "sharing/proto/timestamp.pb.h" +#include "sharing/proto/wire_format.pb.h" +#include "sharing/share_target.h" + +namespace nearby { +namespace sharing { + +using ::location::nearby::proto::sharing::OSType; +using ::nearby::sharing::proto::DeviceVisibility; +using ::nearby::sharing::service::proto::CertificateInfoFrame; +using ::nearby::sharing::service::proto::Frame; +using ::nearby::sharing::service::proto::PairedKeyEncryptionFrame; +using ::nearby::sharing::service::proto::PairedKeyResultFrame; +using ::nearby::sharing::service::proto::V1Frame; + +namespace { + +// The size of the random byte array used for the encryption frame's signed data +// if a valid signature cannot be generated. This size is consistent with the +// GmsCore implementation. +const size_t kNearbyShareNumBytesRandomSignature = 72; +constexpr absl::Duration kRelaxAfterSetVisibilityTimeout = absl::Minutes(15); + +PairedKeyVerificationRunner::PairedKeyVerificationResult Convert( + nearby::sharing::service::proto::PairedKeyResultFrame::Status status) { + switch (status) { + case PairedKeyResultFrame::UNKNOWN: + return PairedKeyVerificationRunner::PairedKeyVerificationResult::kUnknown; + + case PairedKeyResultFrame::SUCCESS: + return PairedKeyVerificationRunner::PairedKeyVerificationResult::kSuccess; + + case PairedKeyResultFrame::FAIL: + return PairedKeyVerificationRunner::PairedKeyVerificationResult::kFail; + + case PairedKeyResultFrame::UNABLE: + return PairedKeyVerificationRunner::PairedKeyVerificationResult::kUnable; + } +} + +std::vector PadPrefix(char prefix, std::vector bytes) { + bytes.insert(bytes.begin(), prefix); + return bytes; +} + +OSType ToProtoOsType(::nearby::api::DeviceInfo::OsType os_type) { + switch (os_type) { + case ::nearby::api::DeviceInfo::OsType::kAndroid: + return OSType::ANDROID; + case ::nearby::api::DeviceInfo::OsType::kChromeOs: + return OSType::CHROME_OS; + case ::nearby::api::DeviceInfo::OsType::kWindows: + return OSType::WINDOWS; + case ::nearby::api::DeviceInfo::OsType::kIos: + return OSType::IOS; + case ::nearby::api::DeviceInfo::OsType::kMacOS: + return OSType::MACOS; + case ::nearby::api::DeviceInfo::OsType::kUnknown: + break; + } + + return OSType::UNKNOWN_OS_TYPE; +} + +} // namespace + +std::ostream& operator<<( + std::ostream& out, + const PairedKeyVerificationRunner::PairedKeyVerificationResult& obj) { + out << static_cast::type>(obj); + return out; +} + +PairedKeyVerificationRunner::PairedKeyVerificationRunner( + Clock* clock, + DeviceInfo& device_info, + NearbyShareSettings* nearby_share_settings, + bool self_share_feature_enabled, const ShareTarget& share_target, + absl::string_view endpoint_id, const std::vector& token, + NearbyConnection* connection, + const std::optional& certificate, + NearbyShareCertificateManager* certificate_manager, + bool restrict_to_contacts, IncomingFramesReader* frames_reader, + absl::Duration read_frame_timeout) + : clock_(clock), + device_info_(device_info), + nearby_share_settings_(nearby_share_settings), + self_share_feature_enabled_(self_share_feature_enabled), + share_target_(share_target), + endpoint_id_(std::string(endpoint_id)), + raw_token_(token), + connection_(connection), + certificate_(certificate), + certificate_manager_(certificate_manager), + restrict_to_contacts_(restrict_to_contacts), + frames_reader_(frames_reader), + read_frame_timeout_(read_frame_timeout) { + NL_DCHECK(clock_); + NL_DCHECK(nearby_share_settings); + NL_DCHECK(connection); + NL_DCHECK(certificate_manager); + NL_DCHECK(frames_reader); + + if (share_target.is_incoming) { + local_prefix_ = kNearbyShareReceiverVerificationPrefix; + remote_prefix_ = kNearbyShareSenderVerificationPrefix; + relax_restrict_to_contacts_ = + RelaxRestrictToContactsIfNeeded() || + nearby_share_settings_->GetVisibility() == + DeviceVisibility::DEVICE_VISIBILITY_EVERYONE; + } else { + remote_prefix_ = kNearbyShareReceiverVerificationPrefix; + local_prefix_ = kNearbyShareSenderVerificationPrefix; + } +} + +PairedKeyVerificationRunner::~PairedKeyVerificationRunner() = default; + +void PairedKeyVerificationRunner::Run( + std::function callback) { + NL_DCHECK(!callback_); + callback_ = std::move(callback); + + SendPairedKeyEncryptionFrame(); + frames_reader_->ReadFrame( + V1Frame::PAIRED_KEY_ENCRYPTION, + [&, runner = GetWeakPtr()](std::optional frame) { + auto verification_runner = runner.lock(); + if (verification_runner == nullptr) { + NL_LOG(WARNING) << "PairedKeyVerificationRunner is released before."; + return; + } + OnReadPairedKeyEncryptionFrame(std::move(frame)); + }, + read_frame_timeout_); +} + +void PairedKeyVerificationRunner::OnReadPairedKeyEncryptionFrame( + std::optional frame) { + if (!frame.has_value()) { + NL_LOG(WARNING) << __func__ + << ": Failed to read remote paired key encryption"; + std::move(callback_)(PairedKeyVerificationResult::kFail, + OSType::UNKNOWN_OS_TYPE); + return; + } + + std::vector verification_results; + + PairedKeyVerificationResult remote_public_certificate_result = + VerifyRemotePublicCertificate(*frame); + + if (remote_public_certificate_result == + PairedKeyVerificationResult::kSuccess) { + SendCertificateInfo(); + } else if (restrict_to_contacts_ && !relax_restrict_to_contacts_) { + NL_VLOG(1) << __func__ + << ": we are only allowing connections with contacts. " + "Rejecting connection from unknown ShareTarget - " + << share_target_.id; + std::move(callback_)(PairedKeyVerificationResult::kFail, + OSType::UNKNOWN_OS_TYPE); + return; + } else if (relax_restrict_to_contacts_) { + remote_public_certificate_result = + VerifyRemotePublicCertificateRelaxed(*frame); + } + + verification_results.push_back(remote_public_certificate_result); + NL_VLOG(1) << __func__ + << ": Remote public certificate verification result " + << remote_public_certificate_result; + + PairedKeyVerificationResult local_result = + VerifyPairedKeyEncryptionFrame(*frame); + verification_results.push_back(local_result); + NL_VLOG(1) << __func__ << ": Paired key encryption verification result " + << local_result; + + SendPairedKeyResultFrame(local_result); + + frames_reader_->ReadFrame( + V1Frame::PAIRED_KEY_RESULT, + [&, runner = GetWeakPtr(), + verification_results = + std::move(verification_results)](std::optional frame) { + auto verification_runner = runner.lock(); + if (verification_runner == nullptr) { + NL_LOG(WARNING) << "PairedKeyVerificationRunner is released before."; + return; + } + OnReadPairedKeyResultFrame(verification_results, std::move(frame)); + }, + read_frame_timeout_); +} + +void PairedKeyVerificationRunner::OnReadPairedKeyResultFrame( + std::vector verification_results, + std::optional frame) { + if (!frame.has_value()) { + NL_LOG(WARNING) << __func__ << ": Failed to read remote paired key result"; + std::move(callback_)(PairedKeyVerificationResult::kFail, + OSType::UNKNOWN_OS_TYPE); + return; + } + + PairedKeyVerificationResult key_result = + Convert(frame->paired_key_result().status()); + verification_results.push_back(key_result); + NL_VLOG(1) << __func__ << ": Paired key result frame result " + << key_result; + + PairedKeyVerificationResult combined_result = + MergeResults(verification_results); + NL_VLOG(1) << __func__ << ": Combined verification result " + << combined_result; + + OSType os_type = OSType::UNKNOWN_OS_TYPE; + if (frame->paired_key_result().has_os_type()) { + os_type = frame->paired_key_result().os_type(); + } + + std::move(callback_)(combined_result, os_type); +} + +void PairedKeyVerificationRunner::SendPairedKeyResultFrame( + PairedKeyVerificationResult result) { + Frame frame; + frame.set_version(Frame::V1); + V1Frame* v1_frame = frame.mutable_v1(); + v1_frame->set_type(V1Frame::PAIRED_KEY_RESULT); + PairedKeyResultFrame* result_frame = v1_frame->mutable_paired_key_result(); + + switch (result) { + case PairedKeyVerificationResult::kUnable: + result_frame->set_status(PairedKeyResultFrame::UNABLE); + break; + + case PairedKeyVerificationResult::kSuccess: + result_frame->set_status(PairedKeyResultFrame::SUCCESS); + break; + + case PairedKeyVerificationResult::kFail: + result_frame->set_status(PairedKeyResultFrame::FAIL); + break; + + case PairedKeyVerificationResult::kUnknown: + result_frame->set_status(PairedKeyResultFrame::UNKNOWN); + break; + } + + // Set OS type to allow remote device knowns the paring device OS type. + result_frame->set_os_type(ToProtoOsType(device_info_.GetOsType())); + + std::vector data(frame.ByteSize()); + frame.SerializeToArray(data.data(), frame.ByteSize()); + + connection_->Write(std::move(data)); +} + +void PairedKeyVerificationRunner::SendCertificateInfo() { + if (self_share_feature_enabled_) return; + + std::vector certificates; + + if (certificates.empty()) return; + + Frame frame; + frame.set_version(Frame::V1); + V1Frame* v1_frame = frame.mutable_v1(); + v1_frame->set_type(V1Frame::CERTIFICATE_INFO); + CertificateInfoFrame* cert_frame = v1_frame->mutable_certificate_info(); + for (const auto& certificate : certificates) { + nearby::sharing::service::proto::PublicCertificate* cert = + cert_frame->add_public_certificate(); + cert->set_secret_id(certificate.secret_id()); + cert->set_authenticity_key(certificate.secret_key()); + cert->set_public_key(certificate.public_key()); + cert->set_start_time(certificate.start_time().seconds() * 1000); + cert->set_end_time(certificate.end_time().seconds() * 1000); + cert->set_encrypted_metadata_bytes(certificate.encrypted_metadata_bytes()); + cert->set_metadata_encryption_key_tag( + certificate.metadata_encryption_key_tag()); + } + + std::vector data(frame.ByteSize()); + frame.SerializeToArray(data.data(), frame.ByteSize()); + + connection_->Write(std::move(data)); +} + +void PairedKeyVerificationRunner::SendPairedKeyEncryptionFrame() { + std::optional> signature = + certificate_manager_->SignWithPrivateCertificate( + nearby_share_settings_->GetVisibility(), + PadPrefix(local_prefix_, raw_token_)); + if (!signature.has_value() || signature->empty()) { + signature = GenerateRandomBytes(kNearbyShareNumBytesRandomSignature); + } + + std::vector certificate_id_hash; + if (certificate_.has_value()) { + certificate_id_hash = certificate_->HashAuthenticationToken(raw_token_); + } + if (certificate_id_hash.empty()) { + certificate_id_hash = + GenerateRandomBytes(kNearbyShareNumBytesAuthenticationTokenHash); + } + + Frame frame; + frame.set_version(Frame::V1); + V1Frame* v1_frame = frame.mutable_v1(); + v1_frame->set_type(V1Frame::PAIRED_KEY_ENCRYPTION); + PairedKeyEncryptionFrame* encryption_frame = + v1_frame->mutable_paired_key_encryption(); + encryption_frame->set_signed_data(signature->data(), signature->size()); + if (RelaxRestrictToContactsIfNeeded()) { + NL_LOG(INFO) + << "Attempts to sign authentication token with a previous private key."; + std::optional> optional_signature = + certificate_manager_->SignWithPrivateCertificate( + nearby_share_settings_->GetLastVisibility(), + PadPrefix(local_prefix_, raw_token_)); + + if (optional_signature.has_value()) { + encryption_frame->set_optional_signed_data(optional_signature->data(), + optional_signature->size()); + } + } + encryption_frame->set_secret_id_hash(certificate_id_hash.data(), + certificate_id_hash.size()); + std::vector data(frame.ByteSize()); + frame.SerializeToArray(data.data(), frame.ByteSize()); + + connection_->Write(std::move(data)); +} + +PairedKeyVerificationRunner::PairedKeyVerificationResult +PairedKeyVerificationRunner::VerifyRemotePublicCertificate( + const V1Frame& frame) { + return VerifyRemotePublicCertificateWithPrivateCertificate( + nearby_share_settings_->GetVisibility(), frame); +} + +PairedKeyVerificationRunner::PairedKeyVerificationResult +PairedKeyVerificationRunner::VerifyRemotePublicCertificateRelaxed( + const nearby::sharing::service::proto::V1Frame& frame) { + return VerifyRemotePublicCertificateWithPrivateCertificate( + nearby_share_settings_->GetLastVisibility(), frame); +} + +PairedKeyVerificationRunner::PairedKeyVerificationResult +PairedKeyVerificationRunner:: + VerifyRemotePublicCertificateWithPrivateCertificate( + DeviceVisibility visibility, + const nearby::sharing::service::proto::V1Frame& frame) { + std::optional> hash = + certificate_manager_->HashAuthenticationTokenWithPrivateCertificate( + visibility, raw_token_); + + const std::string& frame_hash = + frame.paired_key_encryption().secret_id_hash(); + std::vector frame_hash_data{frame_hash.begin(), frame_hash.end()}; + + if (hash.has_value() && *hash == frame_hash_data) { + NL_VLOG(1) << __func__ + << ": Successfully verified remote public certificate."; + return PairedKeyVerificationResult::kSuccess; + } + + NL_VLOG(1) << __func__ + << ": Unable to verify remote public certificate."; + return PairedKeyVerificationResult::kUnable; +} + +PairedKeyVerificationRunner::PairedKeyVerificationResult +PairedKeyVerificationRunner::VerifyPairedKeyEncryptionFrame( + const V1Frame& frame) { + if (!certificate_) { + NL_VLOG(1) << __func__ + << ": Unable to verify remote paired key encryption frame. " + "Certificate not found."; + return PairedKeyVerificationResult::kUnable; + } + + auto signed_data = frame.paired_key_encryption().signed_data(); + std::vector data(signed_data.begin(), signed_data.end()); + if (!certificate_->VerifySignature(PadPrefix(remote_prefix_, raw_token_), + data)) { + if (!frame.paired_key_encryption().has_optional_signed_data()) { + NL_LOG(WARNING) + << __func__ + << ": Unable to verify remote paired key encryption frame. " + "no optional signed data."; + return PairedKeyVerificationResult::kFail; + } + + if (!RelaxRestrictToContactsIfNeeded()) { + NL_LOG(WARNING) + << __func__ + << ": Unable to verify remote paired key encryption frame. " + "no need to try relax check."; + return PairedKeyVerificationResult::kFail; + } + + // Verify optional signed data. + auto optional_signed_data = + frame.paired_key_encryption().optional_signed_data(); + std::vector optional_data(optional_signed_data.begin(), + optional_signed_data.end()); + if (certificate_->VerifySignature(PadPrefix(remote_prefix_, raw_token_), + optional_data)) { + NL_LOG(INFO) << "Successfully verified remote paired key encryption " + "frame with the optional signed data."; + } else { + NL_LOG(WARNING) + << __func__ + << ": Unable to verify remote paired key encryption frame."; + return PairedKeyVerificationResult::kFail; + } + } + + if (!share_target_.is_known) { + NL_LOG(INFO) << __func__ + << ": Unable to verify remote paired key encryption frame. " + "Remote side is not a known share target."; + return PairedKeyVerificationResult::kUnable; + } + + NL_VLOG(1) + << __func__ + << ": Successfully verified remote paired key encryption frame."; + return PairedKeyVerificationResult::kSuccess; +} + +PairedKeyVerificationRunner::PairedKeyVerificationResult +PairedKeyVerificationRunner::MergeResults( + const std::vector& results) { + bool all_success = true; + for (const auto& result : results) { + if (result == PairedKeyVerificationResult::kFail) return result; + + if (result != PairedKeyVerificationResult::kSuccess) + all_success = false; + } + + return all_success ? PairedKeyVerificationResult::kSuccess + : PairedKeyVerificationResult::kUnable; +} + +bool PairedKeyVerificationRunner::RelaxRestrictToContactsIfNeeded() const { + return share_target_.is_known && + (clock_->Now() - nearby_share_settings_->GetLastVisibilityTimestamp() < + kRelaxAfterSetVisibilityTimeout); +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/paired_key_verification_runner.h b/sharing/paired_key_verification_runner.h new file mode 100644 index 00000000..0b5dc97b --- /dev/null +++ b/sharing/paired_key_verification_runner.h @@ -0,0 +1,128 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_PAIRED_KEY_VERIFICATION_RUNNER_H_ +#define THIRD_PARTY_NEARBY_SHARING_PAIRED_KEY_VERIFICATION_RUNNER_H_ + +#include + +#include +#include +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "absl/time/time.h" +#include "internal/platform/clock.h" +#include "internal/platform/device_info.h" +#include "proto/sharing_enums.pb.h" +#include "sharing/certificates/nearby_share_certificate_manager.h" +#include "sharing/certificates/nearby_share_decrypted_public_certificate.h" +#include "sharing/incoming_frames_reader.h" +#include "sharing/nearby_connection.h" +#include "sharing/nearby_sharing_settings.h" +#include "sharing/proto/enums.pb.h" +#include "sharing/proto/wire_format.pb.h" +#include "sharing/share_target.h" + +namespace nearby { +namespace sharing { + +class PairedKeyVerificationRunner + : public std::enable_shared_from_this { + public: + enum class PairedKeyVerificationResult { + // Default value for verification result. + kUnknown, + // Succeeded with verification. + kSuccess, + // Failed to verify. + kFail, + // Unable to verify. Occurs when missing proper certificates. + kUnable, + }; + + PairedKeyVerificationRunner( + nearby::Clock* clock, + nearby::DeviceInfo& device_info, + NearbyShareSettings* nearby_share_settings, + bool self_share_feature_enabled, const ShareTarget& share_target, + absl::string_view endpoint_id, const std::vector& token, + NearbyConnection* connection, + const std::optional& certificate, + NearbyShareCertificateManager* certificate_manager, + bool restrict_to_contacts, IncomingFramesReader* frames_reader, + absl::Duration read_frame_timeout); + + ~PairedKeyVerificationRunner(); + + void Run(std::function< + void(PairedKeyVerificationResult verification_result, + ::location::nearby::proto::sharing::OSType remote_os_type)> + callback); + + std::weak_ptr GetWeakPtr() { + return this->weak_from_this(); + } + + private: + void SendPairedKeyEncryptionFrame(); + void OnReadPairedKeyEncryptionFrame( + std::optional frame); + void OnReadPairedKeyResultFrame( + std::vector verification_results, + std::optional frame); + void SendPairedKeyResultFrame(PairedKeyVerificationResult result); + PairedKeyVerificationResult VerifyRemotePublicCertificate( + const nearby::sharing::service::proto::V1Frame& frame); + PairedKeyVerificationResult VerifyRemotePublicCertificateRelaxed( + const nearby::sharing::service::proto::V1Frame& frame); + PairedKeyVerificationResult + VerifyRemotePublicCertificateWithPrivateCertificate( + proto::DeviceVisibility visibility, + const nearby::sharing::service::proto::V1Frame& frame); + PairedKeyVerificationResult VerifyPairedKeyEncryptionFrame( + const nearby::sharing::service::proto::V1Frame& frame); + PairedKeyVerificationResult MergeResults( + const std::vector& results); + void SendCertificateInfo(); + bool RelaxRestrictToContactsIfNeeded() const; + + nearby::Clock* const clock_; + nearby::DeviceInfo& device_info_; + NearbyShareSettings* nearby_share_settings_; + bool self_share_feature_enabled_; + ShareTarget share_target_; + std::string endpoint_id_; + std::vector raw_token_; + NearbyConnection* connection_; + std::optional certificate_; + NearbyShareCertificateManager* certificate_manager_; + bool restrict_to_contacts_ = false; + IncomingFramesReader* frames_reader_; + const absl::Duration read_frame_timeout_; + std::function + callback_; + bool relax_restrict_to_contacts_ = false; + + char local_prefix_; + char remote_prefix_; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_PAIRED_KEY_VERIFICATION_RUNNER_H_ diff --git a/sharing/paired_key_verification_runner_test.cc b/sharing/paired_key_verification_runner_test.cc new file mode 100644 index 00000000..7a613842 --- /dev/null +++ b/sharing/paired_key_verification_runner_test.cc @@ -0,0 +1,495 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/paired_key_verification_runner.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "gmock/gmock.h" +#include "protobuf-matchers/protocol-buffer-matchers.h" +#include "gtest/gtest.h" +#include "absl/strings/string_view.h" +#include "absl/time/time.h" +#include "internal/flags/nearby_flags.h" +#include "internal/test/fake_clock.h" +#include "internal/test/fake_device_info.h" +#include "proto/sharing_enums.pb.h" +#include "sharing/certificates/fake_nearby_share_certificate_manager.h" +#include "sharing/certificates/nearby_share_decrypted_public_certificate.h" +#include "sharing/certificates/test_util.h" +#include "sharing/fake_nearby_connection.h" +#include "sharing/flags/nearby_sharing_feature_flags.h" +#include "sharing/incoming_frames_reader.h" +#include "sharing/internal/public/context.h" +#include "sharing/internal/test/fake_context.h" +#include "sharing/internal/test/fake_preference_manager.h" +#include "sharing/local_device_data/nearby_share_local_device_data_manager.h" +#include "sharing/nearby_connection.h" +#include "sharing/nearby_sharing_decoder.h" +#include "sharing/nearby_sharing_decoder_impl.h" +#include "sharing/nearby_sharing_settings.h" +#include "sharing/proto/enums.pb.h" +#include "sharing/proto/rpc_resources.pb.h" +#include "sharing/proto/wire_format.pb.h" +#include "sharing/share_target.h" + +namespace nearby { +namespace sharing { +namespace { + +using V1Frame = ::nearby::sharing::service::proto::V1Frame; +using PairedKeyResultFrame = + ::nearby::sharing::service::proto::PairedKeyResultFrame; +using ::nearby::sharing::proto::DeviceVisibility; +using PairedKeyVerificationResult = + PairedKeyVerificationRunner::PairedKeyVerificationResult; +using ::location::nearby::proto::sharing::OSType; + +constexpr char kEndpointId[] = "test_endpoint_id"; + +const std::vector& GetAuthToken() { + static std::vector* auth_token = new std::vector({0, 1, 2}); + return *auth_token; +} + +const std::vector& GetPrivateCertificateHashAuthToken() { + static std::vector* private_certificate_hash_auth_token = + new std::vector({0x8b, 0xcb, 0xa2, 0xf8, 0xe4, 0x06}); + return *private_certificate_hash_auth_token; +} + +const std::vector& GetIncomingConnectionSignedData() { + static std::vector* incoming_connection_signed_data = + new std::vector( + {0x30, 0x45, 0x02, 0x20, 0x4f, 0x83, 0x72, 0xbd, 0x02, 0x70, 0xd9, + 0xda, 0x62, 0x83, 0x5d, 0xb2, 0xdc, 0x6e, 0x3f, 0xa6, 0xa8, 0xa1, + 0x4f, 0x5f, 0xd3, 0xe3, 0xd9, 0x1a, 0x5d, 0x2d, 0x61, 0xd2, 0x6c, + 0xdd, 0x8d, 0xa5, 0x02, 0x21, 0x00, 0xd4, 0xe1, 0x1d, 0x14, 0xcb, + 0x58, 0xf7, 0x02, 0xd5, 0xab, 0x48, 0xe2, 0x2f, 0xcb, 0xc0, 0x53, + 0x41, 0x06, 0x50, 0x65, 0x95, 0x19, 0xa9, 0x22, 0x92, 0x00, 0x42, + 0x01, 0x26, 0x25, 0xcb, 0x8c}); + return *incoming_connection_signed_data; +} + +const std::vector& GetInvalidIncomingConnectionSignedData() { + static std::vector* incoming_connection_signed_data = + new std::vector( + {0x30, 0x45, 0x02, 0x20, 0x4f, 0x83, 0x72, 0xbd, 0x02, 0x70, 0xd9, + 0xda, 0x61, 0x83, 0x5d, 0xb2, 0xdc, 0x6e, 0x3f, 0xa6, 0xa8, 0xa1, + 0x4f, 0x5f, 0xd3, 0xe3, 0xd9, 0x1a, 0x5d, 0x2d, 0x61, 0xd2, 0x6c, + 0xdd, 0x8d, 0xa5, 0x02, 0x21, 0x05, 0xd4, 0xe1, 0x1d, 0x14, 0xcb, + 0x58, 0xf7, 0x02, 0xd5, 0xab, 0x48, 0xe2, 0x2f, 0xcb, 0xc0, 0x53, + 0x41, 0x06, 0x50, 0x65, 0x95, 0x19, 0xa9, 0x22, 0x92, 0x00, 0x42, + 0x01, 0x26, 0x25, 0xcb, 0x82}); + return *incoming_connection_signed_data; +} + +std::list GeneratePairedKeyResultFrame() { + std::list result; + PairedKeyResultFrame frame; + frame.set_status(PairedKeyResultFrame::UNKNOWN); + result.push_back(frame); + + frame.set_status(PairedKeyResultFrame::SUCCESS); + frame.set_os_type(OSType::ANDROID); + result.push_back(frame); + + frame.set_status(PairedKeyResultFrame::FAIL); + frame.set_os_type(OSType::UNKNOWN_OS_TYPE); + result.push_back(frame); + + frame.set_status(PairedKeyResultFrame::UNABLE); + frame.set_os_type(OSType::WINDOWS); + result.push_back(frame); + + return result; +} + +const absl::Duration kTimeout = absl::Seconds(1); + +class MockNearbyShareLocalDeviceDataManager + : public NearbyShareLocalDeviceDataManager { + public: + MOCK_METHOD(std::string, GetId, (), (override)); + MOCK_METHOD(std::string, GetDeviceName, (), (override, const)); + MOCK_METHOD(std::optional, GetFullName, (), (override, const)); + MOCK_METHOD(std::optional, GetIconUrl, (), (override, const)); + MOCK_METHOD(DeviceNameValidationResult, ValidateDeviceName, + (absl::string_view), (override)); + MOCK_METHOD(DeviceNameValidationResult, SetDeviceName, (absl::string_view), + (override)); + MOCK_METHOD(void, DownloadDeviceData, (), (override)); + MOCK_METHOD(void, UploadContacts, + (std::vector, + UploadCompleteCallback), + (override)); + MOCK_METHOD(void, UploadCertificates, + (std::vector, + UploadCompleteCallback), + (override)); + MOCK_METHOD(void, OnStart, (), (override)); + MOCK_METHOD(void, OnStop, (), (override)); +}; + +class MockIncomingFramesReader : public IncomingFramesReader { + public: + MockIncomingFramesReader(Context* context, NearbySharingDecoder* decoder, + NearbyConnection* connection) + : IncomingFramesReader(context, decoder, connection) {} + + MOCK_METHOD(void, ReadFrame, + (std::function)> callback), + (override)); + + MOCK_METHOD(void, ReadFrame, + (service::proto::V1Frame_FrameType frame_type, + std::function)> callback, + absl::Duration timeout), + (override)); +}; + +PairedKeyVerificationRunner::PairedKeyVerificationResult Merge( + PairedKeyVerificationRunner::PairedKeyVerificationResult local_result, + PairedKeyResultFrame::Status remote_result) { + if (remote_result == PairedKeyResultFrame::FAIL || + local_result == + PairedKeyVerificationRunner::PairedKeyVerificationResult::kFail) { + return PairedKeyVerificationRunner::PairedKeyVerificationResult::kFail; + } + + if (remote_result == PairedKeyResultFrame::SUCCESS && + local_result == + PairedKeyVerificationRunner::PairedKeyVerificationResult::kSuccess) { + return PairedKeyVerificationRunner::PairedKeyVerificationResult::kSuccess; + } + + return PairedKeyVerificationRunner::PairedKeyVerificationResult::kUnable; +} + +class PairedKeyVerificationRunnerTest : public testing::Test { + public: + enum class ReturnFrameType { + // Return absl::nullopt for the frame. + kNull, + // Return an empty frame. + kEmpty, + // Return a valid frame. + kValid, + // Return a valid optional frame. + kOptionalValid, + // Return an invalid frame with both signed signature. + kInValid, + }; + + PairedKeyVerificationRunnerTest() + : frames_reader_(&context_, &decoder_, &connection_) {} + + void SetUp() override { + nearby_share_settings_ = std::make_unique( + &context_, context_.GetClock(), fake_device_info_, preference_manager_, + &local_device_data_manager_); + nearby_share_settings_->SetVisibility( + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + FastForward(absl::Minutes(15)); + share_target_.is_incoming = true; + NearbyFlags::GetInstance().OverrideBoolFlagValue( + config_package_nearby::nearby_sharing_feature::kEnableSelfShare, true); + } + + void RunVerification( + bool use_valid_public_certificate, bool restricted_to_contacts, + PairedKeyVerificationRunner::PairedKeyVerificationResult expected_result, + OSType expected_os_type = OSType::UNKNOWN_OS_TYPE) { + std::optional public_certificate = + use_valid_public_certificate + ? std::make_optional( + GetNearbyShareTestDecryptedPublicCertificate()) + : std::nullopt; + + bool self_share_feature_enabled = NearbyFlags::GetInstance().GetBoolFlag( + config_package_nearby::nearby_sharing_feature::kEnableSelfShare); + auto runner = std::make_shared( + context_.GetClock(), fake_device_info_, nearby_share_settings_.get(), + self_share_feature_enabled, share_target_, kEndpointId, GetAuthToken(), + &connection_, std::move(public_certificate), &certificate_manager_, + restricted_to_contacts, &frames_reader_, kTimeout); + + runner->Run( + [&, expected_result, expected_os_type]( + PairedKeyVerificationRunner::PairedKeyVerificationResult result, + OSType remote_os_type) { + EXPECT_EQ(expected_result, result); + EXPECT_EQ(expected_os_type, remote_os_type); + }); + } + + void SetUpPairedKeyEncryptionFrame(ReturnFrameType frame_type) { + EXPECT_CALL(frames_reader_, + ReadFrame(testing::Eq(V1Frame::PAIRED_KEY_ENCRYPTION), + testing::_, testing::Eq(kTimeout))) + .WillOnce(testing::WithArg<1>(testing::Invoke( + [this, + frame_type](std::function)> callback) { + if (frame_type == ReturnFrameType::kNull) { + std::move(callback)(std::nullopt); + return; + } + + auto frame = V1Frame(); + + if (frame_type == ReturnFrameType::kValid) { + nearby::sharing::service::proto::PairedKeyEncryptionFrame* + encryption_frame = frame.mutable_paired_key_encryption(); + encryption_frame->set_signed_data( + GetIncomingConnectionSignedData().data(), + GetIncomingConnectionSignedData().size()); + encryption_frame->set_secret_id_hash( + GetPrivateCertificateHashAuthToken().data(), + GetPrivateCertificateHashAuthToken().size()); + } else if (frame_type == ReturnFrameType::kOptionalValid) { + nearby::sharing::service::proto::PairedKeyEncryptionFrame* + encryption_frame = frame.mutable_paired_key_encryption(); + encryption_frame->set_signed_data( + GetInvalidIncomingConnectionSignedData().data(), + GetInvalidIncomingConnectionSignedData().size()); + encryption_frame->set_optional_signed_data( + GetIncomingConnectionSignedData().data(), + GetIncomingConnectionSignedData().size()); + encryption_frame->set_secret_id_hash( + GetPrivateCertificateHashAuthToken().data(), + GetPrivateCertificateHashAuthToken().size()); + // make sure the optional codes are executed + nearby_share_settings_->SetVisibility( + DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS); + } else if (frame_type == ReturnFrameType::kInValid) { + nearby::sharing::service::proto::PairedKeyEncryptionFrame* + encryption_frame = frame.mutable_paired_key_encryption(); + encryption_frame->set_signed_data( + GetInvalidIncomingConnectionSignedData().data(), + GetInvalidIncomingConnectionSignedData().size()); + encryption_frame->set_optional_signed_data( + GetInvalidIncomingConnectionSignedData().data(), + GetInvalidIncomingConnectionSignedData().size()); + encryption_frame->set_secret_id_hash( + GetPrivateCertificateHashAuthToken().data(), + GetPrivateCertificateHashAuthToken().size()); + } else { + nearby::sharing::service::proto::PairedKeyEncryptionFrame* + encryption_frame = frame.mutable_paired_key_encryption(); + encryption_frame->clear_signed_data(); + encryption_frame->clear_secret_id_hash(); + } + + std::move(callback)(std::move(frame)); + }))); + } + + void SetUpPairedKeyResultFrame( + ReturnFrameType frame_type, + PairedKeyResultFrame::Status status = PairedKeyResultFrame::UNKNOWN, + OSType os_type = OSType::UNKNOWN_OS_TYPE) { + EXPECT_CALL(frames_reader_, + ReadFrame(testing::Eq(V1Frame::PAIRED_KEY_RESULT), testing::_, + testing::Eq(kTimeout))) + .WillOnce(testing::WithArg<1>(testing::Invoke( + [=](std::function)> callback) { + if (frame_type == ReturnFrameType::kNull) { + std::move(callback)(std::nullopt); + return; + } + + auto frame = V1Frame(); + + PairedKeyResultFrame* result_frame = + frame.mutable_paired_key_result(); + + result_frame->set_status(status); + result_frame->set_os_type(os_type); + + std::move(callback)(std::move(frame)); + }))); + } + + nearby::sharing::service::proto::Frame GetWrittenFrame() { + std::vector data = connection_.GetWrittenData(); + nearby::sharing::service::proto::Frame frame; + frame.ParseFromArray(data.data(), data.size()); + return frame; + } + + void ExpectPairedKeyEncryptionFrameSent() { + nearby::sharing::service::proto::Frame frame = GetWrittenFrame(); + ASSERT_TRUE(frame.has_v1()); + ASSERT_TRUE(frame.v1().has_paired_key_encryption()); + } + + void ExpectCertificateInfoSent() {} + + void ExpectPairedKeyResultFrameSent(PairedKeyResultFrame::Status status) { + nearby::sharing::service::proto::Frame frame = GetWrittenFrame(); + ASSERT_TRUE(frame.has_v1()); + ASSERT_TRUE(frame.v1().has_paired_key_result()); + EXPECT_EQ(status, frame.v1().paired_key_result().status()); + } + + void FastForward(absl::Duration duration) { + context_.fake_clock()->FastForward(duration); + } + + protected: + ShareTarget share_target_; + + private: + nearby::FakePreferenceManager preference_manager_; + FakeDeviceInfo fake_device_info_; + FakeContext context_; + FakeNearbyConnection connection_; + NearbySharingDecoderImpl decoder_; + testing::NiceMock frames_reader_; + FakeNearbyShareCertificateManager certificate_manager_; + ::testing::NiceMock + local_device_data_manager_; + std::unique_ptr nearby_share_settings_; +}; + +TEST_F(PairedKeyVerificationRunnerTest, + NullCertificate_InvalidPairedKeyEncryptionFrame_RestrictToContacts) { + // Empty key encryption frame fails the certificate verification. + SetUpPairedKeyEncryptionFrame(ReturnFrameType::kEmpty); + + RunVerification( + /*use_valid_public_certificate=*/false, + /*restricted_to_contacts=*/true, + /*expected_result=*/ + PairedKeyVerificationResult::kFail); + + ExpectPairedKeyEncryptionFrameSent(); +} + +TEST_F(PairedKeyVerificationRunnerTest, + ValidPairedKeyEncryptionFrame_ResultFrameTimedOut) { + SetUpPairedKeyEncryptionFrame(ReturnFrameType::kValid); + + // Null result frame fails the certificate verification process. + SetUpPairedKeyResultFrame(ReturnFrameType::kNull); + + RunVerification( + /*use_valid_public_certificate=*/true, + /*restricted_to_contacts=*/false, + /*expected_result=*/ + PairedKeyVerificationResult::kFail); + + ExpectPairedKeyEncryptionFrameSent(); + ExpectPairedKeyResultFrameSent(PairedKeyResultFrame::UNABLE); +} + +struct TestParameters { + bool is_target_known; + bool is_valid_certificate; + PairedKeyVerificationRunnerTest::ReturnFrameType encryption_frame_type; + PairedKeyVerificationRunner::PairedKeyVerificationResult result; +} kParameters[] = { + {true, true, PairedKeyVerificationRunnerTest::ReturnFrameType::kValid, + PairedKeyVerificationRunner::PairedKeyVerificationResult::kSuccess}, + {true, true, PairedKeyVerificationRunnerTest::ReturnFrameType::kEmpty, + PairedKeyVerificationRunner::PairedKeyVerificationResult::kFail}, + {true, false, PairedKeyVerificationRunnerTest::ReturnFrameType::kValid, + PairedKeyVerificationRunner::PairedKeyVerificationResult::kUnable}, + {true, false, PairedKeyVerificationRunnerTest::ReturnFrameType::kEmpty, + PairedKeyVerificationRunner::PairedKeyVerificationResult::kUnable}, + {false, true, PairedKeyVerificationRunnerTest::ReturnFrameType::kValid, + PairedKeyVerificationRunner::PairedKeyVerificationResult::kUnable}, + {true, true, + PairedKeyVerificationRunnerTest::ReturnFrameType::kOptionalValid, + PairedKeyVerificationRunner::PairedKeyVerificationResult::kSuccess}, + {true, true, PairedKeyVerificationRunnerTest::ReturnFrameType::kInValid, + PairedKeyVerificationRunner::PairedKeyVerificationResult::kFail}, +}; + +using KeyVerificationTestParam = + std::tuple; + +class ParameterisedPairedKeyVerificationRunnerTest + : public PairedKeyVerificationRunnerTest, + public testing::WithParamInterface {}; + +TEST_P(ParameterisedPairedKeyVerificationRunnerTest, + ValidEncryptionFrame_ValidResultFrame) { + const TestParameters& params = std::get<0>(GetParam()); + PairedKeyResultFrame result_frame = std::get<1>(GetParam()); + PairedKeyVerificationRunner::PairedKeyVerificationResult expected_result = + Merge(params.result, result_frame.status()); + + share_target_.is_known = params.is_target_known; + + SetUpPairedKeyEncryptionFrame(params.encryption_frame_type); + SetUpPairedKeyResultFrame( + PairedKeyVerificationRunnerTest::ReturnFrameType::kValid, + result_frame.status(), + result_frame.has_os_type() ? result_frame.os_type() + : OSType::UNKNOWN_OS_TYPE); + + RunVerification( + /*use_valid_public_certificate=*/params.is_valid_certificate, + /*restricted_to_contacts=*/false, expected_result, + result_frame.has_os_type() ? result_frame.os_type() + : OSType::UNKNOWN_OS_TYPE); + + ExpectPairedKeyEncryptionFrameSent(); + if (params.encryption_frame_type == + PairedKeyVerificationRunnerTest::ReturnFrameType::kValid) + ExpectCertificateInfoSent(); + + // Check for result frame sent. + if (!params.is_valid_certificate) { + ExpectPairedKeyResultFrameSent(PairedKeyResultFrame::UNABLE); + return; + } + + if (params.encryption_frame_type == + PairedKeyVerificationRunnerTest::ReturnFrameType::kEmpty) { + ExpectPairedKeyResultFrameSent(PairedKeyResultFrame::FAIL); + return; + } + + if (params.encryption_frame_type == + PairedKeyVerificationRunnerTest::ReturnFrameType::kInValid) { + ExpectPairedKeyResultFrameSent(PairedKeyResultFrame::FAIL); + return; + } + + if (params.is_target_known) { + ExpectPairedKeyResultFrameSent(PairedKeyResultFrame::SUCCESS); + } else { + ExpectPairedKeyResultFrameSent(PairedKeyResultFrame::UNABLE); + } +} + +INSTANTIATE_TEST_SUITE_P( + /*no prefix*/, ParameterisedPairedKeyVerificationRunnerTest, + testing::Combine(testing::ValuesIn(kParameters), + testing::ValuesIn(GeneratePairedKeyResultFrame()))); + +} // namespace +} // namespace sharing +} // namespace nearby diff --git a/sharing/payload_listener.h b/sharing/payload_listener.h new file mode 100644 index 00000000..d6a892d9 --- /dev/null +++ b/sharing/payload_listener.h @@ -0,0 +1,56 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_PAYLOAD_LISTENER_H_ +#define THIRD_PARTY_NEARBY_SHARING_PAYLOAD_LISTENER_H_ + +#include "absl/strings/string_view.h" +#include "sharing/nearby_connections_types.h" + +namespace nearby { +namespace sharing { + +// Listener for payload status. Methods in this interface are called from +// utility process, and are used by the browser process to listen for payload +// status associated with remote endpoints. +class PayloadListener { + public: + virtual ~PayloadListener() = default; + + // Called when a Payload is received from a remote endpoint. Depending on the + // type of the Payload, all the data may or may not have been received at + // the time of this call. OnPayloadTransferUpdate() should be used to get + // updates on the status of the data received. + // + // endpoint_id - The identifier for the remote endpoint that sent the + // payload. + // payload - The Payload object received. + virtual void OnPayloadReceived(absl::string_view endpoint_id, + Payload& payload) = 0; + + // Called with progress information about an active Payload transfer, either + // incoming or outgoing. + // + // endpoint_id - The identifier for the remote endpoint that is sending or + // receiving this payload. + // update - The PayloadTransferUpdate structure describing the status of + // the transfer. + virtual void OnPayloadTransferUpdate(absl::string_view endpoint_id, + PayloadTransferUpdate& update) = 0; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_PAYLOAD_LISTENER_H_ diff --git a/sharing/payload_tracker.cc b/sharing/payload_tracker.cc new file mode 100644 index 00000000..8f4aa795 --- /dev/null +++ b/sharing/payload_tracker.cc @@ -0,0 +1,289 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/payload_tracker.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/time/clock.h" +#include "absl/time/time.h" +#include "sharing/attachment_info.h" +#include "sharing/constants.h" +#include "sharing/file_attachment.h" +#include "sharing/internal/public/context.h" +#include "sharing/internal/public/logging.h" +#include "sharing/nearby_connections_types.h" +#include "sharing/share_target.h" +#include "sharing/text_attachment.h" +#include "sharing/transfer_metadata.h" +#include "sharing/transfer_metadata_builder.h" +#include "sharing/wifi_credentials_attachment.h" + +namespace nearby { +namespace sharing { + +PayloadTracker::PayloadTracker( + Context* context, const ShareTarget& share_target, + const absl::flat_hash_map& attachment_info_map, + std::function update_callback) + : context_(context), + share_target_(share_target), + update_callback_(std::move(update_callback)) { + total_transfer_size_ = 0; + confirmed_transfer_size_ = 0; + + for (const auto& file : share_target.file_attachments) { + auto it = attachment_info_map.find(file.id()); + if (it == attachment_info_map.end() || !it->second.payload_id) { + NL_LOG(WARNING) + << __func__ + << ": Failed to retrieve payload for file attachment id - " + << file.id(); + continue; + } + + payload_state_.emplace(*it->second.payload_id, + State(file.id(), file.size())); + ++num_file_attachments_; + total_transfer_size_ += file.size(); + } + + for (const auto& text : share_target.text_attachments) { + auto it = attachment_info_map.find(text.id()); + if (it == attachment_info_map.end() || !it->second.payload_id) { + NL_LOG(WARNING) + << __func__ + << ": Failed to retrieve payload for text attachment id - " + << text.id(); + continue; + } + + payload_state_.emplace(*it->second.payload_id, + State(text.id(), text.size())); + ++num_text_attachments_; + total_transfer_size_ += text.size(); + } + + for (const auto& wifi_credentials : + share_target.wifi_credentials_attachments) { + auto it = attachment_info_map.find(wifi_credentials.id()); + if (it == attachment_info_map.end() || !it->second.payload_id) { + NL_LOG(WARNING) << __func__ + << ": Failed to retrieve payload for WiFi credentials " + "attachment id - " + << wifi_credentials.id(); + continue; + } + + payload_state_.emplace( + *it->second.payload_id, + State(wifi_credentials.id(), wifi_credentials.size())); + ++num_wifi_credentials_attachments_; + total_transfer_size_ += wifi_credentials.size(); + } +} + +PayloadTracker::~PayloadTracker() = default; + +void PayloadTracker::OnStatusUpdate( + std::unique_ptr update, + std::optional upgraded_medium) { + auto it = payload_state_.find(update->payload_id); + if (it == payload_state_.end()) return; + + // For metrics. + if (!first_update_timestamp_.has_value()) { + first_update_timestamp_ = absl::Now(); + num_first_update_bytes_ = update->bytes_transferred; + } + if (upgraded_medium.has_value()) { + last_upgraded_medium_ = upgraded_medium; + } + + if (it->second.status != update->status) { + it->second.status = update->status; + + NL_VLOG(1) << __func__ << ": Payload id " << update->payload_id + << " had status change: " << update->status; + } + + if (it->second.status == PayloadStatus::kSuccess) { + NL_LOG(INFO) << __func__ << ": Completed transfer of payload " << it->first + << " with attachment id " << it->second.attachment_id; + transferred_attachments_count_++; + confirmed_transfer_size_ += update->bytes_transferred; + } + + // The number of bytes transferred should never go down. That said, some + // status updates like cancellation might send a value of 0. In that case, we + // retain the last known value for use in metrics. + if (update->bytes_transferred > it->second.amount_transferred) { + it->second.amount_transferred = update->bytes_transferred; + } + + // Handle in progress attachment. + if (!in_progress_payload_id_.has_value() || + *in_progress_payload_id_ != update->payload_id) { + in_progress_payload_id_ = update->payload_id; + } + + OnTransferUpdate(it->second); +} + +void PayloadTracker::OnTransferUpdate(const State& state) { + if (IsComplete()) { + NL_VLOG(1) << __func__ << ": All payloads are complete."; + update_callback_( + share_target_, + TransferMetadataBuilder() + .set_status(TransferMetadata::Status::kComplete) + .set_progress(100) + .set_total_attachments_count(payload_state_.size()) + .set_transferred_attachments_count(transferred_attachments_count_) + .build()); + return; + } + + if (IsCancelled(state)) { + NL_VLOG(1) << __func__ << ": Payloads cancelled."; + update_callback_( + share_target_, + TransferMetadataBuilder() + .set_status(TransferMetadata::Status::kCancelled) + .set_total_attachments_count(payload_state_.size()) + .set_transferred_attachments_count(transferred_attachments_count_) + .build()); + return; + } + + if (HasFailed(state)) { + NL_VLOG(1) << __func__ << ": Payloads failed."; + update_callback_( + share_target_, + TransferMetadataBuilder() + .set_status(TransferMetadata::Status::kFailed) + .set_total_attachments_count(payload_state_.size()) + .set_transferred_attachments_count(transferred_attachments_count_) + .build()); + return; + } + + double percent = CalculateProgressPercent(state); + int current_progress = static_cast(percent); + absl::Time current_time = context_->GetClock()->Now(); + uint64_t current_transferred_size = GetTotalTransferred(state); + + if (current_progress == last_update_progress_ && + (current_time - last_update_timestamp_) < kMinProgressUpdateFrequency && + state.status != PayloadStatus::kSuccess) { + return; + } + + // Update transfer speed approximately every `kTransferSpeedUpdateInterval` + // second. + if (current_speed_ == 0 || + current_time - last_transfer_speed_update_timestamp_ > + absl::Seconds(kTransferSpeedUpdateInterval)) { + current_speed_ = (current_transferred_size - last_transferred_size_) / + absl::ToDoubleSeconds( + current_time - last_transfer_speed_update_timestamp_); + + // Use current speed for the ETA calculation for the first + // `kEstimatedTimeRemainingUpdateInterval` seconds to avoid getting stuck at + // showing 24+ hours left. + if ((first_window_ == true) && + (current_time - last_eta_update_timestamp_ < + absl::Seconds(kEstimatedTimeRemainingUpdateInterval))) { + estimated_time_remaining_ = + (total_transfer_size_ - current_transferred_size) / + (current_speed_ + std::numeric_limits::min()); + first_window_ = false; + } + + rolling_window_speed_bucket_ += current_speed_; + last_transferred_size_ = current_transferred_size; + last_transfer_speed_update_timestamp_ = current_time; + } + + // Update estimated time remaining approximately every + // `kEstimatedTimeRemainingUpdateInterval` seconds. + if (current_time - last_eta_update_timestamp_ > + absl::Seconds(kEstimatedTimeRemainingUpdateInterval)) { + double average_speed = + rolling_window_speed_bucket_ / kEstimatedTimeRemainingUpdateInterval; + estimated_time_remaining_ = + (total_transfer_size_ - current_transferred_size) / + (average_speed + std::numeric_limits::min()); + last_eta_update_timestamp_ = current_time; + rolling_window_speed_bucket_ = 0.0; + } + + last_update_progress_ = current_progress; + last_update_timestamp_ = current_time; + + update_callback_( + share_target_, + TransferMetadataBuilder() + .set_status(TransferMetadata::Status::kInProgress) + .set_progress(percent) + .set_transferred_bytes(current_transferred_size) + .set_transfer_speed(static_cast(current_speed_)) + .set_estimated_time_remaining( + static_cast(estimated_time_remaining_)) + .set_total_attachments_count(payload_state_.size()) + .set_transferred_attachments_count(transferred_attachments_count_) + .set_in_progress_attachment_id(state.attachment_id) + .set_in_progress_attachment_total_bytes(state.total_size) + .set_in_progress_attachment_transferred_bytes( + state.amount_transferred) + .build()); +} + +bool PayloadTracker::IsComplete() const { + return transferred_attachments_count_ == payload_state_.size(); +} + +bool PayloadTracker::IsCancelled(const State& state) const { + return state.status == PayloadStatus::kCanceled; +} + +bool PayloadTracker::HasFailed(const State& state) const { + return state.status == PayloadStatus::kFailure; +} + +uint64_t PayloadTracker::GetTotalTransferred(const State& state) const { + if (state.status == PayloadStatus::kSuccess) { + return confirmed_transfer_size_; + } + return confirmed_transfer_size_ + state.amount_transferred; +} + +double PayloadTracker::CalculateProgressPercent(const State& state) const { + if (!total_transfer_size_) { + NL_LOG(WARNING) << __func__ << ": Total attachment size is 0"; + return 100.0; + } + + return (100.0 * GetTotalTransferred(state)) / total_transfer_size_; +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/payload_tracker.h b/sharing/payload_tracker.h new file mode 100644 index 00000000..7a9e2d2a --- /dev/null +++ b/sharing/payload_tracker.h @@ -0,0 +1,112 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_PAYLOAD_TRACKER_H_ +#define THIRD_PARTY_NEARBY_SHARING_PAYLOAD_TRACKER_H_ + +#include + +#include +#include +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/time/time.h" +#include "sharing/attachment_info.h" +#include "sharing/internal/public/context.h" +#include "sharing/nearby_connections_manager.h" +#include "sharing/nearby_connections_types.h" +#include "sharing/share_target.h" +#include "sharing/transfer_metadata.h" + +namespace nearby { +namespace sharing { + +// Listens for incoming or outgoing transfer updates from Nearby Connections and +// forwards the transfer progress to the |update_callback|. +class PayloadTracker : public NearbyConnectionsManager::PayloadStatusListener { + public: + PayloadTracker( + Context* context, const ShareTarget& share_target, + const absl::flat_hash_map& attachment_info_map, + std::function update_callback); + ~PayloadTracker() override; + + // NearbyConnectionsManager::PayloadStatusListener: + void OnStatusUpdate(std::unique_ptr update, + std::optional upgraded_medium) override; + + private: + struct State { + explicit State(int64_t attachment_id, int64_t total_size) + : attachment_id(attachment_id), total_size(total_size) {} + ~State() = default; + + int64_t attachment_id = 0; + uint64_t amount_transferred = 0; + const uint64_t total_size; + PayloadStatus status = PayloadStatus::kInProgress; + }; + + void OnTransferUpdate(const State& state); + + bool IsComplete() const; + bool IsCancelled(const State& state) const; + bool HasFailed(const State& state) const; + + uint64_t GetTotalTransferred(const State& state) const; + double CalculateProgressPercent(const State& state) const; + + Context* context_; + ShareTarget share_target_; + std::function update_callback_; + + // Map of payload id to state of payload. + std::map payload_state_; + + // Tracks in progress payload. + std::optional in_progress_payload_id_ = std::nullopt; + + uint64_t total_transfer_size_; + uint64_t confirmed_transfer_size_; + + int last_update_progress_ = 0; + absl::Time last_update_timestamp_; // progress percentage + absl::Time last_transfer_speed_update_timestamp_; + absl::Time last_eta_update_timestamp_; + uint64_t last_transferred_size_ = 0; + + double current_speed_ = 0.0; + double rolling_window_speed_bucket_ = 0.0; + double estimated_time_remaining_ = 0.0; + bool first_window_ = true; + + // Tracks transferred attachments count. + int transferred_attachments_count_ = 0; + + // For metrics. + size_t num_text_attachments_ = 0; + size_t num_file_attachments_ = 0; + size_t num_wifi_credentials_attachments_ = 0; + uint64_t num_first_update_bytes_ = 0; + std::optional first_update_timestamp_; + std::optional last_upgraded_medium_; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_PAYLOAD_TRACKER_H_ diff --git a/sharing/payload_tracker_test.cc b/sharing/payload_tracker_test.cc new file mode 100644 index 00000000..9a9b8dfa --- /dev/null +++ b/sharing/payload_tracker_test.cc @@ -0,0 +1,114 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/payload_tracker.h" + +#include +#include +#include +#include +#include +#include + +#include "gtest/gtest.h" +#include "absl/container/flat_hash_map.h" +#include "absl/strings/string_view.h" +#include "absl/time/time.h" +#include "internal/test/fake_clock.h" +#include "sharing/attachment_info.h" +#include "sharing/file_attachment.h" +#include "sharing/internal/public/context.h" +#include "sharing/internal/test/fake_context.h" +#include "sharing/nearby_connections_types.h" +#include "sharing/proto/wire_format.pb.h" +#include "sharing/share_target.h" +#include "sharing/transfer_metadata.h" + +namespace nearby { +namespace sharing { +namespace { + +constexpr int64_t kFileId = 1; +constexpr int64_t kFileSize = 100 * 1024; // 100KB +constexpr absl::string_view kFileName = "test.jpg"; +constexpr absl::string_view kMimeType = "image/jpg"; + +class PayloadTrackerTest : public ::testing::Test { + public: + void SetUp() override { + share_target_.file_attachments.clear(); + share_target_.file_attachments.push_back(FileAttachment( + kFileId, kFileSize, std::string(kFileName), std::string(kMimeType), + service::proto::FileMetadata::IMAGE)); + attachment_info_map_.clear(); + AttachmentInfo attachment_info; + attachment_info.payload_id = kFileId; + attachment_info_map_.emplace(share_target_.file_attachments.at(0).id(), + std::move(attachment_info)); + payload_tracker_ = std::make_unique( + context(), share_target_, attachment_info_map_, + [&](ShareTarget share_target, TransferMetadata transfer_metadata) { + current_percentage_ = transfer_metadata.progress(); + }); + } + + float percentage() const { return current_percentage_; } + + void FastForward(absl::Duration duration) { + FakeClock* clock = dynamic_cast(context()->GetClock()); + clock->FastForward(duration); + } + + void PayloadUpdate(int bytes_transferred) { + auto transfer_update = std::make_unique( + /*payload_id=*/kFileId, PayloadStatus::kInProgress, + /*total_bytes=*/kFileSize, /*bytes_transferred=*/bytes_transferred); + payload_tracker_->OnStatusUpdate(std::move(transfer_update), std::nullopt); + } + + private: + Context* context() { + static FakeContext* context = new FakeContext(); + return context; + } + + std::unique_ptr payload_tracker_ = nullptr; + float current_percentage_ = 0.0; + ShareTarget share_target_; + absl::flat_hash_map attachment_info_map_; +}; + +TEST_F(PayloadTrackerTest, StatusUpdateWithoutTimeUpdate) { + EXPECT_EQ(percentage(), 0.0); + PayloadUpdate(1024); + EXPECT_EQ(percentage(), 1.0); + PayloadUpdate(2048); + EXPECT_EQ(percentage(), 2.0); +} + +TEST_F(PayloadTrackerTest, StatusUpdateWithTimeUpdate) { + EXPECT_EQ(percentage(), 0.0); + PayloadUpdate(1024); + EXPECT_EQ(percentage(), 1.0); + FastForward(absl::Milliseconds(100)); + PayloadUpdate(2048); + EXPECT_EQ(percentage(), 2.0); + FastForward(absl::Milliseconds(100)); + PayloadUpdate(3072); + EXPECT_EQ(percentage(), 3.0); +} + +} // namespace +} // namespace sharing +} // namespace nearby diff --git a/sharing/proto/analytics/BUILD b/sharing/proto/analytics/BUILD index edb82fc8..05f5f6c1 100644 --- a/sharing/proto/analytics/BUILD +++ b/sharing/proto/analytics/BUILD @@ -23,16 +23,14 @@ proto_library( srcs = [ "nearby_sharing_log.proto", ], - compatible_with = ["//buildenv/target:non_prod"], deps = [ - "//google/protobuf:duration", "//proto:sharing_enums_proto", + "@com_google_protobuf//:duration_proto", ], ) cc_proto_library( name = "sharing_log_cc_proto", - compatible_with = ["//buildenv/target:non_prod"], deps = [":sharing_log_proto"], ) diff --git a/sharing/proto/analytics/nearby_sharing_log.proto b/sharing/proto/analytics/nearby_sharing_log.proto index f59c4716..147d9e5c 100644 --- a/sharing/proto/analytics/nearby_sharing_log.proto +++ b/sharing/proto/analytics/nearby_sharing_log.proto @@ -19,7 +19,7 @@ package nearby.sharing.analytics.proto; import "google/protobuf/duration.proto"; // import "storage/datapol/annotations/proto/semantic_annotations.proto"; -import "third_party/nearby/proto/sharing_enums.proto"; +import "proto/sharing_enums.proto"; // "wireless/android/privacy/annotations/proto/collection_basis_annotations.proto"; diff --git a/sharing/share_target.cc b/sharing/share_target.cc new file mode 100644 index 00000000..3643c9e6 --- /dev/null +++ b/sharing/share_target.cc @@ -0,0 +1,163 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/share_target.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" +#include "absl/strings/str_join.h" +#include "internal/network/url.h" +#include "sharing/attachment.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/file_attachment.h" +#include "sharing/text_attachment.h" +#include "sharing/wifi_credentials_attachment.h" + +namespace nearby { +namespace sharing { +namespace { +using ::nearby::network::Url; + +// Used to generate device ID. +static int64_t kLastGeneratedId = 0; +} // namespace + +ShareTarget::ShareTarget() { id = ++kLastGeneratedId; } + +ShareTarget::ShareTarget( + std::string device_name, Url image_url, ShareTargetType type, + std::vector text_attachments, + std::vector file_attachments, + std::vector wifi_credentials_attachments, + bool is_incoming, std::optional full_name, bool is_known, + std::optional device_id, bool for_self_share) + : device_name(std::move(device_name)), + image_url(std::move(image_url)), + type(type), + text_attachments(std::move(text_attachments)), + file_attachments(std::move(file_attachments)), + wifi_credentials_attachments(std::move(wifi_credentials_attachments)), + is_incoming(is_incoming), + full_name(std::move(full_name)), + is_known(is_known), + device_id(std::move(device_id)), + for_self_share(for_self_share) { + id = ++kLastGeneratedId; +} + +ShareTarget::ShareTarget(const ShareTarget&) = default; + +ShareTarget::ShareTarget(ShareTarget&&) = default; + +ShareTarget& ShareTarget::operator=(const ShareTarget&) = default; + +ShareTarget& ShareTarget::operator=(ShareTarget&&) = default; + +ShareTarget::~ShareTarget() = default; + +std::vector ShareTarget::GetAttachmentIds() const { + std::vector attachment_ids; + + attachment_ids.reserve(file_attachments.size() + text_attachments.size() + + wifi_credentials_attachments.size()); + for (const auto& file : file_attachments) attachment_ids.push_back(file.id()); + + for (const auto& text : text_attachments) attachment_ids.push_back(text.id()); + + for (const auto& wifi_credentials : wifi_credentials_attachments) + attachment_ids.push_back(wifi_credentials.id()); + + return attachment_ids; +} + +std::vector> ShareTarget::GetAttachments() const { + std::vector> attachments; + attachments.reserve(file_attachments.size() + text_attachments.size() + + wifi_credentials_attachments.size()); + for (const auto& file : file_attachments) { + attachments.push_back(std::make_unique(file)); + } + + for (const auto& text : text_attachments) { + attachments.push_back(std::make_unique(text)); + } + + for (const auto& wifi_credentials : wifi_credentials_attachments) { + attachments.push_back( + std::make_unique(wifi_credentials)); + } + + return attachments; +} + +int64_t ShareTarget::GetTotalAttachmentsSize() const { + int64_t size_in_bytes = 0; + + for (const auto& file : file_attachments) { + size_in_bytes += file.size(); + } + + for (const auto& text : text_attachments) { + size_in_bytes += text.size(); + } + + for (const auto& wifi_credentials : wifi_credentials_attachments) { + size_in_bytes += wifi_credentials.size(); + } + + return size_in_bytes; +} + +std::string ShareTarget::ToString() const { + std::vector fmt; + + fmt.push_back(absl::StrFormat("id: %" PRId64, id)); + fmt.push_back(absl::StrFormat("device_name: %s", device_name)); + if (full_name) { + fmt.push_back(absl::StrFormat("full_name: %s", *full_name)); + } + if (image_url) { + fmt.push_back(absl::StrFormat("image_url: %s", image_url->GetUrlPath())); + } + if (device_id) { + fmt.push_back(absl::StrFormat("device_id: %s", *device_id)); + } + fmt.push_back( + absl::StrFormat("file_attachments_size: %d", file_attachments.size())); + fmt.push_back( + absl::StrFormat("text_attachments_size: %d", text_attachments.size())); + fmt.push_back(absl::StrFormat("wifi_credentials_attachments_size: %d", + wifi_credentials_attachments.size())); + fmt.push_back(absl::StrFormat("is_known: %d", is_known)); + fmt.push_back(absl::StrFormat("is_incoming: %d", is_incoming)); + fmt.push_back(absl::StrFormat("for_self_share: %d", for_self_share)); + + return absl::StrCat("ShareTarget<", absl::StrJoin(fmt, ", "), ">"); +} + +bool ShareTarget::has_attachments() const { + return !text_attachments.empty() || !file_attachments.empty() || + !wifi_credentials_attachments.empty(); +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/share_target.h b/sharing/share_target.h new file mode 100644 index 00000000..f6dc8e9c --- /dev/null +++ b/sharing/share_target.h @@ -0,0 +1,78 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_SHARE_TARGET_H_ +#define THIRD_PARTY_NEARBY_SHARING_SHARE_TARGET_H_ + +#include +#include +#include +#include +#include + +#include "internal/network/url.h" +#include "sharing/attachment.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/file_attachment.h" +#include "sharing/text_attachment.h" +#include "sharing/wifi_credentials_attachment.h" + +namespace nearby { +namespace sharing { + +// A remote device. +struct ShareTarget { + public: + ShareTarget(); + ShareTarget( + std::string device_name, ::nearby::network::Url image_url, + ShareTargetType type, std::vector text_attachments, + std::vector file_attachments, + std::vector wifi_credentials_attachments, + bool is_incoming, std::optional full_name, bool is_known, + std::optional device_id, bool for_self_share); + ShareTarget(const ShareTarget&); + ShareTarget(ShareTarget&&); + ShareTarget& operator=(const ShareTarget&); + ShareTarget& operator=(ShareTarget&&); + ~ShareTarget(); + + bool has_attachments() const; + std::vector GetAttachmentIds() const; + std::vector> GetAttachments() const; + int64_t GetTotalAttachmentsSize() const; + std::string ToString() const; + + int64_t id; + std::string device_name; + // Uri that points to an image of the ShareTarget, if one exists. + std::optional<::nearby::network::Url> image_url; + ShareTargetType type = ShareTargetType::kUnknown; + std::vector text_attachments; + std::vector file_attachments; + std::vector wifi_credentials_attachments; + bool is_incoming = false; + std::optional full_name; + // True if the local device has the PublicCertificate this target is + // advertising. + bool is_known = false; + std::optional device_id; + // True if the remote device is also owned by the current user. + bool for_self_share = false; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_SHARE_TARGET_H_ diff --git a/sharing/share_target_discovered_callback.h b/sharing/share_target_discovered_callback.h new file mode 100644 index 00000000..a6d30c66 --- /dev/null +++ b/sharing/share_target_discovered_callback.h @@ -0,0 +1,34 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_SHARE_TARGET_DISCOVERED_CALLBACK_H_ +#define THIRD_PARTY_NEARBY_SHARING_SHARE_TARGET_DISCOVERED_CALLBACK_H_ + +#include "sharing/share_target.h" + +namespace nearby { +namespace sharing { + +// Reports newly discovered devices. +class ShareTargetDiscoveredCallback { + public: + virtual ~ShareTargetDiscoveredCallback() = default; + virtual void OnShareTargetDiscovered(const ShareTarget& share_target) = 0; + virtual void OnShareTargetLost(const ShareTarget& share_target) = 0; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_SHARE_TARGET_DISCOVERED_CALLBACK_H_ diff --git a/sharing/share_target_info.cc b/sharing/share_target_info.cc new file mode 100644 index 00000000..43989079 --- /dev/null +++ b/sharing/share_target_info.cc @@ -0,0 +1,37 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/share_target_info.h" + +#include + +#include "absl/time/time.h" +#include "sharing/certificates/nearby_share_decrypted_public_certificate.h" +#include "sharing/incoming_frames_reader.h" +#include "sharing/paired_key_verification_runner.h" +#include "sharing/transfer_update_callback.h" + +namespace nearby { +namespace sharing { + +ShareTargetInfo::ShareTargetInfo() = default; + +ShareTargetInfo::ShareTargetInfo(ShareTargetInfo&&) = default; + +ShareTargetInfo& ShareTargetInfo::operator=(ShareTargetInfo&&) = default; + +ShareTargetInfo::~ShareTargetInfo() = default; + +} // namespace sharing +} // namespace nearby diff --git a/sharing/share_target_info.h b/sharing/share_target_info.h new file mode 100644 index 00000000..ee844c26 --- /dev/null +++ b/sharing/share_target_info.h @@ -0,0 +1,140 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_SHARE_TARGET_INFO_H_ +#define THIRD_PARTY_NEARBY_SHARING_SHARE_TARGET_INFO_H_ + +#include +#include +#include +#include +#include + +#include "absl/time/time.h" +#include "proto/sharing_enums.pb.h" +#include "sharing/certificates/nearby_share_decrypted_public_certificate.h" +#include "sharing/incoming_frames_reader.h" +#include "sharing/nearby_connection.h" +#include "sharing/nearby_connections_manager.h" +#include "sharing/paired_key_verification_runner.h" +#include "sharing/payload_tracker.h" +#include "sharing/transfer_update_callback.h" + +namespace nearby { +namespace sharing { + +// Additional information about the connection to a remote device. +class ShareTargetInfo { + public: + ShareTargetInfo(); + ShareTargetInfo(ShareTargetInfo&&); + ShareTargetInfo& operator=(ShareTargetInfo&&); + virtual ~ShareTargetInfo(); + + const std::optional& endpoint_id() const { return endpoint_id_; } + + void set_endpoint_id(std::string endpoint_id) { + endpoint_id_ = std::move(endpoint_id); + } + + const std::optional& certificate() + const { + return certificate_; + } + + void set_certificate(NearbyShareDecryptedPublicCertificate certificate) { + certificate_ = std::move(certificate); + } + + NearbyConnection* connection() const { return connection_; } + + void set_connection(NearbyConnection* connection) { + connection_ = connection; + } + + TransferUpdateCallback* transfer_update_callback() const { + return transfer_update_callback_.get(); + } + + void set_transfer_update_callback( + std::unique_ptr transfer_update_callback) { + transfer_update_callback_ = std::move(transfer_update_callback); + } + + const std::optional& token() const { return token_; } + + void set_token(std::string token) { token_ = std::move(token); } + + IncomingFramesReader* frames_reader() const { return frames_reader_.get(); } + + void set_frames_reader(std::shared_ptr frames_reader) { + frames_reader_ = std::move(frames_reader); + } + + PairedKeyVerificationRunner* key_verification_runner() { + return key_verification_runner_.get(); + } + + void set_key_verification_runner( + std::shared_ptr key_verification_runner) { + key_verification_runner_ = std::move(key_verification_runner); + } + + std::weak_ptr + payload_tracker() { + return payload_tracker_->GetWeakPtr(); + } + + void set_payload_tracker(std::shared_ptr payload_tracker) { + payload_tracker_ = std::move(payload_tracker); + } + + int64_t session_id() { return session_id_; } + + void set_session_id(int64_t session_id) { session_id_ = session_id; } + + std::optional connection_start_time() { + return connection_start_time_; + } + + void set_connection_start_time( + std::optional connection_start_time) { + connection_start_time_ = connection_start_time; + } + + ::location::nearby::proto::sharing::OSType os_type() { return os_type_; } + + void set_os_type(::location::nearby::proto::sharing::OSType os_type) { + os_type_ = os_type; + } + + private: + std::optional endpoint_id_; + std::optional certificate_; + NearbyConnection* connection_ = nullptr; + std::unique_ptr transfer_update_callback_; + std::optional token_; + std::shared_ptr frames_reader_; + std::shared_ptr key_verification_runner_; + std::shared_ptr payload_tracker_; + int64_t session_id_; + std::optional connection_start_time_; + ::location::nearby::proto::sharing::OSType os_type_ = + ::location::nearby::proto::sharing::OSType::UNKNOWN_OS_TYPE; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_SHARE_TARGET_INFO_H_ diff --git a/sharing/share_target_test.cc b/sharing/share_target_test.cc new file mode 100644 index 00000000..4e3cb90a --- /dev/null +++ b/sharing/share_target_test.cc @@ -0,0 +1,84 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/share_target.h" + +#include +#include + +#include "gtest/gtest.h" +#include "internal/network/url.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/file_attachment.h" +#include "sharing/text_attachment.h" +#include "sharing/wifi_credentials_attachment.h" + +namespace nearby { +namespace sharing { +namespace { + +struct ShareTargetToStringTestData { + ShareTarget share_target; + std::string expected_string_result; +}; + +std::vector GetTestData() { + ShareTarget share_target1; + ShareTarget share_target2{"test_name", + ::nearby::network::Url(), + ShareTargetType::kPhone, + std::vector(), + std::vector(), + std::vector(), + /* is_incoming */ true, + "test_full_name", + /* is_known */ false, + "test_device_id", + true}; + share_target1.id = 1; + share_target2.id = 2; + + static std::vector< + ShareTargetToStringTestData>* kShareTargetToStringTestData = + new std::vector({ + {share_target1, + "ShareTarget"}, + {share_target2, + "ShareTarget"}, + }); + + return *kShareTargetToStringTestData; +} + +using ShareTargetToStringTest = + testing::TestWithParam; + +TEST_P(ShareTargetToStringTest, ToStringResultMatches) { + ShareTarget test_share_target = GetParam().share_target; + EXPECT_EQ(GetParam().expected_string_result, test_share_target.ToString()); +} + +INSTANTIATE_TEST_SUITE_P(ShareTargetToStringTest, ShareTargetToStringTest, + testing::ValuesIn(GetTestData())); + +} // namespace +} // namespace sharing +} // namespace nearby diff --git a/sharing/text_attachment.cc b/sharing/text_attachment.cc new file mode 100644 index 00000000..d67a4fa0 --- /dev/null +++ b/sharing/text_attachment.cc @@ -0,0 +1,173 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/text_attachment.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include "absl/status/statusor.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "internal/network/url.h" +#include "sharing/attachment.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/proto/wire_format.pb.h" +#include "sharing/share_target.h" + +namespace nearby { +namespace sharing { +namespace { + +using ::nearby::network::Url; + +// Tries to get a valid host name from the |text|. Returns nullopt otherwise. +std::optional GetHostFromText(absl::string_view text) { + absl::StatusOr url = Url::Create(text); + if (!url.ok() || url->GetHostName().empty()) return std::nullopt; + + return absl::StrCat(url->GetHostName()); +} + +// Masks the given |number| depending on the string length: +// - length <= 4: Masks all characters +// - 4 < length <= 6 Masks the last 4 characters +// - 6 < length <= 10: Skips the first 2 and masks the following 4 characters +// - length > 10: Skips the first 2 and last 4 characters. Masks the rest of +// the string +// Note: We're assuming a formatted phone number and won't try to reformat to +// E164 like on Android as there's no easy way of determining the intended +// region for the phone number. +std::string MaskPhoneNumber(const std::string& number) { + constexpr int kMinMaskedDigits = 4; + constexpr int kMaxLeadingDigits = 2; + constexpr int kMaxTailingDigits = 4; + constexpr int kLengthWithNoLeadingDigits = kMinMaskedDigits; + constexpr int kLengthWithNoTailingDigits = + kMinMaskedDigits + kMaxLeadingDigits; + + if (number.empty()) return number; + + std::string result = number; + bool has_plus = false; + if (number[0] == '+') { + result = number.substr(1); + has_plus = true; + } + + // First calculate how many digits we would mask having exactly + // kMinMaskedDigits digits masked. + int leading_digits = 0; + if (result.length() > kLengthWithNoLeadingDigits) + leading_digits = result.length() - kLengthWithNoLeadingDigits; + + int tailing_digits = 0; + if (result.length() > kLengthWithNoTailingDigits) + tailing_digits = result.length() - kLengthWithNoTailingDigits; + + // Now limit resulting numbers of digits to maximally allowed values. + leading_digits = std::min(kMaxLeadingDigits, leading_digits); + tailing_digits = std::min(kMaxTailingDigits, tailing_digits); + int masked_digits = result.length() - leading_digits - tailing_digits; + + return absl::StrCat((has_plus ? "+" : ""), result.substr(0, leading_digits), + std::string(masked_digits, 'x'), + result.substr(result.length() - tailing_digits)); +} + +std::string GetTextTitle(const std::string& text_body, + TextAttachment::Type type) { + constexpr size_t kMaxPreviewTextLength = 32; + + switch (type) { + case service::proto::TextMetadata::URL: { + std::optional host = GetHostFromText(text_body); + if (host.has_value()) return *host; + + break; + } + case service::proto::TextMetadata::PHONE_NUMBER: + return MaskPhoneNumber(text_body); + default: + break; + } + + if (text_body.size() > kMaxPreviewTextLength) + return absl::StrCat(text_body.substr(0, kMaxPreviewTextLength), "…"); + + return text_body; +} + +} // namespace + +TextAttachment::TextAttachment(Type type, std::string text_body, + std::optional text_title, + std::optional mime_type, + int32_t batch_id, SourceType source_type) + : Attachment(Attachment::Family::kText, text_body.size(), batch_id, + source_type), + type_(type), + text_title_(text_title.has_value() && !text_title->empty() + ? *text_title + : GetTextTitle(text_body, type)), + text_body_(std::move(text_body)), + mime_type_(mime_type ? *mime_type : std::string()) {} + +TextAttachment::TextAttachment(int64_t id, Type type, std::string text_title, + int64_t size, int32_t batch_id, + SourceType source_type) + : Attachment(id, Attachment::Family::kText, size, batch_id, source_type), + type_(type), + text_title_(std::move(text_title)) {} + +void TextAttachment::MoveToShareTarget(ShareTarget& share_target) { + share_target.text_attachments.push_back(std::move(*this)); +} + +absl::string_view TextAttachment::GetDescription() const { return text_title_; } + +ShareType TextAttachment::GetShareType() const { + switch (type()) { + case service::proto::TextMetadata::URL: + if (mime_type_ == "application/vnd.google-apps.document") { + return ShareType::kGoogleDocsFile; + } else if (mime_type_ == "application/vnd.google-apps.spreadsheet") { + return ShareType::kGoogleSheetsFile; + } else if (mime_type_ == "application/vnd.google-apps.presentation") { + return ShareType::kGoogleSlidesFile; + } else { + return ShareType::kUrl; + } + case service::proto::TextMetadata::ADDRESS: + return ShareType::kAddress; + case service::proto::TextMetadata::PHONE_NUMBER: + return ShareType::kPhone; + default: + return ShareType::kText; + } +} + +void TextAttachment::set_text_body(std::string text_body) { + text_body_ = std::move(text_body); + text_title_ = GetTextTitle(text_body_, type_); +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/text_attachment.h b/sharing/text_attachment.h new file mode 100644 index 00000000..a9f6442a --- /dev/null +++ b/sharing/text_attachment.h @@ -0,0 +1,76 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_TEXT_ATTACHMENT_H_ +#define THIRD_PARTY_NEARBY_SHARING_TEXT_ATTACHMENT_H_ + +#include + +#include +#include + +#include "absl/strings/string_view.h" +#include "sharing/attachment.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/proto/wire_format.pb.h" + +namespace nearby { +namespace sharing { + +// Represents a text attachment. +struct ShareTarget; + +class TextAttachment : public Attachment { + public: + using Type = nearby::sharing::service::proto::TextMetadata::Type; + + TextAttachment(Type type, std::string text_body, + std::optional text_title, + std::optional mime_type, int32_t batch_id = 0, + SourceType source_type = SourceType::kUnknown); + TextAttachment(int64_t id, Type type, std::string text_title, int64_t size, + int32_t batch_id = 0, + SourceType source_type = SourceType::kUnknown); + TextAttachment(const TextAttachment&) = default; + TextAttachment(TextAttachment&&) = default; + TextAttachment& operator=(const TextAttachment&) = default; + TextAttachment& operator=(TextAttachment&&) = default; + ~TextAttachment() override = default; + + absl::string_view text_body() const { return text_body_; } + absl::string_view text_title() const { return text_title_; } + Type type() const { return type_; } + + // Attachment: + void MoveToShareTarget(ShareTarget& share_target) override; + absl::string_view GetDescription() const override; + ShareType GetShareType() const override; + + void set_text_body(std::string text_body); + + std::string mime_type() const { return mime_type_; } + SourceType source_type() const { return source_type_; } + + private: + Type type_ = service::proto::TextMetadata::UNKNOWN; + std::string text_title_; + std::string text_body_; + std::string mime_type_; + SourceType source_type_ = SourceType::kUnknown; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_TEXT_ATTACHMENT_H_ diff --git a/sharing/text_attachment_test.cc b/sharing/text_attachment_test.cc new file mode 100644 index 00000000..e6341e14 --- /dev/null +++ b/sharing/text_attachment_test.cc @@ -0,0 +1,114 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/text_attachment.h" + +#include +#include +#include + +#include "gtest/gtest.h" +#include "sharing/proto/wire_format.pb.h" + +namespace nearby { +namespace sharing { +namespace { + +struct TextAttachmentTextTitleTestData { + TextAttachment::Type type; + std::string text_body; + std::string expected_text_title; +}; + +std::vector GetTestData() { + static std::vector< + TextAttachmentTextTitleTestData>* kTextAttachmentTextTitleTestData = + new std::vector({ + {service::proto::TextMetadata::TEXT, "Short text", "Short text"}, + {service::proto::TextMetadata::TEXT, + "Long text that should be truncated", + "Long text that should be truncat…"}, + {service::proto::TextMetadata::URL, + "https://www.google.com/maps/search/restaurants/@/" + "data=!3m1!4b1?disco_ad=1234", + "www.google.com"}, + {service::proto::TextMetadata::URL, "Invalid URL", "Invalid URL"}, + {service::proto::TextMetadata::PHONE_NUMBER, "1234", "xxxx"}, + {service::proto::TextMetadata::PHONE_NUMBER, "+1234", "+xxxx"}, + {service::proto::TextMetadata::PHONE_NUMBER, "123456", "12xxxx"}, + {service::proto::TextMetadata::PHONE_NUMBER, "12345678", "12xxxx78"}, + {service::proto::TextMetadata::PHONE_NUMBER, "+447123456789", + "+44xxxxxx6789"}, + {service::proto::TextMetadata::PHONE_NUMBER, + "+1255555555555555555555555555555555555555555555555556789", + "+12xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx6789"}, + {service::proto::TextMetadata::PHONE_NUMBER, "+16196784004", + "+16xxxxx4004"}, + {service::proto::TextMetadata::PHONE_NUMBER, "+3841235782564", + "+38xxxxxxx2564"}, + {service::proto::TextMetadata::PHONE_NUMBER, "+12345678901", + "+12xxxxx8901"}, + {service::proto::TextMetadata::PHONE_NUMBER, "+1234567891", + "+12xxxx7891"}, + {service::proto::TextMetadata::PHONE_NUMBER, "+123456789", + "+12xxxx789"}, + {service::proto::TextMetadata::PHONE_NUMBER, "+12345678", + "+12xxxx78"}, + {service::proto::TextMetadata::PHONE_NUMBER, "+1234567", "+12xxxx7"}, + {service::proto::TextMetadata::PHONE_NUMBER, "+123456", "+12xxxx"}, + {service::proto::TextMetadata::PHONE_NUMBER, "+12345", "+1xxxx"}, + {service::proto::TextMetadata::PHONE_NUMBER, "+1234", "+xxxx"}, + {service::proto::TextMetadata::PHONE_NUMBER, "+123", "+xxx"}, + {service::proto::TextMetadata::PHONE_NUMBER, "+12", "+xx"}, + {service::proto::TextMetadata::PHONE_NUMBER, "+1", "+x"}, + {service::proto::TextMetadata::PHONE_NUMBER, + "1255555555555555555555555555555555555555555555555556789", + "12xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx6789"}, + {service::proto::TextMetadata::PHONE_NUMBER, "12345678901", + "12xxxxx8901"}, + {service::proto::TextMetadata::PHONE_NUMBER, "1234567891", + "12xxxx7891"}, + {service::proto::TextMetadata::PHONE_NUMBER, "123456789", + "12xxxx789"}, + {service::proto::TextMetadata::PHONE_NUMBER, "12345678", "12xxxx78"}, + {service::proto::TextMetadata::PHONE_NUMBER, "1234567", "12xxxx7"}, + {service::proto::TextMetadata::PHONE_NUMBER, "123456", "12xxxx"}, + {service::proto::TextMetadata::PHONE_NUMBER, "12345", "1xxxx"}, + {service::proto::TextMetadata::PHONE_NUMBER, "1234", "xxxx"}, + {service::proto::TextMetadata::PHONE_NUMBER, "123", "xxx"}, + {service::proto::TextMetadata::PHONE_NUMBER, "12", "xx"}, + {service::proto::TextMetadata::PHONE_NUMBER, "1", "x"}, + {service::proto::TextMetadata::PHONE_NUMBER, "+", "+"}, + }); + + return *kTextAttachmentTextTitleTestData; +} + +using TextAttachmentTextTitleTest = + testing::TestWithParam; + +TEST_P(TextAttachmentTextTitleTest, TextTitleMatches) { + TextAttachment attachment(GetParam().type, GetParam().text_body, + /*text_title=*/std::nullopt, + /*mime_type=*/std::nullopt); + EXPECT_EQ(GetParam().expected_text_title, attachment.text_title()); +} + +INSTANTIATE_TEST_CASE_P(TextAttachmentTextTitleTest, + TextAttachmentTextTitleTest, + testing::ValuesIn(GetTestData())); + +} // namespace +} // namespace sharing +} // namespace nearby diff --git a/sharing/transfer_manager.cc b/sharing/transfer_manager.cc new file mode 100644 index 00000000..aa3396fc --- /dev/null +++ b/sharing/transfer_manager.cc @@ -0,0 +1,144 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/transfer_manager.h" + +#include +#include +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "absl/synchronization/mutex.h" +#include "absl/time/time.h" +#include "sharing/internal/public/context.h" +#include "sharing/internal/public/logging.h" +#include "sharing/nearby_connections_types.h" + +namespace nearby { +namespace sharing { +namespace { + +bool IsHighQualityMedium(Medium medium) { + if (medium == Medium::kWifiLan || medium == Medium::kWifiAware || + medium == Medium::kWifiDirect || medium == Medium::kWifiHotspot || + medium == Medium::kWebRtc) { + return true; + } + + return false; +} + +} // namespace + +TransferManager::TransferManager(Context* context, + absl::string_view endpoint_id) + : context_(context), endpoint_id_(endpoint_id) {} + +void TransferManager::Send(std::function task) { + absl::MutexLock lock(&mutex_); + + if (is_waiting_for_high_quality_medium_) { + NL_LOG(INFO) + << "Connection to endpoint " << endpoint_id_ + << " is waiting for a high quality medium, delaying payload transfer."; + pending_tasks_.push_back(task); + return; + } + + task(); +} + +void TransferManager::OnMediumQualityChanged(Medium current_medium) { + absl::MutexLock lock(&mutex_); + + if (!is_waiting_for_high_quality_medium_) { + NL_LOG(WARNING) << "It is not waiting for high quality medium."; + return; + } + + if (!IsHighQualityMedium(current_medium)) { + NL_LOG(WARNING) << "medium switched to low quality Medium: " + << static_cast(current_medium); + return; + } + + NL_LOG(INFO) << "Connection to endpoint " << endpoint_id_ + << " has changed to a high quality medium: " + << static_cast(current_medium); + StopWaitingForHighQualityMedium(); +} + +bool TransferManager::StartTransfer() { + absl::MutexLock lock(&mutex_); + + if (!is_waiting_for_high_quality_medium_) { + NL_LOG(WARNING) << "No need to wait for high quality medium."; + return false; + } + + if (timeout_timer_ != nullptr && timeout_timer_->IsRunning()) { + NL_LOG(WARNING) << "transfer already started."; + return false; + } + + timeout_timer_ = context_->CreateTimer(); + timeout_timer_->Start( + kMediumUpgradeTimeout / absl::Milliseconds(1), 0, [&]() { + absl::MutexLock lock(&mutex_); + + NL_LOG(INFO) << "Timed out for endpoint " << endpoint_id_ << " after " + << (kMediumUpgradeTimeout / absl::Milliseconds(1)) + << "ms."; + StopWaitingForHighQualityMedium(); + }); + + NL_LOG(INFO) << "Attempting to upgrade the bandwidth for endpoint " + + endpoint_id_ + ". Large payloads will be delayed" + + " until either bandwidth is upgraded or a timeout of " + << (kMediumUpgradeTimeout / absl::Milliseconds(1)) + << " milliseconds is reached"; + return true; +} + +bool TransferManager::CancelTransfer() { + absl::MutexLock lock(&mutex_); + + if (timeout_timer_ == nullptr || !timeout_timer_->IsRunning()) { + NL_LOG(WARNING) << "No running transfer."; + return false; + } + + timeout_timer_->Stop(); + NL_LOG(INFO) << __func__ << "Transfer is canceled"; + return true; +} + +void TransferManager::StopWaitingForHighQualityMedium() { + is_waiting_for_high_quality_medium_ = false; + + for (const auto& task : pending_tasks_) { + NL_LOG(INFO) << "Sending delayed payload to endpoint " << endpoint_id_; + task(); + } + pending_tasks_.clear(); + + if (timeout_timer_ != nullptr) { + timeout_timer_->Stop(); + } +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/transfer_manager.h b/sharing/transfer_manager.h new file mode 100644 index 00000000..bcefebbe --- /dev/null +++ b/sharing/transfer_manager.h @@ -0,0 +1,64 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_TRANSFER_MANAGER_H_ +#define THIRD_PARTY_NEARBY_SHARING_TRANSFER_MANAGER_H_ + +#include +#include +#include +#include + +#include "absl/base/thread_annotations.h" +#include "absl/strings/string_view.h" +#include "absl/synchronization/mutex.h" +#include "absl/time/time.h" +#include "sharing/internal/public/context.h" +#include "sharing/nearby_connections_types.h" + +namespace nearby { +namespace sharing { + +// TransferManager is used to delay the payload transfer until the medium +// quality is in high quality. If the quality doesn't change in a duration, it +// will give up to wait for the medium change. +class TransferManager { + public: + // Used to wait for the medium upgrade. + static constexpr absl::Duration kMediumUpgradeTimeout = absl::Seconds(10); + + TransferManager(Context* context, absl::string_view endpoint_id); + + void Send(std::function task) ABSL_LOCKS_EXCLUDED(mutex_); + void OnMediumQualityChanged(Medium current_medium) + ABSL_LOCKS_EXCLUDED(mutex_); + bool StartTransfer() ABSL_LOCKS_EXCLUDED(mutex_); + bool CancelTransfer() ABSL_LOCKS_EXCLUDED(mutex_); + + private: + void StopWaitingForHighQualityMedium(); + + Context* context_; + bool is_waiting_for_high_quality_medium_ = true; + std::string endpoint_id_; + absl::Mutex mutex_; + std::vector> pending_tasks_; + + std::unique_ptr timeout_timer_ = nullptr; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_TRANSFER_MANAGER_H_ diff --git a/sharing/transfer_manager_test.cc b/sharing/transfer_manager_test.cc new file mode 100644 index 00000000..f6ed6fc0 --- /dev/null +++ b/sharing/transfer_manager_test.cc @@ -0,0 +1,181 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/transfer_manager.h" + +#include + +#include "gtest/gtest.h" +#include "absl/strings/string_view.h" +#include "absl/synchronization/notification.h" +#include "absl/time/time.h" +#include "internal/test/fake_clock.h" +#include "sharing/internal/test/fake_context.h" +#include "sharing/nearby_connections_types.h" + +namespace nearby { +namespace sharing { +namespace { + +constexpr absl::string_view kEndpointId = "endpoint"; +constexpr absl::Duration kNotificationTimeout = absl::Milliseconds(200); + +TEST(TransferManager, MediumUpgradeSuccess) { + FakeContext context; + absl::Notification notification; + bool is_called = false; + + TransferManager transfer_manager{&context, kEndpointId}; + transfer_manager.Send([&]() { + is_called = true; + notification.Notify(); + }); + + ASSERT_FALSE(is_called); + ASSERT_TRUE(transfer_manager.StartTransfer()); + ASSERT_FALSE(transfer_manager.StartTransfer()); + transfer_manager.OnMediumQualityChanged(Medium::kWifiLan); + ASSERT_TRUE( + notification.WaitForNotificationWithTimeout(kNotificationTimeout)); + ASSERT_TRUE(is_called); + ASSERT_FALSE(transfer_manager.StartTransfer()); +} + +TEST(TransferManager, SendAfterMediumUpgradeSuccess) { + FakeContext context; + absl::Notification notification; + bool is_called = false; + + TransferManager transfer_manager{&context, kEndpointId}; + transfer_manager.Send([&]() { + is_called = true; + notification.Notify(); + }); + + ASSERT_FALSE(is_called); + ASSERT_TRUE(transfer_manager.StartTransfer()); + transfer_manager.OnMediumQualityChanged(Medium::kWebRtc); + ASSERT_TRUE( + notification.WaitForNotificationWithTimeout(kNotificationTimeout)); + ASSERT_TRUE(is_called); + is_called = false; + transfer_manager.Send([&]() { is_called = true; }); + ASSERT_TRUE(is_called); +} + +TEST(TransferManager, MediumUpgradeFailed) { + FakeContext context; + absl::Notification notification; + bool is_called = false; + + TransferManager transfer_manager{&context, kEndpointId}; + transfer_manager.Send([&]() { + is_called = true; + notification.Notify(); + }); + + ASSERT_FALSE(is_called); + ASSERT_TRUE(transfer_manager.StartTransfer()); + transfer_manager.OnMediumQualityChanged(Medium::kBluetooth); + ASSERT_FALSE( + notification.WaitForNotificationWithTimeout(kNotificationTimeout)); + ASSERT_FALSE(is_called); +} + +TEST(TransferManager, MediumUpgradeTimeout) { + FakeContext context; + absl::Notification notification; + bool is_called = false; + + TransferManager transfer_manager{&context, kEndpointId}; + transfer_manager.Send([&]() { + is_called = true; + notification.Notify(); + }); + + ASSERT_FALSE(is_called); + ASSERT_TRUE(transfer_manager.StartTransfer()); + FakeClock* clock = static_cast(context.GetClock()); + clock->FastForward(TransferManager::kMediumUpgradeTimeout); + + ASSERT_TRUE( + notification.WaitForNotificationWithTimeout(kNotificationTimeout)); + ASSERT_TRUE(is_called); +} + +TEST(TransferManager, CancelStartedTransfer) { + FakeContext context; + absl::Notification notification; + bool is_called = false; + + TransferManager transfer_manager{&context, kEndpointId}; + transfer_manager.Send([&]() { + is_called = true; + notification.Notify(); + }); + + ASSERT_FALSE(is_called); + ASSERT_TRUE(transfer_manager.StartTransfer()); + FakeClock* clock = static_cast(context.GetClock()); + clock->FastForward(absl::Seconds(5)); + ASSERT_TRUE(transfer_manager.CancelTransfer()); + + ASSERT_FALSE( + notification.WaitForNotificationWithTimeout(kNotificationTimeout)); + ASSERT_FALSE(is_called); +} + +TEST(TransferManager, CancelTimedOutMediumUpgrade) { + FakeContext context; + absl::Notification notification; + bool is_called = false; + + TransferManager transfer_manager{&context, kEndpointId}; + transfer_manager.Send([&]() { + is_called = true; + notification.Notify(); + }); + + ASSERT_FALSE(is_called); + ASSERT_TRUE(transfer_manager.StartTransfer()); + FakeClock* clock = static_cast(context.GetClock()); + clock->FastForward(TransferManager::kMediumUpgradeTimeout); + + ASSERT_TRUE( + notification.WaitForNotificationWithTimeout(kNotificationTimeout)); + ASSERT_TRUE(is_called); + ASSERT_FALSE(transfer_manager.CancelTransfer()); +} + +TEST(TransferManager, MediumUpgradeBeforeStartTransfer) { + FakeContext context; + absl::Notification notification; + bool is_called = false; + + TransferManager transfer_manager{&context, kEndpointId}; + transfer_manager.Send([&]() { + is_called = true; + notification.Notify(); + }); + + transfer_manager.OnMediumQualityChanged(Medium::kWifiLan); + + ASSERT_TRUE( + notification.WaitForNotificationWithTimeout(kNotificationTimeout)); + ASSERT_TRUE(is_called); +} + +} // namespace +} // namespace sharing +} // namespace nearby diff --git a/sharing/transfer_metadata.cc b/sharing/transfer_metadata.cc new file mode 100644 index 00000000..03faf0bd --- /dev/null +++ b/sharing/transfer_metadata.cc @@ -0,0 +1,181 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/transfer_metadata.h" + +#include + +#include +#include +#include +#include + +#include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" +#include "absl/strings/str_join.h" + +namespace nearby { +namespace sharing { + +// static +bool TransferMetadata::IsFinalStatus(Status status) { + switch (status) { + case Status::kAwaitingRemoteAcceptanceFailed: + case Status::kCancelled: + case Status::kComplete: + case Status::kDecodeAdvertisementFailed: + case Status::kExternalProviderLaunched: + case Status::kFailed: + case Status::kFailedToCreateShareTarget: + case Status::kFailedToInitiateOutgoingConnection: + case Status::kFailedToReadOutgoingConnectionResponse: + case Status::kIncompletePayloads: + case Status::kInvalidIntroductionFrame: + case Status::kMediaUnavailable: + case Status::kMissingEndpointId: + case Status::kMissingPayloads: + case Status::kMissingShareTarget: + case Status::kMissingTransferUpdateCallback: + case Status::kNotEnoughSpace: + case Status::kPairedKeyVerificationFailed: + case Status::kRejected: + case Status::kTimedOut: + case Status::kUnexpectedDisconnection: + case Status::kUnsupportedAttachmentType: + return true; + case Status::kAwaitingLocalConfirmation: + case Status::kAwaitingRemoteAcceptance: + case Status::kConnecting: + case Status::kInProgress: + case Status::kMediaDownloading: + case Status::kUnknown: + return false; + } +} + +// static +std::string TransferMetadata::StatusToString(Status status) { + switch (status) { + case Status::kConnecting: + return "kConnecting"; + case Status::kUnknown: + return "kUnknown"; + case Status::kAwaitingLocalConfirmation: + return "kAwaitingLocalConfirmation"; + case Status::kAwaitingRemoteAcceptance: + return "kAwaitingRemoteAcceptance"; + case Status::kAwaitingRemoteAcceptanceFailed: + return "kAwaitingRemoteAcceptanceFailed"; + case Status::kInProgress: + return "kInProgress"; + case Status::kComplete: + return "kComplete"; + case Status::kFailed: + return "kFailed"; + case Status::kRejected: + return "kReject"; + case Status::kCancelled: + return "kCancelled"; + case Status::kTimedOut: + return "kTimedOut"; + case Status::kMediaUnavailable: + return "kMediaUnavailable"; + case Status::kMediaDownloading: + return "kMediaDownloading"; + case Status::kNotEnoughSpace: + return "kNotEnoughSpace"; + case Status::kUnsupportedAttachmentType: + return "kUnsupportedAttachmentType"; + case Status::kExternalProviderLaunched: + return "kExternalProviderLaunched"; + case Status::kDecodeAdvertisementFailed: + return "kDecodeAdvertisementFailed"; + case Status::kMissingTransferUpdateCallback: + return "kMissingTransferUpdateCallback"; + case Status::kMissingShareTarget: + return "kMissingShareTarget"; + case Status::kMissingEndpointId: + return "kMissingEndpointId"; + case Status::kMissingPayloads: + return "kMissingPayloads"; + case Status::kPairedKeyVerificationFailed: + return "kPairedKeyVerificationFailed"; + case Status::kInvalidIntroductionFrame: + return "kInvalidIntroductionFrame"; + case Status::kIncompletePayloads: + return "kIncompletePayloads"; + case Status::kFailedToCreateShareTarget: + return "kFailedToCreateShareTarget"; + case Status::kFailedToInitiateOutgoingConnection: + return "kFailedToInitiateOutgoingConnection"; + case Status::kFailedToReadOutgoingConnectionResponse: + return "kFailedToReadOutgoingConnectionResponse"; + case Status::kUnexpectedDisconnection: + return "kUnexpectedDisconnection"; + } +} + +TransferMetadata::TransferMetadata( + Status status, float progress, std::optional token, + bool is_original, bool is_final_status, bool is_self_share, + uint64_t transferred_bytes, uint64_t transfer_speed, + uint64_t estimated_time_remaining, int total_attachments_count, + int transferred_attachments_count, + std::optional in_progress_attachment_id, + std::optional in_progress_attachment_transferred_bytes, + std::optional in_progress_attachment_total_bytes) + : status_(status), + progress_(progress), + token_(std::move(token)), + is_original_(is_original), + is_final_status_(is_final_status), + is_self_share_(is_self_share), + transferred_bytes_(transferred_bytes), + transfer_speed_(transfer_speed), + estimated_time_remaining_(estimated_time_remaining), + total_attachments_count_(total_attachments_count), + transferred_attachments_count_(transferred_attachments_count), + in_progress_attachment_id_(in_progress_attachment_id), + in_progress_attachment_transferred_bytes_( + in_progress_attachment_transferred_bytes), + in_progress_attachment_total_bytes_(in_progress_attachment_total_bytes) {} + +TransferMetadata::~TransferMetadata() = default; + +TransferMetadata::TransferMetadata(const TransferMetadata&) = default; + +TransferMetadata& TransferMetadata::operator=(const TransferMetadata&) = + default; + +std::string TransferMetadata::ToString() const { + std::vector fmt; + + fmt.push_back(absl::StrFormat("status: %s", StatusToString(status_))); + fmt.push_back(absl::StrFormat("progress: %.2f", progress_)); + if (token_) { + fmt.push_back(absl::StrFormat("token: %s", *token_)); + } + fmt.push_back(absl::StrFormat("is_original: %d", is_original_)); + fmt.push_back(absl::StrFormat("is_final_status: %d", is_final_status_)); + fmt.push_back(absl::StrFormat("is_self_share: %d", is_self_share_)); + fmt.push_back(absl::StrFormat("transferred_bytes: %d", transferred_bytes_)); + fmt.push_back(absl::StrFormat("transfer_speed: %d", transfer_speed_)); + fmt.push_back(absl::StrFormat("estimated_time_remaining: %d", + estimated_time_remaining_)); + + return absl::StrCat("TransferMetadata<", absl::StrJoin(fmt, ", "), ">"); +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/transfer_metadata.h b/sharing/transfer_metadata.h new file mode 100644 index 00000000..ea625daf --- /dev/null +++ b/sharing/transfer_metadata.h @@ -0,0 +1,151 @@ +// Copyright 2022-2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_TRANSFER_METADATA_H_ +#define THIRD_PARTY_NEARBY_SHARING_TRANSFER_METADATA_H_ + +#include + +#include +#include + +namespace nearby { +namespace sharing { + +// Metadata about an ongoing transfer. Wraps transient data like status and +// progress. This is used to refresh the UI with error messages and show +// notifications so additions should be explicitly handled on the frontend. +class TransferMetadata { + public: + enum class Status { + kUnknown, + kConnecting, + kAwaitingLocalConfirmation, + kAwaitingRemoteAcceptance, + kAwaitingRemoteAcceptanceFailed, + kInProgress, + kComplete, + kFailed, + kRejected, + kCancelled, + kTimedOut, + kMediaUnavailable, + kMediaDownloading, + kNotEnoughSpace, + kUnsupportedAttachmentType, + kExternalProviderLaunched, + kDecodeAdvertisementFailed, + kMissingTransferUpdateCallback, + kMissingShareTarget, + kMissingEndpointId, + kMissingPayloads, + kPairedKeyVerificationFailed, + kInvalidIntroductionFrame, + kIncompletePayloads, + kFailedToCreateShareTarget, + kFailedToInitiateOutgoingConnection, + kFailedToReadOutgoingConnectionResponse, + kUnexpectedDisconnection, + kMaxValue = kUnexpectedDisconnection + }; + + static bool IsFinalStatus(Status status); + static std::string StatusToString(TransferMetadata::Status status); + + TransferMetadata( + Status status, float progress, std::optional token, + bool is_original, bool is_final_status, bool is_self_share, + uint64_t transferred_bytes, uint64_t transfer_speed, + uint64_t estimated_time_remaining, int total_attachments_count, + int transferred_attachments_count, + std::optional in_progress_attachment_id, + std::optional in_progress_attachment_transferred_bytes, + std::optional in_progress_attachment_total_bytes); + ~TransferMetadata(); + TransferMetadata(const TransferMetadata&); + TransferMetadata& operator=(const TransferMetadata&); + + Status status() const { return status_; } + + // Returns transfer progress as percentage. + float progress() const { return progress_; } + + // Represents the UKey2 token from Nearby Connection. absl::nullopt if no + // UKey2 comparison is needed for this transfer. + const std::optional& token() const { return token_; } + + // True if this |TransferMetadata| has not been seen. + bool is_original() const { return is_original_; } + + // True if this |TransferMetadata| is the last status for this transfer. + bool is_final_status() const { return is_final_status_; } + + // True if this |TransferMetadata| is for self share. + bool is_self_share() const { return is_self_share_; } + + // Returns transferred attachment size in bytes. + uint64_t transferred_bytes() const { return transferred_bytes_; } + + // Returns transfer speed in bytes per second. + uint64_t transfer_speed() const { return transfer_speed_; } + + // Returns estimated time remaining in seconds. + uint64_t estimated_time_remaining() const { + return estimated_time_remaining_; + } + + // Dumps this |TransferMetadata| to a summary string for logging purposes. + std::string ToString() const; + + // Total attachments count in this sharing. + int total_attachments_count() const { return total_attachments_count_; } + + // Completed attachment transfers in this sharing. + int transferred_attachments_count() const { + return transferred_attachments_count_; + } + + std::optional in_progress_attachment_id() const { + return in_progress_attachment_id_; + } + + std::optional in_progress_attachment_transferred_bytes() const { + return in_progress_attachment_transferred_bytes_; + } + + std::optional in_progress_attachment_total_bytes() const { + return in_progress_attachment_total_bytes_; + } + + private: + Status status_; + float progress_; + std::optional token_; + bool is_original_; + bool is_final_status_; + bool is_self_share_; + uint64_t transferred_bytes_; + uint64_t transfer_speed_; + uint64_t estimated_time_remaining_; + int total_attachments_count_; + int transferred_attachments_count_; + std::optional in_progress_attachment_id_; + std::optional in_progress_attachment_transferred_bytes_; + std::optional in_progress_attachment_total_bytes_; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_TRANSFER_METADATA_H_ diff --git a/sharing/transfer_metadata_builder.cc b/sharing/transfer_metadata_builder.cc new file mode 100644 index 00000000..3f849a2c --- /dev/null +++ b/sharing/transfer_metadata_builder.cc @@ -0,0 +1,150 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/transfer_metadata_builder.h" + +#include +#include +#include +#include + +#include "sharing/transfer_metadata.h" + +namespace nearby { +namespace sharing { +// static +TransferMetadataBuilder TransferMetadataBuilder::Clone( + const TransferMetadata& metadata) { + TransferMetadataBuilder builder; + builder.is_original_ = metadata.is_original(); + builder.progress_ = metadata.progress(); + builder.status_ = metadata.status(); + builder.token_ = metadata.token(); + builder.is_self_share_ = metadata.is_self_share(); + builder.transferred_bytes_ = metadata.transferred_bytes(); + builder.transfer_speed_ = metadata.transfer_speed(); + builder.estimated_time_remaining_ = metadata.estimated_time_remaining(); + builder.total_attachments_count_ = metadata.total_attachments_count(); + builder.transferred_attachments_count_ = + metadata.transferred_attachments_count(); + builder.in_progress_attachment_id_ = metadata.in_progress_attachment_id(); + builder.in_progress_attachment_transferred_bytes_ = + metadata.in_progress_attachment_transferred_bytes(); + return builder; +} + +TransferMetadataBuilder::TransferMetadataBuilder() = default; + +TransferMetadataBuilder::TransferMetadataBuilder(TransferMetadataBuilder&&) = + default; + +TransferMetadataBuilder& TransferMetadataBuilder::operator=( + TransferMetadataBuilder&&) = default; + +TransferMetadataBuilder::~TransferMetadataBuilder() = default; + +TransferMetadataBuilder& TransferMetadataBuilder::set_is_original( + bool is_original) { + is_original_ = is_original; + return *this; +} + +TransferMetadataBuilder& TransferMetadataBuilder::set_progress( + double progress) { + progress_ = progress; + return *this; +} + +TransferMetadataBuilder& TransferMetadataBuilder::set_status( + TransferMetadata::Status status) { + status_ = status; + return *this; +} + +TransferMetadataBuilder& TransferMetadataBuilder::set_token( + std::optional token) { + token_ = std::move(token); + return *this; +} + +TransferMetadataBuilder& TransferMetadataBuilder::set_is_self_share( + bool is_self_share) { + is_self_share_ = is_self_share; + return *this; +} + +TransferMetadataBuilder& TransferMetadataBuilder::set_transferred_bytes( + uint64_t bytes) { + transferred_bytes_ = bytes; + return *this; +} + +TransferMetadataBuilder& TransferMetadataBuilder::set_transfer_speed( + uint64_t speed) { + transfer_speed_ = speed; + return *this; +} + +TransferMetadataBuilder& TransferMetadataBuilder::set_estimated_time_remaining( + uint64_t estimated_time_remaining) { + estimated_time_remaining_ = estimated_time_remaining; + return *this; +} + +TransferMetadataBuilder& TransferMetadataBuilder::set_total_attachments_count( + int total_attachments_count) { + total_attachments_count_ = total_attachments_count; + return *this; +} + +TransferMetadataBuilder& +TransferMetadataBuilder::set_transferred_attachments_count( + int transferred_attachments_count) { + transferred_attachments_count_ = transferred_attachments_count; + return *this; +} + +TransferMetadataBuilder& TransferMetadataBuilder::set_in_progress_attachment_id( + std::optional in_progress_attachment_id) { + in_progress_attachment_id_ = in_progress_attachment_id; + return *this; +} + +TransferMetadataBuilder& +TransferMetadataBuilder::set_in_progress_attachment_transferred_bytes( + std::optional in_progress_attachment_transferred_bytes) { + in_progress_attachment_transferred_bytes_ = + in_progress_attachment_transferred_bytes; + return *this; +} + +TransferMetadataBuilder& +TransferMetadataBuilder::set_in_progress_attachment_total_bytes( + std::optional in_progress_attachment_total_bytes) { + in_progress_attachment_total_bytes_ = in_progress_attachment_total_bytes; + return *this; +} + +TransferMetadata TransferMetadataBuilder::build() const { + return TransferMetadata( + status_, progress_, token_, is_original_, + TransferMetadata::IsFinalStatus(status_), is_self_share_, + transferred_bytes_, transfer_speed_, estimated_time_remaining_, + total_attachments_count_, transferred_attachments_count_, + in_progress_attachment_id_, in_progress_attachment_transferred_bytes_, + in_progress_attachment_total_bytes_); +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/transfer_metadata_builder.h b/sharing/transfer_metadata_builder.h new file mode 100644 index 00000000..208a8cca --- /dev/null +++ b/sharing/transfer_metadata_builder.h @@ -0,0 +1,91 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_TRANSFER_METADATA_BUILDER_H_ +#define THIRD_PARTY_NEARBY_SHARING_TRANSFER_METADATA_BUILDER_H_ + +#include + +#include +#include + +#include "sharing/transfer_metadata.h" + +namespace nearby { +namespace sharing { + +class TransferMetadataBuilder { + public: + static TransferMetadataBuilder Clone(const TransferMetadata& metadata); + + TransferMetadataBuilder(); + TransferMetadataBuilder(TransferMetadataBuilder&&); + TransferMetadataBuilder& operator=(TransferMetadataBuilder&&); + ~TransferMetadataBuilder(); + + TransferMetadataBuilder& set_is_original(bool is_original); + + TransferMetadataBuilder& set_progress(double progress); + + TransferMetadataBuilder& set_status(TransferMetadata::Status status); + + TransferMetadataBuilder& set_token(std::optional token); + + TransferMetadataBuilder& set_is_self_share(bool is_self_share); + + TransferMetadataBuilder& set_transferred_bytes(uint64_t transferred_bytes); + + TransferMetadataBuilder& set_transfer_speed(uint64_t transfer_speed); + + TransferMetadataBuilder& set_estimated_time_remaining( + uint64_t estimated_time_remaining); + + TransferMetadataBuilder& set_total_attachments_count( + int total_attachments_count); + + TransferMetadataBuilder& set_transferred_attachments_count( + int transferred_attachments_count); + + TransferMetadataBuilder& set_in_progress_attachment_id( + std::optional in_progress_attachment_id); + + TransferMetadataBuilder& set_in_progress_attachment_transferred_bytes( + std::optional in_progress_attachment_transferred_bytes); + + TransferMetadataBuilder& set_in_progress_attachment_total_bytes( + std::optional in_progress_attachment_total_bytes); + + TransferMetadata build() const; + + private: + bool is_original_ = false; + double progress_ = 0; + TransferMetadata::Status status_ = TransferMetadata::Status::kInProgress; + std::optional token_; + bool is_self_share_ = false; + uint64_t transferred_bytes_ = 0; + uint64_t transfer_speed_ = 0; + uint64_t estimated_time_remaining_ = 0; + int total_attachments_count_ = 0; + int transferred_attachments_count_ = 0; + std::optional in_progress_attachment_id_ = std::nullopt; + std::optional in_progress_attachment_transferred_bytes_ = + std::nullopt; + std::optional in_progress_attachment_total_bytes_ = std::nullopt; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_TRANSFER_METADATA_BUILDER_H_ diff --git a/sharing/transfer_metadata_test.cc b/sharing/transfer_metadata_test.cc new file mode 100644 index 00000000..ef2d7493 --- /dev/null +++ b/sharing/transfer_metadata_test.cc @@ -0,0 +1,89 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/transfer_metadata.h" + +#include +#include +#include + +#include "gtest/gtest.h" + +namespace nearby { +namespace sharing { +namespace { + +struct TransferMetadataToStringTestData { + TransferMetadata transfer_metadata; + std::string expected_string_result; +}; + +std::vector GetTestData() { + static std::vector* + kTransferMetadataToStringTestData = + new std::vector({ + {TransferMetadata( + TransferMetadata::Status::kConnecting, + /*progress=*/12.321f, /*token=*/std::nullopt, + /*is_original=*/true, /*is_final_status=*/false, + /*is_self_share=*/false, + /*transferred_bytes=*/123456789, + /*transfer_speed=*/300000, + /*estimated_time_remaining=*/123456, + /*total_attachments_count=*/1, + /*transferred_attachments_count=*/0, + /*in_progress_attachment_id=*/std::nullopt, + /*in_progress_attachment_transferred_bytes=*/std::nullopt, + /*in_progress_attachment_total_bytes=*/std::nullopt), + "TransferMetadata"}, + {TransferMetadata( + TransferMetadata::Status::kCancelled, + /*progress=*/77.795f, + std::optional{"test_token"}, + /*is_original=*/false, + /*is_final_status=*/true, /*is_self_share=*/true, + /*transferred_bytes=*/123456789, /*transfer_speed=*/0, + /*estimated_time_remaining=*/123456789, + /*total_attachments_count=*/1, + /*transferred_attachments_count=*/0, + /*in_progress_attachment_id=*/std::nullopt, + /*in_progress_attachment_transferred_bytes=*/std::nullopt, + /*in_progress_attachment_total_bytes=*/std::nullopt), + "TransferMetadata"}, + }); + + return *kTransferMetadataToStringTestData; +} + +using TransferMetadataToStringTest = + testing::TestWithParam; + +TEST_P(TransferMetadataToStringTest, ToStringResultMatches) { + EXPECT_EQ(GetParam().expected_string_result, + GetParam().transfer_metadata.ToString()); +} + +INSTANTIATE_TEST_CASE_P(TransferMetadataToStringTest, + TransferMetadataToStringTest, + testing::ValuesIn(GetTestData())); + +} // namespace +} // namespace sharing +} // namespace nearby diff --git a/sharing/transfer_update_callback.h b/sharing/transfer_update_callback.h new file mode 100644 index 00000000..88a929d3 --- /dev/null +++ b/sharing/transfer_update_callback.h @@ -0,0 +1,36 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_TRANSFER_UPDATE_CALLBACK_H_ +#define THIRD_PARTY_NEARBY_SHARING_TRANSFER_UPDATE_CALLBACK_H_ + +#include "sharing/share_target.h" +#include "sharing/transfer_metadata.h" + +namespace nearby { +namespace sharing { + +// Reports the transfer status for an ongoing transfer with a |share_target|. +class TransferUpdateCallback { + public: + virtual ~TransferUpdateCallback() = default; + + virtual void OnTransferUpdate(const ShareTarget& share_target, + const TransferMetadata& transfer_metadata) = 0; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_TRANSFER_UPDATE_CALLBACK_H_ diff --git a/sharing/wifi_credentials_attachment.cc b/sharing/wifi_credentials_attachment.cc new file mode 100644 index 00000000..101c0016 --- /dev/null +++ b/sharing/wifi_credentials_attachment.cc @@ -0,0 +1,72 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "sharing/wifi_credentials_attachment.h" + +#include + +#include +#include + +#include "absl/strings/string_view.h" +#include "sharing/attachment.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/share_target.h" + +namespace nearby { +namespace sharing { + +WifiCredentialsAttachment::WifiCredentialsAttachment( + std::string ssid, SecurityType security_type, std::string password, + bool is_hidden, int32_t batch_id, SourceType source_type) + : Attachment(Attachment::Family::kWifiCredentials, ssid.size(), batch_id, + source_type), + ssid_(ssid), + security_type_(security_type), + password_(std::move(password)), + is_hidden_(is_hidden) {} + +WifiCredentialsAttachment::WifiCredentialsAttachment( + int64_t id, std::string ssid, SecurityType security_type, + std::string password, bool is_hidden, int32_t batch_id, + SourceType source_type) + : Attachment(id, Attachment::Family::kWifiCredentials, ssid.size(), + batch_id, source_type), + ssid_(ssid), + security_type_(security_type), + password_(std::move(password)), + is_hidden_(is_hidden) {} + +void WifiCredentialsAttachment::MoveToShareTarget(ShareTarget& share_target) { + share_target.wifi_credentials_attachments.push_back(std::move(*this)); +} + +absl::string_view WifiCredentialsAttachment::GetDescription() const { + return ssid_; +} + +ShareType WifiCredentialsAttachment::GetShareType() const { + return ShareType::kWifiCredentials; +} + +void WifiCredentialsAttachment::set_password(std::string password) { + password_ = std::move(password); +} + +void WifiCredentialsAttachment::set_is_hidden(bool is_hidden) { + is_hidden_ = is_hidden; +} + +} // namespace sharing +} // namespace nearby diff --git a/sharing/wifi_credentials_attachment.h b/sharing/wifi_credentials_attachment.h new file mode 100644 index 00000000..88b836f1 --- /dev/null +++ b/sharing/wifi_credentials_attachment.h @@ -0,0 +1,79 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef THIRD_PARTY_NEARBY_SHARING_WIFI_CREDENTIALS_ATTACHMENT_H_ +#define THIRD_PARTY_NEARBY_SHARING_WIFI_CREDENTIALS_ATTACHMENT_H_ + +#include + +#include + +#include "absl/strings/string_view.h" +#include "sharing/attachment.h" +#include "sharing/common/nearby_share_enums.h" +#include "sharing/proto/wire_format.pb.h" + +namespace nearby { +namespace sharing { + +// Represents a WiFi credentials attachment. +struct ShareTarget; + +class WifiCredentialsAttachment : public Attachment { + public: + using SecurityType = + nearby::sharing::service::proto::WifiCredentialsMetadata::SecurityType; + + WifiCredentialsAttachment(std::string ssid, SecurityType security_type, + std::string password = "", bool is_hidden = false, + int32_t batch_id = 0, + SourceType source_type = SourceType::kUnknown); + WifiCredentialsAttachment(int64_t id, std::string ssid, + SecurityType security_type, + std::string password = "", bool is_hidden = false, + int32_t batch_id = 0, + SourceType source_type = SourceType::kUnknown); + WifiCredentialsAttachment(const WifiCredentialsAttachment&) = default; + WifiCredentialsAttachment(WifiCredentialsAttachment&&) = default; + WifiCredentialsAttachment& operator=(const WifiCredentialsAttachment&) = + default; + WifiCredentialsAttachment& operator=(WifiCredentialsAttachment&&) = default; + ~WifiCredentialsAttachment() override = default; + + absl::string_view ssid() const { return ssid_; } + SecurityType security_type() const { return security_type_; } + absl::string_view password() const { return password_; } + bool is_hidden() const { return is_hidden_; } + SourceType source_type() const { return source_type_; } + + // Attachment: + void MoveToShareTarget(ShareTarget& share_target) override; + absl::string_view GetDescription() const override; + ShareType GetShareType() const override; + + void set_password(std::string password); + void set_is_hidden(bool is_hidden); + + private: + std::string ssid_; + SecurityType security_type_; + std::string password_; + bool is_hidden_; + SourceType source_type_; +}; + +} // namespace sharing +} // namespace nearby + +#endif // THIRD_PARTY_NEARBY_SHARING_WIFI_CREDENTIALS_ATTACHMENT_H_