From 9eb1f1d4112ea0accfb5258e3bde62b0a9a50401 Mon Sep 17 00:00:00 2001 From: Lasan Mahaliyana Date: Sat, 7 Mar 2026 01:16:18 +0530 Subject: [PATCH] refractored facade to switch from using nearby connections to linux nearby sharing service. --- sharing/linux/BUILD | 37 +- sharing/linux/README.md | 14 + .../install_nearby_connections_service.sh | 159 +- .../linux/install_nearby_sharing_service.sh | 157 ++ sharing/linux/nearby_connections_qt_facade.cc | 34 +- .../linux/nearby_connections_service_linux.cc | 2 +- sharing/linux/nearby_sharing_api.cc | 444 ++++++ sharing/linux/nearby_sharing_api.h | 121 ++ sharing/linux/nearby_sharing_app.cc | 169 +- sharing/linux/qml_tray_app/CMakeLists.txt | 10 +- sharing/linux/qml_tray_app/FileShareTray.qml | 4 +- sharing/linux/qml_tray_app/README.md | 38 +- .../cmake/BuildNearbySoIfMissing.cmake | 6 +- .../qml_tray_app/components/AnimatedBlob.qml | 51 +- .../qml_tray_app/components/DeviceCard.qml | 20 +- .../qml_tray_app/components/SettingsPanel.qml | 464 +----- .../linux/qml_tray_app/components/SideBar.qml | 6 - .../qml_tray_app/components/TransferCard.qml | 41 +- .../file_share_tray_controller.cc | 1374 +++++------------ .../qml_tray_app/file_share_tray_controller.h | 153 +- .../qml_tray_app/file_share_tray_main.cpp | 3 - 21 files changed, 1362 insertions(+), 1945 deletions(-) create mode 100755 sharing/linux/install_nearby_sharing_service.sh create mode 100644 sharing/linux/nearby_sharing_api.cc create mode 100644 sharing/linux/nearby_sharing_api.h diff --git a/sharing/linux/BUILD b/sharing/linux/BUILD index f00141cd..33c011df 100644 --- a/sharing/linux/BUILD +++ b/sharing/linux/BUILD @@ -72,6 +72,39 @@ cc_binary( ], ) +cc_library( + name = "nearby_sharing_api", + srcs = ["nearby_sharing_api.cc"], + hdrs = ["nearby_sharing_api.h"], + alwayslink = True, + visibility = ["//visibility:public"], + deps = [ + ":nearby_sharing_service_linux", + "//internal/base:file_path", + "//sharing:attachments", + ], +) + +cc_binary( + name = "nearby_sharing_api_shared", + linkshared = True, + srcs = [ + "nearby_sharing_api.cc", + "nearby_sharing_api.h", + ], + visibility = ["//visibility:public"], + linkopts = [ + "-Wl,--exclude-libs,ALL", + ], + deps = [ + ":nearby_sharing_service_linux", + "//internal/base:file_path", + "//internal/platform/implementation:platform", + "//internal/platform/implementation/linux", + "//sharing:attachments", + ], +) + cc_binary( name = "app", @@ -135,7 +168,7 @@ cc_library( "//sharing/local_device_data:nearby_share_local_device_data_manager.h", ], visibility = ["//visibility:public"], - linkopts=[ + linkopts = [ "-lssl", "-lcrypto" ], @@ -163,11 +196,9 @@ cc_binary( deps = [ ":nearby_sharing_service_linux", "//internal/platform:base", - "//internal/platform/implementation:account_manager", "//sharing:attachments", "//sharing:types", "@com_google_absl//absl/strings", - "@com_google_absl//absl/time", ], ) diff --git a/sharing/linux/README.md b/sharing/linux/README.md index e5a501b8..9911653f 100644 --- a/sharing/linux/README.md +++ b/sharing/linux/README.md @@ -28,6 +28,20 @@ The main service class that provides nearby sharing functionality: - **Text Transfer**: Send and receive text messages - **Connection Management**: Handle connection lifecycle (accept, reject, cancel) +### NearbySharingApi + +For external/Linux app consumers (for example Qt/CMake apps), use +`nearby::sharing::linux::NearbySharingApi` from: + +- `sharing/linux/nearby_sharing_api.h` +- `libnearby_sharing_api_shared.so` + +Install artifacts with: + +```bash +./sharing/linux/install_nearby_sharing_service.sh +``` + ### Key Concepts #### 1. Send Surface diff --git a/sharing/linux/install_nearby_connections_service.sh b/sharing/linux/install_nearby_connections_service.sh index b7c72dbc..e2ab5094 100755 --- a/sharing/linux/install_nearby_connections_service.sh +++ b/sharing/linux/install_nearby_connections_service.sh @@ -2,158 +2,17 @@ set -euo pipefail -TARGET="//sharing/linux:nearby_connections_service_linux_shared" -HEADER_SRC="sharing/linux/nearby_connections_qt_facade.h" -BAZEL_CMD="${BAZEL:-bazel}" -PREFIX="/usr/local" -LIBDIR="" -INCLUDEDIR="" -SKIP_BUILD=0 -NEEDS_ELEVATION=0 -INSTALL_PREFIX=() +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DELEGATE_SCRIPT="${SCRIPT_DIR}/install_nearby_sharing_service.sh" -usage() { - cat </lib) - --includedir DIR Include root directory (default: /include) - --bazel CMD Bazel command (default: bazel or env BAZEL) - --skip-build Skip bazel build step and only install from bazel-bin - -h, --help Show this help - -Examples: - $0 - sudo $0 - sudo $0 --prefix /usr - sudo $0 --bazel /usr/bin/bazel -USAGE -} - -run_bazel() { - # If invoked through sudo, run Bazel as the original user so Bazelisk reuses - # that user's cache and does not re-download Bazel as root. - if [[ "$(id -u)" -eq 0 && -n "${SUDO_USER:-}" ]]; then - local caller_home - caller_home="$(getent passwd "$SUDO_USER" | cut -d: -f6)" - if [[ -z "$caller_home" ]]; then - echo "Failed to resolve home directory for SUDO_USER=$SUDO_USER" >&2 - exit 1 - fi - sudo -u "$SUDO_USER" -H env \ - HOME="$caller_home" \ - BAZELISK_HOME="${BAZELISK_HOME:-$caller_home/.cache/bazelisk}" \ - "$BAZEL_CMD" "$@" - else - "$BAZEL_CMD" "$@" - fi -} - -nearest_existing_parent() { - local p="$1" - while [[ ! -e "$p" ]]; do - p="$(dirname "$p")" - done - printf '%s\n' "$p" -} - -while [[ $# -gt 0 ]]; do - case "$1" in - --prefix) - PREFIX="$2" - shift 2 - ;; - --libdir) - LIBDIR="$2" - shift 2 - ;; - --includedir) - INCLUDEDIR="$2" - shift 2 - ;; - --bazel) - BAZEL_CMD="$2" - shift 2 - ;; - --skip-build) - SKIP_BUILD=1 - shift - ;; - -h|--help) - usage - exit 0 - ;; - *) - echo "Unknown argument: $1" >&2 - usage - exit 1 - ;; - esac -done - -if [[ -z "$LIBDIR" ]]; then - LIBDIR="${PREFIX}/lib" -fi - -if [[ -z "$INCLUDEDIR" ]]; then - INCLUDEDIR="${PREFIX}/include" -fi - -LIB_PARENT="$(nearest_existing_parent "$LIBDIR")" -INCLUDE_PARENT="$(nearest_existing_parent "${INCLUDEDIR}/sharing/linux")" - -if [[ ! -w "$LIB_PARENT" || ! -w "$INCLUDE_PARENT" ]]; then - NEEDS_ELEVATION=1 -fi - -if [[ "$NEEDS_ELEVATION" -eq 1 && "$(id -u)" -ne 0 ]]; then - if ! command -v sudo >/dev/null 2>&1; then - echo "Install requires elevated privileges, but sudo is not available." >&2 - exit 1 - fi - INSTALL_PREFIX=(sudo) -fi - -if [[ ! -f "$HEADER_SRC" ]]; then - echo "Header not found: $HEADER_SRC" >&2 - echo "Run this script from the workspace root." >&2 +if [[ ! -x "${DELEGATE_SCRIPT}" ]]; then + echo "Missing installer: ${DELEGATE_SCRIPT}" >&2 exit 1 fi -if [[ "$SKIP_BUILD" -eq 0 ]]; then - echo "[1/4] Building $TARGET" - run_bazel build "$TARGET" -else - echo "[1/4] Skipping build (--skip-build)" -fi +cat <<'MSG' +install_nearby_connections_service.sh is deprecated. +Installing Nearby Sharing API artifacts instead. +MSG -echo "[2/4] Resolving bazel-bin path" -BAZEL_BIN="$(run_bazel info bazel-bin)" -LIB_SRC="${BAZEL_BIN}/sharing/linux/libnearby_connections_service_linux_shared.so" - -if [[ ! -f "$LIB_SRC" ]]; then - echo "Shared library not found: $LIB_SRC" >&2 - echo "Expected Bazel output for $TARGET is missing." >&2 - exit 1 -fi - -echo "[3/4] Installing library and header" -"${INSTALL_PREFIX[@]}" install -d "$LIBDIR" -"${INSTALL_PREFIX[@]}" install -d "${INCLUDEDIR}/sharing/linux" -"${INSTALL_PREFIX[@]}" install -m 0755 "$LIB_SRC" "$LIBDIR/" -"${INSTALL_PREFIX[@]}" install -m 0644 "$HEADER_SRC" "${INCLUDEDIR}/sharing/linux/" - -if command -v ldconfig >/dev/null 2>&1; then - echo "[4/4] Refreshing dynamic linker cache" - "${INSTALL_PREFIX[@]}" ldconfig -else - echo "[4/4] ldconfig not found; skipping linker cache refresh" -fi - -echo "Installed:" -echo " library: $LIBDIR/$(basename "$LIB_SRC")" -echo " header : ${INCLUDEDIR}/sharing/linux/$(basename "$HEADER_SRC")" +exec "${DELEGATE_SCRIPT}" "$@" diff --git a/sharing/linux/install_nearby_sharing_service.sh b/sharing/linux/install_nearby_sharing_service.sh new file mode 100755 index 00000000..b8520a68 --- /dev/null +++ b/sharing/linux/install_nearby_sharing_service.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash + +set -euo pipefail + +TARGET="//sharing/linux:nearby_sharing_api_shared" +HEADER_SRC="sharing/linux/nearby_sharing_api.h" +BAZEL_CMD="${BAZEL:-bazel}" +PREFIX="/usr/local" +LIBDIR="" +INCLUDEDIR="" +SKIP_BUILD=0 +NEEDS_ELEVATION=0 +INSTALL_PREFIX=() + +usage() { + cat </lib) + --includedir DIR Include root directory (default: /include) + --bazel CMD Bazel command (default: bazel or env BAZEL) + --skip-build Skip bazel build step and only install from bazel-bin + -h, --help Show this help + +Examples: + $0 + sudo $0 + sudo $0 --prefix /usr + sudo $0 --bazel /usr/bin/bazel +USAGE +} + +run_bazel() { + if [[ "$(id -u)" -eq 0 && -n "${SUDO_USER:-}" ]]; then + local caller_home + caller_home="$(getent passwd "$SUDO_USER" | cut -d: -f6)" + if [[ -z "$caller_home" ]]; then + echo "Failed to resolve home directory for SUDO_USER=$SUDO_USER" >&2 + exit 1 + fi + sudo -u "$SUDO_USER" -H env \ + HOME="$caller_home" \ + BAZELISK_HOME="${BAZELISK_HOME:-$caller_home/.cache/bazelisk}" \ + "$BAZEL_CMD" "$@" + else + "$BAZEL_CMD" "$@" + fi +} + +nearest_existing_parent() { + local p="$1" + while [[ ! -e "$p" ]]; do + p="$(dirname "$p")" + done + printf '%s\n' "$p" +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --prefix) + PREFIX="$2" + shift 2 + ;; + --libdir) + LIBDIR="$2" + shift 2 + ;; + --includedir) + INCLUDEDIR="$2" + shift 2 + ;; + --bazel) + BAZEL_CMD="$2" + shift 2 + ;; + --skip-build) + SKIP_BUILD=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + usage + exit 1 + ;; + esac +done + +if [[ -z "$LIBDIR" ]]; then + LIBDIR="${PREFIX}/lib" +fi + +if [[ -z "$INCLUDEDIR" ]]; then + INCLUDEDIR="${PREFIX}/include" +fi + +LIB_PARENT="$(nearest_existing_parent "$LIBDIR")" +INCLUDE_PARENT="$(nearest_existing_parent "${INCLUDEDIR}/sharing/linux")" + +if [[ ! -w "$LIB_PARENT" || ! -w "$INCLUDE_PARENT" ]]; then + NEEDS_ELEVATION=1 +fi + +if [[ "$NEEDS_ELEVATION" -eq 1 && "$(id -u)" -ne 0 ]]; then + if ! command -v sudo >/dev/null 2>&1; then + echo "Install requires elevated privileges, but sudo is not available." >&2 + exit 1 + fi + INSTALL_PREFIX=(sudo) +fi + +if [[ ! -f "$HEADER_SRC" ]]; then + echo "Header not found: $HEADER_SRC" >&2 + echo "Run this script from the workspace root." >&2 + exit 1 +fi + +if [[ "$SKIP_BUILD" -eq 0 ]]; then + echo "[1/4] Building $TARGET" + run_bazel build "$TARGET" +else + echo "[1/4] Skipping build (--skip-build)" +fi + +echo "[2/4] Resolving bazel-bin path" +BAZEL_BIN="$(run_bazel info bazel-bin)" +LIB_SRC="${BAZEL_BIN}/sharing/linux/libnearby_sharing_api_shared.so" + +if [[ ! -f "$LIB_SRC" ]]; then + echo "Shared library not found: $LIB_SRC" >&2 + echo "Expected Bazel output for $TARGET is missing." >&2 + exit 1 +fi + +echo "[3/4] Installing library and header" +"${INSTALL_PREFIX[@]}" install -d "$LIBDIR" +"${INSTALL_PREFIX[@]}" install -d "${INCLUDEDIR}/sharing/linux" +"${INSTALL_PREFIX[@]}" install -m 0755 "$LIB_SRC" "$LIBDIR/" +"${INSTALL_PREFIX[@]}" install -m 0644 "$HEADER_SRC" "${INCLUDEDIR}/sharing/linux/" + +if command -v ldconfig >/dev/null 2>&1; then + echo "[4/4] Refreshing dynamic linker cache" + "${INSTALL_PREFIX[@]}" ldconfig +else + echo "[4/4] ldconfig not found; skipping linker cache refresh" +fi + +echo "Installed:" +echo " library: $LIBDIR/$(basename "$LIB_SRC")" +echo " header : ${INCLUDEDIR}/sharing/linux/$(basename "$HEADER_SRC")" diff --git a/sharing/linux/nearby_connections_qt_facade.cc b/sharing/linux/nearby_connections_qt_facade.cc index 7833b8fa..5afed8e6 100644 --- a/sharing/linux/nearby_connections_qt_facade.cc +++ b/sharing/linux/nearby_connections_qt_facade.cc @@ -7,6 +7,7 @@ #include "internal/base/file_path.h" #include "internal/flags/nearby_flags.h" #include "sharing/linux/nearby_connections_service_linux.h" +#include "sharing/nearby_connections_service.h" #include "sharing/nearby_connections_types.h" namespace nearby::sharing { @@ -347,7 +348,9 @@ std::unique_ptr ToNativePayload(Facade::Payload payloa class NearbyConnectionsQtFacade::Impl { public: - linux::NearbyConnectionsServiceLinux service; + Impl() : service(std::make_unique()) {} + + std::unique_ptr service; }; NearbyConnectionsQtFacade::NearbyConnectionsQtFacade() @@ -387,7 +390,7 @@ void NearbyConnectionsQtFacade::StartAdvertising( const AdvertisingOptions& advertising_options, ConnectionListener advertising_listener, std::function callback) { - impl_->service.StartAdvertising( + impl_->service->StartAdvertising( service_id, endpoint_info, ToNativeAdvertisingOptions(advertising_options), ToNativeConnectionListener(std::move(advertising_listener)), ToNativeStatusCallback(std::move(callback))); @@ -395,14 +398,14 @@ void NearbyConnectionsQtFacade::StartAdvertising( void NearbyConnectionsQtFacade::StopAdvertising( const std::string& service_id, std::function callback) { - impl_->service.StopAdvertising(service_id, - ToNativeStatusCallback(std::move(callback))); + impl_->service->StopAdvertising(service_id, + ToNativeStatusCallback(std::move(callback))); } void NearbyConnectionsQtFacade::StartDiscovery( const std::string& service_id, const DiscoveryOptions& discovery_options, DiscoveryListener discovery_listener, std::function callback) { - impl_->service.StartDiscovery( + impl_->service->StartDiscovery( service_id, ToNativeDiscoveryOptions(discovery_options), ToNativeDiscoveryListener(std::move(discovery_listener)), ToNativeStatusCallback(std::move(callback))); @@ -410,15 +413,15 @@ void NearbyConnectionsQtFacade::StartDiscovery( void NearbyConnectionsQtFacade::StopDiscovery(const std::string& service_id, std::function callback) { - impl_->service.StopDiscovery(service_id, - ToNativeStatusCallback(std::move(callback))); + impl_->service->StopDiscovery(service_id, + ToNativeStatusCallback(std::move(callback))); } void NearbyConnectionsQtFacade::RequestConnection( const std::string& service_id, const std::vector& endpoint_info, const std::string& endpoint_id, const ConnectionOptions& connection_options, ConnectionListener connection_listener, std::function callback) { - impl_->service.RequestConnection( + impl_->service->RequestConnection( service_id, endpoint_info, endpoint_id, ToNativeConnectionOptions(connection_options), ToNativeConnectionListener(std::move(connection_listener)), @@ -428,7 +431,7 @@ void NearbyConnectionsQtFacade::RequestConnection( void NearbyConnectionsQtFacade::DisconnectFromEndpoint( const std::string& service_id, const std::string& endpoint_id, std::function callback) { - impl_->service.DisconnectFromEndpoint( + impl_->service->DisconnectFromEndpoint( service_id, endpoint_id, ToNativeStatusCallback(std::move(callback))); } @@ -442,28 +445,29 @@ void NearbyConnectionsQtFacade::SendPayload( } return; } - impl_->service.SendPayload(service_id, endpoint_ids, std::move(native_payload), - ToNativeStatusCallback(std::move(callback))); + impl_->service->SendPayload(service_id, endpoint_ids, + std::move(native_payload), + ToNativeStatusCallback(std::move(callback))); } void NearbyConnectionsQtFacade::InitiateBandwidthUpgrade( const std::string& service_id, const std::string& endpoint_id, std::function callback) { - impl_->service.InitiateBandwidthUpgrade( + impl_->service->InitiateBandwidthUpgrade( service_id, endpoint_id, ToNativeStatusCallback(std::move(callback))); } void NearbyConnectionsQtFacade::AcceptConnection( const std::string& service_id, const std::string& endpoint_id, PayloadListener payload_listener, std::function callback) { - impl_->service.AcceptConnection( + impl_->service->AcceptConnection( service_id, endpoint_id, ToNativePayloadListener(std::move(payload_listener)), ToNativeStatusCallback(std::move(callback))); } void NearbyConnectionsQtFacade::StopAllEndpoints( std::function callback) { - impl_->service.StopAllEndpoints(ToNativeStatusCallback(std::move(callback))); + impl_->service->StopAllEndpoints(ToNativeStatusCallback(std::move(callback))); } -} // namespace nearby::sharing::linux +} // namespace nearby::sharing diff --git a/sharing/linux/nearby_connections_service_linux.cc b/sharing/linux/nearby_connections_service_linux.cc index acffedfa..9a408331 100644 --- a/sharing/linux/nearby_connections_service_linux.cc +++ b/sharing/linux/nearby_connections_service_linux.cc @@ -187,7 +187,7 @@ void NearbyConnectionsServiceLinux::StartAdvertising( advertising_options.enable_bluetooth_listening; options.enable_webrtc_listening = advertising_options.enable_webrtc_listening; options.use_stable_endpoint_id = advertising_options.use_stable_endpoint_id; - options.force_new_endpoint_id = advertising_options.force_new_endpoint_id; + options.force_new_endpoint_id = false; options.fast_advertisement_service_uuid = advertising_options.fast_advertisement_service_uuid.uuid; diff --git a/sharing/linux/nearby_sharing_api.cc b/sharing/linux/nearby_sharing_api.cc new file mode 100644 index 00000000..8df2b147 --- /dev/null +++ b/sharing/linux/nearby_sharing_api.cc @@ -0,0 +1,444 @@ +#include "sharing/linux/nearby_sharing_api.h" + +#include +#include + +#include "connections/implementation/flags/nearby_connections_feature_flags.h" +#include "internal/base/file_path.h" +#include "internal/flags/nearby_flags.h" +#include "sharing/attachment_container.h" +#include "sharing/file_attachment.h" +#include "sharing/linux/nearby_sharing_service_linux.h" +#include "sharing/share_target_discovered_callback.h" +#include "sharing/transfer_metadata.h" +#include "sharing/transfer_update_callback.h" + +namespace nearby::sharing::linux { + +namespace { + +using NativeService = nearby::sharing::linux::NearbySharingServiceLinux; + +void EnableBleL2capDefaults() { + nearby::NearbyFlags::GetInstance().OverrideBoolFlagValue( + nearby::connections::config_package_nearby::nearby_connections_feature:: + kEnableBleL2cap, + true); + nearby::NearbyFlags::GetInstance().OverrideBoolFlagValue( + nearby::connections::config_package_nearby::nearby_connections_feature:: + kRefactorBleL2cap, + false); +} + +NearbySharingApi::StatusCode ToFacadeStatus( + nearby::sharing::NearbySharingService::StatusCodes status) { + switch (status) { + case nearby::sharing::NearbySharingService::StatusCodes::kOk: + return NearbySharingApi::StatusCode::kOk; + case nearby::sharing::NearbySharingService::StatusCodes::kError: + return NearbySharingApi::StatusCode::kError; + case nearby::sharing::NearbySharingService::StatusCodes::kOutOfOrderApiCall: + return NearbySharingApi::StatusCode::kOutOfOrderApiCall; + case nearby::sharing::NearbySharingService::StatusCodes::kStatusAlreadyStopped: + return NearbySharingApi::StatusCode::kStatusAlreadyStopped; + case nearby::sharing::NearbySharingService::StatusCodes::kTransferAlreadyInProgress: + return NearbySharingApi::StatusCode::kTransferAlreadyInProgress; + case nearby::sharing::NearbySharingService::StatusCodes::kNoAvailableConnectionMedium: + return NearbySharingApi::StatusCode::kNoAvailableConnectionMedium; + case nearby::sharing::NearbySharingService::StatusCodes::kIrrecoverableHardwareError: + return NearbySharingApi::StatusCode::kIrrecoverableHardwareError; + case nearby::sharing::NearbySharingService::StatusCodes::kInvalidArgument: + return NearbySharingApi::StatusCode::kInvalidArgument; + } + return NearbySharingApi::StatusCode::kError; +} + +NearbySharingApi::TransferStatus ToFacadeTransferStatus( + nearby::sharing::TransferMetadata::Status status) { + using NativeStatus = nearby::sharing::TransferMetadata::Status; + using FacadeStatus = NearbySharingApi::TransferStatus; + switch (status) { + case NativeStatus::kUnknown: + return FacadeStatus::kUnknown; + case NativeStatus::kConnecting: + return FacadeStatus::kConnecting; + case NativeStatus::kAwaitingLocalConfirmation: + return FacadeStatus::kAwaitingLocalConfirmation; + case NativeStatus::kAwaitingRemoteAcceptance: + return FacadeStatus::kAwaitingRemoteAcceptance; + case NativeStatus::kInProgress: + return FacadeStatus::kInProgress; + case NativeStatus::kComplete: + return FacadeStatus::kComplete; + case NativeStatus::kFailed: + return FacadeStatus::kFailed; + case NativeStatus::kRejected: + return FacadeStatus::kRejected; + case NativeStatus::kCancelled: + return FacadeStatus::kCancelled; + case NativeStatus::kTimedOut: + return FacadeStatus::kTimedOut; + case NativeStatus::kMediaUnavailable: + return FacadeStatus::kMediaUnavailable; + case NativeStatus::kNotEnoughSpace: + return FacadeStatus::kNotEnoughSpace; + case NativeStatus::kUnsupportedAttachmentType: + return FacadeStatus::kUnsupportedAttachmentType; + case NativeStatus::kDeviceAuthenticationFailed: + return FacadeStatus::kDeviceAuthenticationFailed; + case NativeStatus::kIncompletePayloads: + return FacadeStatus::kIncompletePayloads; + } + return FacadeStatus::kUnknown; +} + +} // namespace + +class NearbySharingApi::Impl : public nearby::sharing::ShareTargetDiscoveredCallback, + public nearby::sharing::TransferUpdateCallback { + public: + Impl() : service() {} + explicit Impl(std::string device_name_override) + : service(std::move(device_name_override)) {} + + void OnShareTargetDiscovered(const nearby::sharing::ShareTarget& share_target) + override { + Listener listener_copy; + { + std::scoped_lock lock(listener_mutex); + listener_copy = listener; + } + if (!listener_copy.target_discovered_cb) { + return; + } + listener_copy.target_discovered_cb(ToShareTargetInfo(share_target)); + } + + void OnShareTargetLost(const nearby::sharing::ShareTarget& share_target) + override { + Listener listener_copy; + { + std::scoped_lock lock(listener_mutex); + listener_copy = listener; + } + if (!listener_copy.target_lost_cb) { + return; + } + listener_copy.target_lost_cb(share_target.id); + } + + void OnShareTargetUpdated(const nearby::sharing::ShareTarget& share_target) + override { + Listener listener_copy; + { + std::scoped_lock lock(listener_mutex); + listener_copy = listener; + } + if (!listener_copy.target_updated_cb) { + return; + } + listener_copy.target_updated_cb(ToShareTargetInfo(share_target)); + } + + void OnTransferUpdate( + const nearby::sharing::ShareTarget& share_target, + const nearby::sharing::AttachmentContainer& attachment_container, + const nearby::sharing::TransferMetadata& transfer_metadata) override { + Listener listener_copy; + { + std::scoped_lock lock(listener_mutex); + listener_copy = listener; + } + if (!listener_copy.transfer_update_cb) { + return; + } + + NearbySharingApi::TransferUpdateInfo info; + info.share_target_id = share_target.id; + info.device_name = share_target.device_name; + info.is_incoming = share_target.is_incoming; + info.status = ToFacadeTransferStatus(transfer_metadata.status()); + info.progress = transfer_metadata.progress(); + info.transferred_bytes = transfer_metadata.transferred_bytes(); + info.total_attachments = transfer_metadata.total_attachments_count(); + info.transferred_attachments = transfer_metadata.transferred_attachments_count(); + if (!attachment_container.GetFileAttachments().empty()) { + const nearby::sharing::FileAttachment& file = + attachment_container.GetFileAttachments().front(); + info.first_file_name = std::string(file.file_name()); + if (file.file_path().has_value()) { + info.first_file_path = file.file_path()->ToString(); + } + } + listener_copy.transfer_update_cb(info); + } + + NearbySharingApi::ShareTargetInfo ToShareTargetInfo( + const nearby::sharing::ShareTarget& share_target) { + NearbySharingApi::ShareTargetInfo info; + info.id = share_target.id; + info.device_name = share_target.device_name; + info.is_incoming = share_target.is_incoming; + info.device_type = static_cast(share_target.type); + return info; + } + + NativeService service; + bool send_mode_started = false; + bool receive_mode_started = false; + std::mutex listener_mutex; + NearbySharingApi::Listener listener; +}; + +NearbySharingApi::NearbySharingApi() { + EnableBleL2capDefaults(); + impl_ = std::make_unique(); +} + +NearbySharingApi::NearbySharingApi(std::string device_name_override) + : impl_(nullptr) { + EnableBleL2capDefaults(); + impl_ = std::make_unique(std::move(device_name_override)); +} + +NearbySharingApi::~NearbySharingApi() = default; + +NearbySharingApi::NearbySharingApi(NearbySharingApi&&) noexcept = default; + +NearbySharingApi& NearbySharingApi::operator=(NearbySharingApi&&) noexcept = default; + +void NearbySharingApi::SetListener(Listener listener) { + std::scoped_lock lock(impl_->listener_mutex); + impl_->listener = std::move(listener); +} + +void NearbySharingApi::StartSendMode(std::function callback) { + if (impl_->send_mode_started) { + if (callback) { + callback(StatusCode::kOk); + } + return; + } + impl_->service.RegisterSendSurface( + impl_.get(), impl_.get(), + nearby::sharing::NearbySharingService::SendSurfaceState::kForeground, + nearby::sharing::Advertisement::BlockedVendorId::kNone, + /*disable_wifi_hotspot=*/false, + [this, cb = std::move(callback)]( + nearby::sharing::NearbySharingService::StatusCodes status) mutable { + if (status == nearby::sharing::NearbySharingService::StatusCodes::kOk) { + impl_->send_mode_started = true; + } + if (cb) { + cb(ToFacadeStatus(status)); + } + }); +} + +void NearbySharingApi::StopSendMode(std::function callback) { + if (!impl_->send_mode_started) { + if (callback) { + callback(StatusCode::kStatusAlreadyStopped); + } + return; + } + impl_->service.UnregisterSendSurface( + impl_.get(), + [this, cb = std::move(callback)]( + nearby::sharing::NearbySharingService::StatusCodes status) mutable { + if (status == nearby::sharing::NearbySharingService::StatusCodes::kOk) { + impl_->send_mode_started = false; + } + if (cb) { + cb(ToFacadeStatus(status)); + } + }); +} + +void NearbySharingApi::StartReceiveMode(std::function callback) { + if (impl_->receive_mode_started) { + if (callback) { + callback(StatusCode::kOk); + } + return; + } + impl_->service.RegisterReceiveSurface( + impl_.get(), + nearby::sharing::NearbySharingService::ReceiveSurfaceState::kForeground, + nearby::sharing::Advertisement::BlockedVendorId::kNone, + [this, cb = std::move(callback)]( + nearby::sharing::NearbySharingService::StatusCodes status) mutable { + if (status == nearby::sharing::NearbySharingService::StatusCodes::kOk) { + impl_->receive_mode_started = true; + } + if (cb) { + cb(ToFacadeStatus(status)); + } + }); +} + +void NearbySharingApi::StopReceiveMode(std::function callback) { + if (!impl_->receive_mode_started) { + if (callback) { + callback(StatusCode::kStatusAlreadyStopped); + } + return; + } + impl_->service.UnregisterReceiveSurface( + impl_.get(), + [this, cb = std::move(callback)]( + nearby::sharing::NearbySharingService::StatusCodes status) mutable { + if (status == nearby::sharing::NearbySharingService::StatusCodes::kOk) { + impl_->receive_mode_started = false; + } + if (cb) { + cb(ToFacadeStatus(status)); + } + }); +} + +void NearbySharingApi::SendFile(int64_t share_target_id, + const std::string& file_path, + std::function callback) { + if (file_path.empty()) { + if (callback) { + callback(StatusCode::kInvalidArgument); + } + return; + } + + nearby::sharing::AttachmentContainer::Builder builder; + builder.AddFileAttachment( + nearby::sharing::FileAttachment(FilePath(file_path))); + std::unique_ptr attachments = + builder.Build(); + if (!attachments || !attachments->HasAttachments()) { + if (callback) { + callback(StatusCode::kInvalidArgument); + } + return; + } + + impl_->service.SendAttachments( + share_target_id, std::move(attachments), + [cb = std::move(callback)]( + nearby::sharing::NearbySharingService::StatusCodes status) mutable { + if (cb) { + cb(ToFacadeStatus(status)); + } + }); +} + +void NearbySharingApi::Accept(int64_t share_target_id, + std::function callback) { + impl_->service.Accept( + share_target_id, + [cb = std::move(callback)]( + nearby::sharing::NearbySharingService::StatusCodes status) mutable { + if (cb) { + cb(ToFacadeStatus(status)); + } + }); +} + +void NearbySharingApi::Reject(int64_t share_target_id, + std::function callback) { + impl_->service.Reject( + share_target_id, + [cb = std::move(callback)]( + nearby::sharing::NearbySharingService::StatusCodes status) mutable { + if (cb) { + cb(ToFacadeStatus(status)); + } + }); +} + +void NearbySharingApi::Cancel(int64_t share_target_id, + std::function callback) { + impl_->service.Cancel( + share_target_id, + [cb = std::move(callback)]( + nearby::sharing::NearbySharingService::StatusCodes status) mutable { + if (cb) { + cb(ToFacadeStatus(status)); + } + }); +} + +void NearbySharingApi::Shutdown(std::function callback) { + impl_->service.Shutdown( + [this, cb = std::move(callback)]( + nearby::sharing::NearbySharingService::StatusCodes status) mutable { + if (status == nearby::sharing::NearbySharingService::StatusCodes::kOk) { + impl_->send_mode_started = false; + impl_->receive_mode_started = false; + } + if (cb) { + cb(ToFacadeStatus(status)); + } + }); +} + +std::string NearbySharingApi::GetQrCodeUrl() const { + return impl_->service.GetQrCodeUrl(); +} + +std::string NearbySharingApi::StatusCodeToString(StatusCode status) { + switch (status) { + case StatusCode::kOk: + return "Ok"; + case StatusCode::kError: + return "Error"; + case StatusCode::kOutOfOrderApiCall: + return "OutOfOrderApiCall"; + case StatusCode::kStatusAlreadyStopped: + return "StatusAlreadyStopped"; + case StatusCode::kTransferAlreadyInProgress: + return "TransferAlreadyInProgress"; + case StatusCode::kNoAvailableConnectionMedium: + return "NoAvailableConnectionMedium"; + case StatusCode::kIrrecoverableHardwareError: + return "IrrecoverableHardwareError"; + case StatusCode::kInvalidArgument: + return "InvalidArgument"; + } + return "Error"; +} + +std::string NearbySharingApi::TransferStatusToString(TransferStatus status) { + switch (status) { + case TransferStatus::kUnknown: + return "Unknown"; + case TransferStatus::kConnecting: + return "Connecting"; + case TransferStatus::kAwaitingLocalConfirmation: + return "AwaitingLocalConfirmation"; + case TransferStatus::kAwaitingRemoteAcceptance: + return "AwaitingRemoteAcceptance"; + case TransferStatus::kInProgress: + return "InProgress"; + case TransferStatus::kComplete: + return "Complete"; + case TransferStatus::kFailed: + return "Failed"; + case TransferStatus::kRejected: + return "Rejected"; + case TransferStatus::kCancelled: + return "Cancelled"; + case TransferStatus::kTimedOut: + return "TimedOut"; + case TransferStatus::kMediaUnavailable: + return "MediaUnavailable"; + case TransferStatus::kNotEnoughSpace: + return "NotEnoughSpace"; + case TransferStatus::kUnsupportedAttachmentType: + return "UnsupportedAttachmentType"; + case TransferStatus::kDeviceAuthenticationFailed: + return "DeviceAuthenticationFailed"; + case TransferStatus::kIncompletePayloads: + return "IncompletePayloads"; + } + return "Unknown"; +} + +} // namespace nearby::sharing::linux diff --git a/sharing/linux/nearby_sharing_api.h b/sharing/linux/nearby_sharing_api.h new file mode 100644 index 00000000..67380e64 --- /dev/null +++ b/sharing/linux/nearby_sharing_api.h @@ -0,0 +1,121 @@ +// Copyright 2026 +// +// Thin app-facing API for NearbySharingServiceLinux that avoids exposing +// internal Nearby headers to external consumers. + +#ifndef SHARING_LINUX_NEARBY_SHARING_API_H_ +#define SHARING_LINUX_NEARBY_SHARING_API_H_ + +#include + +#include +#include +#include + +// Some toolchains define `linux` as a macro (e.g. `#define linux 1`), which +// breaks namespace tokens like `nearby::sharing::linux`. +#ifdef linux +#undef linux +#endif + +namespace nearby { +namespace sharing { +namespace linux { + +class __attribute__((visibility("default"))) NearbySharingApi { + public: + enum class StatusCode { + kOk = 0, + kError = 1, + kOutOfOrderApiCall = 2, + kStatusAlreadyStopped = 3, + kTransferAlreadyInProgress = 4, + kNoAvailableConnectionMedium = 5, + kIrrecoverableHardwareError = 6, + kInvalidArgument = 7, + }; + + enum class TransferStatus { + kUnknown = 0, + kConnecting = 1, + kAwaitingLocalConfirmation = 2, + kAwaitingRemoteAcceptance = 3, + kInProgress = 4, + kComplete = 5, + kFailed = 6, + kRejected = 7, + kCancelled = 8, + kTimedOut = 9, + kMediaUnavailable = 10, + kNotEnoughSpace = 11, + kUnsupportedAttachmentType = 12, + kDeviceAuthenticationFailed = 13, + kIncompletePayloads = 14, + }; + + struct ShareTargetInfo { + int64_t id = 0; + std::string device_name; + bool is_incoming = false; + int device_type = 0; + }; + + struct TransferUpdateInfo { + int64_t share_target_id = 0; + std::string device_name; + bool is_incoming = false; + TransferStatus status = TransferStatus::kUnknown; + float progress = 0.0f; + uint64_t transferred_bytes = 0; + int total_attachments = 0; + int transferred_attachments = 0; + std::string first_file_name; + std::string first_file_path; + }; + + struct Listener { + std::function target_discovered_cb; + std::function target_updated_cb; + std::function target_lost_cb; + std::function transfer_update_cb; + }; + + NearbySharingApi(); + explicit NearbySharingApi(std::string device_name_override); + ~NearbySharingApi(); + + NearbySharingApi(const NearbySharingApi&) = delete; + NearbySharingApi& operator=(const NearbySharingApi&) = delete; + NearbySharingApi(NearbySharingApi&&) noexcept; + NearbySharingApi& operator=(NearbySharingApi&&) noexcept; + + void SetListener(Listener listener); + + void StartSendMode(std::function callback); + void StopSendMode(std::function callback); + + void StartReceiveMode(std::function callback); + void StopReceiveMode(std::function callback); + + void SendFile(int64_t share_target_id, const std::string& file_path, + std::function callback); + void Accept(int64_t share_target_id, std::function callback); + void Reject(int64_t share_target_id, std::function callback); + void Cancel(int64_t share_target_id, std::function callback); + + void Shutdown(std::function callback); + std::string GetQrCodeUrl() const; + + static std::string StatusCodeToString(StatusCode status); + static std::string TransferStatusToString(TransferStatus status); + + private: + class Impl; + std::unique_ptr impl_; +}; + +} // namespace linux +} // namespace sharing +} // namespace nearby + +#endif // SHARING_LINUX_NEARBY_SHARING_API_H_ diff --git a/sharing/linux/nearby_sharing_app.cc b/sharing/linux/nearby_sharing_app.cc index dc114aa9..c62abdf2 100644 --- a/sharing/linux/nearby_sharing_app.cc +++ b/sharing/linux/nearby_sharing_app.cc @@ -12,15 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include "connections/implementation/flags/nearby_connections_feature_flags.h" + +#include #include #include #include #include -#include #include -#include "absl/time/time.h" -#include "internal/platform/implementation/account_manager.h" #include "sharing/linux/nearby_sharing_service_linux.h" #include "sharing/attachment_container.h" #include "sharing/file_attachment.h" @@ -31,6 +31,7 @@ #include "sharing/transfer_update_callback.h" #include "internal/base/file_path.h" #include "internal/base/files.h" +#include "internal/flags/nearby_flags.h" using namespace nearby::sharing; using namespace nearby::sharing::linux; @@ -96,9 +97,6 @@ class MyTransferUpdateCallback : public TransferUpdateCallback { std::cout << "║ 3. List devices │ 8. Cancel transfer ║" << std::endl; std::cout << "║ 4. Send file │ 9. Print status ║" << std::endl; std::cout << "║ 5. Send text │ 0. Exit ║" << std::endl; - std::cout << "║10. Sync credentials │11. Credential status ║" << std::endl; - std::cout << "║12. Visibility: Everyone │13. Visibility: Contacts ║" << std::endl; - std::cout << "║14. Visibility: Hidden │ ║" << std::endl; std::cout << "╚═════════════════════════════════════════════════════════╝" << std::endl; std::cout << "Choice: "; } @@ -160,9 +158,6 @@ class MyShareTargetDiscoveredCallback : public ShareTargetDiscoveredCallback { std::cout << "║ 3. List devices │ 8. Cancel transfer ║" << std::endl; std::cout << "║ 4. Send file │ 9. Print status ║" << std::endl; std::cout << "║ 5. Send text │ 0. Exit ║" << std::endl; - std::cout << "║10. Sync credentials │11. Credential status ║" << std::endl; - std::cout << "║12. Visibility: Everyone │13. Visibility: Contacts ║" << std::endl; - std::cout << "║14. Visibility: Hidden │ ║" << std::endl; std::cout << "╚═════════════════════════════════════════════════════════╝" << std::endl; std::cout << "Choice: "; } @@ -183,8 +178,6 @@ class NearbySharingApp { } void StartAsReceiver() { - PrepareCredentialFlow(/*for_receiver=*/true); - std::cout << "\n=== Starting as Receiver (Foreground) ===" << std::endl; service_->RegisterReceiveSurface( @@ -193,8 +186,6 @@ class NearbySharingApp { Advertisement::BlockedVendorId::kNone, [this](NearbySharingService::StatusCodes status) { if (status == NearbySharingService::StatusCodes::kOk) { - receive_surface_state_ = - NearbySharingService::ReceiveSurfaceState::kForeground; std::cout << "Successfully registered as receiver!" << std::endl; // Display QR code URL @@ -212,7 +203,6 @@ class NearbySharingApp { std::cout << "│ Scan this with your phone to connect! │" << std::endl; std::cout << "└──────────────────────────────────────────────────────────────┘" << std::endl; } - ForceCredentialSync("receiver surface registration"); } else { std::cout << "Failed to register as receiver: " << NearbySharingService::StatusCodeToString(status) << std::endl; @@ -223,8 +213,6 @@ class NearbySharingApp { } void StartAsSender() { - PrepareCredentialFlow(/*for_receiver=*/false); - std::cout << "\n=== Starting as Sender (Foreground) ===" << std::endl; service_->RegisterSendSurface( @@ -235,7 +223,6 @@ class NearbySharingApp { false, // disable_wifi_hotspot [this](NearbySharingService::StatusCodes status) { if (status == NearbySharingService::StatusCodes::kOk) { - send_surface_state_ = NearbySharingService::SendSurfaceState::kForeground; std::cout << "Successfully registered as sender!" << std::endl; // Display QR code URL @@ -253,7 +240,6 @@ class NearbySharingApp { std::cout << "│ Scan this with your phone to connect! │" << std::endl; std::cout << "└──────────────────────────────────────────────────────────────┘" << std::endl; } - ForceCredentialSync("sender surface registration"); } else { std::cout << "Failed to register as sender: " << NearbySharingService::StatusCodeToString(status) << std::endl; @@ -428,46 +414,6 @@ class NearbySharingApp { std::cout << "======================" << std::endl; } - void SyncCredentialsNow() { ForceCredentialSync("manual menu request"); } - - void PrintCredentialStatus() { - std::cout << "\n=== Credential Flow Status ===" << std::endl; - auto* account_manager = service_->GetAccountManager(); - if (account_manager == nullptr) { - std::cout << "Account manager: unavailable on this service implementation." - << std::endl; - } else if (account_manager->GetCurrentAccount().has_value()) { - const auto& account = *account_manager->GetCurrentAccount(); - std::cout << "Signed in account: " << account.email << std::endl; - std::cout << "Account id: " << account.id << std::endl; - } else { - std::cout << "Signed in account: none" << std::endl; - } - - std::cout << "Certificate manager pointer: " - << (service_->GetCertificateManager() != nullptr ? "available" - : "null") - << std::endl; - - std::cout << "Service dump:\n" << service_->Dump() << std::endl; - std::cout << "==============================" << std::endl; - } - - void SetVisibilityEveryone() { - ApplyVisibility(proto::DeviceVisibility::DEVICE_VISIBILITY_EVERYONE, - absl::Minutes(15), "EVERYONE"); - } - - void SetVisibilityContacts() { - ApplyVisibility(proto::DeviceVisibility::DEVICE_VISIBILITY_ALL_CONTACTS, - absl::ZeroDuration(), "ALL_CONTACTS"); - } - - void SetVisibilityHidden() { - ApplyVisibility(proto::DeviceVisibility::DEVICE_VISIBILITY_HIDDEN, - absl::ZeroDuration(), "HIDDEN"); - } - void Shutdown() { std::cout << "\n=== Shutting Down ===" << std::endl; service_->Shutdown([](NearbySharingService::StatusCodes status) { @@ -477,84 +423,9 @@ class NearbySharingApp { } private: - void ApplyVisibility(proto::DeviceVisibility visibility, - absl::Duration expiration, - const std::string& label) { - service_->SetVisibility( - visibility, expiration, - [label](NearbySharingService::StatusCodes status) mutable { - if (status == NearbySharingService::StatusCodes::kOk) { - std::cout << "Visibility updated to " << label << std::endl; - } else { - std::cout << "Failed to set visibility to " << label << ": " - << NearbySharingService::StatusCodeToString(status) - << std::endl; - } - }); - } - - void PrepareCredentialFlow(bool for_receiver) { - auto* account_manager = service_->GetAccountManager(); - bool has_account = account_manager != nullptr && - account_manager->GetCurrentAccount().has_value(); - - // Match NearbySharingServiceImpl behavior: outgoing paths are contacts-based - // when account data is available; receiver mode generally uses everyone. - if (for_receiver) { - SetVisibilityEveryone(); - } else if (has_account) { - SetVisibilityContacts(); - } else { - SetVisibilityEveryone(); - } - } - - void ForceCredentialSync(const std::string& reason) { - std::cout << "[Credential flow] Requesting sync via service hooks (" - << reason << ")." << std::endl; - - // Keep visibility explicitly valid for cert-backed advertising. - SetVisibilityEveryone(); - - // NearbySharingServiceImpl forces private cert upload from - // RegisterReceiveSurface when visibility is not hidden. Re-registering - // the current receive surface is enough to trigger that path. - auto state = receive_surface_state_.value_or( - NearbySharingService::ReceiveSurfaceState::kBackground); - service_->RegisterReceiveSurface( - transfer_callback_.get(), state, Advertisement::BlockedVendorId::kNone, - [](NearbySharingService::StatusCodes status) { - if (status == NearbySharingService::StatusCodes::kOk) { - std::cout << "[Credential flow] Receive surface refreshed for sync." - << std::endl; - } else { - std::cout << "[Credential flow] Failed to refresh receive surface: " - << NearbySharingService::StatusCodeToString(status) - << std::endl; - } - }); - - // Discovery-triggered public cert download is internal to the full service. - if (send_surface_state_.has_value()) { - service_->RegisterSendSurface( - transfer_callback_.get(), discovery_callback_.get(), - *send_surface_state_, Advertisement::BlockedVendorId::kNone, - /*disable_wifi_hotspot=*/false, - [](NearbySharingService::StatusCodes status) { - if (status == NearbySharingService::StatusCodes::kOk) { - std::cout << "[Credential flow] Send surface refreshed for scan " - "side sync." - << std::endl; - } - }); - } - } - std::unique_ptr service_; std::unique_ptr transfer_callback_; std::unique_ptr discovery_callback_; - std::optional receive_surface_state_; - std::optional send_surface_state_; }; void PrintMenu() { @@ -566,14 +437,20 @@ void PrintMenu() { std::cout << "║ 3. List devices │ 8. Cancel transfer ║" << std::endl; std::cout << "║ 4. Send file │ 9. Print status ║" << std::endl; std::cout << "║ 5. Send text │ 0. Exit ║" << std::endl; - std::cout << "║10. Sync credentials │11. Credential status ║" << std::endl; - std::cout << "║12. Visibility: Everyone │13. Visibility: Contacts ║" << std::endl; - std::cout << "║14. Visibility: Hidden │ ║" << std::endl; std::cout << "╚═════════════════════════════════════════════════════════╝" << std::endl; std::cout << "Choice: "; } int main(int argc, char* argv[]) { + + nearby::NearbyFlags::GetInstance().OverrideBoolFlagValue( + nearby::connections::config_package_nearby::nearby_connections_feature:: + kEnableBleL2cap, + true); + nearby::NearbyFlags::GetInstance().OverrideBoolFlagValue( + nearby::connections::config_package_nearby::nearby_connections_feature:: + kRefactorBleL2cap, + true); std::string device_name = "MyLinuxDevice"; if (argc > 1) { @@ -657,26 +534,6 @@ int main(int argc, char* argv[]) { case 9: app.PrintStatus(); break; - - case 10: - app.SyncCredentialsNow(); - break; - - case 11: - app.PrintCredentialStatus(); - break; - - case 12: - app.SetVisibilityEveryone(); - break; - - case 13: - app.SetVisibilityContacts(); - break; - - case 14: - app.SetVisibilityHidden(); - break; case 0: running = false; diff --git a/sharing/linux/qml_tray_app/CMakeLists.txt b/sharing/linux/qml_tray_app/CMakeLists.txt index 4e10f9fc..2133b4b6 100644 --- a/sharing/linux/qml_tray_app/CMakeLists.txt +++ b/sharing/linux/qml_tray_app/CMakeLists.txt @@ -11,10 +11,10 @@ find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Qml Quick QuickControls2) # Where the Bazel-built Nearby .so and header were installed set(NEARBY_PREFIX "/usr/local" CACHE PATH "Install prefix for the Nearby shared library") -find_library(NEARBY_LIB nearby_connections_service_linux_shared +find_library(NEARBY_SHARING_LIB nearby_sharing_api_shared HINTS "${NEARBY_PREFIX}/lib" REQUIRED) -find_path(NEARBY_INCLUDE sharing/linux/nearby_connections_qt_facade.h +find_path(NEARBY_SHARING_INCLUDE sharing/linux/nearby_sharing_api.h HINTS "${NEARBY_PREFIX}/include" REQUIRED) qt_add_executable(nearby_qml_file_tray_app @@ -24,11 +24,11 @@ qt_add_executable(nearby_qml_file_tray_app resources_file_share.qrc ) -target_include_directories(nearby_qml_file_tray_app PRIVATE "${NEARBY_INCLUDE}") +target_include_directories(nearby_qml_file_tray_app PRIVATE "${NEARBY_SHARING_INCLUDE}") target_link_libraries(nearby_qml_file_tray_app PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Qml Qt6::Quick Qt6::QuickControls2 - "${NEARBY_LIB}" + "${NEARBY_SHARING_LIB}" ) set_target_properties(nearby_qml_file_tray_app PROPERTIES @@ -38,4 +38,4 @@ set_target_properties(nearby_qml_file_tray_app PROPERTIES include(GNUInstallDirs) install(TARGETS nearby_qml_file_tray_app RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") -install(FILES "${NEARBY_LIB}" DESTINATION "${CMAKE_INSTALL_LIBDIR}") +install(FILES "${NEARBY_SHARING_LIB}" DESTINATION "${CMAKE_INSTALL_LIBDIR}") diff --git a/sharing/linux/qml_tray_app/FileShareTray.qml b/sharing/linux/qml_tray_app/FileShareTray.qml index d8f35309..bbdd112b 100644 --- a/sharing/linux/qml_tray_app/FileShareTray.qml +++ b/sharing/linux/qml_tray_app/FileShareTray.qml @@ -50,7 +50,7 @@ ApplicationWindow { topLeftRadius: 48 clip: true - readonly property bool isIdle: fileShareController.discoveredDevices.length === 0 + readonly property bool isIdle: fileShareController.discoveredTargets.length === 0 && fileShareController.transfers.length === 0 // ── Idle: animated blob ─────────────────────────────────── @@ -81,7 +81,7 @@ ApplicationWindow { } Repeater { - model: fileShareController.discoveredDevices + model: fileShareController.discoveredTargets delegate: DeviceCard {} } diff --git a/sharing/linux/qml_tray_app/README.md b/sharing/linux/qml_tray_app/README.md index a639e534..d2ff955f 100644 --- a/sharing/linux/qml_tray_app/README.md +++ b/sharing/linux/qml_tray_app/README.md @@ -1,19 +1,19 @@ # Nearby File Share Tray App -This folder contains the Qt/QML **FileShareTray** application — a system tray app for file sharing via Nearby Connections, wired to: +This folder contains the Qt/QML **FileShareTray** application — a system tray +app for file sharing via Nearby Sharing, wired to: -- `nearby::sharing::linux::NearbyConnectionsServiceLinux` -- Send mode (discovery + connect) +- `nearby::sharing::linux::NearbySharingApi` +- Send mode (discover nearby share targets + send file) - Receive mode (incoming requests + accept/reject) -- Transfer status list (progress + payload status) -- Connected medium display per endpoint +- Transfer status list (progress + transfer status) - Persistent tray behavior (window close hides app to tray) -- File logging to `/tmp/nearby_qml_tray.log` +- File logging to `/tmp/nearby_qml_file_tray.log` ## Files - `file_share_tray_main.cpp`: Qt app bootstrap + system tray behavior. -- `file_share_tray_controller.h/.cc`: QML-facing backend wrapper around Nearby Connections. +- `file_share_tray_controller.h/.cc`: QML-facing backend wrapper around Nearby Sharing. - `FileShareTray.qml`: Top-level UI for the file share tray app. - `components/`: Shared QML UI components used by `FileShareTray.qml`. - `resources_file_share.qrc`: Embeds `FileShareTray.qml` and components. @@ -24,36 +24,36 @@ This folder contains the Qt/QML **FileShareTray** application — a system tray - Use tray icon menu to show/hide/quit. - Mode `Send`: - Starts discovery. - - Shows discovered endpoints. - - Lets you connect and send text payloads. + - Shows discovered share targets. + - Sends the selected file to a chosen target. - Mode `Receive`: - Starts advertising. - - Shows pending incoming connection requests. + - Shows pending incoming transfer requests. - Lets you accept/reject incoming requests. -- Transfers are shown with endpoint, direction, status, progress, and medium. -- Logs are appended to `/tmp/nearby_qml_tray.log`. +- Transfers are shown with target, direction, status, and progress. +- Logs are appended to `/tmp/nearby_qml_file_tray.log`. ## Building This CMake app links against the installed Nearby shared library and header: -- `libnearby_connections_service_linux_shared.so` -- `sharing/linux/nearby_connections_qt_facade.h` +- `libnearby_sharing_api_shared.so` +- `sharing/linux/nearby_sharing_api.h` Install them first (repo root): ```bash -./sharing/linux/install_nearby_connections_service.sh +./sharing/linux/install_nearby_sharing_service.sh ``` Then build the app (from `sharing/linux/qml_tray_app`): ```bash -cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DNEARBY_INSTALL_PREFIX=/usr/local +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DNEARBY_PREFIX=/usr/local cmake --build build -j ``` -## Bundle `libnearby_connections_service_linux_shared.so` with the app +## Bundle `libnearby_sharing_api_shared.so` with the app From `sharing/linux/qml_tray_app`: @@ -66,7 +66,7 @@ cmake --install build Bundle output: - `dist/bin/nearby_qml_file_tray_app` -- `dist/bin/libnearby_connections_service_linux_shared.so` +- `dist/bin/libnearby_sharing_api_shared.so` The app is installed with `INSTALL_RPATH=$ORIGIN`, so it resolves the Nearby shared library from the same folder in the bundle. @@ -88,5 +88,5 @@ Output: This zip is created from the CMake install tree and includes: - `nearby_qml_file_tray_app` -- `libnearby_connections_service_linux_shared.so` +- `libnearby_sharing_api_shared.so` - Qt runtime libs/plugins/QML imports discovered by Qt deploy tooling diff --git a/sharing/linux/qml_tray_app/cmake/BuildNearbySoIfMissing.cmake b/sharing/linux/qml_tray_app/cmake/BuildNearbySoIfMissing.cmake index 5ab72263..9446245d 100644 --- a/sharing/linux/qml_tray_app/cmake/BuildNearbySoIfMissing.cmake +++ b/sharing/linux/qml_tray_app/cmake/BuildNearbySoIfMissing.cmake @@ -26,7 +26,7 @@ if(EXISTS "${_output_so}") endforeach() if(NOT _needs_rebuild) - # Reuse an existing .so when it already exports the Qt facade symbols. + # Reuse an existing .so when it already exports the NearbySharingApi symbols. # This avoids stale-cache link failures after facade changes. find_program(_nm_program nm) if(_nm_program) @@ -37,11 +37,11 @@ if(EXISTS "${_output_so}") ERROR_QUIET ) if(_nm_result EQUAL 0) - string(FIND "${_nm_output}" "nearby::sharing::linux::NearbyConnectionsQtFacade::NearbyConnectionsQtFacade()" _facade_ctor_idx) + string(FIND "${_nm_output}" "nearby::sharing::linux::NearbySharingApi::NearbySharingApi()" _api_ctor_idx) string(FIND "${_nm_output}" " U nearby::api::ImplementationPlatform::CreateScheduledExecutor()" _undef_platform_idx) string(FIND "${_nm_output}" " U nearby::SystemClock::ElapsedRealtime()" _undef_clock_idx) string(FIND "${_nm_output}" " U nearby::Crypto::Sha256(" _undef_crypto_idx) - if(NOT _facade_ctor_idx EQUAL -1 + if(NOT _api_ctor_idx EQUAL -1 AND _undef_platform_idx EQUAL -1 AND _undef_clock_idx EQUAL -1 AND _undef_crypto_idx EQUAL -1) diff --git a/sharing/linux/qml_tray_app/components/AnimatedBlob.qml b/sharing/linux/qml_tray_app/components/AnimatedBlob.qml index 5a16cb0b..7ba0f8bd 100644 --- a/sharing/linux/qml_tray_app/components/AnimatedBlob.qml +++ b/sharing/linux/qml_tray_app/components/AnimatedBlob.qml @@ -6,11 +6,14 @@ Item { readonly property color textPrimary: "#111827" readonly property color textMuted: "#6b7280" + readonly property bool isSendMode: fileShareController.pendingSendFilePath.length > 0 Label { x: 48; y: 48 visible: fileShareController.running - text: "Ready to receive" + (fileShareController.pendingSendFilePath.length > 0 ? " / send" : "") + text: isSendMode + ? "Ready to send" + : "Ready to receive" font.pixelSize: 20 font.weight: Font.Medium color: textPrimary @@ -20,6 +23,7 @@ Item { id: blobCanvas width: 380; height: 380 anchors.centerIn: parent + visible: !isSendMode property real t: 0 property double startMs: Date.now() @@ -76,10 +80,55 @@ Item { } } + // QR code URL panel shown when a file is pending to send + Column { + anchors.centerIn: parent + visible: isSendMode + spacing: 16 + width: parent.width - 96 + + Label { + anchors.horizontalCenter: parent.horizontalCenter + text: "Scan to connect" + font.pixelSize: 16 + font.weight: Font.Medium + color: textPrimary + } + + Rectangle { + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width + height: urlLabel.implicitHeight + 24 + radius: 12 + color: "#f0fdf4" + border.color: "#bbf7d0" + border.width: 1 + + Label { + id: urlLabel + anchors.centerIn: parent + width: parent.width - 32 + text: fileShareController.qrCodeUrl + font.pixelSize: 12 + font.family: "monospace" + color: "#166534" + wrapMode: Text.WrapAnywhere + } + } + + Label { + anchors.horizontalCenter: parent.horizontalCenter + text: "Sending: " + fileShareController.pendingSendFileName + font.pixelSize: 13 + color: textMuted + } + } + Label { anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom anchors.bottomMargin: 48 + visible: !isSendMode text: fileShareController.statusMessage font.pixelSize: 13 color: textMuted diff --git a/sharing/linux/qml_tray_app/components/DeviceCard.qml b/sharing/linux/qml_tray_app/components/DeviceCard.qml index 32749e40..1b1a1dfa 100644 --- a/sharing/linux/qml_tray_app/components/DeviceCard.qml +++ b/sharing/linux/qml_tray_app/components/DeviceCard.qml @@ -3,7 +3,7 @@ import QtQuick.Controls import QtQuick.Layouts Rectangle { - required property string modelData + required property var modelData Layout.fillWidth: true height: 76 @@ -15,10 +15,12 @@ Rectangle { readonly property color textPrimary: "#111827" readonly property color textMuted: "#6b7280" - function endpointLabel(endpointId) { - var label = fileShareController.peerNameForEndpoint(endpointId) - if (!label || label.length === 0 || label === "Unknown device") return "Unknown device" - return label + readonly property string targetName: modelData.name && modelData.name.length > 0 + ? modelData.name : "Unknown device" + + function initialLetter(label) { + if (!label || label.length === 0) return "?" + return label.charAt(0).toUpperCase() } RowLayout { @@ -33,7 +35,7 @@ Rectangle { Label { anchors.centerIn: parent - text: endpointLabel(modelData).charAt(0).toUpperCase() + text: initialLetter(targetName) font.pixelSize: 18 font.weight: Font.Medium color: textPrimary @@ -46,7 +48,7 @@ Rectangle { Label { Layout.fillWidth: true - text: endpointLabel(modelData) + text: targetName font.weight: Font.Medium elide: Text.ElideRight color: textPrimary @@ -54,7 +56,7 @@ Rectangle { Label { Layout.fillWidth: true - text: modelData + text: "#" + modelData.id font.pixelSize: 11 color: textMuted elide: Text.ElideRight @@ -83,7 +85,7 @@ Rectangle { cursorShape: Qt.PointingHandCursor enabled: fileShareController.mode === "Send" && fileShareController.pendingSendFilePath.length > 0 - onClicked: fileShareController.sendPendingFileToEndpoint(modelData) + onClicked: fileShareController.sendPendingFileToTarget(modelData.id) } } } diff --git a/sharing/linux/qml_tray_app/components/SettingsPanel.qml b/sharing/linux/qml_tray_app/components/SettingsPanel.qml index 22ec149f..0fcb6ca3 100644 --- a/sharing/linux/qml_tray_app/components/SettingsPanel.qml +++ b/sharing/linux/qml_tray_app/components/SettingsPanel.qml @@ -11,9 +11,7 @@ Drawer { implicitHeight: parent ? parent.height : 0 readonly property color bg: "#f0fdf4" - readonly property color surface: "#ffffff" - readonly property color accent: "#38aa62" - readonly property color accentLight: "#dcfce7" + readonly property color cardBg: "#ffffff" readonly property color borderColor: "#bbf7d0" readonly property color textPrimary: "#111827" readonly property color textMuted: "#6b7280" @@ -25,12 +23,10 @@ Drawer { height: root.height spacing: 0 - // ── Header ──────────────────────────────────────────────────────── Rectangle { Layout.fillWidth: true height: 64 color: "transparent" - // border.color: root.borderColor RowLayout { anchors.fill: parent @@ -46,448 +42,114 @@ Drawer { Item { Layout.fillWidth: true } - Rectangle { - width: 32 - height: 32 - radius: 8 - // color: closeArea.containsMouse ? "#f3f4f6" : "transparent" - - Label { - anchors.centerIn: parent - text: "✕" - font.pixelSize: 14 - color: root.textMuted - } - - MouseArea { - id: closeArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: root.close() - } + ToolButton { + text: "✕" + onClicked: root.close() } } } - // ── Scrollable content ──────────────────────────────────────────── - Flickable { - id: flick + ScrollView { Layout.fillWidth: true Layout.fillHeight: true - clip: false - contentWidth: width - contentHeight: settingsCol.height + 32 - ScrollBar.vertical: ScrollBar {} + clip: true - Column { - id: settingsCol - x: 20 - y: 20 - width: flick.width - 40 - spacing: 20 + ColumnLayout { + width: root.width - 40 + anchors.horizontalCenter: parent.horizontalCenter + spacing: 16 - // ── Device ─────────────────────────────────────────────── - SectionLabel { text: "DEVICE" } + GroupBox { + title: "Device" + Layout.fillWidth: true - SectionCard { - width: settingsCol.width + background: Rectangle { + color: root.cardBg + radius: 10 + border.color: root.borderColor + } ColumnLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: 16 - spacing: 14 + anchors.fill: parent + anchors.margins: 12 + spacing: 8 - RowLayout { + Label { + text: "Device name" + color: root.textMuted + } + TextField { Layout.fillWidth: true - spacing: 10 - Label { - text: "Device name" - font.pixelSize: 13 - color: root.textMuted - Layout.preferredWidth: 110 - } - ThemedField { - text: fileShareController.deviceName - onEditingFinished: fileShareController.deviceName = text - } + text: fileShareController.deviceName + onEditingFinished: fileShareController.deviceName = text } } } - // ── Connection ──────────────────────────────────────────── - SectionLabel { text: "CONNECTION" } + GroupBox { + title: "Sharing" + Layout.fillWidth: true - SectionCard { - width: settingsCol.width + background: Rectangle { + color: root.cardBg + radius: 10 + border.color: root.borderColor + } ColumnLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: 16 - spacing: 14 + anchors.fill: parent + anchors.margins: 12 + spacing: 8 - // ── Advanced Settings ───────────────────────────── - ColumnLayout { + Label { Layout.fillWidth: true - spacing: 0 - - // Header row (always visible) - RowLayout { - Layout.fillWidth: true - spacing: 6 - - Canvas { - id: advChevron - width: 14; height: 14 - property bool open: false - onPaint: { - var ctx = getContext("2d") - ctx.clearRect(0, 0, width, height) - ctx.strokeStyle = root.accent - ctx.lineWidth = 1.8 - ctx.lineCap = "round" - ctx.lineJoin = "round" - ctx.beginPath() - if (open) { - ctx.moveTo(2, 5); ctx.lineTo(7, 10); ctx.lineTo(12, 5) - } else { - ctx.moveTo(5, 2); ctx.lineTo(10, 7); ctx.lineTo(5, 12) - } - ctx.stroke() - } - } - - Label { - text: "Advanced Settings" - font.pixelSize: 12 - font.weight: Font.Medium - color: root.accent - } - - Item { Layout.fillWidth: true } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - advChevron.open = !advChevron.open - advChevron.requestPaint() - } - } - } - - // Warning + Strategy (shown when expanded) - ColumnLayout { - visible: advChevron.open - Layout.fillWidth: true - spacing: 10 - - Label { - Layout.fillWidth: true - text: "⚠️ Only mess with these if you know what you're doing." - font.pixelSize: 11 - color: "#b45309" - wrapMode: Text.WordWrap - topPadding: 6 - } - - RowLayout { - Layout.fillWidth: true - spacing: 10 - Label { - text: "Strategy" - font.pixelSize: 13 - color: root.textMuted - Layout.preferredWidth: 110 - } - ThemedCombo { - id: strategyCombo - model: ["P2pPointToPoint", "P2pStar", "P2pCluster"] - currentIndex: { - var s = fileShareController.connectionStrategy - if (s === "P2pStar") return 1 - if (s === "P2pCluster") return 2 - return 0 - } - onActivated: fileShareController.connectionStrategy = currentText - Connections { - target: fileShareController - function onConnectionStrategyChanged() { - var s = fileShareController.connectionStrategy - strategyCombo.currentIndex = s === "P2pStar" ? 1 : s === "P2pCluster" ? 2 : 0 - } - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: 10 - Label { - text: "Service ID" - font.pixelSize: 13 - color: root.textMuted - Layout.preferredWidth: 110 - } - ThemedField { - text: fileShareController.serviceId - onEditingFinished: fileShareController.serviceId = text - } - } - } - } - - Rectangle { - Layout.fillWidth: true - height: 1 - color: root.borderColor + wrapMode: Text.WordWrap + color: root.textMuted + text: "Nearby Sharing uses built-in transport and discovery settings." } RowLayout { Layout.fillWidth: true Label { - text: "Auto-accept incoming" - font.pixelSize: 13 - color: root.textPrimary Layout.fillWidth: true + color: root.textPrimary + text: "Auto-accept incoming" } Switch { checked: fileShareController.autoAcceptIncoming - palette.highlight: root.accent - onCheckedChanged: fileShareController.autoAcceptIncoming = checked + onToggled: fileShareController.autoAcceptIncoming = checked } } } } - // ── Transport mediums ───────────────────────────────────── - SectionLabel { text: "TRANSPORT MEDIUMS" } + GroupBox { + title: "Logging" + Layout.fillWidth: true - SectionCard { - width: settingsCol.width - - ColumnLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: 16 - spacing: 4 - - MediumRow { label: "Bluetooth"; checked: fileShareController.bluetoothEnabled; onToggled: (v) => fileShareController.bluetoothEnabled = v } - MediumRow { label: "BLE"; checked: fileShareController.bleEnabled; onToggled: (v) => fileShareController.bleEnabled = v } - MediumRow { label: "WiFi LAN"; checked: fileShareController.wifiLanEnabled; onToggled: (v) => fileShareController.wifiLanEnabled = v } - MediumRow { label: "WiFi Hotspot";checked: fileShareController.wifiHotspotEnabled; onToggled: (v) => fileShareController.wifiHotspotEnabled = v } - MediumRow { label: "WebRTC"; checked: fileShareController.webRtcEnabled; onToggled: (v) => fileShareController.webRtcEnabled = v } + background: Rectangle { + color: root.cardBg + radius: 10 + border.color: root.borderColor } - } - - // ── Logging ─────────────────────────────────────────────── - SectionLabel { text: "LOGGING" } - - SectionCard { - width: settingsCol.width ColumnLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.margins: 16 - spacing: 14 + anchors.fill: parent + anchors.margins: 12 + spacing: 8 - RowLayout { + Label { + text: "Log path" + color: root.textMuted + } + TextField { Layout.fillWidth: true - spacing: 10 - Label { - text: "Log path" - font.pixelSize: 13 - color: root.textMuted - Layout.preferredWidth: 110 - } - ThemedField { - font.pixelSize: 11 - text: fileShareController.logPath - onEditingFinished: fileShareController.logPath = text - } + text: fileShareController.logPath + onEditingFinished: fileShareController.logPath = text } } } } } } - - // ── Section heading label ───────────────────────────────────────────── - component SectionLabel: Label { - font.pixelSize: 11 - font.weight: Font.DemiBold - font.letterSpacing: 0.8 - color: root.accent - } - - // ── Rounded card that sizes itself to its content ───────────────────── - component SectionCard: Rectangle { - radius: 12 - color: root.surface - border.color: root.borderColor - // height wraps the first ColumnLayout child placed inside via anchors.top - height: (children.length > 0 ? children[0].implicitHeight : 0) + 32 - } - - // ── Themed text field ───────────────────────────────────────────────── - component ThemedField: TextField { - Layout.fillWidth: true - implicitHeight: 38 - font.pixelSize: 13 - leftPadding: 12 - rightPadding: 12 - topPadding: 0 - bottomPadding: 0 - verticalAlignment: TextInput.AlignVCenter - color: root.textPrimary - background: Rectangle { - radius: 8 - color: "#f9fafb" - border.color: parent.activeFocus ? root.accent : root.borderColor - border.width: parent.activeFocus ? 2 : 1 - } - } - - // ── Themed combo box ────────────────────────────────────────────────── - component ThemedCombo: ComboBox { - id: theComboBox - Layout.fillWidth: true - implicitHeight: 38 - font.pixelSize: 13 - leftPadding: 12 - rightPadding: 36 // leave room for the indicator - topPadding: 0 - bottomPadding: 0 - - background: Rectangle { - radius: 8 - color: "#f9fafb" - border.color: theComboBox.down || theComboBox.hovered ? root.accent : root.borderColor - border.width: theComboBox.down ? 2 : 1 - } - - contentItem: Label { - text: theComboBox.displayText - font: theComboBox.font - color: root.textPrimary - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } - - indicator: Item { - x: theComboBox.width - width - 10 - y: (theComboBox.height - height) / 2 - width: 16 - height: 16 - - Canvas { - anchors.fill: parent - onPaint: { - var ctx = getContext("2d") - ctx.clearRect(0, 0, width, height) - ctx.strokeStyle = root.accent - ctx.lineWidth = 2 - ctx.lineCap = "round" - ctx.lineJoin = "round" - ctx.beginPath() - ctx.moveTo(2, 5) - ctx.lineTo(8, 11) - ctx.lineTo(14, 5) - ctx.stroke() - } - } - } - - popup: Popup { - y: theComboBox.height + 4 - width: theComboBox.width - padding: 4 - - background: Rectangle { - radius: 10 - color: root.surface - border.color: root.borderColor - } - - contentItem: ListView { - clip: true - implicitHeight: contentHeight - model: theComboBox.delegateModel - currentIndex: theComboBox.highlightedIndex - ScrollIndicator.vertical: ScrollIndicator {} - } - } - - delegate: ItemDelegate { - width: ListView.view ? ListView.view.width : 0 - highlighted: theComboBox.highlightedIndex === index - - contentItem: Label { - leftPadding: 8 - text: modelData - font.pixelSize: 13 - color: root.textPrimary - verticalAlignment: Text.AlignVCenter - } - - background: Rectangle { - radius: 7 - color: parent.highlighted ? root.accentLight : "transparent" - } - } - } - - // ── One toggle row for a transport medium ───────────────────────────── - component MediumRow: RowLayout { - id: mrow - required property string label - required property bool checked - signal toggled(bool value) - - Layout.fillWidth: true - implicitHeight: 38 - spacing: 12 - - Rectangle { - width: 20 - height: 20 - radius: 5 - color: mrow.checked ? root.accentLight : root.surface - border.color: mrow.checked ? root.accent : "#d1d5db" - - Label { - anchors.centerIn: parent - text: "✓" - font.pixelSize: 11 - font.weight: Font.Bold - color: root.accent - visible: mrow.checked - } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: mrow.toggled(!mrow.checked) - } - } - - Label { - text: mrow.label - font.pixelSize: 13 - color: root.textPrimary - Layout.fillWidth: true - - TapHandler { - cursorShape: Qt.PointingHandCursor - onTapped: mrow.toggled(!mrow.checked) - } - } - } } diff --git a/sharing/linux/qml_tray_app/components/SideBar.qml b/sharing/linux/qml_tray_app/components/SideBar.qml index 89961529..653de941 100644 --- a/sharing/linux/qml_tray_app/components/SideBar.qml +++ b/sharing/linux/qml_tray_app/components/SideBar.qml @@ -51,12 +51,6 @@ Item { font.weight: Font.Medium color: textPrimary } - - Label { - text: "›" - font.pixelSize: 22 - color: textMuted - } } } diff --git a/sharing/linux/qml_tray_app/components/TransferCard.qml b/sharing/linux/qml_tray_app/components/TransferCard.qml index 50fc4d03..2acc2d5d 100644 --- a/sharing/linux/qml_tray_app/components/TransferCard.qml +++ b/sharing/linux/qml_tray_app/components/TransferCard.qml @@ -15,15 +15,13 @@ Rectangle { readonly property color textPrimary: "#111827" readonly property color textMuted: "#6b7280" - readonly property bool isActive: modelData.status === "InProgress" || modelData.status === "Queued" + readonly property bool isActive: modelData.status === "InProgress" + || modelData.status === "Queued" + || modelData.status === "Connecting" + || modelData.status === "AwaitingLocalConfirmation" + || modelData.status === "AwaitingRemoteAcceptance" readonly property bool isTerminal: !isActive - function endpointLabel(endpointId) { - var label = fileShareController.peerNameForEndpoint(endpointId) - if (!label || label.length === 0 || label === "Unknown device") return "Unknown device" - return label - } - function formatBytes(bytes) { if (bytes === undefined || bytes === null) return "0 B" var value = Number(bytes) @@ -52,7 +50,8 @@ Rectangle { Label { anchors.centerIn: parent - text: endpointLabel(modelData.endpointId).charAt(0).toUpperCase() + text: modelData.targetName && modelData.targetName.length > 0 + ? modelData.targetName.charAt(0).toUpperCase() : "?" font.pixelSize: 18 font.weight: Font.Medium color: textPrimary @@ -65,7 +64,7 @@ Rectangle { Label { Layout.fillWidth: true - text: endpointLabel(modelData.endpointId) + text: modelData.targetName font.weight: Font.Medium elide: Text.ElideRight color: textPrimary @@ -76,27 +75,12 @@ Rectangle { color: textMuted text: { if (isActive) - return modelData.direction === "Send" ? "Sending..." : "Receiving..." - return modelData.status + return modelData.direction === "outgoing" ? "Sending..." : "Receiving..." + return modelData.status + (modelData.fileName && modelData.fileName.length > 0 + ? " • " + modelData.fileName : "") } } } - - Rectangle { - height: 22 - width: Math.max(68, medLbl.implicitWidth + 16) - radius: 11 - color: "#e2e8f0" - border.color: "#cbd5e1" - - Label { - id: medLbl - anchors.centerIn: parent - text: modelData.medium - font.pixelSize: 11 - color: "#334155" - } - } } ProgressBar { @@ -110,7 +94,8 @@ Rectangle { Layout.fillWidth: true visible: isActive horizontalAlignment: Text.AlignRight - text: formatBytes(modelData.bytesTransferred) + " / " + formatBytes(modelData.totalBytes) + text: Math.round((modelData.progress || 0) * 100) + "% • " + + formatBytes(modelData.transferredBytes) font.pixelSize: 12 color: textMuted } diff --git a/sharing/linux/qml_tray_app/file_share_tray_controller.cc b/sharing/linux/qml_tray_app/file_share_tray_controller.cc index d2783183..9c602623 100644 --- a/sharing/linux/qml_tray_app/file_share_tray_controller.cc +++ b/sharing/linux/qml_tray_app/file_share_tray_controller.cc @@ -1,65 +1,31 @@ #include "file_share_tray_controller.h" #include -#include #include #include #include #include #include - -#include -#include +#include namespace { -using NearbyConnectionsQtFacade = nearby::sharing::NearbyConnectionsQtFacade; - -std::atomic g_local_payload_id{1'000'000}; - -bool IsTerminalPayloadStatus(NearbyConnectionsQtFacade::PayloadStatus status) { - return status == NearbyConnectionsQtFacade::PayloadStatus::kSuccess || - status == NearbyConnectionsQtFacade::PayloadStatus::kFailure || - status == NearbyConnectionsQtFacade::PayloadStatus::kCanceled; -} - -// Mirrors kOutgoingDisconnectionDelay from nearby_sharing_service_impl. -constexpr int kOutgoingDisconnectionDelayMs = 60'000; - -QString NormalizeConnectionStrategy(const QString& strategy) { - const QString token = strategy.trimmed().toLower(); - if (token == QStringLiteral("p2pstar") || token == QStringLiteral("star")) { - return QStringLiteral("P2pStar"); - } - if (token == QStringLiteral("p2ppointtopoint") || - token == QStringLiteral("pointtopoint") || - token == QStringLiteral("point_to_point")) { - return QStringLiteral("P2pPointToPoint"); - } - return QStringLiteral("P2pCluster"); -} - -NearbyConnectionsQtFacade::Strategy StrategyFromName( - const QString& normalized_strategy) { - if (normalized_strategy == QStringLiteral("P2pStar")) { - return NearbyConnectionsQtFacade::Strategy::kP2pStar; - } - if (normalized_strategy == QStringLiteral("P2pPointToPoint")) { - return NearbyConnectionsQtFacade::Strategy::kP2pPointToPoint; - } - return NearbyConnectionsQtFacade::Strategy::kP2pCluster; +QString TrimmedOrFallback(const QString& value, const QString& fallback) { + const QString trimmed = value.trimmed(); + return trimmed.isEmpty() ? fallback : trimmed; } } // namespace FileShareTrayController::FileShareTrayController(QObject* parent) : QObject(parent) { - const QString host = QSysInfo::machineHostName(); + const QString host = QSysInfo::machineHostName().trimmed(); if (!host.isEmpty()) { device_name_ = host; } LoadSettings(); + CreateService(); ReopenLogFile(); LogLine(QStringLiteral("Started file share tray controller")); } @@ -71,6 +37,158 @@ FileShareTrayController::~FileShareTrayController() { } } +void FileShareTrayController::CreateService() { + service_ = std::make_unique(device_name_.toStdString()); + qr_code_url_ = QString::fromStdString(service_->GetQrCodeUrl()); + emit qrCodeUrlChanged(); + AttachServiceListeners(); +} + +void FileShareTrayController::AttachServiceListeners() { + NearbySharingApi::Listener listener; + + listener.target_discovered_cb = [this](const NearbySharingApi::ShareTargetInfo& info) { + QMetaObject::invokeMethod( + this, + [this, info]() { + const QString name = TrimmedOrFallback( + QString::fromStdString(info.device_name), + QStringLiteral("Unknown device")); + UpsertTarget(info.id, name, info.is_incoming); + LogLine(QStringLiteral("Target discovered id=%1 name=%2") + .arg(info.id) + .arg(name)); + }, + Qt::QueuedConnection); + }; + + listener.target_updated_cb = [this](const NearbySharingApi::ShareTargetInfo& info) { + QMetaObject::invokeMethod( + this, + [this, info]() { + const QString name = TrimmedOrFallback( + QString::fromStdString(info.device_name), + QStringLiteral("Unknown device")); + UpsertTarget(info.id, name, info.is_incoming); + LogLine(QStringLiteral("Target updated id=%1 name=%2") + .arg(info.id) + .arg(name)); + }, + Qt::QueuedConnection); + }; + + listener.target_lost_cb = [this](int64_t share_target_id) { + QMetaObject::invokeMethod( + this, + [this, share_target_id]() { + RemoveTarget(share_target_id); + LogLine(QStringLiteral("Target lost id=%1").arg(share_target_id)); + }, + Qt::QueuedConnection); + }; + + listener.transfer_update_cb = + [this](const NearbySharingApi::TransferUpdateInfo& update) { + QMetaObject::invokeMethod( + this, + [this, update]() { + const QString update_name = + QString::fromStdString(update.device_name).trimmed(); + if (!update_name.isEmpty()) { + target_names_[update.share_target_id] = update_name; + } + const QString name = TargetName(update.share_target_id); + const QString status = TransferStatusToString(update.status); + const QString direction = + update.is_incoming ? QStringLiteral("incoming") + : QStringLiteral("outgoing"); + QString file_name = QString::fromStdString(update.first_file_name); + if (file_name.isEmpty() && !update.is_incoming && + pending_send_target_id_ == update.share_target_id && + !pending_send_file_name_.isEmpty()) { + file_name = pending_send_file_name_; + } + + UpsertTransfer(update.share_target_id, name, status, update.progress, + update.transferred_bytes, direction, file_name); + SetStatus(QStringLiteral("%1 (%2)") + .arg(status) + .arg(name)); + + if (update.status == + NearbySharingApi::TransferStatus::kAwaitingLocalConfirmation) { + if (auto_accept_incoming_) { + service_->Accept( + update.share_target_id, + [this, id = update.share_target_id]( + NearbySharingApi::StatusCode result) { + QMetaObject::invokeMethod( + this, + [this, id, result]() { + LogLine(QStringLiteral("Accept(%1): %2") + .arg(id) + .arg(StatusToString(result))); + }, + Qt::QueuedConnection); + }); + } + } + + if (!IsFinalTransferStatus(update.status)) { + return; + } + + const bool success = + update.status == NearbySharingApi::TransferStatus::kComplete; + + if (update.is_incoming) { + if (success) { + const QString received_name = + file_name.isEmpty() ? QStringLiteral("file") : file_name; + emit requestTrayMessage( + QStringLiteral("File received"), + QStringLiteral("%1 from %2").arg(received_name, name)); + } else { + emit requestTrayMessage( + QStringLiteral("Receive failed"), + QStringLiteral("Transfer from %1 failed (%2)") + .arg(name, status)); + } + return; + } + + const QString sent_name = file_name.isEmpty() + ? QStringLiteral("file") + : file_name; + if (success) { + emit requestTrayMessage(QStringLiteral("Send complete"), + QStringLiteral("%1 sent to %2") + .arg(sent_name, name)); + } else { + emit requestTrayMessage( + QStringLiteral("Send failed"), + QStringLiteral("%1 failed to send to %2") + .arg(sent_name, name)); + } + + if (pending_send_target_id_ == update.share_target_id) { + pending_send_target_id_ = 0; + if (!pending_send_file_path_.isEmpty()) { + pending_send_file_path_.clear(); + emit pendingSendFilePathChanged(); + } + if (!pending_send_file_name_.isEmpty()) { + pending_send_file_name_.clear(); + emit pendingSendFileNameChanged(); + } + } + }, + Qt::QueuedConnection); + }; + + service_->SetListener(std::move(listener)); +} + void FileShareTrayController::LoadSettings() { QSettings settings(QStringLiteral("Nearby"), QStringLiteral("QmlFileTrayApp")); @@ -85,25 +203,6 @@ void FileShareTrayController::LoadSettings() { auto_accept_incoming_ = settings.value(QStringLiteral("autoAcceptIncoming"), auto_accept_incoming_) .toBool(); - bluetooth_enabled_ = - settings.value(QStringLiteral("bluetoothEnabled"), bluetooth_enabled_).toBool(); - ble_enabled_ = - settings.value(QStringLiteral("bleEnabled"), ble_enabled_).toBool(); - wifi_lan_enabled_ = - settings.value(QStringLiteral("wifiLanEnabled"), wifi_lan_enabled_).toBool(); - wifi_hotspot_enabled_ = - settings.value(QStringLiteral("wifiHotspotEnabled"), wifi_hotspot_enabled_).toBool(); - web_rtc_enabled_ = - settings.value(QStringLiteral("webRtcEnabled"), web_rtc_enabled_).toBool(); - connection_strategy_ = NormalizeConnectionStrategy( - settings.value(QStringLiteral("connectionStrategy"), connection_strategy_) - .toString()); - - const QString stored_service_id = - settings.value(QStringLiteral("serviceId"), serviceId()).toString().trimmed(); - if (!stored_service_id.isEmpty()) { - service_id_ = stored_service_id.toStdString(); - } const QString stored_log_path = settings.value(QStringLiteral("logPath"), log_path_).toString().trimmed(); @@ -116,13 +215,6 @@ void FileShareTrayController::SaveSettings() const { QSettings settings(QStringLiteral("Nearby"), QStringLiteral("QmlFileTrayApp")); settings.setValue(QStringLiteral("deviceName"), device_name_); settings.setValue(QStringLiteral("autoAcceptIncoming"), auto_accept_incoming_); - settings.setValue(QStringLiteral("bluetoothEnabled"), bluetooth_enabled_); - settings.setValue(QStringLiteral("bleEnabled"), ble_enabled_); - settings.setValue(QStringLiteral("wifiLanEnabled"), wifi_lan_enabled_); - settings.setValue(QStringLiteral("wifiHotspotEnabled"), wifi_hotspot_enabled_); - settings.setValue(QStringLiteral("webRtcEnabled"), web_rtc_enabled_); - settings.setValue(QStringLiteral("connectionStrategy"), connection_strategy_); - settings.setValue(QStringLiteral("serviceId"), serviceId()); settings.setValue(QStringLiteral("logPath"), log_path_); settings.sync(); } @@ -133,13 +225,18 @@ void FileShareTrayController::setDeviceName(const QString& device_name) { return; } + const bool was_running = running_; + if (was_running) { + stop(); + } + device_name_ = trimmed; emit deviceNameChanged(); SaveSettings(); LogLine(QStringLiteral("Device name changed to %1").arg(device_name_)); + CreateService(); - if (running_) { - stop(); + if (was_running) { start(); } } @@ -151,57 +248,6 @@ void FileShareTrayController::setAutoAcceptIncoming(bool enabled) { SaveSettings(); } -void FileShareTrayController::setBluetoothEnabled(bool enabled) { - if (bluetooth_enabled_ == enabled) return; - bluetooth_enabled_ = enabled; - emit bluetoothEnabledChanged(); - SaveSettings(); -} - -void FileShareTrayController::setBleEnabled(bool enabled) { - if (ble_enabled_ == enabled) return; - ble_enabled_ = enabled; - emit bleEnabledChanged(); - SaveSettings(); -} - -void FileShareTrayController::setWifiLanEnabled(bool enabled) { - if (wifi_lan_enabled_ == enabled) return; - wifi_lan_enabled_ = enabled; - emit wifiLanEnabledChanged(); - SaveSettings(); -} - -void FileShareTrayController::setWifiHotspotEnabled(bool enabled) { - if (wifi_hotspot_enabled_ == enabled) return; - wifi_hotspot_enabled_ = enabled; - emit wifiHotspotEnabledChanged(); - SaveSettings(); -} - -void FileShareTrayController::setWebRtcEnabled(bool enabled) { - if (web_rtc_enabled_ == enabled) return; - web_rtc_enabled_ = enabled; - emit webRtcEnabledChanged(); - SaveSettings(); -} - -void FileShareTrayController::setConnectionStrategy(const QString& strategy) { - const QString normalized = NormalizeConnectionStrategy(strategy); - if (connection_strategy_ == normalized) return; - connection_strategy_ = normalized; - emit connectionStrategyChanged(); - SaveSettings(); -} - -void FileShareTrayController::setServiceId(const QString& service_id) { - const QString trimmed = service_id.trimmed(); - if (trimmed.isEmpty() || trimmed.toStdString() == service_id_) return; - service_id_ = trimmed.toStdString(); - emit serviceIdChanged(); - SaveSettings(); -} - void FileShareTrayController::setLogPath(const QString& path) { const QString trimmed = path.trimmed(); if (trimmed.isEmpty() || trimmed == log_path_) return; @@ -234,55 +280,43 @@ void FileShareTrayController::stop() { running_ = false; emit runningChanged(); - service_.StopDiscovery( - service_id_, [this](NearbyConnectionsQtFacade::Status status) { - QMetaObject::invokeMethod( - this, - [this, status]() { - LogLine(QStringLiteral("StopDiscovery: %1") - .arg(StatusToString(status))); - }, - Qt::QueuedConnection); - }); - - service_.StopAdvertising( - service_id_, [this](NearbyConnectionsQtFacade::Status status) { - QMetaObject::invokeMethod( - this, - [this, status]() { - LogLine(QStringLiteral("StopAdvertising: %1") - .arg(StatusToString(status))); - }, - Qt::QueuedConnection); - }); - - service_.StopAllEndpoints([this](NearbyConnectionsQtFacade::Status status) { + service_->StopSendMode([this](NearbySharingApi::StatusCode status) { QMetaObject::invokeMethod( this, [this, status]() { - LogLine(QStringLiteral("StopAllEndpoints: %1") - .arg(StatusToString(status))); + LogLine(QStringLiteral("StopSendMode: %1").arg(StatusToString(status))); }, Qt::QueuedConnection); }); - discovered_devices_.clear(); - connected_devices_.clear(); - endpoint_peer_names_.clear(); - endpoint_mediums_.clear(); - target_endpoint_for_send_.clear(); + service_->StopReceiveMode([this](NearbySharingApi::StatusCode status) { + QMetaObject::invokeMethod( + this, + [this, status]() { + LogLine( + QStringLiteral("StopReceiveMode: %1").arg(StatusToString(status))); + }, + Qt::QueuedConnection); + }); - // Cancel any pending outgoing-completion timers. - for (QTimer* timer : outgoing_disconnect_timers_) { - timer->stop(); - timer->deleteLater(); - } - outgoing_disconnect_timers_.clear(); - pending_outgoing_completions_.clear(); + service_->Shutdown([this](NearbySharingApi::StatusCode status) { + QMetaObject::invokeMethod( + this, + [this, status]() { + LogLine(QStringLiteral("Shutdown: %1").arg(StatusToString(status))); + }, + Qt::QueuedConnection); + }); - emit discoveredDevicesChanged(); - emit connectedDevicesChanged(); - emit endpointMediumsChanged(); + discovered_targets_.clear(); + discovered_row_by_target_.clear(); + target_names_.clear(); + transfers_.clear(); + transfer_row_by_target_.clear(); + pending_send_target_id_ = 0; + + emit discoveredTargetsChanged(); + emit transfersChanged(); SetStatus(QStringLiteral("Stopped")); } @@ -356,49 +390,56 @@ void FileShareTrayController::switchToSendModeWithFile(const QString& file_path) .arg(pending_send_file_name_)); } -void FileShareTrayController::sendPendingFileToEndpoint(const QString& endpoint_id) { - const QString endpoint = endpoint_id.trimmed(); - if (endpoint.isEmpty()) { +void FileShareTrayController::sendPendingFileToTarget(qlonglong share_target_id) { + if (share_target_id <= 0) { return; } - if (pending_send_file_path_.isEmpty()) { - SetStatus(QStringLiteral("No file selected")); + QFileInfo file_info(pending_send_file_path_); + if (pending_send_file_path_.isEmpty() || !file_info.exists() || + !file_info.isFile()) { + SetStatus(QStringLiteral("Selected file is not available")); emit requestTrayMessage(QStringLiteral("Send failed"), - QStringLiteral("Select a file first.")); + QStringLiteral("Selected file is not available.")); return; } - target_endpoint_for_send_ = endpoint; + const QString target_name = TargetName(share_target_id); + pending_send_target_id_ = share_target_id; + UpsertTransfer(share_target_id, target_name, QStringLiteral("Queued"), 0.0, 0, + QStringLiteral("outgoing"), pending_send_file_name_); - if (connected_devices_.contains(endpoint)) { - sendPendingFile(endpoint); - return; - } + service_->SendFile( + share_target_id, file_info.absoluteFilePath().toStdString(), + [this, share_target_id](NearbySharingApi::StatusCode status) { + QMetaObject::invokeMethod( + this, + [this, share_target_id, status]() { + const QString target_name = TargetName(share_target_id); + LogLine(QStringLiteral("SendFile(%1): %2") + .arg(share_target_id) + .arg(StatusToString(status))); + if (status == NearbySharingApi::StatusCode::kOk) { + SetStatus(QStringLiteral("Sending %1 to %2") + .arg(pending_send_file_name_, target_name)); + return; + } - requestConnectionForSend(endpoint); -} - -QString FileShareTrayController::mediumForEndpoint(const QString& endpoint_id) const { - return endpoint_mediums_.value(endpoint_id).toString(); -} - -QString FileShareTrayController::peerNameForEndpoint(const QString& endpoint_id) const { - return PeerLabelForEndpoint(endpoint_id); + emit requestTrayMessage( + QStringLiteral("Send failed"), + QStringLiteral("Could not send to %1").arg(target_name)); + UpsertTransfer(share_target_id, target_name, QStringLiteral("Failed"), + 0.0, 0, QStringLiteral("outgoing"), + pending_send_file_name_); + pending_send_target_id_ = 0; + }, + Qt::QueuedConnection); + }); } void FileShareTrayController::clearTransfers() { transfers_.clear(); - transfer_row_for_payload_.clear(); - outgoing_file_payload_to_endpoint_.clear(); - outgoing_file_payload_to_name_.clear(); - send_terminal_notified_.clear(); - for (QTimer* timer : outgoing_disconnect_timers_) { - timer->stop(); - timer->deleteLater(); - } - outgoing_disconnect_timers_.clear(); - pending_outgoing_completions_.clear(); + transfer_row_by_target_.clear(); emit transfersChanged(); } @@ -409,776 +450,120 @@ void FileShareTrayController::hideToTray() { } void FileShareTrayController::startSendMode() { - discovered_devices_.clear(); - emit discoveredDevicesChanged(); - - service_.StartDiscovery( - service_id_, BuildDiscoveryOptions(), BuildDiscoveryListener(), - [this](NearbyConnectionsQtFacade::Status status) { - QMetaObject::invokeMethod( - this, - [this, status]() { - SetStatus(QStringLiteral("StartDiscovery: %1") - .arg(StatusToString(status))); - LogLine(QStringLiteral("StartDiscovery: %1") - .arg(StatusToString(status))); - if (status != NearbyConnectionsQtFacade::Status::kSuccess) { - running_ = false; - emit runningChanged(); - } - }, - Qt::QueuedConnection); - }); + service_->StartSendMode([this](NearbySharingApi::StatusCode status) { + QMetaObject::invokeMethod( + this, + [this, status]() { + SetStatus(QStringLiteral("StartSendMode: %1").arg(StatusToString(status))); + LogLine(QStringLiteral("StartSendMode: %1").arg(StatusToString(status))); + if (status != NearbySharingApi::StatusCode::kOk) { + running_ = false; + emit runningChanged(); + } + }, + Qt::QueuedConnection); + }); } void FileShareTrayController::startReceiveMode() { - service_.StartAdvertising( - service_id_, BuildEndpointInfo(), BuildAdvertisingOptions(), - BuildConnectionListener(), - [this](NearbyConnectionsQtFacade::Status status) { - QMetaObject::invokeMethod( - this, - [this, status]() { - SetStatus(QStringLiteral("StartAdvertising: %1") - .arg(StatusToString(status))); - LogLine(QStringLiteral("StartAdvertising: %1") - .arg(StatusToString(status))); - if (status != NearbyConnectionsQtFacade::Status::kSuccess) { - running_ = false; - emit runningChanged(); - } - }, - Qt::QueuedConnection); - }); -} - -std::vector FileShareTrayController::BuildEndpointInfo() const { - QByteArray endpoint = device_name_.toUtf8(); - return std::vector(endpoint.begin(), endpoint.end()); -} - -NearbyConnectionsQtFacade::ConnectionListener -FileShareTrayController::BuildConnectionListener() { - NearbyConnectionsQtFacade::ConnectionListener listener; - - listener.initiated_cb = [this](const std::string& endpoint_id, - const NearbyConnectionsQtFacade::ConnectionInfo& info) { + service_->StartReceiveMode([this](NearbySharingApi::StatusCode status) { QMetaObject::invokeMethod( this, - [this, endpoint = QString::fromStdString(endpoint_id), - peer_name = QString::fromStdString(info.peer_name), - incoming = info.is_incoming_connection]() { - SetPeerNameForEndpoint(endpoint, peer_name); - const QString peer = PeerLabelForEndpoint(endpoint); - - if (incoming) { - SetStatus(QStringLiteral("Incoming connection from %1").arg(peer)); - if (auto_accept_incoming_) { - acceptIncomingInternal(endpoint); - } - } else { - // Always accept outgoing connections (initiated by us for send). - acceptIncomingInternal(endpoint); + [this, status]() { + SetStatus( + QStringLiteral("StartReceiveMode: %1").arg(StatusToString(status))); + LogLine( + QStringLiteral("StartReceiveMode: %1").arg(StatusToString(status))); + if (status != NearbySharingApi::StatusCode::kOk) { + running_ = false; + emit runningChanged(); } }, Qt::QueuedConnection); + }); +} + +void FileShareTrayController::UpsertTarget(qlonglong share_target_id, + const QString& name, + bool is_incoming) { + target_names_[share_target_id] = name; + if (is_incoming) { + return; + } + + QVariantMap row{ + {QStringLiteral("id"), share_target_id}, + {QStringLiteral("name"), name}, }; - listener.accepted_cb = [this](const std::string& endpoint_id) { - QMetaObject::invokeMethod( - this, - [this, endpoint = QString::fromStdString(endpoint_id)]() { - const QString peer = PeerLabelForEndpoint(endpoint); - AddConnectedDevice(endpoint); - SetStatus(QStringLiteral("Connected to %1").arg(peer)); - LogLine(QStringLiteral("Connection accepted endpoint=%1 peer=%2") - .arg(endpoint, peer)); + if (discovered_row_by_target_.contains(share_target_id)) { + const int row_index = discovered_row_by_target_.value(share_target_id); + if (row_index >= 0 && row_index < discovered_targets_.size()) { + discovered_targets_[row_index] = row; + emit discoveredTargetsChanged(); + return; + } + } - if (!target_endpoint_for_send_.isEmpty() && - target_endpoint_for_send_ == endpoint && - !pending_send_file_path_.isEmpty()) { - sendPendingFile(endpoint); - } - }, - Qt::QueuedConnection); + discovered_row_by_target_.insert(share_target_id, discovered_targets_.size()); + discovered_targets_.append(row); + emit discoveredTargetsChanged(); +} + +void FileShareTrayController::RemoveTarget(qlonglong share_target_id) { + target_names_.remove(share_target_id); + if (!discovered_row_by_target_.contains(share_target_id)) { + return; + } + + const int removed_index = discovered_row_by_target_.take(share_target_id); + if (removed_index < 0 || removed_index >= discovered_targets_.size()) { + return; + } + + discovered_targets_.removeAt(removed_index); + for (auto it = discovered_row_by_target_.begin(); + it != discovered_row_by_target_.end(); ++it) { + if (it.value() > removed_index) { + it.value() = it.value() - 1; + } + } + emit discoveredTargetsChanged(); +} + +QString FileShareTrayController::TargetName(qlonglong share_target_id) const { + const QString name = target_names_.value(share_target_id).trimmed(); + return name.isEmpty() ? QStringLiteral("Unknown device") : name; +} + +void FileShareTrayController::UpsertTransfer( + qlonglong share_target_id, const QString& target_name, const QString& status, + double progress, qulonglong transferred_bytes, const QString& direction, + const QString& file_name) { + QVariantMap transfer{ + {QStringLiteral("targetId"), share_target_id}, + {QStringLiteral("targetName"), target_name}, + {QStringLiteral("status"), status}, + {QStringLiteral("progress"), progress}, + {QStringLiteral("transferredBytes"), transferred_bytes}, + {QStringLiteral("direction"), direction}, + {QStringLiteral("fileName"), file_name}, }; - listener.rejected_cb = - [this](const std::string& endpoint_id, NearbyConnectionsQtFacade::Status status) { - QMetaObject::invokeMethod( - this, - [this, endpoint = QString::fromStdString(endpoint_id), status]() { - const QString peer = PeerLabelForEndpoint(endpoint); - SetStatus(QStringLiteral("Connection rejected by %1 (%2)") - .arg(peer, StatusToString(status))); - LogLine(QStringLiteral("Connection rejected endpoint=%1 status=%2") - .arg(endpoint, StatusToString(status))); - }, - Qt::QueuedConnection); - }; - - listener.disconnected_cb = [this](const std::string& endpoint_id) { - QMetaObject::invokeMethod( - this, - [this, endpoint = QString::fromStdString(endpoint_id)]() { - // If there is a pending outgoing completion for this endpoint, the - // receiver just disconnected — mirror OutgoingShareSession:: - // OnConnectionDisconnected: cancel the fallback timer and emit the - // final tray notification now. - auto pending_it = pending_outgoing_completions_.find(endpoint); - if (pending_it != pending_outgoing_completions_.end()) { - const PendingOutgoingCompletion completion = pending_it.value(); - pending_outgoing_completions_.erase(pending_it); - auto timer_it = outgoing_disconnect_timers_.find(endpoint); - if (timer_it != outgoing_disconnect_timers_.end()) { - timer_it.value()->stop(); - timer_it.value()->deleteLater(); - outgoing_disconnect_timers_.erase(timer_it); - } - LogLine(QStringLiteral("Receiver disconnected, emitting send result endpoint=%1").arg(endpoint)); - if (completion.success) { - emit requestTrayMessage( - QStringLiteral("Send complete"), - QStringLiteral("%1 sent to %2").arg(completion.file_name, completion.peer)); - } else { - emit requestTrayMessage( - QStringLiteral("Send failed"), - QStringLiteral("%1 failed to send to %2") - .arg(completion.file_name, completion.peer)); - } - } - - const QString peer = PeerLabelForEndpoint(endpoint); - RemoveConnectedDevice(endpoint); - endpoint_mediums_.remove(endpoint); - emit endpointMediumsChanged(); - SetStatus(QStringLiteral("Disconnected from %1").arg(peer)); - LogLine(QStringLiteral("Disconnected endpoint=%1").arg(endpoint)); - }, - Qt::QueuedConnection); - }; - - listener.bandwidth_changed_cb = - [this](const std::string& endpoint_id, NearbyConnectionsQtFacade::Medium medium) { - QMetaObject::invokeMethod( - this, - [this, endpoint = QString::fromStdString(endpoint_id), medium]() { - const QString medium_name = MediumToString(medium); - endpoint_mediums_[endpoint] = medium_name; - emit endpointMediumsChanged(); - UpdateTransferMediumForEndpoint(endpoint, medium_name); - LogLine(QStringLiteral("Bandwidth changed endpoint=%1 medium=%2") - .arg(endpoint, medium_name)); - }, - Qt::QueuedConnection); - }; - - return listener; -} - -NearbyConnectionsQtFacade::DiscoveryListener -FileShareTrayController::BuildDiscoveryListener() { - NearbyConnectionsQtFacade::DiscoveryListener listener; - - listener.endpoint_found_cb = - [this](const std::string& endpoint_id, - const NearbyConnectionsQtFacade::DiscoveredEndpointInfo& info) { - QMetaObject::invokeMethod( - this, - [this, endpoint = QString::fromStdString(endpoint_id), - peer_name = QString::fromStdString(info.peer_name)]() { - SetPeerNameForEndpoint(endpoint, peer_name); - AddDiscoveredDevice(endpoint); - LogLine(QStringLiteral("Discovered endpoint=%1 peer=%2") - .arg(endpoint, PeerLabelForEndpoint(endpoint))); - }, - Qt::QueuedConnection); - }; - - listener.endpoint_lost_cb = [this](const std::string& endpoint_id) { - QMetaObject::invokeMethod( - this, - [this, endpoint = QString::fromStdString(endpoint_id)]() { - RemoveDiscoveredDevice(endpoint); - LogLine(QStringLiteral("Lost endpoint=%1").arg(endpoint)); - }, - Qt::QueuedConnection); - }; - - listener.endpoint_distance_changed_cb = - [this](const std::string& endpoint_id, NearbyConnectionsQtFacade::DistanceInfo info) { - QMetaObject::invokeMethod( - this, - [this, endpoint = QString::fromStdString(endpoint_id), info]() { - LogLine(QStringLiteral("Distance changed endpoint=%1 value=%2") - .arg(endpoint) - .arg(static_cast(info))); - }, - Qt::QueuedConnection); - }; - - return listener; -} - -NearbyConnectionsQtFacade::PayloadListener -FileShareTrayController::BuildPayloadListener() { - NearbyConnectionsQtFacade::PayloadListener listener; - - listener.payload_cb = - [this](const std::string& endpoint_id, NearbyConnectionsQtFacade::Payload payload) { - QMetaObject::invokeMethod( - this, - [this, endpoint = QString::fromStdString(endpoint_id), - payload = std::move(payload)]() { - if (payload.type == NearbyConnectionsQtFacade::Payload::Type::kBytes) { - const QString text = QString::fromUtf8( - reinterpret_cast(payload.bytes.data()), - static_cast(payload.bytes.size())); - if (text.startsWith(QStringLiteral("FILE:"))) { - const QString filename = text.mid(5).trimmed(); - if (!filename.isEmpty()) { - pending_file_names_[endpoint] = filename; - } - } - return; - } - - if (payload.type != NearbyConnectionsQtFacade::Payload::Type::kFile) { - return; - } - - QString file_name = QString::fromStdString(payload.file_name).trimmed(); - if (pending_file_names_.contains(endpoint)) { - file_name = pending_file_names_.take(endpoint); - } - incoming_file_endpoints_[payload.id] = endpoint; - incoming_file_names_[payload.id] = file_name; - incoming_file_paths_[payload.id] = - QString::fromStdString(payload.file_path); - - LogLine(QStringLiteral("Incoming file payload announced endpoint=%1 id=%2 name=%3 path=%4") - .arg(endpoint) - .arg(payload.id) - .arg(file_name, QString::fromStdString(payload.file_path))); - }, - Qt::QueuedConnection); - }; - - listener.payload_progress_cb = - [this](const std::string& endpoint_id, - const NearbyConnectionsQtFacade::PayloadTransferUpdate& update) { - QMetaObject::invokeMethod( - this, - [this, endpoint = QString::fromStdString(endpoint_id), update]() { - const bool is_outgoing_file = - outgoing_file_payload_to_endpoint_.contains(update.payload_id); - const QString direction = - is_outgoing_file ? QStringLiteral("outgoing") : QStringLiteral("incoming"); - - UpsertTransfer(endpoint, update.payload_id, - PayloadStatusToString(update.status), - update.bytes_transferred, update.total_bytes, - direction); - - if (!is_outgoing_file || !IsTerminalPayloadStatus(update.status) || - send_terminal_notified_.contains(update.payload_id)) { - if (!is_outgoing_file && - update.status == NearbyConnectionsQtFacade::PayloadStatus::kSuccess && - incoming_file_paths_.contains(update.payload_id)) { - const QString received_path = incoming_file_paths_.take(update.payload_id); - const QString received_name = incoming_file_names_.take(update.payload_id); - const QString incoming_endpoint = - incoming_file_endpoints_.take(update.payload_id); - const QString final_path = FinalizeReceivedFilePath( - received_path, received_name, update.payload_id); - const QString peer = PeerLabelForEndpoint(incoming_endpoint); - const QString final_name = QFileInfo(final_path).fileName(); - emit requestTrayMessage( - QStringLiteral("File received"), - QStringLiteral("%1 from %2").arg(final_name, peer)); - LogLine(QStringLiteral("Received file endpoint=%1 id=%2 saved=%3") - .arg(incoming_endpoint) - .arg(update.payload_id) - .arg(final_path)); - // Mirror nearby_sharing_service_impl: receiver disconnects - // immediately on transfer complete, which then triggers the - // sender's OnConnectionDisconnected / DelayComplete path. - disconnectDevice(incoming_endpoint); - } else if (!is_outgoing_file && - IsTerminalPayloadStatus(update.status)) { - incoming_file_paths_.remove(update.payload_id); - incoming_file_names_.remove(update.payload_id); - incoming_file_endpoints_.remove(update.payload_id); - } - return; - } - - send_terminal_notified_.insert(update.payload_id); - - const QString peer = PeerLabelForEndpoint(endpoint); - const QString file_name = - outgoing_file_payload_to_name_.value(update.payload_id, - QStringLiteral("file")); - const bool send_success = - update.status == NearbyConnectionsQtFacade::PayloadStatus::kSuccess; - - outgoing_file_payload_to_endpoint_.remove(update.payload_id); - outgoing_file_payload_to_name_.remove(update.payload_id); - send_terminal_notified_.remove(update.payload_id); - - if (!pending_send_file_path_.isEmpty()) { - pending_send_file_path_.clear(); - emit pendingSendFilePathChanged(); - } - if (!pending_send_file_name_.isEmpty()) { - pending_send_file_name_.clear(); - emit pendingSendFileNameChanged(); - } - target_endpoint_for_send_.clear(); - - // Mirror nearby_sharing_service_impl OutgoingShareSession::DelayComplete: - // wait for the receiver to disconnect first so we don't cut the - // connection before in-flight bytes are fully processed. A - // 60-second timer fires disconnectDevice() as a fallback. - LogLine(QStringLiteral("Outgoing transfer complete endpoint=%1 success=%2, waiting for receiver disconnect") - .arg(endpoint) - .arg(send_success ? QStringLiteral("true") : QStringLiteral("false"))); - pending_outgoing_completions_.insert( - endpoint, PendingOutgoingCompletion{file_name, peer, endpoint, send_success}); - - QTimer* timer = new QTimer(this); - timer->setSingleShot(true); - connect(timer, &QTimer::timeout, this, - [this, endpoint, timer]() { - auto it = pending_outgoing_completions_.find(endpoint); - if (it == pending_outgoing_completions_.end()) { - timer->deleteLater(); - outgoing_disconnect_timers_.remove(endpoint); - return; - } - const PendingOutgoingCompletion completion = it.value(); - pending_outgoing_completions_.erase(it); - outgoing_disconnect_timers_.remove(endpoint); - timer->deleteLater(); - LogLine(QStringLiteral("Outgoing disconnect timeout fired endpoint=%1").arg(endpoint)); - if (completion.success) { - emit requestTrayMessage( - QStringLiteral("Send complete"), - QStringLiteral("%1 sent to %2").arg(completion.file_name, completion.peer)); - } else { - emit requestTrayMessage( - QStringLiteral("Send failed"), - QStringLiteral("%1 failed to send to %2") - .arg(completion.file_name, completion.peer)); - } - disconnectDevice(endpoint); - }); - outgoing_disconnect_timers_.insert(endpoint, timer); - timer->start(kOutgoingDisconnectionDelayMs); - }, - Qt::QueuedConnection); - }; - - return listener; -} - -NearbyConnectionsQtFacade::MediumSelection -FileShareTrayController::BuildMediumSelection() const { - NearbyConnectionsQtFacade::MediumSelection selection; - selection.bluetooth = bluetooth_enabled_; - selection.ble = ble_enabled_; - selection.wifi_lan = wifi_lan_enabled_; - selection.wifi_hotspot = wifi_hotspot_enabled_; - selection.web_rtc = web_rtc_enabled_; - return selection; -} - -NearbyConnectionsQtFacade::AdvertisingOptions -FileShareTrayController::BuildAdvertisingOptions() const { - NearbyConnectionsQtFacade::AdvertisingOptions options; - options.strategy = StrategyFromName(connection_strategy_); - options.allowed_mediums = BuildMediumSelection(); - options.auto_upgrade_bandwidth = true; - options.enable_bluetooth_listening = true; - options.enforce_topology_constraints = true; - return options; -} - -NearbyConnectionsQtFacade::DiscoveryOptions -FileShareTrayController::BuildDiscoveryOptions() const { - NearbyConnectionsQtFacade::DiscoveryOptions options; - options.strategy = StrategyFromName(connection_strategy_); - options.allowed_mediums = BuildMediumSelection(); - return options; -} - -NearbyConnectionsQtFacade::ConnectionOptions -FileShareTrayController::BuildConnectionOptions() const { - NearbyConnectionsQtFacade::ConnectionOptions options; - options.allowed_mediums = BuildMediumSelection(); - options.non_disruptive_hotspot_mode = true; - return options; -} - -void FileShareTrayController::acceptIncomingInternal(const QString& endpoint_id) { - const QString endpoint = endpoint_id.trimmed(); - if (endpoint.isEmpty()) { - return; - } - - service_.AcceptConnection( - service_id_, endpoint.toStdString(), BuildPayloadListener(), - [this, endpoint](NearbyConnectionsQtFacade::Status status) { - QMetaObject::invokeMethod( - this, - [this, endpoint, status]() { - const QString peer = PeerLabelForEndpoint(endpoint); - SetStatus(QStringLiteral("AcceptConnection(%1): %2") - .arg(peer, StatusToString(status))); - LogLine(QStringLiteral("AcceptConnection(%1): %2") - .arg(endpoint, StatusToString(status))); - }, - Qt::QueuedConnection); - }); -} - -void FileShareTrayController::requestConnectionForSend(const QString& endpoint_id) { - const QString endpoint = endpoint_id.trimmed(); - if (endpoint.isEmpty()) { - return; - } - - const QString peer = PeerLabelForEndpoint(endpoint); - SetStatus(QStringLiteral("Requesting connection to %1").arg(peer)); - LogLine(QStringLiteral("RequestConnection %1").arg(endpoint)); - - service_.RequestConnection( - service_id_, BuildEndpointInfo(), endpoint.toStdString(), - BuildConnectionOptions(), BuildConnectionListener(), - [this, endpoint](NearbyConnectionsQtFacade::Status status) { - QMetaObject::invokeMethod( - this, - [this, endpoint, status]() { - const QString peer = PeerLabelForEndpoint(endpoint); - SetStatus(QStringLiteral("RequestConnection(%1): %2") - .arg(peer, StatusToString(status))); - LogLine(QStringLiteral("RequestConnection(%1): %2") - .arg(endpoint, StatusToString(status))); - if (status != NearbyConnectionsQtFacade::Status::kSuccess) { - emit requestTrayMessage( - QStringLiteral("Send failed"), - QStringLiteral("Could not connect to %1").arg(peer)); - } - }, - Qt::QueuedConnection); - }); -} - -void FileShareTrayController::sendPendingFile(const QString& endpoint_id) { - const QString endpoint = endpoint_id.trimmed(); - QFileInfo file_info(pending_send_file_path_); - if (endpoint.isEmpty() || pending_send_file_path_.isEmpty() || - !file_info.exists() || !file_info.isFile()) { - SetStatus(QStringLiteral("Selected file is not available")); - emit requestTrayMessage(QStringLiteral("Send failed"), - QStringLiteral("Selected file is not available.")); - return; - } - - const QString peer = PeerLabelForEndpoint(endpoint); - const QString file_name = - pending_send_file_name_.isEmpty() ? file_info.fileName() - : pending_send_file_name_; - - // Send metadata message first to preserve file names on receiver side. - const QString metadata = QStringLiteral("FILE:%1").arg(file_name); - QByteArray metadata_bytes = metadata.toUtf8(); - std::vector metadata_vec(metadata_bytes.begin(), metadata_bytes.end()); - auto metadata_payload = service_.CreateBytesPayload(std::move(metadata_vec)); - - service_.SendPayload( - service_id_, {endpoint.toStdString()}, std::move(metadata_payload), - [this, endpoint](NearbyConnectionsQtFacade::Status status) { - QMetaObject::invokeMethod( - this, - [this, endpoint, status]() { - LogLine(QStringLiteral("Send metadata payload (%1): %2") - .arg(endpoint, StatusToString(status))); - }, - Qt::QueuedConnection); - }); - - NearbyConnectionsQtFacade::Payload file_payload; - file_payload.id = g_local_payload_id.fetch_add(1); - file_payload.type = NearbyConnectionsQtFacade::Payload::Type::kFile; - file_payload.file_path = file_info.absoluteFilePath().toStdString(); - file_payload.file_name = file_name.toStdString(); - file_payload.parent_folder = ""; - - const qlonglong payload_id = file_payload.id; - const qulonglong total_bytes = - static_cast(qMax(0, file_info.size())); - - UpsertTransfer(endpoint, payload_id, QStringLiteral("Queued"), 0, - total_bytes, QStringLiteral("outgoing")); - - outgoing_file_payload_to_endpoint_.insert(payload_id, endpoint); - outgoing_file_payload_to_name_.insert(payload_id, file_name); - send_terminal_notified_.remove(payload_id); - - emit requestTrayMessage( - QStringLiteral("Sending file"), - QStringLiteral("Sending %1 to %2").arg(file_name, peer)); - - service_.SendPayload( - service_id_, {endpoint.toStdString()}, std::move(file_payload), - [this, endpoint, payload_id, file_name, - total_bytes](NearbyConnectionsQtFacade::Status status) { - QMetaObject::invokeMethod( - this, - [this, endpoint, payload_id, file_name, status, total_bytes]() { - LogLine(QStringLiteral("Send file payload (%1, %2): %3") - .arg(endpoint) - .arg(payload_id) - .arg(StatusToString(status))); - - if (status == NearbyConnectionsQtFacade::Status::kSuccess) { - SetStatus(QStringLiteral("Sending %1...").arg(file_name)); - return; - } - - UpsertTransfer(endpoint, payload_id, QStringLiteral("SendFailed"), - 0, total_bytes, QStringLiteral("outgoing")); - - const QString peer = PeerLabelForEndpoint(endpoint); - emit requestTrayMessage( - QStringLiteral("Send failed"), - QStringLiteral("%1 failed to send to %2") - .arg(file_name, peer)); - - outgoing_file_payload_to_endpoint_.remove(payload_id); - outgoing_file_payload_to_name_.remove(payload_id); - send_terminal_notified_.remove(payload_id); - - if (!pending_send_file_path_.isEmpty()) { - pending_send_file_path_.clear(); - emit pendingSendFilePathChanged(); - } - if (!pending_send_file_name_.isEmpty()) { - pending_send_file_name_.clear(); - emit pendingSendFileNameChanged(); - } - - target_endpoint_for_send_.clear(); - disconnectDevice(endpoint); - }, - Qt::QueuedConnection); - }); -} - -void FileShareTrayController::disconnectDevice(const QString& endpoint_id) { - const QString endpoint = endpoint_id.trimmed(); - if (endpoint.isEmpty()) { - return; - } - - service_.DisconnectFromEndpoint( - service_id_, endpoint.toStdString(), - [this, endpoint](NearbyConnectionsQtFacade::Status status) { - QMetaObject::invokeMethod( - this, - [this, endpoint, status]() { - const QString peer = PeerLabelForEndpoint(endpoint); - LogLine(QStringLiteral("Disconnect(%1): %2") - .arg(endpoint, StatusToString(status))); - if (status == NearbyConnectionsQtFacade::Status::kSuccess) { - RemoveConnectedDevice(endpoint); - endpoint_mediums_.remove(endpoint); - emit endpointMediumsChanged(); - SetStatus(QStringLiteral("Disconnected from %1").arg(peer)); - } - }, - Qt::QueuedConnection); - }); -} - -void FileShareTrayController::AddDiscoveredDevice(const QString& endpoint_id) { - if (discovered_devices_.contains(endpoint_id)) { - return; - } - // Skip ourselves – the peer name is set before this is called. - if (PeerLabelForEndpoint(endpoint_id).trimmed() == device_name_.trimmed()) { - return; - } - discovered_devices_.append(endpoint_id); - emit discoveredDevicesChanged(); -} - -void FileShareTrayController::RemoveDiscoveredDevice(const QString& endpoint_id) { - if (!discovered_devices_.removeOne(endpoint_id)) { - return; - } - emit discoveredDevicesChanged(); -} - -void FileShareTrayController::AddConnectedDevice(const QString& endpoint_id) { - if (connected_devices_.contains(endpoint_id)) { - return; - } - connected_devices_.append(endpoint_id); - emit connectedDevicesChanged(); -} - -void FileShareTrayController::RemoveConnectedDevice(const QString& endpoint_id) { - if (!connected_devices_.removeOne(endpoint_id)) { - return; - } - emit connectedDevicesChanged(); -} - -void FileShareTrayController::SetPeerNameForEndpoint(const QString& endpoint_id, - const QString& peer_name) { - const QString endpoint = endpoint_id.trimmed(); - if (endpoint.isEmpty()) { - return; - } - - const QString trimmed_name = peer_name.trimmed(); - const QString previous = endpoint_peer_names_.value(endpoint).trimmed(); - if (previous == trimmed_name) { - return; - } - - if (trimmed_name.isEmpty()) { - endpoint_peer_names_.remove(endpoint); - } else { - endpoint_peer_names_[endpoint] = trimmed_name; - } - - emit discoveredDevicesChanged(); - emit connectedDevicesChanged(); -} - -QString FileShareTrayController::PeerLabelForEndpoint(const QString& endpoint_id) const { - const QString endpoint = endpoint_id.trimmed(); - if (endpoint.isEmpty()) { - return QStringLiteral("Unknown device"); - } - - const QString peer_name = endpoint_peer_names_.value(endpoint).trimmed(); - return peer_name.isEmpty() ? QStringLiteral("Unknown device") : peer_name; -} - -QString FileShareTrayController::FinalizeReceivedFilePath( - const QString& received_path, const QString& received_file_name, - qlonglong payload_id) const { - const QString source = received_path.trimmed(); - if (source.isEmpty()) { - return source; - } - - QFileInfo source_info(source); - const QString source_abs = source_info.absoluteFilePath(); - const QString source_dir = source_info.absolutePath(); - - QString target_name = QFileInfo(received_file_name.trimmed()).fileName(); - if (target_name.isEmpty()) { - target_name = source_info.fileName(); - } - if (target_name.isEmpty()) { - target_name = QStringLiteral("payload_%1.bin").arg(payload_id); - } - - const QFileInfo target_name_info(target_name); - const QString stem = - target_name_info.completeBaseName().isEmpty() - ? target_name_info.fileName() - : target_name_info.completeBaseName(); - const QString suffix = target_name_info.completeSuffix(); - QString target_path = QDir(source_dir).filePath(target_name); - - int suffix_index = 1; - while (target_path != source_abs && QFileInfo::exists(target_path)) { - const QString next_name = - suffix.isEmpty() - ? QStringLiteral("%1_%2").arg(stem).arg(suffix_index) - : QStringLiteral("%1_%2.%3") - .arg(stem) - .arg(suffix_index) - .arg(suffix); - target_path = QDir(source_dir).filePath(next_name); - ++suffix_index; - } - - if (target_path == source_abs) { - return source_abs; - } - - if (QFile::rename(source_abs, target_path)) { - return target_path; - } - - if (QFile::copy(source_abs, target_path)) { - QFile::remove(source_abs); - return target_path; - } - - return source_abs; -} - -void FileShareTrayController::UpsertTransfer(const QString& endpoint_id, - qlonglong payload_id, - const QString& status, - qulonglong bytes_transferred, - qulonglong total_bytes, - const QString& direction) { - const QString medium = mediumForEndpoint(endpoint_id); - const double progress = - total_bytes > 0 - ? static_cast(bytes_transferred) / - static_cast(total_bytes) - : 0.0; - - QVariantMap transfer{{QStringLiteral("payloadId"), payload_id}, - {QStringLiteral("endpointId"), endpoint_id}, - {QStringLiteral("status"), status}, - {QStringLiteral("bytesTransferred"), bytes_transferred}, - {QStringLiteral("totalBytes"), total_bytes}, - {QStringLiteral("progress"), progress}, - {QStringLiteral("medium"), medium}, - {QStringLiteral("direction"), direction}}; - - if (transfer_row_for_payload_.contains(payload_id)) { - const int row = transfer_row_for_payload_.value(payload_id); - if (row >= 0 && row < transfers_.size()) { - transfers_[row] = transfer; + if (transfer_row_by_target_.contains(share_target_id)) { + const int row_index = transfer_row_by_target_.value(share_target_id); + if (row_index >= 0 && row_index < transfers_.size()) { + transfers_[row_index] = transfer; emit transfersChanged(); return; } } - transfer_row_for_payload_.insert(payload_id, transfers_.size()); + transfer_row_by_target_.insert(share_target_id, transfers_.size()); transfers_.append(transfer); emit transfersChanged(); } -void FileShareTrayController::UpdateTransferMediumForEndpoint( - const QString& endpoint_id, const QString& medium) { - bool changed = false; - for (int i = 0; i < transfers_.size(); ++i) { - QVariantMap row = transfers_[i].toMap(); - if (row.value(QStringLiteral("endpointId")).toString() != endpoint_id) { - continue; - } - row[QStringLiteral("medium")] = medium; - transfers_[i] = row; - changed = true; - } - if (changed) { - emit transfersChanged(); - } -} - void FileShareTrayController::SetStatus(const QString& status) { if (status == status_message_) { return; @@ -1192,8 +577,10 @@ bool FileShareTrayController::HasActiveTransfers() const { for (const QVariant& row_value : transfers_) { const QVariantMap row = row_value.toMap(); const QString status = row.value(QStringLiteral("status")).toString(); - if (status == QStringLiteral("InProgress") || - status == QStringLiteral("Queued")) { + if (status == QStringLiteral("Queued") || status == QStringLiteral("Connecting") || + status == QStringLiteral("AwaitingLocalConfirmation") || + status == QStringLiteral("AwaitingRemoteAcceptance") || + status == QStringLiteral("InProgress")) { return true; } } @@ -1222,92 +609,35 @@ void FileShareTrayController::ReopenLogFile() { log_file_.open(QIODevice::Append | QIODevice::Text | QIODevice::WriteOnly); } -QString FileShareTrayController::StatusToString( - NearbyConnectionsQtFacade::Status status) { - switch (status) { - case NearbyConnectionsQtFacade::Status::kSuccess: - return QStringLiteral("Success"); - case NearbyConnectionsQtFacade::Status::kError: - return QStringLiteral("Error"); - case NearbyConnectionsQtFacade::Status::kOutOfOrderApiCall: - return QStringLiteral("OutOfOrderApiCall"); - case NearbyConnectionsQtFacade::Status::kAlreadyHaveActiveStrategy: - return QStringLiteral("AlreadyHaveActiveStrategy"); - case NearbyConnectionsQtFacade::Status::kAlreadyAdvertising: - return QStringLiteral("AlreadyAdvertising"); - case NearbyConnectionsQtFacade::Status::kAlreadyDiscovering: - return QStringLiteral("AlreadyDiscovering"); - case NearbyConnectionsQtFacade::Status::kAlreadyListening: - return QStringLiteral("AlreadyListening"); - case NearbyConnectionsQtFacade::Status::kEndpointIOError: - return QStringLiteral("EndpointIOError"); - case NearbyConnectionsQtFacade::Status::kEndpointUnknown: - return QStringLiteral("EndpointUnknown"); - case NearbyConnectionsQtFacade::Status::kConnectionRejected: - return QStringLiteral("ConnectionRejected"); - case NearbyConnectionsQtFacade::Status::kAlreadyConnectedToEndpoint: - return QStringLiteral("AlreadyConnectedToEndpoint"); - case NearbyConnectionsQtFacade::Status::kNotConnectedToEndpoint: - return QStringLiteral("NotConnectedToEndpoint"); - case NearbyConnectionsQtFacade::Status::kBluetoothError: - return QStringLiteral("BluetoothError"); - case NearbyConnectionsQtFacade::Status::kBleError: - return QStringLiteral("BleError"); - case NearbyConnectionsQtFacade::Status::kWifiLanError: - return QStringLiteral("WifiLanError"); - case NearbyConnectionsQtFacade::Status::kPayloadUnknown: - return QStringLiteral("PayloadUnknown"); - case NearbyConnectionsQtFacade::Status::kReset: - return QStringLiteral("Reset"); - case NearbyConnectionsQtFacade::Status::kTimeout: - return QStringLiteral("Timeout"); - case NearbyConnectionsQtFacade::Status::kUnknown: - return QStringLiteral("Unknown"); - case NearbyConnectionsQtFacade::Status::kNextValue: - return QStringLiteral("NextValue"); - } - return QStringLiteral("Unknown"); +QString FileShareTrayController::StatusToString(NearbySharingApi::StatusCode status) { + return QString::fromStdString(NearbySharingApi::StatusCodeToString(status)); } -QString FileShareTrayController::PayloadStatusToString( - NearbyConnectionsQtFacade::PayloadStatus status) { - switch (status) { - case NearbyConnectionsQtFacade::PayloadStatus::kSuccess: - return QStringLiteral("Success"); - case NearbyConnectionsQtFacade::PayloadStatus::kFailure: - return QStringLiteral("Failure"); - case NearbyConnectionsQtFacade::PayloadStatus::kInProgress: - return QStringLiteral("InProgress"); - case NearbyConnectionsQtFacade::PayloadStatus::kCanceled: - return QStringLiteral("Canceled"); - } - return QStringLiteral("Unknown"); +QString FileShareTrayController::TransferStatusToString( + NearbySharingApi::TransferStatus status) { + return QString::fromStdString(NearbySharingApi::TransferStatusToString(status)); } -QString FileShareTrayController::MediumToString(NearbyConnectionsQtFacade::Medium medium) { - switch (medium) { - case NearbyConnectionsQtFacade::Medium::kUnknown: - return QStringLiteral("Unknown"); - case NearbyConnectionsQtFacade::Medium::kMdns: - return QStringLiteral("mDNS"); - case NearbyConnectionsQtFacade::Medium::kBluetooth: - return QStringLiteral("Bluetooth"); - case NearbyConnectionsQtFacade::Medium::kWifiHotspot: - return QStringLiteral("WiFiHotspot"); - case NearbyConnectionsQtFacade::Medium::kBle: - return QStringLiteral("BLE"); - case NearbyConnectionsQtFacade::Medium::kWifiLan: - return QStringLiteral("WiFiLAN"); - case NearbyConnectionsQtFacade::Medium::kWifiAware: - return QStringLiteral("WiFiAware"); - case NearbyConnectionsQtFacade::Medium::kNfc: - return QStringLiteral("NFC"); - case NearbyConnectionsQtFacade::Medium::kWifiDirect: - return QStringLiteral("WiFiDirect"); - case NearbyConnectionsQtFacade::Medium::kWebRtc: - return QStringLiteral("WebRTC"); - case NearbyConnectionsQtFacade::Medium::kBleL2Cap: - return QStringLiteral("BLEL2CAP"); +bool FileShareTrayController::IsFinalTransferStatus( + NearbySharingApi::TransferStatus status) { + switch (status) { + case NearbySharingApi::TransferStatus::kComplete: + case NearbySharingApi::TransferStatus::kFailed: + case NearbySharingApi::TransferStatus::kRejected: + case NearbySharingApi::TransferStatus::kCancelled: + case NearbySharingApi::TransferStatus::kTimedOut: + case NearbySharingApi::TransferStatus::kMediaUnavailable: + case NearbySharingApi::TransferStatus::kNotEnoughSpace: + case NearbySharingApi::TransferStatus::kUnsupportedAttachmentType: + case NearbySharingApi::TransferStatus::kDeviceAuthenticationFailed: + case NearbySharingApi::TransferStatus::kIncompletePayloads: + return true; + case NearbySharingApi::TransferStatus::kUnknown: + case NearbySharingApi::TransferStatus::kConnecting: + case NearbySharingApi::TransferStatus::kAwaitingLocalConfirmation: + case NearbySharingApi::TransferStatus::kAwaitingRemoteAcceptance: + case NearbySharingApi::TransferStatus::kInProgress: + return false; } - return QStringLiteral("Unknown"); + return false; } diff --git a/sharing/linux/qml_tray_app/file_share_tray_controller.h b/sharing/linux/qml_tray_app/file_share_tray_controller.h index e16cdefb..a1563ae6 100644 --- a/sharing/linux/qml_tray_app/file_share_tray_controller.h +++ b/sharing/linux/qml_tray_app/file_share_tray_controller.h @@ -5,19 +5,15 @@ #include #include -#include #include #include -#include #include -#include -#include -#include +#include -#include "sharing/linux/nearby_connections_qt_facade.h" +#include "sharing/linux/nearby_sharing_api.h" -using NearbyConnectionsQtFacade = nearby::sharing::NearbyConnectionsQtFacade; +using NearbySharingApi = nearby::sharing::linux::NearbySharingApi; class FileShareTrayController : public QObject { Q_OBJECT @@ -27,18 +23,10 @@ class FileShareTrayController : public QObject { Q_PROPERTY(bool running READ running NOTIFY runningChanged) Q_PROPERTY(QString pendingSendFileName READ pendingSendFileName NOTIFY pendingSendFileNameChanged) Q_PROPERTY(QString pendingSendFilePath READ pendingSendFilePath NOTIFY pendingSendFilePathChanged) - Q_PROPERTY(QStringList discoveredDevices READ discoveredDevices NOTIFY discoveredDevicesChanged) - Q_PROPERTY(QStringList connectedDevices READ connectedDevices NOTIFY connectedDevicesChanged) - Q_PROPERTY(QVariantMap endpointMediums READ endpointMediums NOTIFY endpointMediumsChanged) + Q_PROPERTY(QVariantList discoveredTargets READ discoveredTargets NOTIFY discoveredTargetsChanged) Q_PROPERTY(QVariantList transfers READ transfers NOTIFY transfersChanged) Q_PROPERTY(bool autoAcceptIncoming READ autoAcceptIncoming WRITE setAutoAcceptIncoming NOTIFY autoAcceptIncomingChanged) - Q_PROPERTY(bool bluetoothEnabled READ bluetoothEnabled WRITE setBluetoothEnabled NOTIFY bluetoothEnabledChanged) - Q_PROPERTY(bool bleEnabled READ bleEnabled WRITE setBleEnabled NOTIFY bleEnabledChanged) - Q_PROPERTY(bool wifiLanEnabled READ wifiLanEnabled WRITE setWifiLanEnabled NOTIFY wifiLanEnabledChanged) - Q_PROPERTY(bool wifiHotspotEnabled READ wifiHotspotEnabled WRITE setWifiHotspotEnabled NOTIFY wifiHotspotEnabledChanged) - Q_PROPERTY(bool webRtcEnabled READ webRtcEnabled WRITE setWebRtcEnabled NOTIFY webRtcEnabledChanged) - Q_PROPERTY(QString connectionStrategy READ connectionStrategy WRITE setConnectionStrategy NOTIFY connectionStrategyChanged) - Q_PROPERTY(QString serviceId READ serviceId WRITE setServiceId NOTIFY serviceIdChanged) + Q_PROPERTY(QString qrCodeUrl READ qrCodeUrl NOTIFY qrCodeUrlChanged) Q_PROPERTY(QString logPath READ logPath WRITE setLogPath NOTIFY logPathChanged) public: @@ -56,34 +44,13 @@ class FileShareTrayController : public QObject { QString pendingSendFileName() const { return pending_send_file_name_; } QString pendingSendFilePath() const { return pending_send_file_path_; } - QStringList discoveredDevices() const { return discovered_devices_; } - QStringList connectedDevices() const { return connected_devices_; } - QVariantMap endpointMediums() const { return endpoint_mediums_; } + QVariantList discoveredTargets() const { return discovered_targets_; } QVariantList transfers() const { return transfers_; } bool autoAcceptIncoming() const { return auto_accept_incoming_; } void setAutoAcceptIncoming(bool enabled); - bool bluetoothEnabled() const { return bluetooth_enabled_; } - void setBluetoothEnabled(bool enabled); - - bool bleEnabled() const { return ble_enabled_; } - void setBleEnabled(bool enabled); - - bool wifiLanEnabled() const { return wifi_lan_enabled_; } - void setWifiLanEnabled(bool enabled); - - bool wifiHotspotEnabled() const { return wifi_hotspot_enabled_; } - void setWifiHotspotEnabled(bool enabled); - - bool webRtcEnabled() const { return web_rtc_enabled_; } - void setWebRtcEnabled(bool enabled); - - QString connectionStrategy() const { return connection_strategy_; } - void setConnectionStrategy(const QString& strategy); - - QString serviceId() const { return QString::fromStdString(service_id_); } - void setServiceId(const QString& service_id); + QString qrCodeUrl() const { return qr_code_url_; } QString logPath() const { return log_path_; } void setLogPath(const QString& path); @@ -92,9 +59,7 @@ class FileShareTrayController : public QObject { Q_INVOKABLE void stop(); Q_INVOKABLE void switchToReceiveMode(); Q_INVOKABLE void switchToSendModeWithFile(const QString& file_path); - Q_INVOKABLE void sendPendingFileToEndpoint(const QString& endpoint_id); - Q_INVOKABLE QString mediumForEndpoint(const QString& endpoint_id) const; - Q_INVOKABLE QString peerNameForEndpoint(const QString& endpoint_id) const; + Q_INVOKABLE void sendPendingFileToTarget(qlonglong share_target_id); Q_INVOKABLE void clearTransfers(); Q_INVOKABLE void hideToTray(); @@ -105,117 +70,63 @@ class FileShareTrayController : public QObject { void runningChanged(); void pendingSendFileNameChanged(); void pendingSendFilePathChanged(); - void discoveredDevicesChanged(); - void connectedDevicesChanged(); - void endpointMediumsChanged(); + void discoveredTargetsChanged(); void transfersChanged(); void autoAcceptIncomingChanged(); - void bluetoothEnabledChanged(); - void bleEnabledChanged(); - void wifiLanEnabledChanged(); - void wifiHotspotEnabledChanged(); - void webRtcEnabledChanged(); - void connectionStrategyChanged(); - void serviceIdChanged(); + void qrCodeUrlChanged(); void logPathChanged(); void requestTrayMessage(const QString& title, const QString& body); private: + void CreateService(); + void AttachServiceListeners(); + void startSendMode(); void startReceiveMode(); void LoadSettings(); void SaveSettings() const; - std::vector BuildEndpointInfo() const; + void UpsertTarget(qlonglong share_target_id, const QString& name, + bool is_incoming); + void RemoveTarget(qlonglong share_target_id); + QString TargetName(qlonglong share_target_id) const; - NearbyConnectionsQtFacade::ConnectionListener BuildConnectionListener(); - NearbyConnectionsQtFacade::DiscoveryListener BuildDiscoveryListener(); - NearbyConnectionsQtFacade::PayloadListener BuildPayloadListener(); - - NearbyConnectionsQtFacade::AdvertisingOptions BuildAdvertisingOptions() const; - NearbyConnectionsQtFacade::DiscoveryOptions BuildDiscoveryOptions() const; - NearbyConnectionsQtFacade::ConnectionOptions BuildConnectionOptions() const; - NearbyConnectionsQtFacade::MediumSelection BuildMediumSelection() const; - - void acceptIncomingInternal(const QString& endpoint_id); - void requestConnectionForSend(const QString& endpoint_id); - void sendPendingFile(const QString& endpoint_id); - void disconnectDevice(const QString& endpoint_id); - - void AddDiscoveredDevice(const QString& endpoint_id); - void RemoveDiscoveredDevice(const QString& endpoint_id); - void AddConnectedDevice(const QString& endpoint_id); - void RemoveConnectedDevice(const QString& endpoint_id); - void SetPeerNameForEndpoint(const QString& endpoint_id, - const QString& peer_name); - QString PeerLabelForEndpoint(const QString& endpoint_id) const; - - QString FinalizeReceivedFilePath(const QString& received_path, - const QString& received_file_name, - qlonglong payload_id) const; - - void UpsertTransfer(const QString& endpoint_id, qlonglong payload_id, - const QString& status, qulonglong bytes_transferred, - qulonglong total_bytes, const QString& direction); - void UpdateTransferMediumForEndpoint(const QString& endpoint_id, - const QString& medium); + void UpsertTransfer(qlonglong share_target_id, const QString& target_name, + const QString& status, double progress, + qulonglong transferred_bytes, const QString& direction, + const QString& file_name); void SetStatus(const QString& status); bool HasActiveTransfers() const; void LogLine(const QString& line); void ReopenLogFile(); - static QString StatusToString(NearbyConnectionsQtFacade::Status status); - static QString PayloadStatusToString( - NearbyConnectionsQtFacade::PayloadStatus status); - static QString MediumToString(NearbyConnectionsQtFacade::Medium medium); + static QString StatusToString(NearbySharingApi::StatusCode status); + static QString TransferStatusToString(NearbySharingApi::TransferStatus status); + static bool IsFinalTransferStatus(NearbySharingApi::TransferStatus status); - NearbyConnectionsQtFacade service_; + std::unique_ptr service_; QString mode_ = QStringLiteral("Receive"); - QString device_name_ = QStringLiteral("NearbyQtFile"); - std::string service_id_ = "com.nearby.qml.tray"; + QString device_name_ = QStringLiteral("NearbyLinux"); QString status_message_ = QStringLiteral("Idle"); bool running_ = false; bool auto_accept_incoming_ = true; - bool bluetooth_enabled_ = true; - bool ble_enabled_ = true; - bool wifi_lan_enabled_ = true; - bool wifi_hotspot_enabled_ = true; - bool web_rtc_enabled_ = false; - QString connection_strategy_ = QStringLiteral("P2pPointToPoint"); + QString qr_code_url_; QString log_path_ = QStringLiteral("/tmp/nearby_qml_file_tray.log"); QString pending_send_file_path_; QString pending_send_file_name_; - QString target_endpoint_for_send_; + qlonglong pending_send_target_id_ = 0; - QStringList discovered_devices_; - QStringList connected_devices_; - QHash endpoint_peer_names_; - QVariantMap endpoint_mediums_; + QVariantList discovered_targets_; + QHash discovered_row_by_target_; + QHash target_names_; QVariantList transfers_; - QHash transfer_row_for_payload_; - - QHash pending_file_names_; - QHash incoming_file_paths_; - QHash incoming_file_names_; - QHash incoming_file_endpoints_; - QHash outgoing_file_payload_to_endpoint_; - QHash outgoing_file_payload_to_name_; - QSet send_terminal_notified_; - - struct PendingOutgoingCompletion { - QString file_name; - QString peer; - QString endpoint; - bool success = false; - }; - QHash pending_outgoing_completions_; - QHash outgoing_disconnect_timers_; + QHash transfer_row_by_target_; QFile log_file_; }; diff --git a/sharing/linux/qml_tray_app/file_share_tray_main.cpp b/sharing/linux/qml_tray_app/file_share_tray_main.cpp index f2663544..03e08571 100644 --- a/sharing/linux/qml_tray_app/file_share_tray_main.cpp +++ b/sharing/linux/qml_tray_app/file_share_tray_main.cpp @@ -46,9 +46,6 @@ QIcon BuildTintedSymbolicIcon(const QString& source, const QColor& color) { } // namespace int main(int argc, char* argv[]) { - nearby::sharing::NearbyConnectionsQtFacade::SetBleL2capFlagOverrides( - true, false); - QApplication app(argc, argv); app.setQuitOnLastWindowClosed(false);