File indexing completed on 2024-04-28 05:36:49

0001 /*
0002  * SPDX-FileCopyrightText: 2020 Red Hat Inc
0003  * SPDX-License-Identifier: LGPL-2.0-or-later
0004  *
0005  * SPDX-FileCopyrightText: 2020 Jan Grulich <jgrulich@redhat.com>
0006  */
0007 
0008 #include "background.h"
0009 #include "background_debug.h"
0010 #include "ksharedconfig.h"
0011 #include "waylandintegration.h"
0012 
0013 #include <QDBusConnection>
0014 #include <QDBusContext>
0015 #include <QDBusMessage>
0016 #include <QDBusMetaType>
0017 
0018 #include <QDir>
0019 #include <QFile>
0020 #include <QMessageBox>
0021 #include <QPushButton>
0022 #include <QSettings>
0023 #include <QStandardPaths>
0024 
0025 #include <KConfigGroup>
0026 #include <KDesktopFile>
0027 #include <KLocalizedString>
0028 #include <KNotification>
0029 #include <KService>
0030 #include <KShell>
0031 
0032 #include <KWayland/Client/plasmawindowmanagement.h>
0033 
0034 BackgroundPortal::BackgroundPortal(QObject *parent, QDBusContext *context)
0035     : QDBusAbstractAdaptor(parent)
0036     , m_context(context)
0037 {
0038     connect(WaylandIntegration::waylandIntegration(), &WaylandIntegration::WaylandIntegration::plasmaWindowManagementInitialized, this, [this]() {
0039         connect(WaylandIntegration::plasmaWindowManagement(),
0040                 &KWayland::Client::PlasmaWindowManagement::windowCreated,
0041                 this,
0042                 [this](KWayland::Client::PlasmaWindow *window) {
0043                     addWindow(window);
0044                 });
0045 
0046         m_windows = WaylandIntegration::plasmaWindowManagement()->windows();
0047         for (KWayland::Client::PlasmaWindow *window : std::as_const(m_windows)) {
0048             addWindow(window);
0049         }
0050     });
0051 }
0052 
0053 BackgroundPortal::~BackgroundPortal()
0054 {
0055 }
0056 
0057 QVariantMap BackgroundPortal::GetAppState()
0058 {
0059     qCDebug(XdgDesktopPortalKdeBackground) << "GetAppState called: no parameters";
0060     return m_appStates;
0061 }
0062 
0063 uint BackgroundPortal::NotifyBackground(const QDBusObjectPath &handle, const QString &app_id, const QString &name, QVariantMap &results)
0064 {
0065     Q_UNUSED(results);
0066 
0067     qCDebug(XdgDesktopPortalKdeBackground) << "NotifyBackground called with parameters:";
0068     qCDebug(XdgDesktopPortalKdeBackground) << "    handle: " << handle.path();
0069     qCDebug(XdgDesktopPortalKdeBackground) << "    app_id: " << app_id;
0070     qCDebug(XdgDesktopPortalKdeBackground) << "    name: " << name;
0071 
0072     // If KWayland::Client::PlasmaWindowManagement hasn't been created, we would be notified about every
0073     // application, which is not what we want. This will be mostly happening on X11 session.
0074     if (!WaylandIntegration::plasmaWindowManagement()) {
0075         results.insert(QStringLiteral("result"), static_cast<uint>(BackgroundPortal::AllowOnce));
0076         return 0;
0077     }
0078 
0079     QDBusMessage message = m_context->message();
0080     auto allow = [message]() {
0081         const QVariantMap map = {{QStringLiteral("result"), static_cast<uint>(BackgroundPortal::Allow)}};
0082         QDBusMessage reply = message.createReply({static_cast<uint>(0), map});
0083         if (!QDBusConnection::sessionBus().send(reply)) {
0084             qCWarning(XdgDesktopPortalKdeBackground) << "Failed to send response";
0085         }
0086     };
0087 
0088     auto allowOnce = [message]() {
0089         const QVariantMap map = {{QStringLiteral("result"), static_cast<uint>(BackgroundPortal::AllowOnce)}};
0090         QDBusMessage reply = message.createReply({static_cast<uint>(0), map});
0091         if (!QDBusConnection::sessionBus().send(reply)) {
0092             qCWarning(XdgDesktopPortalKdeBackground) << "Failed to send response";
0093         }
0094     };
0095 
0096     auto forbid = [message]() {
0097         const QVariantMap map = {{QStringLiteral("result"), static_cast<uint>(BackgroundPortal::Forbid)}};
0098         QDBusMessage reply = message.createReply({static_cast<uint>(0), map});
0099         if (!QDBusConnection::sessionBus().send(reply)) {
0100             qCWarning(XdgDesktopPortalKdeBackground) << "Failed to send response";
0101         }
0102     };
0103 
0104     if (KSharedConfig::openConfig()->group("Background").readEntry("NotifyBackgroundApps", true)) {
0105         allowOnce();
0106         return 0;
0107     }
0108 
0109     const KService::Ptr app = KService::serviceByDesktopName(app_id);
0110 
0111     QObject *obj = QObject::parent();
0112 
0113     if (!obj) {
0114         qCWarning(XdgDesktopPortalKdeBackground) << "Failed to get dbus context";
0115         return 2;
0116     }
0117 
0118     const QString appName = app ? app->name() : app_id;
0119     if (m_backgroundAppWarned.contains(app_id)) {
0120         const QVariantMap map = {
0121             {QStringLiteral("result"), static_cast<uint>(BackgroundPortal::AllowOnce)},
0122         };
0123         QDBusMessage reply = message.createReply({uint(0), map});
0124         if (!QDBusConnection::sessionBus().send(reply)) {
0125             qCWarning(XdgDesktopPortalKdeBackground) << "Failed to send response";
0126         }
0127 
0128         return 0;
0129     }
0130 
0131     KNotification *notify = new KNotification(QStringLiteral("notification"), KNotification::Persistent | KNotification::DefaultEvent, this);
0132     notify->setTitle(i18n("Background Activity"));
0133     notify->setText(
0134         i18nc("@info %1 is the name of an application",
0135               "%1 wants to remain running when it has no visible windows. If you forbid this, the application will quit when its last window is closed.",
0136               appName));
0137     notify->setProperty("activated", false);
0138 
0139     message.setDelayedReply(true);
0140 
0141     auto allowAction = notify->addAction(i18nc("@action:button Allow the app to keep running with no open windows", "Allow"));
0142 
0143     connect(allowAction, &KNotificationAction::activated, this, [allow, notify] {
0144         allow();
0145         notify->setProperty("activated", true);
0146     });
0147 
0148     auto forbidAction = notify->addAction(i18nc("@action:button Don't allow the app to keep running without any open windows", "Forbid"));
0149 
0150     connect(forbidAction, &KNotificationAction::activated, this, [this, appName, allow, forbid, notify] {
0151         const QString title =
0152             i18nc("@title title of a dialog to confirm whether to allow an app to remain running with no visible windows", "Background App Usage");
0153         const QString text = i18nc("%1 is the name of an application",
0154                                    "Note that this will force %1 to quit when its last window is closed. This could cause data loss if the application has "
0155                                    "any unsaved changes when it happens.",
0156                                    appName);
0157         auto messageBox = new QMessageBox(QMessageBox::Question, title, text);
0158         messageBox->addButton(i18nc("@action:button Allow the app to keep running with no open windows", "Allow"), QMessageBox::AcceptRole);
0159         messageBox->addButton(i18nc("@action:button Don't allow the app to keep running without any open windows", "Forbid Anyway"), QMessageBox::RejectRole);
0160         messageBox->show();
0161 
0162         connect(messageBox, &QMessageBox::accepted, this, [messageBox, allow]() {
0163             allow();
0164             messageBox->deleteLater();
0165         });
0166         connect(messageBox, &QMessageBox::rejected, this, [messageBox, forbid]() {
0167             forbid();
0168             messageBox->deleteLater();
0169         });
0170 
0171         notify->setProperty("activated", true);
0172     });
0173 
0174     connect(notify, &KNotification::closed, this, [notify, allowOnce]() {
0175         if (notify->property("activated").toBool()) {
0176             return;
0177         }
0178 
0179         allowOnce();
0180     });
0181 
0182     notify->sendEvent();
0183 
0184     Q_ASSERT(!m_backgroundAppWarned.contains(app_id));
0185     connect(notify, &KNotification::closed, this, [this, app_id] {
0186         m_backgroundAppWarned.remove(app_id);
0187     });
0188     m_backgroundAppWarned.insert(app_id);
0189 
0190     return 0;
0191 }
0192 
0193 bool BackgroundPortal::EnableAutostart(const QString &app_id, bool enable, const QStringList &commandline, uint flags)
0194 {
0195     qCDebug(XdgDesktopPortalKdeBackground) << "EnableAutostart called with parameters:";
0196     qCDebug(XdgDesktopPortalKdeBackground) << "    app_id: " << app_id;
0197     qCDebug(XdgDesktopPortalKdeBackground) << "    enable: " << enable;
0198     qCDebug(XdgDesktopPortalKdeBackground) << "    commandline: " << commandline;
0199     qCDebug(XdgDesktopPortalKdeBackground) << "    flags: " << flags;
0200 
0201     const QString fileName = app_id + QStringLiteral(".desktop");
0202     const QString directory = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + QStringLiteral("/autostart/");
0203     const QString fullPath = directory + fileName;
0204     const AutostartFlags autostartFlags = static_cast<AutostartFlags>(flags);
0205 
0206     if (!enable) {
0207         QFile file(fullPath);
0208         if (!file.remove()) {
0209             qCDebug(XdgDesktopPortalKdeBackground) << "Failed to remove " << fileName << " to disable autostart.";
0210         }
0211         return false;
0212     }
0213 
0214     QDir dir(directory);
0215     if (!dir.mkpath(dir.absolutePath())) {
0216         qCDebug(XdgDesktopPortalKdeBackground) << "Failed to create autostart directory.";
0217         return false;
0218     }
0219 
0220     KDesktopFile desktopFile(fullPath);
0221     KConfigGroup desktopEntryConfigGroup = desktopFile.desktopGroup();
0222     desktopEntryConfigGroup.writeEntry(QStringLiteral("Type"), QStringLiteral("Application"));
0223     desktopEntryConfigGroup.writeEntry(QStringLiteral("Name"), app_id);
0224     desktopEntryConfigGroup.writeEntry(QStringLiteral("Exec"), KShell::joinArgs(commandline));
0225     if (autostartFlags.testFlag(AutostartFlag::Activatable)) {
0226         desktopEntryConfigGroup.writeEntry(QStringLiteral("DBusActivatable"), true);
0227     }
0228     desktopEntryConfigGroup.writeEntry(QStringLiteral("X-Flatpak"), app_id);
0229 
0230     return true;
0231 }
0232 
0233 void BackgroundPortal::addWindow(KWayland::Client::PlasmaWindow *window)
0234 {
0235     const QString appId = window->appId();
0236     const bool isActive = window->isActive();
0237     m_appStates[appId] = QVariant::fromValue<uint>(isActive ? Active : Running);
0238 
0239     connect(window, &KWayland::Client::PlasmaWindow::activeChanged, this, [this, window]() {
0240         setActiveWindow(window->appId(), window->isActive());
0241     });
0242     connect(window, &KWayland::Client::PlasmaWindow::unmapped, this, [this, window]() {
0243         uint windows = 0;
0244         const QString appId = window->appId();
0245         const auto plasmaWindows = WaylandIntegration::plasmaWindowManagement()->windows();
0246         for (KWayland::Client::PlasmaWindow *otherWindow : plasmaWindows) {
0247             if (otherWindow->appId() == appId && otherWindow->uuid() != window->uuid()) {
0248                 windows++;
0249             }
0250         }
0251 
0252         if (!windows) {
0253             m_appStates.remove(appId);
0254             Q_EMIT RunningApplicationsChanged();
0255         }
0256     });
0257 
0258     Q_EMIT RunningApplicationsChanged();
0259 }
0260 
0261 void BackgroundPortal::setActiveWindow(const QString &appId, bool active)
0262 {
0263     m_appStates[appId] = QVariant::fromValue<uint>(active ? Active : Running);
0264 
0265     Q_EMIT RunningApplicationsChanged();
0266 }