diff --git a/sharing/linux/nearby_sharing_service_linux.cc b/sharing/linux/nearby_sharing_service_linux.cc new file mode 100644 index 00000000..42af1563 --- /dev/null +++ b/sharing/linux/nearby_sharing_service_linux.cc @@ -0,0 +1,1002 @@ +// Copyright 2025 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 "nearby_sharing_service_linux.h" +#include "sharing/proto/enums.pb.h" + +#include +#include +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "absl/time/time.h" +#include "connections/advertising_options.h" +#include "connections/connection_options.h" +#include "connections/discovery_options.h" +#include "connections/listeners.h" +#include "connections/medium_selector.h" +#include "connections/payload.h" +#include "connections/status.h" +#include "connections/strategy.h" +#include "internal/platform/byte_array.h" +#include "internal/platform/file.h" +#include "internal/platform/logging.h" +#include "sharing/certificates/common.h" + +namespace nearby::sharing::linux { +namespace { +constexpr char kServiceId[] = "NearbySharing"; +const connections::Strategy kStrategy = connections::Strategy::kP2pPointToPoint; +constexpr uint8_t kAdvertisementSaltSize = 2; +constexpr uint8_t kAdvertisementMetadataKeySize = 14; +constexpr uint8_t kAdvertisementVersion = 0; +constexpr uint8_t kVersionBitmask = 0b111; +constexpr uint8_t kDeviceTypeBitmask = 0b111; +constexpr uint8_t kVisibilityBitmask = 0b1; +constexpr uint8_t kTlvMinLength = 2; +constexpr uint8_t kVendorIdLength = 1; + +enum class TlvTypes : uint8_t { + kUnknown = 0, + kQrCode = 1, + kVendorId = 2, +}; + +uint8_t EncodeHeaderByte(bool has_device_name, ShareTargetType device_type) { + uint8_t version = static_cast((kAdvertisementVersion & kVersionBitmask) << 5); + uint8_t visibility = static_cast(((has_device_name ? 0 : 1) & kVisibilityBitmask) << 4); + uint8_t type = static_cast((static_cast(device_type) & kDeviceTypeBitmask) << 1); + return static_cast(version | visibility | type); +} + +bool ShouldIncludeDeviceName(const std::optional& device_name) { + return device_name.has_value() && !device_name->empty(); +} + +TransferMetadata::Status StatusFromPayloadStatus( + connections::PayloadProgressInfo::Status status) { + switch (status) { + case connections::PayloadProgressInfo::Status::kInProgress: + return TransferMetadata::Status::kInProgress; + case connections::PayloadProgressInfo::Status::kSuccess: + return TransferMetadata::Status::kComplete; + case connections::PayloadProgressInfo::Status::kFailure: + return TransferMetadata::Status::kFailed; + case connections::PayloadProgressInfo::Status::kCanceled: + return TransferMetadata::Status::kCancelled; + } + return TransferMetadata::Status::kUnknown; +} + +} // namespace + +NearbySharingServiceLinux::NearbySharingServiceLinux() + : device_info_(::nearby::api::ImplementationPlatform::CreateDeviceInfo()), + router_(std::make_unique()), + core_(std::make_unique(router_.get())) { + if (device_info_) { + auto name = device_info_->GetOsDeviceName(); + if (name.has_value()) { + device_name_override_ = *name; + } + } +} + +NearbySharingServiceLinux::NearbySharingServiceLinux( + std::string device_name_override) + : device_name_override_(std::move(device_name_override)), + device_info_(::nearby::api::ImplementationPlatform::CreateDeviceInfo()), + router_(std::make_unique()), + core_(std::make_unique(router_.get())) {} + +NearbySharingServiceLinux::~NearbySharingServiceLinux() = default; + +void NearbySharingServiceLinux::AddObserver(Observer* observer) { + if (!observer) { + return; + } + observers_.insert(observer); +} + +void NearbySharingServiceLinux::RemoveObserver(Observer* observer) { + observers_.erase(observer); +} + +void NearbySharingServiceLinux::Shutdown( + std::function + status_codes_callback) { + StopDiscovery(); + StopAdvertising(); + endpoint_to_target_.clear(); + target_id_to_endpoint_.clear(); + active_transfers_.clear(); + is_transferring_ = false; + std::move(status_codes_callback)(StatusCodes::kOk); +} + +void NearbySharingServiceLinux::RegisterSendSurface( + TransferUpdateCallback* transfer_callback, + ShareTargetDiscoveredCallback* discovery_callback, SendSurfaceState state, + Advertisement::BlockedVendorId blocked_vendor_id, + bool disable_wifi_hotspot, + std::function + status_codes_callback) { + static_cast(blocked_vendor_id); + if (!transfer_callback) { + std::move(status_codes_callback)(StatusCodes::kInvalidArgument); + return; + } + + send_surfaces_[transfer_callback] = SendSurface{ + .discovery_callback = discovery_callback, + .state = state, + .disable_wifi_hotspot = disable_wifi_hotspot, + }; + + StartDiscoveryIfNeeded(); + std::move(status_codes_callback)(StatusCodes::kOk); +} + +void NearbySharingServiceLinux::UnregisterSendSurface( + TransferUpdateCallback* transfer_callback, + std::function + status_codes_callback) { + if (!transfer_callback) { + std::move(status_codes_callback)(StatusCodes::kInvalidArgument); + return; + } + + send_surfaces_.erase(transfer_callback); + if (send_surfaces_.empty()) { + StopDiscovery(); + } + + std::move(status_codes_callback)(StatusCodes::kOk); +} + +void NearbySharingServiceLinux::RegisterReceiveSurface( + TransferUpdateCallback* transfer_callback, ReceiveSurfaceState state, + Advertisement::BlockedVendorId vendor_id, + std::function + status_codes_callback) { + if (!transfer_callback) { + std::move(status_codes_callback)(StatusCodes::kInvalidArgument); + return; + } + + receive_surfaces_[transfer_callback] = ReceiveSurface{ + .state = state, + .vendor_id = vendor_id, + }; + + StartAdvertisingIfNeeded(); + std::move(status_codes_callback)(StatusCodes::kOk); +} + +void NearbySharingServiceLinux::UnregisterReceiveSurface( + TransferUpdateCallback* transfer_callback, + std::function + status_codes_callback) { + if (!transfer_callback) { + std::move(status_codes_callback)(StatusCodes::kInvalidArgument); + return; + } + + receive_surfaces_.erase(transfer_callback); + if (receive_surfaces_.empty()) { + StopAdvertising(); + } else { + StartAdvertisingIfNeeded(); + } + + std::move(status_codes_callback)(StatusCodes::kOk); +} + +void NearbySharingServiceLinux::ClearForegroundReceiveSurfaces( + std::function + status_codes_callback) { + for (auto it = receive_surfaces_.begin(); it != receive_surfaces_.end();) { + if (it->second.state == ReceiveSurfaceState::kForeground) { + it = receive_surfaces_.erase(it); + } else { + ++it; + } + } + + if (receive_surfaces_.empty()) { + StopAdvertising(); + } else { + StartAdvertisingIfNeeded(); + } + + std::move(status_codes_callback)(StatusCodes::kOk); +} + +bool NearbySharingServiceLinux::IsTransferring() const { + return is_transferring_; +} + +bool NearbySharingServiceLinux::IsScanning() const { return is_scanning_; } + +bool NearbySharingServiceLinux::IsBluetoothPresent() const { + return bluetooth_adapter_.IsValid(); +} + +bool NearbySharingServiceLinux::IsBluetoothPowered() const { + return bluetooth_adapter_.IsValid() && bluetooth_adapter_.IsEnabled(); +} + +bool NearbySharingServiceLinux::IsExtendedAdvertisingSupported() const { + return false; +} + +bool NearbySharingServiceLinux::IsLanConnected() const { return false; } + +std::string NearbySharingServiceLinux::GetQrCodeUrl() const { return ""; } + +void NearbySharingServiceLinux::SendAttachments( + int64_t share_target_id, + std::unique_ptr + attachment_container, + std::function + status_codes_callback) { + auto endpoint_id = GetEndpointIdForTarget(share_target_id); + if (!endpoint_id.has_value() || !attachment_container || + !attachment_container->HasAttachments()) { + std::move(status_codes_callback)(StatusCodes::kInvalidArgument); + return; + } + + TransferUpdateCallback* callback = PickSendTransferCallback(); + if (!callback) { + std::move(status_codes_callback)(StatusCodes::kOutOfOrderApiCall); + return; + } + + TransferState transfer_state; + transfer_state.attachments = *attachment_container; + transfer_state.callback = callback; + transfer_state.is_incoming = false; + active_transfers_[*endpoint_id] = transfer_state; + + TransferMetadata metadata = + TransferMetadataBuilder().set_status(TransferMetadata::Status::kConnecting) + .set_progress(0) + .set_total_attachments_count( + attachment_container->GetAttachmentCount()) + .build(); + if (auto share_target = GetShareTarget(*endpoint_id)) { + NotifyTransferUpdate(*share_target, transfer_state, metadata); + } + + connections::ConnectionOptions options; + options.strategy = kStrategy; + options.allowed.SetAll(true); + + std::optional device_name = + device_name_override_.empty() + ? std::optional(std::nullopt) + : std::optional(device_name_override_); + if (!device_name.has_value() && device_info_) { + auto name = device_info_->GetOsDeviceName(); + if (name.has_value()) { + device_name = *name; + } + } + + ShareTargetType device_type = ShareTargetType::kUnknown; + if (device_info_) { + device_type = + static_cast(device_info_->GetDeviceType()); + } + + std::vector endpoint_info = + BuildAdvertisement(device_name, device_type, + static_cast( + Advertisement::BlockedVendorId::kNone)); + connections::ConnectionRequestInfo request_info; + request_info.endpoint_info = + ByteArray(std::string(endpoint_info.begin(), endpoint_info.end())); + request_info.listener.initiated_cb = + [this](const std::string& id, + const connections::ConnectionResponseInfo& info) { + HandleOutgoingConnectionInitiated(id, info); + }; + request_info.listener.accepted_cb = [this](const std::string& id) { + HandleConnectionAccepted(id, /*is_incoming=*/false); + }; + request_info.listener.rejected_cb = + [this](const std::string& id, connections::Status status) { + HandleConnectionRejected(id, status, /*is_incoming=*/false); + }; + request_info.listener.disconnected_cb = [this](const std::string& id) { + HandleConnectionDisconnected(id); + }; + + core_->RequestConnection(*endpoint_id, request_info, options, + [this, cb = std::move(status_codes_callback)]( + connections::Status status) mutable { + cb(StatusFromConnections(status)); + }); +} + +void NearbySharingServiceLinux::Accept( + int64_t share_target_id, + std::function + status_codes_callback) { + auto endpoint_id = GetEndpointIdForTarget(share_target_id); + if (!endpoint_id.has_value()) { + std::move(status_codes_callback)(StatusCodes::kInvalidArgument); + return; + } + + core_->AcceptConnection(*endpoint_id, MakePayloadListener(true), + [this, cb = std::move(status_codes_callback)]( + connections::Status status) mutable { + cb(StatusFromConnections(status)); + }); +} + +void NearbySharingServiceLinux::Reject( + int64_t share_target_id, + std::function + status_codes_callback) { + auto endpoint_id = GetEndpointIdForTarget(share_target_id); + if (!endpoint_id.has_value()) { + std::move(status_codes_callback)(StatusCodes::kInvalidArgument); + return; + } + + core_->RejectConnection(*endpoint_id, + [this, cb = std::move(status_codes_callback)]( + connections::Status status) mutable { + cb(StatusFromConnections(status)); + }); +} + +void NearbySharingServiceLinux::Cancel( + int64_t share_target_id, + std::function + status_codes_callback) { + auto endpoint_id = GetEndpointIdForTarget(share_target_id); + if (!endpoint_id.has_value()) { + std::move(status_codes_callback)(StatusCodes::kInvalidArgument); + return; + } + + core_->DisconnectFromEndpoint( + *endpoint_id, + [this, cb = std::move(status_codes_callback)](connections::Status status) mutable { + cb(StatusFromConnections(status)); + }); +} + +void NearbySharingServiceLinux::SetVisibility( + proto::DeviceVisibility visibility, absl::Duration expiration, + absl::AnyInvocable callback) { + static_cast(visibility); + static_cast(expiration); + std::move(callback)(StatusCodes::kOk); +} + +std::string NearbySharingServiceLinux::Dump() const { + std::stringstream ss; + ss << "NearbySharingServiceLinux"; + ss << " advertising=" << (is_advertising_ ? "true" : "false"); + ss << " scanning=" << (is_scanning_ ? "true" : "false"); + ss << " transfers=" << active_transfers_.size(); + ss << " targets=" << endpoint_to_target_.size(); + return ss.str(); +} + +void NearbySharingServiceLinux::UpdateFilePathsInProgress( + bool update_file_paths) {} + +NearbyShareSettings* NearbySharingServiceLinux::GetSettings() { + return nullptr; +} + +NearbyShareLocalDeviceDataManager* +NearbySharingServiceLinux::GetLocalDeviceDataManager() { + return nullptr; +} + +NearbyShareContactManager* NearbySharingServiceLinux::GetContactManager() { + return nullptr; +} + +NearbyShareCertificateManager* +NearbySharingServiceLinux::GetCertificateManager() { + return nullptr; +} + +AccountManager* NearbySharingServiceLinux::GetAccountManager() { + return nullptr; +} + +Clock& NearbySharingServiceLinux::GetClock() { return clock_; } + +void NearbySharingServiceLinux::SetAlternateServiceUuidForDiscovery( + uint16_t alternate_service_uuid) { + alternate_service_uuid_ = alternate_service_uuid; + if (is_scanning_) { + StopDiscovery(); + StartDiscoveryIfNeeded(); + } +} + +void NearbySharingServiceLinux::StartAdvertisingIfNeeded() { + if (receive_surfaces_.empty()) { + StopAdvertising(); + return; + } + + bool has_foreground = false; + uint8_t vendor_id = 0; + for (const auto& [callback, surface] : receive_surfaces_) { + if (surface.state == ReceiveSurfaceState::kForeground) { + has_foreground = true; + vendor_id = static_cast(surface.vendor_id); + break; + } + } + + std::optional device_name = std::nullopt; + if (has_foreground) { + if (!device_name_override_.empty()) { + device_name = device_name_override_; + } else if (device_info_) { + auto name = device_info_->GetOsDeviceName(); + if (name.has_value()) { + device_name = *name; + } + } + } + + ShareTargetType device_type = ShareTargetType::kUnknown; + if (device_info_) { + device_type = + static_cast(device_info_->GetDeviceType()); + } + + if (is_advertising_ && has_foreground == last_advertise_with_name_ && + vendor_id == last_advertise_vendor_id_) { + return; + } + + if (is_advertising_) { + StopAdvertising(); + } + + std::vector endpoint_info = + BuildAdvertisement(device_name, device_type, vendor_id); + + connections::AdvertisingOptions options; + options.strategy = kStrategy; + options.allowed.SetAll(true); + options.use_stable_endpoint_id = has_foreground; + + connections::ConnectionRequestInfo request_info; + request_info.endpoint_info = + ByteArray(std::string(endpoint_info.begin(), endpoint_info.end())); + request_info.listener.initiated_cb = + [this](const std::string& id, + const connections::ConnectionResponseInfo& info) { + HandleIncomingConnectionInitiated(id, info); + }; + request_info.listener.accepted_cb = [this](const std::string& id) { + HandleConnectionAccepted(id, /*is_incoming=*/true); + }; + request_info.listener.rejected_cb = + [this](const std::string& id, connections::Status status) { + HandleConnectionRejected(id, status, /*is_incoming=*/true); + }; + request_info.listener.disconnected_cb = [this](const std::string& id) { + HandleConnectionDisconnected(id); + }; + + core_->StartAdvertising( + kServiceId, options, std::move(request_info), + [this, has_foreground, vendor_id](connections::Status status) { + is_advertising_ = status.Ok(); + if (is_advertising_) { + last_advertise_with_name_ = has_foreground; + last_advertise_vendor_id_ = vendor_id; + } + }); +} + +void NearbySharingServiceLinux::StopAdvertising() { + if (!is_advertising_) { + return; + } + is_advertising_ = false; + core_->StopAdvertising([this](connections::Status status) { + static_cast(status); + }); +} + +void NearbySharingServiceLinux::StartDiscoveryIfNeeded() { + bool needs_scanning = false; + for (const auto& [callback, surface] : send_surfaces_) { + if (surface.state == SendSurfaceState::kForeground) { + needs_scanning = true; + break; + } + } + + if (!needs_scanning) { + StopDiscovery(); + return; + } + + if (is_scanning_) { + return; + } + + connections::DiscoveryOptions options; + options.strategy = kStrategy; + options.allowed.SetAll(true); + if (alternate_service_uuid_.has_value()) { + options.ble_options.alternate_uuid = *alternate_service_uuid_; + } + + connections::DiscoveryListener listener; + listener.endpoint_found_cb = + [this](const std::string& endpoint_id, const ByteArray& endpoint_info, + const std::string& service_id) { + static_cast(service_id); + std::string info_string = std::string(endpoint_info); + std::vector info_bytes(info_string.begin(), info_string.end()); + ParsedAdvertisement parsed; + if (auto parsed_opt = ParseAdvertisement(info_bytes)) { + parsed = *parsed_opt; + } + + ShareTarget target; + target.id = next_share_target_id_++; + if (parsed.device_name.has_value()) { + target.device_name = *parsed.device_name; + } else { + target.device_name = endpoint_id; + } + target.type = parsed.device_type; + target.is_incoming = false; + target.vendor_id = parsed.vendor_id; + + auto existing = endpoint_to_target_.find(endpoint_id); + if (existing == endpoint_to_target_.end()) { + endpoint_to_target_[endpoint_id] = target; + target_id_to_endpoint_[target.id] = endpoint_id; + NotifyShareTargetDiscovered(target); + } else { + target.id = existing->second.id; + endpoint_to_target_[endpoint_id] = target; + NotifyShareTargetUpdated(target); + } + }; + listener.endpoint_lost_cb = [this](const std::string& endpoint_id) { + auto it = endpoint_to_target_.find(endpoint_id); + if (it == endpoint_to_target_.end()) { + return; + } + ShareTarget target = it->second; + endpoint_to_target_.erase(it); + target_id_to_endpoint_.erase(target.id); + NotifyShareTargetLost(target); + }; + + core_->StartDiscovery( + kServiceId, options, std::move(listener), + [this](connections::Status status) { is_scanning_ = status.Ok(); }); +} + +void NearbySharingServiceLinux::StopDiscovery() { + if (!is_scanning_) { + return; + } + core_->StopDiscovery([this](connections::Status status) { + if (status.Ok()) { + is_scanning_ = false; + } + }); +} + +std::vector NearbySharingServiceLinux::BuildAdvertisement( + const std::optional& device_name, ShareTargetType device_type, + uint8_t vendor_id) const { + const bool has_device_name = ShouldIncludeDeviceName(device_name); + std::vector salt = GenerateRandomBytes(kAdvertisementSaltSize); + std::vector metadata_key = + GenerateRandomBytes(kAdvertisementMetadataKeySize); + + size_t size = 1 + salt.size() + metadata_key.size(); + if (has_device_name) { + size += 1 + device_name->size(); + } + if (vendor_id != 0) { + size += kTlvMinLength + kVendorIdLength; + } + + std::vector endpoint_info; + endpoint_info.reserve(size); + endpoint_info.push_back(EncodeHeaderByte(has_device_name, device_type)); + endpoint_info.insert(endpoint_info.end(), salt.begin(), salt.end()); + endpoint_info.insert(endpoint_info.end(), metadata_key.begin(), + metadata_key.end()); + + if (has_device_name) { + endpoint_info.push_back( + static_cast(device_name->size() & 0xff)); + endpoint_info.insert(endpoint_info.end(), device_name->begin(), + device_name->end()); + } + + if (vendor_id != 0) { + endpoint_info.push_back(static_cast(TlvTypes::kVendorId)); + endpoint_info.push_back(kVendorIdLength); + endpoint_info.push_back(vendor_id); + } + + return endpoint_info; +} + +std::optional +NearbySharingServiceLinux::ParseAdvertisement( + absl::Span endpoint_info) const { + ParsedAdvertisement parsed; + const size_t minimum_size = + 1 + kAdvertisementSaltSize + kAdvertisementMetadataKeySize; + if (endpoint_info.size() < minimum_size) { + return std::nullopt; + } + + size_t offset = 0; + uint8_t header = endpoint_info[offset++]; + bool has_device_name = ((header >> 4) & kVisibilityBitmask) == 0; + uint8_t type = (header >> 1) & kDeviceTypeBitmask; + if (type <= static_cast(ShareTargetType::kXR)) { + parsed.device_type = static_cast(type); + } else { + parsed.device_type = ShareTargetType::kUnknown; + } + + offset += kAdvertisementSaltSize + kAdvertisementMetadataKeySize; + if (has_device_name) { + if (offset >= endpoint_info.size()) { + return parsed; + } + uint8_t name_length = endpoint_info[offset++]; + if (name_length == 0 || offset + name_length > endpoint_info.size()) { + return parsed; + } + parsed.device_name = std::string( + reinterpret_cast(endpoint_info.data() + offset), + name_length); + offset += name_length; + } + + while (offset + kTlvMinLength <= endpoint_info.size()) { + uint8_t tlv_type = endpoint_info[offset++]; + uint8_t tlv_length = endpoint_info[offset++]; + if (offset + tlv_length > endpoint_info.size()) { + break; + } + if (tlv_type == static_cast(TlvTypes::kVendorId) && + tlv_length == kVendorIdLength) { + parsed.vendor_id = endpoint_info[offset]; + } + offset += tlv_length; + } + + return parsed; +} + +void NearbySharingServiceLinux::NotifyShareTargetDiscovered( + const ShareTarget& share_target) { + for (const auto& [transfer_callback, surface] : send_surfaces_) { + if (surface.state != SendSurfaceState::kForeground || + surface.discovery_callback == nullptr) { + continue; + } + surface.discovery_callback->OnShareTargetDiscovered(share_target); + } +} + +void NearbySharingServiceLinux::NotifyShareTargetUpdated( + const ShareTarget& share_target) { + for (const auto& [transfer_callback, surface] : send_surfaces_) { + if (surface.state != SendSurfaceState::kForeground || + surface.discovery_callback == nullptr) { + continue; + } + surface.discovery_callback->OnShareTargetUpdated(share_target); + } +} + +void NearbySharingServiceLinux::NotifyShareTargetLost( + const ShareTarget& share_target) { + for (const auto& [transfer_callback, surface] : send_surfaces_) { + if (surface.state != SendSurfaceState::kForeground || + surface.discovery_callback == nullptr) { + continue; + } + surface.discovery_callback->OnShareTargetLost(share_target); + } +} + +void NearbySharingServiceLinux::NotifyTransferUpdate( + const ShareTarget& share_target, const TransferState& transfer_state, + const TransferMetadata& metadata) { + if (!transfer_state.callback) { + return; + } + transfer_state.callback->OnTransferUpdate(share_target, + transfer_state.attachments, + metadata); +} + +TransferUpdateCallback* NearbySharingServiceLinux::PickSendTransferCallback() + const { + if (send_surfaces_.empty()) { + return nullptr; + } + return send_surfaces_.begin()->first; +} + +TransferUpdateCallback* NearbySharingServiceLinux::PickReceiveTransferCallback() + const { + if (receive_surfaces_.empty()) { + return nullptr; + } + return receive_surfaces_.begin()->first; +} + +std::optional NearbySharingServiceLinux::GetEndpointIdForTarget( + int64_t share_target_id) const { + auto it = target_id_to_endpoint_.find(share_target_id); + if (it == target_id_to_endpoint_.end()) { + return std::nullopt; + } + return it->second; +} + +std::optional NearbySharingServiceLinux::GetShareTarget( + absl::string_view endpoint_id) const { + auto it = endpoint_to_target_.find(std::string(endpoint_id)); + if (it == endpoint_to_target_.end()) { + return std::nullopt; + } + return it->second; +} + +void NearbySharingServiceLinux::HandleIncomingConnectionInitiated( + const std::string& endpoint_id, + const connections::ConnectionResponseInfo& info) { + static_cast(info); + std::vector info_bytes(info.remote_endpoint_info.begin(), + info.remote_endpoint_info.end()); + ParsedAdvertisement parsed; + if (auto parsed_opt = ParseAdvertisement(info_bytes)) { + parsed = *parsed_opt; + } + + ShareTarget target; + target.id = next_share_target_id_++; + target.device_name = parsed.device_name.value_or(endpoint_id); + target.type = parsed.device_type; + target.is_incoming = true; + target.vendor_id = parsed.vendor_id; + + auto existing = endpoint_to_target_.find(endpoint_id); + if (existing == endpoint_to_target_.end()) { + endpoint_to_target_[endpoint_id] = target; + target_id_to_endpoint_[target.id] = endpoint_id; + } else { + target.id = existing->second.id; + endpoint_to_target_[endpoint_id] = target; + } + + TransferState transfer_state; + transfer_state.attachments = AttachmentContainer(); + transfer_state.callback = PickReceiveTransferCallback(); + transfer_state.is_incoming = true; + active_transfers_[endpoint_id] = transfer_state; + + TransferMetadata metadata = TransferMetadataBuilder() + .set_status(TransferMetadata::Status::kAwaitingLocalConfirmation) + .set_progress(0) + .build(); + NotifyTransferUpdate(target, transfer_state, metadata); +} + +void NearbySharingServiceLinux::HandleOutgoingConnectionInitiated( + const std::string& endpoint_id, + const connections::ConnectionResponseInfo& info) { + static_cast(info); + core_->AcceptConnection(endpoint_id, MakePayloadListener(false), + [this, endpoint_id](connections::Status status) { + if (!status.Ok()) { + HandleConnectionRejected(endpoint_id, status, + /*is_incoming=*/false); + } + }); + + auto share_target = GetShareTarget(endpoint_id); + auto transfer_it = active_transfers_.find(endpoint_id); + if (share_target && transfer_it != active_transfers_.end()) { + TransferMetadata metadata = + TransferMetadataBuilder() + .set_status(TransferMetadata::Status::kAwaitingRemoteAcceptance) + .set_progress(0) + .build(); + NotifyTransferUpdate(*share_target, transfer_it->second, metadata); + } +} + +void NearbySharingServiceLinux::HandleConnectionAccepted( + const std::string& endpoint_id, bool is_incoming) { + auto transfer_it = active_transfers_.find(endpoint_id); + if (transfer_it == active_transfers_.end()) { + return; + } + + auto share_target = GetShareTarget(endpoint_id); + if (share_target) { + TransferMetadata metadata = TransferMetadataBuilder() + .set_status(TransferMetadata::Status::kInProgress) + .set_progress(0) + .set_total_attachments_count( + transfer_it->second.attachments.GetAttachmentCount()) + .build(); + NotifyTransferUpdate(*share_target, transfer_it->second, metadata); + } + + if (!is_incoming) { + const AttachmentContainer& attachments = transfer_it->second.attachments; + std::unique_ptr payload; + if (!attachments.GetTextAttachments().empty()) { + std::string text = + std::string(attachments.GetTextAttachments()[0].text_body()); + payload = std::make_unique(ByteArray(text)); + } else if (!attachments.GetFileAttachments().empty()) { + const auto& file_attachment = attachments.GetFileAttachments()[0]; + if (file_attachment.file_path().has_value()) { + std::string file_path = file_attachment.file_path()->ToString(); + nearby::InputFile input_file(file_path, file_attachment.size()); + payload = std::make_unique( + std::string(file_attachment.parent_folder()), + std::string(file_attachment.file_name()), std::move(input_file)); + } + } + + if (payload) { + std::vector endpoints; + endpoints.push_back(endpoint_id); + core_->SendPayload( + endpoints, std::move(*payload), + [this](connections::Status status) { + if (!status.Ok()) { + is_transferring_ = false; + } + }); + } + } + + is_transferring_ = true; +} + +void NearbySharingServiceLinux::HandleConnectionRejected( + const std::string& endpoint_id, connections::Status status, + bool is_incoming) { + static_cast(status); + static_cast(is_incoming); + auto transfer_it = active_transfers_.find(endpoint_id); + if (transfer_it == active_transfers_.end()) { + return; + } + auto share_target = GetShareTarget(endpoint_id); + if (share_target) { + TransferMetadata metadata = TransferMetadataBuilder() + .set_status(TransferMetadata::Status::kRejected) + .set_progress(0) + .build(); + NotifyTransferUpdate(*share_target, transfer_it->second, metadata); + } + active_transfers_.erase(transfer_it); + is_transferring_ = false; +} + +void NearbySharingServiceLinux::HandleConnectionDisconnected( + const std::string& endpoint_id) { + active_transfers_.erase(endpoint_id); + if (active_transfers_.empty()) { + is_transferring_ = false; + } +} + +connections::PayloadListener NearbySharingServiceLinux::MakePayloadListener( + bool is_incoming) { + static_cast(is_incoming); + connections::PayloadListener listener; + listener.payload_cb = + [this, is_incoming](absl::string_view endpoint_id, + connections::Payload payload) { + auto transfer_it = active_transfers_.find(std::string(endpoint_id)); + if (transfer_it == active_transfers_.end()) { + return; + } + auto share_target = GetShareTarget(endpoint_id); + if (!share_target) { + return; + } + + TransferMetadata metadata = TransferMetadataBuilder() + .set_status(TransferMetadata::Status::kInProgress) + .set_progress(0) + .build(); + NotifyTransferUpdate(*share_target, transfer_it->second, metadata); + }; + + listener.payload_progress_cb = + [this, is_incoming](absl::string_view endpoint_id, + const connections::PayloadProgressInfo& info) { + auto transfer_it = active_transfers_.find(std::string(endpoint_id)); + if (transfer_it == active_transfers_.end()) { + return; + } + auto share_target = GetShareTarget(endpoint_id); + if (!share_target) { + return; + } + + float progress = 0.0f; + if (info.total_bytes > 0) { + progress = static_cast(info.bytes_transferred) / + static_cast(info.total_bytes); + } + + TransferMetadata metadata = + TransferMetadataBuilder() + .set_status(StatusFromPayloadStatus(info.status)) + .set_progress(progress) + .set_transferred_bytes(info.bytes_transferred) + .build(); + + NotifyTransferUpdate(*share_target, transfer_it->second, metadata); + + if (TransferMetadata::IsFinalStatus(metadata.status())) { + active_transfers_.erase(transfer_it); + if (active_transfers_.empty()) { + is_transferring_ = false; + } + } + }; + return listener; +} + +NearbySharingService::StatusCodes NearbySharingServiceLinux::StatusFromConnections( + connections::Status status) const { + if (status.Ok()) { + return StatusCodes::kOk; + } + if (status.value == connections::Status::kOutOfOrderApiCall) { + return StatusCodes::kOutOfOrderApiCall; + } + return StatusCodes::kError; +} + +} // namespace nearby::sharing::linux diff --git a/sharing/linux/nearby_sharing_service_linux.h b/sharing/linux/nearby_sharing_service_linux.h new file mode 100644 index 00000000..bbfc0beb --- /dev/null +++ b/sharing/linux/nearby_sharing_service_linux.h @@ -0,0 +1,229 @@ +// Copyright 2025 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_LINUX_NEARBY_SHARING_SERVICE_LINUX_H_ +#define THIRD_PARTY_NEARBY_SHARING_LINUX_NEARBY_SHARING_SERVICE_LINUX_H_ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "absl/functional/any_invocable.h" +#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/payload.h" +#include "connections/status.h" +#include "connections/strategy.h" +#include "internal/platform/bluetooth_adapter.h" +#include "internal/platform/clock_impl.h" +#include "internal/platform/implementation/platform.h" +#include "sharing/attachment_container.h" +#include "sharing/nearby_sharing_service.h" +#include "sharing/share_target.h" +#include "sharing/share_target_discovered_callback.h" +#include "sharing/transfer_metadata.h" +#include "sharing/transfer_metadata_builder.h" + +namespace nearby::sharing::linux { + +class NearbySharingServiceLinux : public NearbySharingService { + public: + using StatusCodes = NearbySharingService::StatusCodes; + + NearbySharingServiceLinux(); + explicit NearbySharingServiceLinux(std::string device_name_override); + ~NearbySharingServiceLinux() override; + + void AddObserver(Observer* observer) override; + void RemoveObserver(Observer* observer) override; + + void Shutdown( + std::function status_codes_callback) override; + + void RegisterSendSurface( + TransferUpdateCallback* transfer_callback, + ShareTargetDiscoveredCallback* discovery_callback, SendSurfaceState state, + Advertisement::BlockedVendorId blocked_vendor_id, + bool disable_wifi_hotspot, + std::function status_codes_callback) override; + + void UnregisterSendSurface( + TransferUpdateCallback* transfer_callback, + std::function status_codes_callback) override; + + void RegisterReceiveSurface( + TransferUpdateCallback* transfer_callback, ReceiveSurfaceState state, + Advertisement::BlockedVendorId vendor_id, + 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 IsTransferring() const override; + bool IsScanning() const override; + bool IsBluetoothPresent() const override; + bool IsBluetoothPowered() const override; + bool IsExtendedAdvertisingSupported() const override; + bool IsLanConnected() const override; + std::string GetQrCodeUrl() const override; + + void SendAttachments( + int64_t share_target_id, + std::unique_ptr + attachment_container, + std::function status_codes_callback) override; + + void Accept(int64_t share_target_id, + std::function + status_codes_callback) override; + + void Reject(int64_t share_target_id, + std::function + status_codes_callback) override; + + void Cancel(int64_t share_target_id, + std::function + status_codes_callback) override; + + void SetVisibility(proto::DeviceVisibility visibility, + absl::Duration expiration, + absl::AnyInvocable + callback) override; + + std::string Dump() const override; + void UpdateFilePathsInProgress(bool update_file_paths) override; + + NearbyShareSettings* GetSettings() override; + NearbyShareLocalDeviceDataManager* GetLocalDeviceDataManager() override; + NearbyShareContactManager* GetContactManager() override; + NearbyShareCertificateManager* GetCertificateManager() override; + AccountManager* GetAccountManager() override; + Clock& GetClock() override; + void SetAlternateServiceUuidForDiscovery( + uint16_t alternate_service_uuid) override; + + private: + struct SendSurface { + ShareTargetDiscoveredCallback* discovery_callback = nullptr; + SendSurfaceState state = SendSurfaceState::kUnknown; + bool disable_wifi_hotspot = false; + }; + + struct ReceiveSurface { + ReceiveSurfaceState state = ReceiveSurfaceState::kUnknown; + Advertisement::BlockedVendorId vendor_id = + Advertisement::BlockedVendorId::kNone; + }; + + struct TransferState { + nearby::sharing::AttachmentContainer attachments; + TransferUpdateCallback* callback = nullptr; + bool is_incoming = false; + }; + + struct ParsedAdvertisement { + ShareTargetType device_type = ShareTargetType::kUnknown; + std::optional device_name; + uint8_t vendor_id = 0; + }; + + void StartAdvertisingIfNeeded(); + void StopAdvertising(); + void StartDiscoveryIfNeeded(); + void StopDiscovery(); + + std::vector BuildAdvertisement( + const std::optional& device_name, + ShareTargetType device_type, uint8_t vendor_id) const; + + std::optional ParseAdvertisement( + absl::Span endpoint_info) const; + + void NotifyShareTargetDiscovered(const ShareTarget& share_target); + void NotifyShareTargetUpdated(const ShareTarget& share_target); + void NotifyShareTargetLost(const ShareTarget& share_target); + void NotifyTransferUpdate(const ShareTarget& share_target, + const TransferState& transfer_state, + const TransferMetadata& metadata); + + TransferUpdateCallback* PickSendTransferCallback() const; + TransferUpdateCallback* PickReceiveTransferCallback() const; + + std::optional GetEndpointIdForTarget( + int64_t share_target_id) const; + + std::optional GetShareTarget( + absl::string_view endpoint_id) const; + + void HandleIncomingConnectionInitiated( + const std::string& endpoint_id, + const connections::ConnectionResponseInfo& info); + + void HandleOutgoingConnectionInitiated( + const std::string& endpoint_id, + const connections::ConnectionResponseInfo& info); + + void HandleConnectionAccepted(const std::string& endpoint_id, + bool is_incoming); + void HandleConnectionRejected(const std::string& endpoint_id, + connections::Status status, bool is_incoming); + void HandleConnectionDisconnected(const std::string& endpoint_id); + + connections::PayloadListener MakePayloadListener(bool is_incoming); + + StatusCodes StatusFromConnections(connections::Status status) const; + + std::string device_name_override_; + std::unique_ptr<::nearby::api::DeviceInfo> device_info_; + BluetoothAdapter bluetooth_adapter_; + ClockImpl clock_; + + std::unique_ptr router_; + std::unique_ptr core_; + + std::unordered_set observers_; + std::unordered_map send_surfaces_; + std::unordered_map receive_surfaces_; + + std::unordered_map endpoint_to_target_; + std::unordered_map target_id_to_endpoint_; + std::unordered_map active_transfers_; + + std::optional alternate_service_uuid_; + bool is_scanning_ = false; + bool is_advertising_ = false; + bool is_transferring_ = false; + int64_t next_share_target_id_ = 1; + bool last_advertise_with_name_ = false; + uint8_t last_advertise_vendor_id_ = 0; +}; + +} // namespace nearby::sharing::linux + +#endif // THIRD_PARTY_NEARBY_SHARING_LINUX_NEARBY_SHARING_SERVICE_LINUX_H_