Warning, file /plasma/discover/notifier/DiscoverNotifier.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 /*
0002  *   SPDX-FileCopyrightText: 2014 Aleix Pol Gonzalez <aleixpol@blue-systems.com>
0003  *
0004  *   SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005  */
0006 
0007 #include "DiscoverNotifier.h"
0008 #include "BackendNotifierFactory.h"
0009 #include "UnattendedUpdates.h"
0010 #include <KLocalizedString>
0011 #include <KNotificationJobUiDelegate>
0012 #include <KPluginFactory>
0013 #include <QDBusConnection>
0014 #include <QDBusMessage>
0015 #include <QDBusPendingCall>
0016 #include <QDebug>
0017 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0018 #include <QNetworkConfigurationManager>
0019 #else
0020 #include <QNetworkInformation>
0021 #endif
0022 #include <QProcess>
0023 
0024 #include <KIO/ApplicationLauncherJob>
0025 #include <KIO/CommandLauncherJob>
0026 
0027 #include "../libdiscover/utils.h"
0028 #include "updatessettings.h"
0029 #include <chrono>
0030 
0031 using namespace std::chrono_literals;
0032 
0033 DiscoverNotifier::DiscoverNotifier(QObject *parent)
0034     : QObject(parent)
0035 {
0036     m_settings = new UpdatesSettings(this);
0037     m_settingsWatcher = KConfigWatcher::create(m_settings->sharedConfig());
0038 #if QT_VERSION >= QT_VERSION_CHECK(6, 3, 0)
0039     QNetworkInformation::instance()->load(QNetworkInformation::Feature::Reachability | QNetworkInformation::Feature::TransportMedium);
0040     connect(QNetworkInformation::instance(), &QNetworkInformation::reachabilityChanged, this, &DiscoverNotifier::stateChanged);
0041     connect(QNetworkInformation::instance(), &QNetworkInformation::transportMediumChanged, this, &DiscoverNotifier::stateChanged);
0042 #endif
0043 
0044     refreshUnattended();
0045     connect(m_settingsWatcher.data(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group, const QByteArrayList &names) {
0046         if (group.config()->name() == m_settings->config()->name() && group.name() == "Global" && names.contains("UseUnattendedUpdates")) {
0047             refreshUnattended();
0048         }
0049     });
0050 
0051     m_backends = BackendNotifierFactory().allBackends();
0052     for (BackendNotifierModule *module : qAsConst(m_backends)) {
0053         connect(module, &BackendNotifierModule::foundUpdates, this, &DiscoverNotifier::updateStatusNotifier);
0054         connect(module, &BackendNotifierModule::needsRebootChanged, this, [this]() {
0055             // If we are using offline updates, there is no need to badger the user to
0056             // reboot since it is safe to continue using the system in its current state
0057             if (!m_needsReboot && !m_settings->useUnattendedUpdates()) {
0058                 m_needsReboot = true;
0059                 showRebootNotification();
0060                 Q_EMIT stateChanged();
0061                 Q_EMIT needsRebootChanged(true);
0062             }
0063         });
0064 
0065         connect(module, &BackendNotifierModule::foundUpgradeAction, this, &DiscoverNotifier::foundUpgradeAction);
0066     }
0067     connect(&m_timer, &QTimer::timeout, this, &DiscoverNotifier::showUpdatesNotification);
0068     m_timer.setSingleShot(true);
0069     m_timer.setInterval(1s);
0070     updateStatusNotifier();
0071 
0072     // Only fetch updates after the system is comfortably booted
0073     QTimer::singleShot(0s, this, &DiscoverNotifier::recheckSystemUpdateNeeded);
0074 }
0075 
0076 DiscoverNotifier::~DiscoverNotifier() = default;
0077 
0078 void DiscoverNotifier::showDiscover(const QString &xdgActivationToken)
0079 {
0080     auto *job = new KIO::ApplicationLauncherJob(KService::serviceByDesktopName(QStringLiteral("org.kde.discover")));
0081     job->setStartupId(xdgActivationToken.toUtf8());
0082     job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled));
0083     job->start();
0084 
0085     if (m_updatesAvailableNotification) {
0086         m_updatesAvailableNotification->close();
0087     }
0088 }
0089 
0090 void DiscoverNotifier::showDiscoverUpdates(const QString &xdgActivationToken)
0091 {
0092     auto *job = new KIO::CommandLauncherJob(QStringLiteral("plasma-discover"), {QStringLiteral("--mode"), QStringLiteral("update")});
0093     job->setUiDelegate(new KNotificationJobUiDelegate(KJobUiDelegate::AutoErrorHandlingEnabled));
0094     job->setDesktopName(QStringLiteral("org.kde.discover"));
0095     job->setStartupId(xdgActivationToken.toUtf8());
0096     job->start();
0097 
0098     if (m_updatesAvailableNotification) {
0099         m_updatesAvailableNotification->close();
0100     }
0101 }
0102 
0103 bool DiscoverNotifier::notifyAboutUpdates() const
0104 {
0105     if (state() != NormalUpdates && state() != SecurityUpdates) {
0106         // it's not very helpful to notify that everything is in order
0107         return false;
0108     }
0109 
0110     if (m_settings->requiredNotificationInterval() < 0) {
0111         return false;
0112     }
0113 
0114     // To configure to a random value, execute:
0115     // kwriteconfig5 --file PlasmaDiscoverUpdates --group Global --key RequiredNotificationInterval 3600
0116     const QDateTime earliestNextNotificationTime = m_settings->lastNotificationTime().addSecs(m_settings->requiredNotificationInterval());
0117     if (earliestNextNotificationTime.isValid() && earliestNextNotificationTime > QDateTime::currentDateTimeUtc()) {
0118         return false;
0119     }
0120 
0121     m_settings->setLastNotificationTime(QDateTime::currentDateTimeUtc());
0122     m_settings->save();
0123 
0124     auto method = QDBusMessage::createMethodCall(QStringLiteral("org.kde.discover"),
0125                                                  QStringLiteral("/"),
0126                                                  QStringLiteral("org.freedesktop.DBus.Peer"),
0127                                                  QStringLiteral("Ping"));
0128     auto call = QDBusConnection::sessionBus().asyncCall(method);
0129     call.waitForFinished();
0130     if (call.isValid()) {
0131         return false;
0132     }
0133     return true;
0134 }
0135 
0136 void DiscoverNotifier::showUpdatesNotification()
0137 {
0138     if (m_updatesAvailableNotification) {
0139         m_updatesAvailableNotification->close();
0140     }
0141 
0142     if (!notifyAboutUpdates()) {
0143         return;
0144     }
0145 
0146     m_updatesAvailableNotification = KNotification::event(QStringLiteral("Update"),
0147                                                           message(),
0148                                                           {},
0149                                                           iconName(),
0150                                                           nullptr,
0151                                                           KNotification::CloseOnTimeout,
0152                                                           QStringLiteral("discoverabstractnotifier"));
0153     m_updatesAvailableNotification->setHint(QStringLiteral("resident"), true);
0154     const QString name = i18n("View Updates");
0155     m_updatesAvailableNotification->setDefaultAction(name);
0156     m_updatesAvailableNotification->setActions({name});
0157     connect(m_updatesAvailableNotification, QOverload<unsigned int>::of(&KNotification::activated), this, [this] {
0158         showDiscoverUpdates(m_updatesAvailableNotification->xdgActivationToken());
0159     });
0160 }
0161 
0162 void DiscoverNotifier::updateStatusNotifier()
0163 {
0164     const bool hasSecurityUpdates = kContains(m_backends, [](BackendNotifierModule *module) {
0165         return module->hasSecurityUpdates();
0166     });
0167     const bool hasUpdates = hasSecurityUpdates || kContains(m_backends, [](BackendNotifierModule *module) {
0168                                 return module->hasUpdates();
0169                             });
0170 
0171     if (m_hasUpdates == hasUpdates && m_hasSecurityUpdates == hasSecurityUpdates)
0172         return;
0173 
0174     m_hasSecurityUpdates = hasSecurityUpdates;
0175     m_hasUpdates = hasUpdates;
0176 
0177     if (state() != NoUpdates) {
0178         m_timer.start();
0179     }
0180 
0181     Q_EMIT stateChanged();
0182 }
0183 
0184 // we only want to do unattended updates when on an ethernet or wlan network
0185 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0186 static bool isConnectionAdequate(const QNetworkConfiguration &network)
0187 {
0188     return (network.bearerType() == QNetworkConfiguration::BearerEthernet || network.bearerType() == QNetworkConfiguration::BearerWLAN);
0189 }
0190 #elif QT_VERSION >= QT_VERSION_CHECK(6, 3, 0)
0191 static bool isConnectionAdequate()
0192 {
0193     const auto info = QNetworkInformation::instance();
0194     if (info->supports(QNetworkInformation::Feature::Metered)) {
0195         return !info->isMetered();
0196     } else {
0197         const auto transport = info->transportMedium();
0198         return transport == QNetworkInformation::TransportMedium::Ethernet || transport == QNetworkInformation::TransportMedium::WiFi;
0199     }
0200 }
0201 #endif
0202 
0203 void DiscoverNotifier::refreshUnattended()
0204 {
0205     m_settings->read();
0206 
0207     if (!notifyAboutUpdates()) {
0208         return;
0209     }
0210 
0211     const auto enabled = m_settings->useUnattendedUpdates()
0212 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0213         && m_manager->isOnline() && isConnectionAdequate(m_manager->defaultConfiguration());
0214 #elif QT_VERSION >= QT_VERSION_CHECK(6, 3, 0)
0215         && QNetworkInformation::instance()->reachability() == QNetworkInformation::Reachability::Online && isConnectionAdequate();
0216 #endif
0217     if (bool(m_unattended) == enabled)
0218         return;
0219 
0220     if (enabled) {
0221         m_unattended = new UnattendedUpdates(this);
0222     } else {
0223         delete m_unattended;
0224         m_unattended = nullptr;
0225     }
0226 }
0227 
0228 DiscoverNotifier::State DiscoverNotifier::state() const
0229 {
0230     if (m_needsReboot)
0231         return RebootRequired;
0232     else if (m_isBusy)
0233         return Busy;
0234 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0235     else if (m_manager && !m_manager->isOnline())
0236 #else
0237     else if (QNetworkInformation::instance()->reachability() != QNetworkInformation::Reachability::Online)
0238 #endif
0239         return Offline;
0240     else if (m_hasSecurityUpdates)
0241         return SecurityUpdates;
0242     else if (m_hasUpdates)
0243         return NormalUpdates;
0244     else
0245         return NoUpdates;
0246 }
0247 
0248 QString DiscoverNotifier::iconName() const
0249 {
0250     switch (state()) {
0251     case SecurityUpdates:
0252         return QStringLiteral("update-high");
0253     case NormalUpdates:
0254         return QStringLiteral("update-low");
0255     case NoUpdates:
0256         return QStringLiteral("update-none");
0257     case RebootRequired:
0258         return QStringLiteral("system-reboot");
0259     case Offline:
0260         return QStringLiteral("offline");
0261     case Busy:
0262         return QStringLiteral("state-download");
0263     }
0264     return QString();
0265 }
0266 
0267 QString DiscoverNotifier::message() const
0268 {
0269     switch (state()) {
0270     case SecurityUpdates:
0271         return i18n("Security updates available");
0272     case NormalUpdates:
0273         return i18n("Updates available");
0274     case NoUpdates:
0275         return i18n("System up to date");
0276     case RebootRequired:
0277         return i18n("Computer needs to restart");
0278     case Offline:
0279         return i18n("Offline");
0280     case Busy:
0281         return i18n("Applying unattended updates…");
0282     }
0283     return QString();
0284 }
0285 
0286 void DiscoverNotifier::recheckSystemUpdateNeeded()
0287 {
0288 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0289     if (!m_manager) {
0290         m_manager = new QNetworkConfigurationManager(this);
0291         connect(m_manager, &QNetworkConfigurationManager::onlineStateChanged, this, &DiscoverNotifier::stateChanged);
0292         if (!m_manager->isOnline()) {
0293             Q_EMIT stateChanged();
0294         }
0295     }
0296 #endif
0297 
0298     for (BackendNotifierModule *module : qAsConst(m_backends))
0299         module->recheckSystemUpdateNeeded();
0300 
0301     refreshUnattended();
0302 }
0303 
0304 QStringList DiscoverNotifier::loadedModules() const
0305 {
0306     QStringList ret;
0307     for (BackendNotifierModule *module : m_backends)
0308         ret += QString::fromLatin1(module->metaObject()->className());
0309     return ret;
0310 }
0311 
0312 void DiscoverNotifier::showRebootNotification()
0313 {
0314     KNotification *notification = KNotification::event(QStringLiteral("UpdateRestart"),
0315                                                        i18n("Restart is required"),
0316                                                        i18n("The system needs to be restarted for the updates to take effect."),
0317                                                        QStringLiteral("system-software-update"),
0318                                                        nullptr,
0319                                                        KNotification::Persistent | KNotification::DefaultEvent,
0320                                                        QStringLiteral("discoverabstractnotifier"));
0321 
0322     notification->setActions(QStringList{i18nc("@action:button", "Restart")});
0323     notification->setDefaultAction(notification->actions().constFirst());
0324     connect(notification, &KNotification::action1Activated, this, &DiscoverNotifier::reboot);
0325 
0326     notification->sendEvent();
0327 }
0328 
0329 void DiscoverNotifier::reboot()
0330 {
0331     auto method = QDBusMessage::createMethodCall(QStringLiteral("org.kde.LogoutPrompt"),
0332                                                  QStringLiteral("/LogoutPrompt"),
0333                                                  QStringLiteral("org.kde.LogoutPrompt"),
0334                                                  QStringLiteral("promptReboot"));
0335     QDBusConnection::sessionBus().asyncCall(method);
0336 }
0337 
0338 void DiscoverNotifier::foundUpgradeAction(UpgradeAction *action)
0339 {
0340     KNotification *notification = new KNotification(QStringLiteral("distupgrade-notification"), KNotification::Persistent | KNotification::DefaultEvent);
0341     notification->setIconName(QStringLiteral("system-software-update"));
0342     notification->setActions(QStringList{i18nc("@action:button", "Upgrade")});
0343     notification->setTitle(i18n("Upgrade available"));
0344     notification->setText(i18n("New version: %1", action->description()));
0345 
0346     connect(notification, &KNotification::action1Activated, this, [action]() {
0347         action->trigger();
0348     });
0349 
0350     notification->sendEvent();
0351 }
0352 
0353 void DiscoverNotifier::setBusy(bool isBusy)
0354 {
0355     if (isBusy == m_isBusy)
0356         return;
0357 
0358     m_isBusy = isBusy;
0359     Q_EMIT busyChanged(isBusy);
0360     Q_EMIT stateChanged();
0361 }