mirror of
https://github.com/kidfromjupiter/nearby.git
synced 2026-09-14 14:46:12 -04:00
added tray support for qt application
This commit is contained in:
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
@@ -103,7 +104,7 @@ void InstallTerminationCleanup(QApplication& app) {
|
||||
app.quit();
|
||||
});
|
||||
|
||||
struct sigaction action {};
|
||||
struct sigaction action{};
|
||||
action.sa_handler = HandleTerminationSignal;
|
||||
sigemptyset(&action.sa_mask);
|
||||
action.sa_flags = 0;
|
||||
@@ -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"));
|
||||
|
||||
|
||||
@@ -9,6 +9,12 @@ Window {
|
||||
title: qsTr("QuickShare")
|
||||
color: "#DCF5FF"
|
||||
|
||||
onClosing: function(close) {
|
||||
if (appController.closeToTray) {
|
||||
close.accepted = false
|
||||
root.hide()
|
||||
}
|
||||
}
|
||||
|
||||
FontLoader {
|
||||
id: googlesans
|
||||
|
||||
Reference in New Issue
Block a user