added tray support for qt application

This commit is contained in:
Lasan Mahaliyana
2026-08-01 04:23:37 +05:30
parent 459b46462a
commit 24e15f5692
10 changed files with 779 additions and 11 deletions
@@ -72,6 +72,7 @@ jobs:
libbluetooth-dev \
libxkbcommon-dev \
libxcb-cursor0 \
dbus-daemon \
patchelf \
qt6-base-dev \
qt6-declarative-dev \
@@ -80,6 +81,14 @@ jobs:
qt6-tools-dev-tools \
libqrencode-dev
- name: Test tray application lifecycle
run: |
bazel test \
--@com_google_protobuf//bazel/toolchains:prefer_prebuilt_protoc=true \
--copt='-DGITHUB_BUILD' \
//sharing/linux/app:application_controller_test \
//sharing/linux/app:backend_test
- name: Build minimal AppImage inputs
run: |
bazel build \
@@ -88,7 +97,9 @@ jobs:
//sharing/linux/app:appimage
- name: Package and validate minimal AppImage
run: ./sharing/linux/app/packaging/package_minimal_appimage.sh
env:
RUN_BACKEND_SMOKE_TEST: 1
run: dbus-run-session -- ./sharing/linux/app/packaging/package_minimal_appimage.sh
- name: Generate SHA256 checksum
working-directory: sharing/linux/dist-minimal
@@ -100,6 +111,9 @@ jobs:
- name: Validate AppImage output
run: |
test -x sharing/linux/dist-minimal/QuickShare-x86_64.AppImage
test -x sharing/linux/dist-minimal/QuickShare.AppDir/AppRun
test -e sharing/linux/dist-minimal/QuickShare.AppDir/usr/lib/libQt6DBus.so.6
test -s sharing/linux/dist-minimal/backend-smoke-test.log
file sharing/linux/dist-minimal/QuickShare-x86_64.AppImage
- name: Publish GitHub Release
+41 -1
View File
@@ -1,6 +1,6 @@
load("@rules_cc//cc:cc_library.bzl", "cc_library")
load("@rules_cc//cc:cc_test.bzl", "cc_test")
load("@rules_qt//:qt.bzl", "qt_cc_binary", "qt_cc_library", "qt_resource_via_qrc")
load("@rules_qt//:qt.bzl", "qt_cc_binary", "qt_cc_library", "qt_cc_test", "qt_resource_via_qrc")
load("@rules_shell//shell:sh_binary.bzl", "sh_binary")
load("@hedron_compile_commands//:refresh_compile_commands.bzl", "refresh_compile_commands")
@@ -69,6 +69,45 @@ qt_cc_library(
],
)
qt_cc_library(
name = "application_controller",
srcs = ["application_controller.cc"],
hdrs = ["application_controller.h"],
deps = [
":backend",
"@rules_qt//:qt_core",
"@rules_qt//:qt_dbus",
"@rules_qt//:qt_gui",
"@rules_qt//:qt_hdrs",
"@rules_qt//:qt_quick",
"@rules_qt//:qt_widgets",
],
)
qt_cc_test(
name = "application_controller_test",
size = "small",
srcs = ["application_controller_test.cc"],
env = {"QT_QPA_PLATFORM": "offscreen"},
deps = [
":application_controller",
"@com_google_googletest//:gtest_main",
"@rules_qt//:qt_core",
"@rules_qt//:qt_gui",
"@rules_qt//:qt_hdrs",
"@rules_qt//:qt_labs_platform",
"@rules_qt//:qt_opengl",
"@rules_qt//:qt_qml",
"@rules_qt//:qt_qml_meta",
"@rules_qt//:qt_qml_workerscript",
"@rules_qt//:qt_quick",
"@rules_qt//:qt_quick_controls2",
"@rules_qt//:qt_quick_layouts",
"@rules_qt//:qt_quick_shapes",
"@rules_qt//:qt_widgets",
],
)
cc_test(
name = "backend_test",
size = "small",
@@ -134,6 +173,7 @@ qt_cc_binary(
"QT_QPA_PLATFORM": "wayland;xcb",
},
deps = [
":application_controller",
":app_resources",
":backend",
":native_file_logging",
+337
View File
@@ -0,0 +1,337 @@
#include "sharing/linux/app/application_controller.h"
#include <QApplication>
#include <QDBusConnection>
#include <QDBusInterface>
#include <QDBusReply>
#include <QDebug>
#include <QIcon>
#include <QMenu>
#include <QQuickWindow>
#include <QStringList>
#include <QSystemTrayIcon>
#include <QTimer>
#include <QVariantMap>
#include "sharing/linux/app/backend.h"
namespace {
constexpr char kApplicationService[] = "com.google.quickshare";
constexpr char kApplicationPath[] = "/com/google/quickshare";
constexpr char kApplicationInterface[] = "com.google.quickshare";
constexpr char kNotificationService[] = "org.freedesktop.Notifications";
constexpr char kNotificationPath[] = "/org/freedesktop/Notifications";
constexpr char kNotificationInterface[] = "org.freedesktop.Notifications";
constexpr int kOfferTimeoutMilliseconds = 45 * 1000;
constexpr int kResultTimeoutMilliseconds = 10 * 1000;
QString FormatBytes(qint64 bytes) {
if (bytes <= 0) {
return {};
}
static constexpr const char* kUnits[] = {"B", "KB", "MB", "GB", "TB"};
double value = static_cast<double>(bytes);
int unit = 0;
while (value >= 1024.0 && unit < 4) {
value /= 1024.0;
++unit;
}
const int precision = unit == 0 ? 0 : (value >= 10.0 ? 1 : 2);
return QStringLiteral("%1 %2").arg(QString::number(value, 'f', precision),
QString::fromLatin1(kUnits[unit]));
}
} // namespace
ApplicationController::ApplicationController(QApplication& application,
QObject* parent)
: QObject(parent), application_(application) {
QDBusConnection bus = QDBusConnection::sessionBus();
if (bus.isConnected()) {
bus.connect(kNotificationService, kNotificationPath, kNotificationInterface,
QStringLiteral("ActionInvoked"), this,
SLOT(OnNotificationAction(uint, QString)));
bus.connect(kNotificationService, kNotificationPath, kNotificationInterface,
QStringLiteral("NotificationClosed"), this,
SLOT(OnNotificationClosed(uint, uint)));
QDBusInterface notifications(kNotificationService, kNotificationPath,
kNotificationInterface, bus);
const QDBusReply<QStringList> capabilities =
notifications.call(QStringLiteral("GetCapabilities"));
actions_supported_ =
capabilities.isValid() && capabilities.value().contains("actions");
}
}
ApplicationController::~ApplicationController() {
const QList<qint64> pending_offers = transfer_to_notification_.keys();
for (qint64 share_target_id : pending_offers) {
CloseOfferNotification(share_target_id);
}
if (tray_icon_ != nullptr) {
tray_icon_->setContextMenu(nullptr);
}
QDBusConnection bus = QDBusConnection::sessionBus();
if (owns_dbus_service_ && bus.isConnected()) {
bus.unregisterService(kApplicationService);
}
if (bus.isConnected()) {
bus.unregisterObject(kApplicationPath);
}
}
bool ApplicationController::ClaimSingleInstance() {
QDBusConnection bus = QDBusConnection::sessionBus();
if (!bus.isConnected()) {
qWarning() << "Session D-Bus is unavailable; single-instance activation "
"and actionable notifications are disabled";
return true;
}
if (!bus.registerObject(kApplicationPath, this,
QDBusConnection::ExportScriptableSlots)) {
qWarning() << "Unable to export QuickShare activation object:"
<< bus.lastError().message();
}
if (bus.registerService(kApplicationService)) {
owns_dbus_service_ = true;
return true;
}
QDBusInterface existing(kApplicationService, kApplicationPath,
kApplicationInterface, bus);
const QDBusReply<void> activation =
existing.call(QStringLiteral("ShowWindow"));
if (activation.isValid()) {
return false;
}
qWarning() << "Unable to claim or activate the QuickShare D-Bus service:"
<< activation.error().message();
return true;
}
void ApplicationController::InitializeTray() {
tray_menu_ = std::make_unique<QMenu>();
tray_menu_->addAction(tr("Show QuickShare"), this,
&ApplicationController::ShowWindow);
tray_menu_->addSeparator();
tray_menu_->addAction(tr("Quit"), this, &ApplicationController::Quit);
tray_icon_ = new QSystemTrayIcon(
QIcon(QStringLiteral(":/icons/quickshare.svg")), this);
tray_icon_->setToolTip(tr("QuickShare"));
tray_icon_->setContextMenu(tray_menu_.get());
connect(tray_icon_, &QSystemTrayIcon::activated, this,
[this](QSystemTrayIcon::ActivationReason reason) {
if (reason == QSystemTrayIcon::Trigger ||
reason == QSystemTrayIcon::DoubleClick) {
ShowWindow();
}
});
tray_icon_->show();
auto* tray_availability_timer = new QTimer(this);
tray_availability_timer->setInterval(2000);
connect(tray_availability_timer, &QTimer::timeout, this,
&ApplicationController::UpdateTrayAvailability);
tray_availability_timer->start();
UpdateTrayAvailability();
}
void ApplicationController::SetBackend(Backend* backend) {
if (backend == nullptr) {
return;
}
connect(backend, &Backend::incomingOffer, this,
&ApplicationController::OnIncomingOffer);
connect(backend, &Backend::incomingOfferResolved, this,
&ApplicationController::OnIncomingOfferResolved);
connect(backend, &Backend::transferFinished, this,
&ApplicationController::OnTransferFinished);
connect(this, &ApplicationController::acceptRequested, backend,
&Backend::accept);
connect(this, &ApplicationController::rejectRequested, backend,
&Backend::reject);
}
void ApplicationController::AttachWindow(QObject* root_object) {
window_ = qobject_cast<QQuickWindow*>(root_object);
if (window_ != nullptr && pending_activation_) {
pending_activation_ = false;
ShowWindow();
}
}
void ApplicationController::ShowWindow() {
if (window_ == nullptr) {
pending_activation_ = true;
return;
}
window_->show();
window_->raise();
window_->requestActivate();
}
void ApplicationController::Quit() {
application_.quit();
}
void ApplicationController::OnIncomingOffer(qint64 share_target_id,
const QString& device_name,
int attachment_count,
qint64 total_bytes) {
if (!actions_supported_) {
ShowWindow();
return;
}
CloseOfferNotification(share_target_id);
const QStringList actions = {
QStringLiteral("default"), QStringLiteral("Open"),
QStringLiteral("accept"), QStringLiteral("Accept"),
QStringLiteral("decline"), QStringLiteral("Decline"),
};
const uint notification_id = SendNotification(
tr("Incoming share"),
IncomingOfferBody(device_name, attachment_count, total_bytes), actions,
kOfferTimeoutMilliseconds);
if (notification_id == 0) {
ShowWindow();
return;
}
notification_to_transfer_.insert(notification_id, share_target_id);
transfer_to_notification_.insert(share_target_id, notification_id);
}
void ApplicationController::OnIncomingOfferResolved(qint64 share_target_id) {
CloseOfferNotification(share_target_id);
}
void ApplicationController::OnTransferFinished(qint64 share_target_id,
bool receive_mode,
const QString& device_name,
const QString& status) {
CloseOfferNotification(share_target_id);
if (!receive_mode ||
!background_accepted_transfers_.remove(share_target_id)) {
return;
}
const bool complete = status == QStringLiteral("kComplete");
const QString peer =
device_name.isEmpty() ? tr("the nearby device") : device_name;
SendNotification(complete ? tr("Share received") : tr("Share failed"),
complete
? tr("Finished receiving from %1").arg(peer)
: tr("Could not receive the share from %1").arg(peer),
{}, kResultTimeoutMilliseconds);
}
void ApplicationController::OnNotificationAction(uint notification_id,
const QString& action_key) {
const auto transfer = notification_to_transfer_.constFind(notification_id);
if (transfer == notification_to_transfer_.cend()) {
return;
}
const qint64 share_target_id = transfer.value();
if (action_key == QStringLiteral("default")) {
ShowWindow();
return;
}
if (action_key == QStringLiteral("accept")) {
background_accepted_transfers_.insert(share_target_id);
emit acceptRequested(share_target_id);
} else if (action_key == QStringLiteral("decline")) {
emit rejectRequested(share_target_id);
} else {
return;
}
CloseOfferNotification(share_target_id);
}
void ApplicationController::OnNotificationClosed(uint notification_id,
uint /*reason*/) {
RemoveNotificationMapping(notification_id);
}
void ApplicationController::UpdateTrayAvailability() {
const bool available = QSystemTrayIcon::isSystemTrayAvailable();
application_.setQuitOnLastWindowClosed(!available);
if (close_to_tray_ == available) {
return;
}
close_to_tray_ = available;
emit closeToTrayChanged();
}
uint ApplicationController::SendNotification(const QString& summary,
const QString& body,
const QStringList& actions,
int timeout_milliseconds) {
QDBusConnection bus = QDBusConnection::sessionBus();
if (!bus.isConnected()) {
return 0;
}
QDBusInterface notifications(kNotificationService, kNotificationPath,
kNotificationInterface, bus);
const QDBusReply<uint> reply =
notifications.call(QStringLiteral("Notify"), QStringLiteral("QuickShare"),
uint{0}, QStringLiteral("quickshare"), summary, body,
actions, QVariantMap{}, timeout_milliseconds);
if (!reply.isValid()) {
qWarning() << "Unable to send desktop notification:"
<< reply.error().message();
return 0;
}
return reply.value();
}
void ApplicationController::CloseOfferNotification(qint64 share_target_id) {
const auto notification =
transfer_to_notification_.constFind(share_target_id);
if (notification == transfer_to_notification_.cend()) {
return;
}
const uint notification_id = notification.value();
RemoveNotificationMapping(notification_id);
QDBusConnection bus = QDBusConnection::sessionBus();
if (!bus.isConnected()) {
return;
}
QDBusInterface notifications(kNotificationService, kNotificationPath,
kNotificationInterface, bus);
notifications.asyncCall(QStringLiteral("CloseNotification"), notification_id);
}
void ApplicationController::RemoveNotificationMapping(uint notification_id) {
const auto transfer = notification_to_transfer_.find(notification_id);
if (transfer == notification_to_transfer_.end()) {
return;
}
transfer_to_notification_.remove(transfer.value());
notification_to_transfer_.erase(transfer);
}
QString ApplicationController::IncomingOfferBody(const QString& device_name,
int attachment_count,
qint64 total_bytes) const {
const QString peer =
device_name.isEmpty() ? tr("A nearby device") : device_name;
QString contents;
if (attachment_count > 0) {
contents = attachment_count == 1 ? tr("1 item")
: tr("%1 items").arg(attachment_count);
} else {
contents = FormatBytes(total_bytes);
}
if (contents.isEmpty()) {
contents = tr("a share");
}
return tr("%1 wants to send %2")
.arg(peer, contents);
}
@@ -0,0 +1,75 @@
#pragma once
#include <memory>
#include <QHash>
#include <QObject>
#include <QPointer>
#include <QSet>
#include <QStringList>
class Backend;
class QApplication;
class QMenu;
class QQuickWindow;
class QSystemTrayIcon;
class ApplicationController : public QObject {
Q_OBJECT
Q_CLASSINFO("D-Bus Interface", "com.google.quickshare")
Q_PROPERTY(bool closeToTray READ closeToTray NOTIFY closeToTrayChanged)
public:
explicit ApplicationController(QApplication& application,
QObject* parent = nullptr);
~ApplicationController() override;
// Returns false after successfully activating an existing instance.
bool ClaimSingleInstance();
void InitializeTray();
void SetBackend(Backend* backend);
void AttachWindow(QObject* root_object);
bool closeToTray() const { return close_to_tray_; }
public slots:
Q_SCRIPTABLE void ShowWindow();
void Quit();
signals:
void closeToTrayChanged();
void acceptRequested(qint64 share_target_id);
void rejectRequested(qint64 share_target_id);
private slots:
void OnIncomingOffer(qint64 share_target_id, const QString& device_name,
int attachment_count, qint64 total_bytes);
void OnIncomingOfferResolved(qint64 share_target_id);
void OnTransferFinished(qint64 share_target_id, bool receive_mode,
const QString& device_name, const QString& status);
void OnNotificationAction(uint notification_id, const QString& action_key);
void OnNotificationClosed(uint notification_id, uint reason);
void UpdateTrayAvailability();
private:
friend class ApplicationControllerTestPeer;
uint SendNotification(const QString& summary, const QString& body,
const QStringList& actions, int timeout_milliseconds);
void CloseOfferNotification(qint64 share_target_id);
void RemoveNotificationMapping(uint notification_id);
QString IncomingOfferBody(const QString& device_name, int attachment_count,
qint64 total_bytes) const;
QApplication& application_;
QPointer<QQuickWindow> window_;
QSystemTrayIcon* tray_icon_ = nullptr;
std::unique_ptr<QMenu> tray_menu_;
QHash<uint, qint64> notification_to_transfer_;
QHash<qint64, uint> transfer_to_notification_;
QSet<qint64> background_accepted_transfers_;
bool close_to_tray_ = false;
bool actions_supported_ = false;
bool owns_dbus_service_ = false;
bool pending_activation_ = false;
};
@@ -0,0 +1,111 @@
#include "sharing/linux/app/application_controller.h"
#include <QApplication>
#include <QQuickWindow>
#include "gtest/gtest.h"
class ApplicationControllerTestPeer {
public:
static void AddOfferNotification(ApplicationController& controller,
uint notification_id,
qint64 share_target_id) {
controller.notification_to_transfer_.insert(notification_id,
share_target_id);
controller.transfer_to_notification_.insert(share_target_id,
notification_id);
}
static void InvokeAction(ApplicationController& controller,
uint notification_id, const QString& action) {
controller.OnNotificationAction(notification_id, action);
}
static void Dismiss(ApplicationController& controller, uint notification_id) {
controller.OnNotificationClosed(notification_id, /*reason=*/2);
}
static void DeliverOfferWithoutActions(ApplicationController& controller,
qint64 share_target_id) {
controller.actions_supported_ = false;
controller.OnIncomingOffer(share_target_id, QStringLiteral("Peer"), 1, 100);
}
static bool HasNotification(const ApplicationController& controller,
uint notification_id) {
return controller.notification_to_transfer_.contains(notification_id);
}
};
namespace {
QApplication& TestApplication() {
static int argc = 1;
static char application_name[] = "application_controller_test";
static char* argv[] = {application_name, nullptr};
static QApplication application(argc, argv);
return application;
}
TEST(ApplicationControllerTest, NativeActionsRouteToTransfer) {
ApplicationController controller(TestApplication());
qint64 accepted_target = 0;
qint64 rejected_target = 0;
QObject::connect(&controller, &ApplicationController::acceptRequested,
[&accepted_target](qint64 id) { accepted_target = id; });
QObject::connect(&controller, &ApplicationController::rejectRequested,
[&rejected_target](qint64 id) { rejected_target = id; });
ApplicationControllerTestPeer::AddOfferNotification(controller, 10, 101);
ApplicationControllerTestPeer::InvokeAction(controller, 10,
QStringLiteral("accept"));
ApplicationControllerTestPeer::AddOfferNotification(controller, 11, 102);
ApplicationControllerTestPeer::InvokeAction(controller, 11,
QStringLiteral("decline"));
EXPECT_EQ(accepted_target, 101);
EXPECT_EQ(rejected_target, 102);
EXPECT_FALSE(ApplicationControllerTestPeer::HasNotification(controller, 10));
EXPECT_FALSE(ApplicationControllerTestPeer::HasNotification(controller, 11));
}
TEST(ApplicationControllerTest, DismissalDoesNotResolveTransfer) {
ApplicationController controller(TestApplication());
int responses = 0;
QObject::connect(&controller, &ApplicationController::acceptRequested,
[&responses](qint64) { ++responses; });
QObject::connect(&controller, &ApplicationController::rejectRequested,
[&responses](qint64) { ++responses; });
ApplicationControllerTestPeer::AddOfferNotification(controller, 20, 201);
ApplicationControllerTestPeer::Dismiss(controller, 20);
EXPECT_EQ(responses, 0);
EXPECT_FALSE(ApplicationControllerTestPeer::HasNotification(controller, 20));
}
TEST(ApplicationControllerTest, DefaultActionShowsWindow) {
ApplicationController controller(TestApplication());
QQuickWindow window;
window.hide();
controller.AttachWindow(&window);
ApplicationControllerTestPeer::AddOfferNotification(controller, 30, 301);
ApplicationControllerTestPeer::InvokeAction(controller, 30,
QStringLiteral("default"));
EXPECT_TRUE(window.isVisible());
}
TEST(ApplicationControllerTest, MissingNotificationActionsShowsWindow) {
ApplicationController controller(TestApplication());
QQuickWindow window;
window.hide();
controller.AttachWindow(&window);
ApplicationControllerTestPeer::DeliverOfferWithoutActions(controller, 401);
EXPECT_TRUE(window.isVisible());
}
} // namespace
+65 -1
View File
@@ -31,6 +31,7 @@ using NearbySharingService = nearby::sharing::NearbySharingService;
constexpr std::chrono::seconds kShutdownTimeout(10);
constexpr int kReceiveTimeoutMilliseconds = 30 * 1000;
constexpr int kIncomingOfferTimeoutMilliseconds = 45 * 1000;
QString ToQString(const std::string& value) {
return QString::fromStdString(value);
@@ -491,6 +492,7 @@ bool Backend::sendFile(qint64 share_target_id, const QString& path) {
void Backend::accept(qint64 share_target_id) {
if (service_ != nullptr && !shutting_down_) {
ResolveIncomingOffer(share_target_id);
service_->Accept(share_target_id,
StatusCallback(QStringLiteral("Accept transfer")));
}
@@ -498,6 +500,7 @@ void Backend::accept(qint64 share_target_id) {
void Backend::reject(qint64 share_target_id) {
if (service_ != nullptr && !shutting_down_) {
ResolveIncomingOffer(share_target_id);
service_->Reject(share_target_id,
StatusCallback(QStringLiteral("Reject transfer")));
}
@@ -517,6 +520,11 @@ void Backend::shutdown() {
shutting_down_ = true;
monitoring_requested_ = false;
receive_timeout_timer_.stop();
const QList<QTimer*> offer_timers = incoming_offer_timers_.values();
incoming_offer_timers_.clear();
for (QTimer* timer : offer_timers) {
timer->stop();
}
auto& fast_init_manager = *fast_init_manager_;
if (fast_init_manager.IsScanning()) {
fast_init_manager.StopScanning(nullptr);
@@ -559,24 +567,80 @@ void Backend::OnTransferUpdate(
const nearby::sharing::AttachmentContainer& attachments,
const TransferMetadata& transfer) {
const int64_t total_bytes = attachments.GetTotalAttachmentsSize();
const int attachment_count = attachments.GetAttachmentCount();
QPointer<Backend> backend(this);
PostStatus(this, [backend, receive_mode, target, transfer, total_bytes]() {
PostStatus(this, [backend, receive_mode, target, transfer, total_bytes,
attachment_count]() {
if (!backend || backend->shutting_down_) {
return;
}
backend->targets_.ApplyTarget(target);
backend->transfers_.ApplyTransfer(receive_mode, target, transfer,
total_bytes);
if (!transfer.is_final_status()) {
backend->finished_transfer_ids_.remove(target.id);
}
if (receive_mode &&
transfer.status() ==
TransferMetadata::Status::kAwaitingLocalConfirmation) {
backend->receive_offer_received_ = true;
backend->receive_timeout_timer_.stop();
backend->StartIncomingOfferTimer(target.id);
emit backend->incomingTransfer(target.id);
emit backend->incomingOffer(target.id, ToQString(target.device_name),
attachment_count, total_bytes);
return;
}
if (receive_mode) {
backend->ResolveIncomingOffer(target.id);
}
if (transfer.is_final_status() &&
!backend->finished_transfer_ids_.contains(target.id)) {
backend->finished_transfer_ids_.insert(target.id);
emit backend->transferFinished(
target.id, receive_mode, ToQString(target.device_name),
ToQString(TransferMetadata::StatusToString(transfer.status())));
if (receive_mode) {
backend->StartFastInitiationMonitoring();
}
}
});
}
void Backend::StartIncomingOfferTimer(qint64 share_target_id) {
ResolveIncomingOffer(share_target_id);
auto* timer = new QTimer(this);
timer->setSingleShot(true);
timer->setInterval(kIncomingOfferTimeoutMilliseconds);
connect(timer, &QTimer::timeout, this, [this, share_target_id]() {
OnIncomingOfferTimeout(share_target_id);
});
incoming_offer_timers_.insert(share_target_id, timer);
timer->start();
}
void Backend::ResolveIncomingOffer(qint64 share_target_id) {
QTimer* timer = incoming_offer_timers_.take(share_target_id);
if (timer == nullptr) {
return;
}
timer->stop();
timer->deleteLater();
emit incomingOfferResolved(share_target_id);
}
void Backend::OnIncomingOfferTimeout(qint64 share_target_id) {
if (!incoming_offer_timers_.contains(share_target_id)) {
return;
}
ResolveIncomingOffer(share_target_id);
if (service_ != nullptr && !shutting_down_) {
service_->Reject(share_target_id,
StatusCallback(QStringLiteral("Expire incoming offer")));
}
}
void Backend::SetDesiredMode(Mode mode) {
desired_mode_ = mode;
DriveMode();
+12
View File
@@ -8,7 +8,9 @@
#include <vector>
#include <QAbstractListModel>
#include <QHash>
#include <QObject>
#include <QSet>
#include <QString>
#include <QTimer>
@@ -129,6 +131,11 @@ class Backend : public QObject,
signals:
void incomingTransfer(qint64 share_target_id);
void incomingOffer(qint64 share_target_id, QString device_name,
int attachment_count, qint64 total_bytes);
void incomingOfferResolved(qint64 share_target_id);
void transferFinished(qint64 share_target_id, bool receive_mode,
QString device_name, QString status);
void outgoingTransferStartFailed(qint64 share_target_id);
private:
@@ -185,6 +192,9 @@ class Backend : public QObject,
nearby::api::FastInitiationManager::Error error);
void OpenReceiveWindow(bool fallback);
void OnReceiveTimeout();
void StartIncomingOfferTimer(qint64 share_target_id);
void ResolveIncomingOffer(qint64 share_target_id);
void OnIncomingOfferTimeout(qint64 share_target_id);
std::function<void(NearbySharingService::StatusCodes)> StatusCallback(
QString operation);
void ReportStatus(const QString& operation,
@@ -199,6 +209,8 @@ class Backend : public QObject,
TransferCallback send_transfer_callback_;
TransferCallback receive_transfer_callback_;
QTimer receive_timeout_timer_;
QHash<qint64, QTimer*> incoming_offer_timers_;
QSet<qint64> finished_transfer_ids_;
Mode active_mode_ = Mode::kNone;
Mode desired_mode_ = Mode::kNone;
bool initialized_ = false;
+93 -2
View File
@@ -39,9 +39,18 @@ class BackendTestPeer {
return backend.monitoring_requested_;
}
static bool HasIncomingOfferTimer(const Backend& backend,
int64_t share_target_id) {
return backend.incoming_offer_timers_.contains(share_target_id);
}
static void ExpireReceiveWindow(Backend& backend) {
backend.OnReceiveTimeout();
}
static void ExpireIncomingOffer(Backend& backend, int64_t share_target_id) {
backend.OnIncomingOfferTimeout(share_target_id);
}
};
namespace {
@@ -97,10 +106,16 @@ class TestSharingService final : public nearby::sharing::NearbySharingService {
std::function<void(StatusCodes)> callback) override {
callback(StatusCodes::kOk);
}
void Accept(int64_t, std::function<void(StatusCodes)> callback) override {
void Accept(int64_t share_target_id,
std::function<void(StatusCodes)> callback) override {
++accept_count_;
last_accepted_target_ = share_target_id;
callback(StatusCodes::kOk);
}
void Reject(int64_t, std::function<void(StatusCodes)> callback) override {
void Reject(int64_t share_target_id,
std::function<void(StatusCodes)> callback) override {
++reject_count_;
last_rejected_target_ = share_target_id;
callback(StatusCodes::kOk);
}
void Cancel(int64_t, std::function<void(StatusCodes)> callback) override {
@@ -148,9 +163,18 @@ class TestSharingService final : public nearby::sharing::NearbySharingService {
receive_callback_->OnTransferUpdate(target, attachments, metadata);
}
int accept_count() const { return accept_count_; }
int reject_count() const { return reject_count_; }
int64_t last_accepted_target() const { return last_accepted_target_; }
int64_t last_rejected_target() const { return last_rejected_target_; }
private:
bool send_registered_ = false;
nearby::sharing::TransferUpdateCallback* receive_callback_ = nullptr;
int accept_count_ = 0;
int reject_count_ = 0;
int64_t last_accepted_target_ = 0;
int64_t last_rejected_target_ = 0;
};
class TestFastInitiationManager final
@@ -286,6 +310,73 @@ TEST_F(BackendFastInitiationTest, IncomingOfferCancelsReceiveTimeout) {
EXPECT_TRUE(BackendTestPeer::IsReceiving(*backend_));
EXPECT_FALSE(BackendTestPeer::IsReceiveTimerActive(*backend_));
EXPECT_TRUE(BackendTestPeer::HasIncomingOfferTimer(*backend_, target.id));
}
TEST_F(BackendFastInitiationTest, AcceptCancelsIncomingOfferTimeout) {
fast_init_manager_.FireDiscovered();
DrainEvents();
nearby::sharing::ShareTarget target;
target.id = 11;
auto attachments = nearby::sharing::AttachmentContainer::Builder().Build();
service_.FireReceiveTransferUpdate(
target, *attachments,
nearby::sharing::TransferMetadataBuilder()
.set_status(nearby::sharing::TransferMetadata::Status::
kAwaitingLocalConfirmation)
.build());
DrainEvents();
backend_->accept(target.id);
EXPECT_FALSE(BackendTestPeer::HasIncomingOfferTimer(*backend_, target.id));
EXPECT_EQ(service_.accept_count(), 1);
EXPECT_EQ(service_.last_accepted_target(), target.id);
BackendTestPeer::ExpireIncomingOffer(*backend_, target.id);
EXPECT_EQ(service_.reject_count(), 0);
}
TEST_F(BackendFastInitiationTest, IncomingOfferTimeoutRejectsExactlyOnce) {
fast_init_manager_.FireDiscovered();
DrainEvents();
nearby::sharing::ShareTarget target;
target.id = 12;
auto attachments = nearby::sharing::AttachmentContainer::Builder().Build();
service_.FireReceiveTransferUpdate(
target, *attachments,
nearby::sharing::TransferMetadataBuilder()
.set_status(nearby::sharing::TransferMetadata::Status::
kAwaitingLocalConfirmation)
.build());
DrainEvents();
BackendTestPeer::ExpireIncomingOffer(*backend_, target.id);
BackendTestPeer::ExpireIncomingOffer(*backend_, target.id);
EXPECT_FALSE(BackendTestPeer::HasIncomingOfferTimer(*backend_, target.id));
EXPECT_EQ(service_.reject_count(), 1);
EXPECT_EQ(service_.last_rejected_target(), target.id);
}
TEST_F(BackendFastInitiationTest, FinalReceiveRestartsMonitoring) {
fast_init_manager_.FireDiscovered();
DrainEvents();
nearby::sharing::ShareTarget target;
target.id = 13;
auto attachments = nearby::sharing::AttachmentContainer::Builder().Build();
service_.FireReceiveTransferUpdate(
target, *attachments,
nearby::sharing::TransferMetadataBuilder()
.set_status(nearby::sharing::TransferMetadata::Status::kComplete)
.build());
DrainEvents();
EXPECT_TRUE(BackendTestPeer::IsIdle(*backend_));
EXPECT_TRUE(BackendTestPeer::IsMonitoringRequested(*backend_));
EXPECT_TRUE(fast_init_manager_.IsScanning());
}
TEST_F(BackendFastInitiationTest, DiscoveryStopsFastInitiationScanning) {
+23 -5
View File
@@ -16,6 +16,7 @@
#include <QTimer>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "application_controller.h"
#include "backend.h"
#include "qobject.h"
#include "sharing/linux/app/native_file_logging.h"
@@ -92,8 +93,8 @@ void InstallTerminationCleanup(QApplication& app) {
fcntl(g_signal_pipe[1], F_SETFL,
fcntl(g_signal_pipe[1], F_GETFL, 0) | O_NONBLOCK);
auto* notifier = new QSocketNotifier(g_signal_pipe[0],
QSocketNotifier::Read, &app);
auto* notifier =
new QSocketNotifier(g_signal_pipe[0], QSocketNotifier::Read, &app);
QObject::connect(notifier, &QSocketNotifier::activated, &app,
[&app, notifier](int socket) {
notifier->setEnabled(false);
@@ -118,13 +119,33 @@ int main(int argc, char* argv[]) {
nearby::sharing::linux::NativeFileLogging::Initialize();
QApplication app(argc, argv);
app.setApplicationName(QStringLiteral("QuickShare"));
app.setOrganizationDomain(QStringLiteral("com.google"));
app.setDesktopFileName(QStringLiteral("quickshare"));
app.setWindowIcon(QIcon(QStringLiteral(":/icons/quickshare.svg")));
app.setQuitOnLastWindowClosed(false);
ApplicationController application_controller(app);
if (!application_controller.ClaimSingleInstance()) {
return 0;
}
Backend backend;
application_controller.SetBackend(&backend);
application_controller.InitializeTray();
QQmlApplicationEngine engine;
InstallTerminationCleanup(app);
engine.rootContext()->setContextProperty("backend", &backend);
engine.rootContext()->setContextProperty("appController",
&application_controller);
QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
&application_controller,
[&application_controller](QObject* object, const QUrl&) {
if (object != nullptr) {
application_controller.AttachWindow(object);
}
});
const int fontId = QFontDatabase::addApplicationFont(":/googlesans_var.ttf");
if (fontId != -1) {
@@ -137,8 +158,6 @@ int main(int argc, char* argv[]) {
}
if (IsHotReloadEnabled()) {
engine.rootContext()->setContextProperty("backend", &backend);
const QDir qml_source_dir = QmlSourceDir();
const QUrl source_url =
QUrl::fromLocalFile(qml_source_dir.absoluteFilePath("main.qml"));
@@ -191,7 +210,6 @@ int main(int argc, char* argv[]) {
return result;
}
engine.rootContext()->setContextProperty("backend", &backend);
// Loaded via the qrc scheme since it's compiled into the binary
const QUrl url(QStringLiteral("qrc:/main.qml"));
+6
View File
@@ -9,6 +9,12 @@ Window {
title: qsTr("QuickShare")
color: "#DCF5FF"
onClosing: function(close) {
if (appController.closeToTray) {
close.accepted = false
root.hide()
}
}
FontLoader {
id: googlesans