mirror of
https://github.com/kidfromjupiter/nearby.git
synced 2026-09-14 14:46:12 -04:00
338 lines
12 KiB
C++
338 lines
12 KiB
C++
#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);
|
|
}
|