[network/kdeconnect-kde] fileitemactionplugin: fileitemactionplugin: Make it fully async

Kai Uwe Broulik <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit abd78ef70d0101dfbfb52581adf41ccf701baed0 by Kai Uwe Broulik.
Committed on 26/07/2026 at 15:17.
Pushed by broulik into branch 'master'.

fileitemactionplugin: Make it fully async

Don't block the file manager waiting for KDE Connect.
For clearer lifetime the handling is done in a separate job class
because the async process might outliff the actual file item plugin.

While at it, make use of "sendUrls".

M  +1    -1    fileitemactionplugin/CMakeLists.txt
A  +171  -0    fileitemactionplugin/deviceactionjob.cpp     [License: GPL(v2.0+)]
A  +55   -0    fileitemactionplugin/deviceactionjob.h     [License: GPL(v2.0+)]
M  +22   -50   fileitemactionplugin/sendfileitemaction.cpp

https://invent.kde.org/network/kdeconnect-kde/-/commit/abd78ef70d0101dfbfb52581adf41ccf701baed0

diff --git a/fileitemactionplugin/CMakeLists.txt b/fileitemactionplugin/CMakeLists.txt
index b0e888976..a5bfc841e 100644
--- a/fileitemactionplugin/CMakeLists.txt
+++ b/fileitemactionplugin/CMakeLists.txt
@@ -1,6 +1,6 @@
 add_definitions(-DTRANSLATION_DOMAIN="kdeconnect-fileitemaction")
 
-add_library(kdeconnectfileitemaction MODULE sendfileitemaction.cpp)
+add_library(kdeconnectfileitemaction MODULE sendfileitemaction.cpp deviceactionjob.cpp)
 
 ecm_qt_declare_logging_category(kdeconnectfileitemaction
     HEADER kdeconnect_fileitemaction_debug.h
diff --git a/fileitemactionplugin/deviceactionjob.cpp b/fileitemactionplugin/deviceactionjob.cpp
new file mode 100644
index 000000000..8c5ec3993
--- /dev/null
+++ b/fileitemactionplugin/deviceactionjob.cpp
@@ -0,0 +1,171 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Kai Uwe Broulik <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "deviceactionjob.h"
+
+#include <QAction>
+#include <QDBusPendingCallWatcher>
+#include <QIcon>
+#include <QUrl>
+#include <QVariantList>
+#include <QWidget>
+
+#include "dbusinterfaces/dbusinterfaces.h"
+#include "models/devicesmodel.h"
+
+#include <dbushelper.h>
+
+#include "kdeconnect_fileitemaction_debug.h"
+
+DeviceActionJob::DeviceActionJob(const QList<QUrl> &urls, QObject *parent)
+    : QObject(parent)
+    , m_urls(urls)
+{
+    connect(this, &DeviceActionJob::finished, this, &QObject::deleteLater);
+}
+
+QList<QAction *> DeviceActionJob::actions() const
+{
+    return m_actions;
+}
+
+void DeviceActionJob::start(QObject *actionParent)
+{
+    m_actionParent = actionParent;
+    // Must only ever call start() once.
+    Q_ASSERT(m_actions.isEmpty());
+
+    DaemonDbusInterface iface;
+    auto reply = iface.devices(true, true);
+    auto *watcher = new QDBusPendingCallWatcher(reply, this);
+    connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher] {
+        watcher->deleteLater();
+        QDBusPendingReply<QStringList> reply = *watcher;
+        if (reply.isError()) {
+            qCWarning(KDECONNECT_FILEITEMACTION) << "Failed to get list of devices:" << reply.error().message();
+            Q_EMIT finished({});
+            return;
+        }
+
+        const QStringList deviceIds = reply.value();
+        for (const QString &deviceId : deviceIds) {
+            DeviceDbusInterface deviceIface(deviceId);
+
+            auto pluginReply = deviceIface.hasPlugin(QStringLiteral("kdeconnect_share"));
+            ++m_pendingCalls;
+            auto *pluginWatcher = new QDBusPendingCallWatcher(pluginReply, this);
+            connect(pluginWatcher, &QDBusPendingCallWatcher::finished, this, [this, pluginWatcher, deviceId] {
+                onPluginReplyFinished(pluginWatcher, deviceId);
+            });
+        }
+    });
+}
+
+void DeviceActionJob::onPluginReplyFinished(QDBusPendingCallWatcher *watcher, const QString &deviceId)
+{
+    QDBusPendingReply<bool> reply = *watcher;
+
+    if (reply.isError()) {
+        qCWarning(KDECONNECT_FILEITEMACTION) << "Failed to get check whether device" << deviceId << "supports plugin:" << reply.error().message();
+    } else {
+        if (reply.value()) {
+            m_supportedDeviceIds.append(deviceId);
+        }
+    }
+
+    if (--m_pendingCalls == 0) {
+        fetchDeviceInfo();
+    }
+    watcher->deleteLater();
+}
+
+void DeviceActionJob::fetchDeviceInfo()
+{
+    if (m_supportedDeviceIds.isEmpty()) {
+        Q_EMIT finished({});
+        return;
+    }
+
+    const QString propertiesIface = QStringLiteral("org.freedesktop.DBus.Properties");
+    const QString getMethod = QStringLiteral("Get");
+
+    for (const QString &deviceId : std::as_const(m_supportedDeviceIds)) {
+        DeviceDbusInterface deviceIface(deviceId);
+
+        {
+            // Unfortunately no easy built-in async property read on QDBusAbstractInterface.
+            auto nameMessage = QDBusMessage::createMethodCall(deviceIface.service(), deviceIface.path(), propertiesIface, getMethod);
+            nameMessage << deviceIface.interface() << QStringLiteral("name");
+
+            auto nameReply = deviceIface.connection().asyncCall(nameMessage);
+            ++m_pendingCalls;
+            auto *nameWatcher = new QDBusPendingCallWatcher(nameReply, this);
+            connect(nameWatcher, &QDBusPendingCallWatcher::finished, this, [this, nameWatcher, deviceId] {
+                onDeviceNameReplyFinished(nameWatcher, deviceId);
+            });
+        }
+
+        {
+            // Unfortunately no easy built-in async property read on QDBusAbstractInterface.
+            auto iconNameMessage = QDBusMessage::createMethodCall(deviceIface.service(), deviceIface.path(), propertiesIface, getMethod);
+            iconNameMessage << deviceIface.interface() << QStringLiteral("iconName");
+
+            auto iconNameReply = deviceIface.connection().asyncCall(iconNameMessage);
+            ++m_pendingCalls;
+            auto *iconNameWatcher = new QDBusPendingCallWatcher(iconNameReply, this);
+            connect(iconNameWatcher, &QDBusPendingCallWatcher::finished, this, [this, iconNameWatcher, deviceId] {
+                onDeviceIconNameReplyFinished(iconNameWatcher, deviceId);
+            });
+        }
+    }
+}
+
+void DeviceActionJob::onDeviceNameReplyFinished(QDBusPendingCallWatcher *watcher, const QString &deviceId)
+{
+    QDBusPendingReply<QDBusVariant> reply = *watcher;
+    if (reply.isError()) {
+        qCWarning(KDECONNECT_FILEITEMACTION).nospace() << "Failed to get name of" << deviceId << ": " << reply.error().message();
+    } else {
+        m_devices[deviceId].name = reply.value().variant().toString();
+    }
+
+    if (--m_pendingCalls == 0) {
+        finalizeActions();
+    }
+    watcher->deleteLater();
+}
+
+void DeviceActionJob::onDeviceIconNameReplyFinished(QDBusPendingCallWatcher *watcher, const QString &deviceId)
+{
+    QDBusPendingReply<QDBusVariant> reply = *watcher;
+    if (reply.isError()) {
+        qCWarning(KDECONNECT_FILEITEMACTION).nospace() << "Failed to get icon name of" << deviceId << ": " << reply.error().message();
+    } else {
+        m_devices[deviceId].iconName = reply.value().variant().toString();
+    }
+
+    if (--m_pendingCalls == 0) {
+        finalizeActions();
+    }
+    watcher->deleteLater();
+}
+
+void DeviceActionJob::finalizeActions()
+{
+    for (const auto &[deviceId, device] : std::as_const(m_devices).asKeyValueRange()) {
+        QAction *action = new QAction(QIcon::fromTheme(device.iconName), device.name, m_actionParent);
+        action->setProperty("id", deviceId);
+        connect(action, &QAction::triggered, action, [deviceId, urls = m_urls] {
+            ShareDbusInterface shareIface(deviceId);
+            shareIface.shareUrls(QUrl::toStringList(urls));
+        });
+
+        m_actions.append(action);
+    }
+    Q_EMIT finished(m_actions);
+}
+
+#include "moc_deviceactionjob.cpp"
diff --git a/fileitemactionplugin/deviceactionjob.h b/fileitemactionplugin/deviceactionjob.h
new file mode 100644
index 000000000..48135d296
--- /dev/null
+++ b/fileitemactionplugin/deviceactionjob.h
@@ -0,0 +1,55 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Kai Uwe Broulik <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef DEVICEACTIONJOB_H
+#define DEVICEACTIONJOB_H
+
+#include <QList>
+#include <QMap>
+#include <QObject>
+#include <QString>
+#include <QUrl>
+
+class QAction;
+class QDBusPendingCallWatcher;
+
+struct DeviceInfo {
+    QString name;
+    QString iconName;
+};
+
+class DeviceActionJob : public QObject
+{
+    Q_OBJECT
+
+public:
+    explicit DeviceActionJob(const QList<QUrl> &urls, QObject *parent);
+
+    [[nodiscard]] QList<QAction *> actions() const;
+
+    void start(QObject *actionParent);
+
+Q_SIGNALS:
+    void finished(const QList<QAction *> &actions);
+
+private:
+    void onPluginReplyFinished(QDBusPendingCallWatcher *watcher, const QString &deviceId);
+    void onDeviceNameReplyFinished(QDBusPendingCallWatcher *watcher, const QString &deviceId);
+    void onDeviceIconNameReplyFinished(QDBusPendingCallWatcher *watcher, const QString &deviceId);
+
+    void fetchDeviceInfo();
+    void finalizeActions();
+
+    QList<QUrl> m_urls;
+    QList<QAction *> m_actions;
+    QObject *m_actionParent = nullptr;
+
+    int m_pendingCalls = 0;
+    QStringList m_supportedDeviceIds;
+    QMap<QString, DeviceInfo> m_devices;
+};
+
+#endif // DEVICEACTIONJOB_H
diff --git a/fileitemactionplugin/sendfileitemaction.cpp b/fileitemactionplugin/sendfileitemaction.cpp
index 9e6ec60c2..47d01118b 100644
--- a/fileitemactionplugin/sendfileitemaction.cpp
+++ b/fileitemactionplugin/sendfileitemaction.cpp
@@ -1,6 +1,7 @@
 /*
  * SPDX-FileCopyrightText: 2011 Alejandro Fiestas Olivares <[email protected]>
  * SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez <[email protected]>
+ * SPDX-FileCopyrightText: 2026 Kai Uwe Broulik <[email protected]>
  *
  * SPDX-License-Identifier: GPL-2.0-or-later
  */
@@ -18,12 +19,7 @@
 #include <KLocalizedString>
 #include <KPluginFactory>
 
-#include "dbusinterfaces/dbusinterfaces.h"
-#include "models/devicesmodel.h"
-
-#include <dbushelper.h>
-
-#include "kdeconnect_fileitemaction_debug.h"
+#include "deviceactionjob.h"
 
 K_PLUGIN_CLASS_WITH_JSON(SendFileItemAction, "kdeconnectsendfile.json")
 
@@ -34,52 +30,28 @@ SendFileItemAction::SendFileItemAction(QObject *parent, const QVariantList &)
 
 QList<QAction *> SendFileItemAction::actions(const KFileItemListProperties &fileItemInfos, QWidget *parentWidget)
 {
-    QList<QAction *> actions;
-
-    DaemonDbusInterface iface;
-    if (!iface.isValid()) {
-        return actions;
-    }
-
-    QDBusPendingReply<QStringList> reply = iface.devices(true, true);
-    reply.waitForFinished();
-    const QStringList devices = reply.value();
-    for (const QString &id : devices) {
-        DeviceDbusInterface deviceIface(id);
-        if (!deviceIface.isValid()) {
-            continue;
-        }
-        if (!deviceIface.hasPlugin(QStringLiteral("kdeconnect_share"))) {
-            continue;
+    // We have to return an action right away.
+    // Return a placeholder that we then asynchronously populate.
+    auto *action = new QAction(QIcon::fromTheme(QStringLiteral("kdeconnect")), i18n("Send via KDE Connect"), parentWidget);
+    action->setVisible(false);
+
+    auto *job = new DeviceActionJob(fileItemInfos.urlList(), parentWidget);
+    connect(job, &DeviceActionJob::finished, parentWidget, [action, parentWidget](const QList<QAction *> &actions) {
+        if (actions.count() > 1) {
+            QMenu *menu = new QMenu(parentWidget);
+            menu->addActions(actions);
+            action->setMenu(menu);
+            action->setVisible(true);
+        } else if (actions.count() == 1) {
+            auto *firstAction = actions.first();
+            action->setText(i18n("Send to '%1' via KDE Connect", firstAction->text()));
+            connect(action, &QAction::triggered, firstAction, &QAction::trigger);
+            action->setVisible(true);
         }
-        QAction *action = new QAction(QIcon::fromTheme(deviceIface.iconName()), deviceIface.name(), parentWidget);
-        action->setProperty("id", id);
-        const QList<QUrl> urls = fileItemInfos.urlList();
-        connect(action, &QAction::triggered, this, [id, urls]() {
-            for (const QUrl &url : urls) {
-                QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"),
-                                                                  QLatin1String("/modules/kdeconnect/devices/%1/share").arg(id),
-                                                                  QStringLiteral("org.kde.kdeconnect.device.share"),
-                                                                  QStringLiteral("shareUrl"));
-                msg.setArguments(QVariantList{url.toString()});
-                QDBusConnection::sessionBus().asyncCall(msg);
-            }
-        });
-        actions += action;
-    }
+    });
+    job->start(parentWidget);
 
-    if (actions.count() > 1) {
-        QAction *menuAction = new QAction(QIcon::fromTheme(QStringLiteral("kdeconnect")), i18n("Send via KDE Connect"), parentWidget);
-        QMenu *menu = new QMenu(parentWidget);
-        menu->addActions(actions);
-        menuAction->setMenu(menu);
-        return QList<QAction *>() << menuAction;
-    } else {
-        if (actions.count() == 1) {
-            actions.first()->setText(i18n("Send to '%1' via KDE Connect", actions.first()->text()));
-        }
-        return actions;
-    }
+    return {action};
 }
 
 #include "moc_sendfileitemaction.cpp"
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.