File indexing completed on 2024-05-12 09:41:27

0001 /*
0002  *   SPDX-FileCopyrightText: 2010 Dario Freddi <drf@kde.org>
0003  *
0004  *   SPDX-License-Identifier: GPL-2.0-or-later
0005  */
0006 
0007 #include "powerdevilcore.h"
0008 
0009 #include "powerdevil_debug.h"
0010 #include "powerdevilaction.h"
0011 #include "powerdevilenums.h"
0012 #include "powerdevilmigrateconfig.h"
0013 #include "powerdevilpolicyagent.h"
0014 #include "powerdevilpowermanagement.h"
0015 
0016 #include <PowerDevilActivitySettings.h>
0017 #include <PowerDevilGlobalSettings.h>
0018 #include <PowerDevilProfileSettings.h>
0019 
0020 #include <Solid/Battery>
0021 #include <Solid/Device>
0022 #include <Solid/DeviceNotifier>
0023 
0024 #include <KAuth/Action>
0025 #include <KAuth/ExecuteJob>
0026 #include <kauth_version.h>
0027 
0028 #include <KIdleTime>
0029 #include <KLocalizedString>
0030 #include <KNotification>
0031 #include <KPluginFactory>
0032 #include <KPluginMetaData>
0033 
0034 #include <PlasmaActivities/Consumer>
0035 
0036 #include <QDBusConnection>
0037 #include <QDBusConnectionInterface>
0038 #include <QDBusServiceWatcher>
0039 #include <QTimer>
0040 
0041 #include <QDebug>
0042 
0043 #include <Kirigami/Platform/TabletModeWatcher>
0044 #include <algorithm>
0045 
0046 #ifdef Q_OS_LINUX
0047 #include <sys/timerfd.h>
0048 #endif
0049 
0050 namespace PowerDevil
0051 {
0052 Core::Core(QObject *parent)
0053     : QObject(parent)
0054     , m_hasDualGpu(false)
0055     , m_criticalBatteryTimer(new QTimer(this))
0056     , m_activityConsumer(new KActivities::Consumer(this))
0057     , m_pendingWakeupEvent(true)
0058 {
0059     KAuth::Action discreteGpuAction(QStringLiteral("org.kde.powerdevil.discretegpuhelper.hasdualgpu"));
0060     discreteGpuAction.setHelperId(QStringLiteral("org.kde.powerdevil.discretegpuhelper"));
0061     KAuth::ExecuteJob *discreteGpuJob = discreteGpuAction.execute();
0062     connect(discreteGpuJob, &KJob::result, this, [this, discreteGpuJob] {
0063         if (discreteGpuJob->error()) {
0064             qCWarning(POWERDEVIL) << "org.kde.powerdevil.discretegpuhelper.hasdualgpu failed";
0065             qCDebug(POWERDEVIL) << discreteGpuJob->errorText();
0066             return;
0067         }
0068         m_hasDualGpu = discreteGpuJob->data()[QStringLiteral("hasdualgpu")].toBool();
0069     });
0070     discreteGpuJob->start();
0071 
0072     readChargeThreshold();
0073 }
0074 
0075 Core::~Core()
0076 {
0077     qCDebug(POWERDEVIL) << "Core unloading";
0078     // Unload all actions before exiting
0079     unloadAllActiveActions();
0080 }
0081 
0082 void Core::loadCore(BackendInterface *backend)
0083 {
0084     m_backend = backend;
0085 
0086     m_suspendController = std::make_unique<SuspendController>();
0087     m_batteryController = std::make_unique<BatteryController>();
0088     m_lidController = std::make_unique<LidController>();
0089 
0090     // Async backend init - so that KDED gets a bit of a speed up
0091     qCDebug(POWERDEVIL) << "Core loaded, initializing backend";
0092     connect(m_backend, &BackendInterface::backendReady, this, &Core::onBackendReady);
0093     m_backend->init();
0094 }
0095 
0096 void Core::onBackendReady()
0097 {
0098     qCDebug(POWERDEVIL) << "Backend ready, KDE Power Management system initialized";
0099 
0100     const bool isMobile = Kirigami::Platform::TabletModeWatcher::self()->isTabletMode();
0101     const bool isVM = PowerDevil::PowerManagement::instance()->isVirtualMachine();
0102     const bool canSuspend = m_suspendController->canSuspend();
0103     const bool canHibernate = m_suspendController->canHibernate();
0104 
0105     PowerDevil::migrateConfig(isMobile, isVM, canSuspend);
0106     m_globalSettings = new PowerDevil::GlobalSettings(canSuspend, canHibernate, this);
0107 
0108     // Get the battery devices ready
0109     {
0110         using namespace Solid;
0111         connect(DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceAdded, this, &Core::onDeviceAdded);
0112         connect(DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceRemoved, this, &Core::onDeviceRemoved);
0113 
0114         // Force the addition of already existent batteries
0115         const auto devices = Device::listFromType(DeviceInterface::Battery, QString());
0116         for (const Device &device : devices) {
0117             onDeviceAdded(device.udi());
0118         }
0119     }
0120 
0121     connect(m_batteryController.get(), &BatteryController::acAdapterStateChanged, this, &Core::onAcAdapterStateChanged);
0122     connect(m_batteryController.get(), &BatteryController::batteryRemainingTimeChanged, this, &Core::onBatteryRemainingTimeChanged);
0123     connect(m_batteryController.get(), &BatteryController::smoothedBatteryRemainingTimeChanged, this, &Core::onSmoothedBatteryRemainingTimeChanged);
0124     connect(m_lidController.get(), &LidController::lidClosedChanged, this, &Core::onLidClosedChanged);
0125     connect(m_suspendController.get(), &SuspendController::aboutToSuspend, this, &Core::onAboutToSuspend);
0126     connect(KIdleTime::instance(), &KIdleTime::timeoutReached, this, &Core::onKIdleTimeoutReached);
0127     connect(KIdleTime::instance(), &KIdleTime::resumingFromIdle, this, &Core::onResumingFromIdle);
0128     connect(m_activityConsumer, &KActivities::Consumer::currentActivityChanged, this, [this]() {
0129         loadProfile();
0130     });
0131 
0132     // Set up the policy agent
0133     PowerDevil::PolicyAgent::instance()->init();
0134     // When inhibitions change, simulate user activity, see Bug 315438
0135     connect(PowerDevil::PolicyAgent::instance(),
0136             &PowerDevil::PolicyAgent::unavailablePoliciesChanged,
0137             this,
0138             [](PowerDevil::PolicyAgent::RequiredPolicies newPolicies) {
0139                 Q_UNUSED(newPolicies);
0140                 KIdleTime::instance()->simulateUserActivity();
0141             });
0142 
0143     // Bug 354250: Simulate user activity when session becomes inactive,
0144     // this keeps us from sending the computer to sleep when switching to an idle session.
0145     // (this is just being lazy as it will result in us clearing everything
0146     connect(PowerDevil::PolicyAgent::instance(), &PowerDevil::PolicyAgent::sessionActiveChanged, this, [this](bool active) {
0147         if (active) {
0148             // force reload profile so all actions re-register their idle timeouts
0149             loadProfile(true /*force*/);
0150         } else {
0151             // Bug 354250: Keep us from sending the computer to sleep when switching
0152             // to an idle session by removing all idle timeouts
0153             KIdleTime::instance()->removeAllIdleTimeouts();
0154             m_registeredActionTimeouts.clear();
0155         }
0156     });
0157 
0158     // Initialize the action pool, which will also load the needed startup actions.
0159     initActions();
0160 
0161     // Set up the critical battery timer
0162     m_criticalBatteryTimer->setSingleShot(true);
0163     m_criticalBatteryTimer->setInterval(60000);
0164     connect(m_criticalBatteryTimer, &QTimer::timeout, this, &Core::onCriticalBatteryTimerExpired);
0165 
0166     // wait until the notification system is set up before firing notifications
0167     // to avoid them showing ontop of ksplash...
0168     if (QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.freedesktop.Notifications"))) {
0169         onServiceRegistered(QString());
0170     } else {
0171         m_notificationsWatcher = new QDBusServiceWatcher(QStringLiteral("org.freedesktop.Notifications"),
0172                                                          QDBusConnection::sessionBus(),
0173                                                          QDBusServiceWatcher::WatchForRegistration,
0174                                                          this);
0175         connect(m_notificationsWatcher, &QDBusServiceWatcher::serviceRegistered, this, &Core::onServiceRegistered);
0176 
0177         // ...but fire them after 30s nonetheless to ensure they've been shown
0178         QTimer::singleShot(30000, this, &Core::onNotificationTimeout);
0179     }
0180 
0181 #ifdef Q_OS_LINUX
0182 
0183     // try creating a timerfd which can wake system from suspend
0184     m_timerFd = timerfd_create(CLOCK_REALTIME_ALARM, TFD_CLOEXEC);
0185 
0186     // if that fails due to privilges maybe, try normal timerfd
0187     if (m_timerFd == -1) {
0188         qCDebug(POWERDEVIL)
0189             << "Unable to create a CLOCK_REALTIME_ALARM timer, trying CLOCK_REALTIME\n This would mean that wakeup requests won't wake device from suspend";
0190         m_timerFd = timerfd_create(CLOCK_REALTIME, TFD_CLOEXEC);
0191     }
0192 
0193     if (m_timerFd != -1) {
0194         m_timerFdSocketNotifier = new QSocketNotifier(m_timerFd, QSocketNotifier::Read, this);
0195         connect(m_timerFdSocketNotifier, &QSocketNotifier::activated, this, &Core::timerfdEventHandler);
0196         // we disable events reading for now
0197         m_timerFdSocketNotifier->setEnabled(false);
0198     } else {
0199         qCDebug(POWERDEVIL) << "Unable to create a CLOCK_REALTIME timer, scheduled wakeups won't be available";
0200     }
0201 
0202 #endif
0203 
0204     // All systems up Houston, let's go!
0205     Q_EMIT coreReady();
0206     refreshStatus();
0207 }
0208 
0209 void Core::initActions()
0210 {
0211     const QList<KPluginMetaData> offers = KPluginMetaData::findPlugins(QStringLiteral("powerdevil/action"));
0212     for (const KPluginMetaData &data : offers) {
0213         if (auto plugin = KPluginFactory::instantiatePlugin<PowerDevil::Action>(data, this).plugin) {
0214             m_actionPool.insert(data.value(QStringLiteral("X-KDE-PowerDevil-Action-ID")), plugin);
0215         }
0216     }
0217 
0218     // Verify support
0219     QHash<QString, Action *>::iterator i = m_actionPool.begin();
0220     while (i != m_actionPool.end()) {
0221         Action *action = i.value();
0222         if (!action->isSupported()) {
0223             i = m_actionPool.erase(i);
0224             action->deleteLater();
0225         } else {
0226             ++i;
0227         }
0228     }
0229 
0230     // Register DBus objects
0231     for (const KPluginMetaData &offer : offers) {
0232         QString actionId = offer.value(QStringLiteral("X-KDE-PowerDevil-Action-ID"));
0233         if (offer.value(QStringLiteral("X-KDE-PowerDevil-Action-RegistersDBusInterface"), false) && m_actionPool.contains(actionId)) {
0234             QDBusConnection::sessionBus().registerObject(QStringLiteral("/org/kde/Solid/PowerManagement/Actions/") + actionId, m_actionPool[actionId]);
0235         }
0236     }
0237 }
0238 
0239 bool Core::isActionSupported(const QString &actionName)
0240 {
0241     Action *act = action(actionName);
0242     return act ? act->isSupported() : false;
0243 }
0244 
0245 void Core::refreshStatus()
0246 {
0247     /* The configuration could have changed if this function was called, so
0248      * let's resync it.
0249      */
0250     reparseConfiguration();
0251 
0252     loadProfile(true);
0253 }
0254 
0255 void Core::reparseConfiguration()
0256 {
0257     m_globalSettings->load();
0258 
0259     // Config reloaded
0260     Q_EMIT configurationReloaded();
0261 
0262     // Check if critical threshold might have changed and cancel the timer if necessary.
0263     if (currentChargePercent() > m_globalSettings->batteryCriticalLevel()) {
0264         m_criticalBatteryTimer->stop();
0265         if (m_criticalBatteryNotification) {
0266             m_criticalBatteryNotification->close();
0267         }
0268     }
0269 
0270     if (m_lowBatteryNotification && currentChargePercent() > m_globalSettings->batteryLowLevel()) {
0271         m_lowBatteryNotification->close();
0272     }
0273 
0274     readChargeThreshold();
0275 }
0276 
0277 QString Core::currentProfile() const
0278 {
0279     return m_currentProfile;
0280 }
0281 
0282 void Core::loadProfile(bool force)
0283 {
0284     QString profileId;
0285 
0286     // Check the activity in which we are in
0287     QString activity = m_activityConsumer->currentActivity();
0288     qCDebug(POWERDEVIL) << "Currently using activity " << activity;
0289 
0290     PowerDevil::ActivitySettings activitySettings(activity);
0291 
0292     qCDebug(POWERDEVIL) << "Settings for loaded activity:";
0293     for (KConfigSkeletonItem *item : activitySettings.items()) {
0294         qCDebug(POWERDEVIL) << item->key() << "=" << item->property();
0295     }
0296 
0297     // let's load the current state's profile
0298     if (m_batteriesPercent.isEmpty()) {
0299         qCDebug(POWERDEVIL) << "No batteries found, loading AC";
0300         profileId = QStringLiteral("AC");
0301     } else {
0302         // Compute the previous and current global percentage
0303         const int percent = currentChargePercent();
0304 
0305         if (m_batteryController->acAdapterState() == BatteryController::Plugged) {
0306             profileId = QStringLiteral("AC");
0307             qCDebug(POWERDEVIL) << "Loading profile for plugged AC";
0308         } else if (percent <= m_globalSettings->batteryLowLevel()) {
0309             profileId = QStringLiteral("LowBattery");
0310             qCDebug(POWERDEVIL) << "Loading profile for low battery";
0311         } else {
0312             profileId = QStringLiteral("Battery");
0313             qCDebug(POWERDEVIL) << "Loading profile for unplugged AC";
0314         }
0315     }
0316 
0317     // Load settings for the current profile
0318     const bool isMobile = Kirigami::Platform::TabletModeWatcher::self()->isTabletMode();
0319     const bool isVM = PowerDevil::PowerManagement::instance()->isVirtualMachine();
0320     bool canSuspend = m_suspendController->canSuspend();
0321 
0322     PowerDevil::ProfileSettings profileSettings(profileId, isMobile, isVM, canSuspend);
0323 
0324     // Release any special inhibitions
0325     {
0326         QHash<QString, int>::iterator i = m_sessionActivityInhibit.begin();
0327         while (i != m_sessionActivityInhibit.end()) {
0328             PolicyAgent::instance()->ReleaseInhibition(i.value());
0329             i = m_sessionActivityInhibit.erase(i);
0330         }
0331 
0332         i = m_screenActivityInhibit.begin();
0333         while (i != m_screenActivityInhibit.end()) {
0334             PolicyAgent::instance()->ReleaseInhibition(i.value());
0335             i = m_screenActivityInhibit.erase(i);
0336         }
0337     }
0338 
0339     // Check: do we need to change profile at all?
0340     if (m_currentProfile == profileId && !force) {
0341         // No, let's leave things as they are
0342         qCDebug(POWERDEVIL) << "Skipping action reload routine as profile has not changed";
0343 
0344         // Do we need to force a wakeup?
0345         if (m_pendingWakeupEvent) {
0346             // Fake activity at this stage, when no timeouts are registered
0347             onResumingFromIdle();
0348             m_pendingWakeupEvent = false;
0349         }
0350     } else {
0351         // First of all, let's clean the old actions. This will also call the onProfileUnload callback
0352         unloadAllActiveActions();
0353 
0354         // Do we need to force a wakeup?
0355         if (m_pendingWakeupEvent) {
0356             // Fake activity at this stage, when no timeouts are registered
0357             onResumingFromIdle();
0358             m_pendingWakeupEvent = false;
0359         }
0360 
0361         // Cool, now let's load the needed actions. Mark the ones as active that want to be loaded
0362         for (auto it = m_actionPool.begin(); it != m_actionPool.end(); ++it) {
0363             if (it.value()->loadAction(profileSettings)) {
0364                 m_activeActions.append(it.key());
0365                 it.value()->onProfileLoad(m_currentProfile, profileId);
0366             }
0367         }
0368 
0369         // We are now on a different profile
0370         m_currentProfile = profileId;
0371         Q_EMIT profileChanged(m_currentProfile);
0372     }
0373 
0374     // Now... any special behaviors we'd like to consider?
0375     if (activitySettings.inhibitSuspend()) {
0376         qCDebug(POWERDEVIL) << "Activity triggers a suspend inhibition"; // debug hence not sleep
0377         // Trigger a special inhibition - if we don't have one yet
0378         if (!m_sessionActivityInhibit.contains(activity)) {
0379             int cookie = PolicyAgent::instance()->AddInhibition(PolicyAgent::InterruptSession,
0380                                                                 i18n("Activity Manager"),
0381                                                                 i18n("This activity's policies prevent the system from going to sleep"));
0382 
0383             m_sessionActivityInhibit.insert(activity, cookie);
0384         }
0385     }
0386     if (activitySettings.inhibitScreenManagement()) {
0387         qCDebug(POWERDEVIL) << "Activity triggers a screen management inhibition";
0388         // Trigger a special inhibition - if we don't have one yet
0389         if (!m_screenActivityInhibit.contains(activity)) {
0390             int cookie = PolicyAgent::instance()->AddInhibition(PolicyAgent::ChangeScreenSettings,
0391                                                                 i18n("Activity Manager"),
0392                                                                 i18n("This activity's policies prevent screen power management"));
0393 
0394             m_screenActivityInhibit.insert(activity, cookie);
0395         }
0396     }
0397 
0398     // If the lid is closed, retrigger the lid close signal
0399     // so that "switching profile then closing the lid" has the same result as
0400     // "closing lid then switching profile".
0401     if (m_lidController->isLidClosed()) {
0402         Q_EMIT m_lidController->lidClosedChanged(true);
0403     }
0404 }
0405 
0406 void Core::onDeviceAdded(const QString &udi)
0407 {
0408     if (m_batteriesPercent.contains(udi) || m_peripheralBatteriesPercent.contains(udi)) {
0409         // We already know about this device
0410         return;
0411     }
0412 
0413     using namespace Solid;
0414     Device device(udi);
0415     Battery *b = qobject_cast<Battery *>(device.asDeviceInterface(DeviceInterface::Battery));
0416 
0417     if (!b) {
0418         return;
0419     }
0420 
0421     connect(b, &Battery::chargePercentChanged, this, &Core::onBatteryChargePercentChanged);
0422     connect(b, &Battery::chargeStateChanged, this, &Core::onBatteryChargeStateChanged);
0423 
0424     qCDebug(POWERDEVIL) << "Battery with UDI" << udi << "was detected";
0425 
0426     if (b->isPowerSupply()) {
0427         m_batteriesPercent[udi] = b->chargePercent();
0428         m_batteriesCharged[udi] = (b->chargeState() == Solid::Battery::FullyCharged);
0429     } else { // non-power supply batteries are treated separately
0430         m_peripheralBatteriesPercent[udi] = b->chargePercent();
0431 
0432         // notify the user about the empty mouse/keyboard when plugging it in; don't when
0433         // notifications aren't ready yet so we avoid showing them ontop of ksplash;
0434         // also we'll notify about all devices when notifications are ready anyway
0435         if (m_notificationsReady) {
0436             emitBatteryChargePercentNotification(b->chargePercent(), 1000 /* so current is always lower than previous */, udi);
0437         }
0438     }
0439 
0440     // If a new battery has been added, let's clear some pending suspend actions if the new global batteries percentage is
0441     // higher than the battery critical level. (See bug 329537)
0442     if (m_lowBatteryNotification && currentChargePercent() > m_globalSettings->batteryLowLevel()) {
0443         m_lowBatteryNotification->close();
0444     }
0445 
0446     if (currentChargePercent() > m_globalSettings->batteryCriticalLevel()) {
0447         if (m_criticalBatteryNotification) {
0448             m_criticalBatteryNotification->close();
0449         }
0450 
0451         if (m_criticalBatteryTimer->isActive()) {
0452             m_criticalBatteryTimer->stop();
0453             emitRichNotification(QStringLiteral("pluggedin"), //
0454                                  i18n("Extra Battery Added"),
0455                                  i18n("The computer will no longer go to sleep."));
0456         }
0457     }
0458 }
0459 
0460 void Core::onDeviceRemoved(const QString &udi)
0461 {
0462     if (!m_batteriesPercent.contains(udi) && !m_peripheralBatteriesPercent.contains(udi)) {
0463         // We don't know about this device
0464         return;
0465     }
0466 
0467     using namespace Solid;
0468     Device device(udi);
0469     Battery *b = qobject_cast<Battery *>(device.asDeviceInterface(DeviceInterface::Battery));
0470 
0471     disconnect(b, &Battery::chargePercentChanged, this, &Core::onBatteryChargePercentChanged);
0472     disconnect(b, &Battery::chargeStateChanged, this, &Core::onBatteryChargeStateChanged);
0473 
0474     qCDebug(POWERDEVIL) << "Battery with UDI" << udi << "has been removed";
0475 
0476     m_batteriesPercent.remove(udi);
0477     m_peripheralBatteriesPercent.remove(udi);
0478     m_batteriesCharged.remove(udi);
0479 }
0480 
0481 void Core::emitNotification(const QString &eventId, const QString &title, const QString &message, const QString &iconName)
0482 {
0483     KNotification::event(eventId, title, message, iconName, KNotification::CloseOnTimeout, QStringLiteral("powerdevil"));
0484 }
0485 
0486 void Core::emitRichNotification(const QString &evid, const QString &title, const QString &message)
0487 {
0488     KNotification::event(evid, title, message, QPixmap(), KNotification::CloseOnTimeout, QStringLiteral("powerdevil"));
0489 }
0490 
0491 bool Core::emitBatteryChargePercentNotification(int currentPercent, int previousPercent, const QString &udi, Core::ChargeNotificationFlags flags)
0492 {
0493     if (m_peripheralBatteriesPercent.contains(udi)) {
0494         // Show the notification just once on each normal->low transition
0495         if (currentPercent > m_globalSettings->peripheralBatteryLowLevel() || previousPercent <= m_globalSettings->peripheralBatteryLowLevel()) {
0496             return false;
0497         }
0498 
0499         using namespace Solid;
0500         Device device(udi);
0501         Battery *b = qobject_cast<Battery *>(device.asDeviceInterface(DeviceInterface::Battery));
0502         if (!b) {
0503             return false;
0504         }
0505 
0506         // if you leave the device out of reach or it has not been initialized yet
0507         // it won't be "there" and report 0%, don't show anything in this case
0508         if (!b->isPresent() || b->chargePercent() == 0) {
0509             return false;
0510         }
0511 
0512         // Bluetooth devices don't report charge state, so it's "NoCharge" in all cases for them
0513         if (b->chargeState() != Battery::Discharging && b->chargeState() != Battery::NoCharge) {
0514             return false;
0515         }
0516 
0517         {
0518             QString name = device.product();
0519             if (!device.vendor().isEmpty()) {
0520                 name = i18nc("%1 is vendor name, %2 is product name", "%1 %2", device.vendor(), device.product());
0521             }
0522 
0523             QString title = i18nc("The battery in an external device", "Device Battery Low (%1% Remaining)", currentPercent);
0524             QString msg = i18nc("Placeholder is device name",
0525                                 "The battery in \"%1\" is running low, and the device may turn off at any time. "
0526                                 "Please recharge or replace the battery.",
0527                                 name);
0528             QString icon = QStringLiteral("battery-caution");
0529 
0530             switch (b->type()) {
0531             case Battery::MouseBattery:
0532                 title = i18n("Mouse Battery Low (%1% Remaining)", currentPercent);
0533                 icon = QStringLiteral("input-mouse");
0534                 break;
0535             case Battery::KeyboardBattery:
0536                 title = i18n("Keyboard Battery Low (%1% Remaining)", currentPercent);
0537                 icon = QStringLiteral("input-keyboard");
0538                 break;
0539             case Battery::BluetoothBattery:
0540                 title = i18n("Bluetooth Device Battery Low (%1% Remaining)", currentPercent);
0541                 msg = i18nc("Placeholder is device name",
0542                             "The battery in Bluetooth device \"%1\" is running low, and the device may turn off at any time. "
0543                             "Please recharge or replace the battery.",
0544                             name);
0545                 icon = QStringLiteral("preferences-system-bluetooth");
0546                 break;
0547             default:
0548                 break;
0549             }
0550 
0551             emitNotification(QStringLiteral("lowperipheralbattery"), title, msg, icon);
0552         }
0553 
0554         return true;
0555     }
0556 
0557     // Make sure a notificaton that's kept open updates its percentage live.
0558     updateBatteryNotifications(currentPercent);
0559 
0560     if (m_batteryController->acAdapterState() == BatteryController::Plugged && !flags.testFlag(ChargeNotificationFlag::NotifyWhenAcPluggedIn)) {
0561         return false;
0562     }
0563 
0564     if (currentPercent <= m_globalSettings->batteryCriticalLevel() && previousPercent > m_globalSettings->batteryCriticalLevel()) {
0565         handleCriticalBattery(currentPercent);
0566         return true;
0567     } else if (currentPercent <= m_globalSettings->batteryLowLevel() && previousPercent > m_globalSettings->batteryLowLevel()) {
0568         handleLowBattery(currentPercent);
0569         return true;
0570     }
0571     return false;
0572 }
0573 
0574 void Core::handleLowBattery(int percent)
0575 {
0576     if (m_lowBatteryNotification) {
0577         return;
0578     }
0579 
0580     m_lowBatteryNotification = new KNotification(QStringLiteral("lowbattery"), KNotification::Persistent, nullptr);
0581     m_lowBatteryNotification->setComponentName(QStringLiteral("powerdevil"));
0582     updateBatteryNotifications(percent); // sets title
0583     if (m_batteryController->acAdapterState() == BatteryController::Plugged) {
0584         m_lowBatteryNotification->setText(i18n("Ensure that the power adapter is plugged in and provides enough power."));
0585     } else {
0586         m_lowBatteryNotification->setText(i18n("Plug in the computer."));
0587     }
0588     m_lowBatteryNotification->setUrgency(KNotification::CriticalUrgency);
0589     m_lowBatteryNotification->sendEvent();
0590 }
0591 
0592 void Core::handleCriticalBattery(int percent)
0593 {
0594     if (m_lowBatteryNotification) {
0595         m_lowBatteryNotification->close();
0596     }
0597 
0598     // no parent, but it won't leak, since it will be closed both in case of timeout or direct action
0599     m_criticalBatteryNotification = new KNotification(QStringLiteral("criticalbattery"), KNotification::Persistent, nullptr);
0600     m_criticalBatteryNotification->setComponentName(QStringLiteral("powerdevil"));
0601     updateBatteryNotifications(percent); // sets title
0602 
0603     switch (static_cast<PowerButtonAction>(m_globalSettings->batteryCriticalAction())) {
0604     case PowerButtonAction::Shutdown: {
0605         m_criticalBatteryNotification->setText(i18n("Battery level critical. Your computer will shut down in 60 seconds."));
0606         auto action =
0607             m_criticalBatteryNotification->addAction(i18nc("@action:button Shut down without waiting for the battery critical timer", "Shut Down Now"));
0608         connect(action, &KNotificationAction::activated, this, &Core::triggerCriticalBatteryAction);
0609         m_criticalBatteryTimer->start();
0610         break;
0611     }
0612     case PowerButtonAction::Hibernate: {
0613         m_criticalBatteryNotification->setText(i18n("Battery level critical. Your computer will enter hibernation mode in 60 seconds."));
0614         auto action = m_criticalBatteryNotification->addAction(
0615             i18nc("@action:button Enter hibernation mode without waiting for the battery critical timer", "Hibernate Now"));
0616         connect(action, &KNotificationAction::activated, this, &Core::triggerCriticalBatteryAction);
0617         m_criticalBatteryTimer->start();
0618         break;
0619     }
0620     case PowerButtonAction::Sleep: {
0621         m_criticalBatteryNotification->setText(i18n("Battery level critical. Your computer will go to sleep in 60 seconds."));
0622         auto action =
0623             m_criticalBatteryNotification->addAction(i18nc("@action:button Suspend to ram without waiting for the battery critical timer", "Sleep Now"));
0624         connect(action, &KNotificationAction::activated, this, &Core::triggerCriticalBatteryAction);
0625         m_criticalBatteryTimer->start();
0626         break;
0627     }
0628     default:
0629         m_criticalBatteryNotification->setText(i18n("Please save your work."));
0630         // no timer, no actions
0631         break;
0632     }
0633 
0634     auto cancelAction =
0635         m_criticalBatteryNotification->addAction(i18nc("Cancel timeout that will automatically put system to sleep because of low battery", "Cancel"));
0636     connect(cancelAction, &KNotificationAction::activated, this, [this] {
0637         m_criticalBatteryTimer->stop();
0638         m_criticalBatteryNotification->close();
0639     });
0640 
0641     m_criticalBatteryNotification->sendEvent();
0642 }
0643 
0644 void Core::updateBatteryNotifications(int percent)
0645 {
0646     if (m_lowBatteryNotification) {
0647         m_lowBatteryNotification->setTitle(i18n("Battery Low (%1% Remaining)", percent));
0648     }
0649 
0650     if (m_criticalBatteryNotification) {
0651         m_criticalBatteryNotification->setTitle(i18n("Battery Critical (%1% Remaining)", percent));
0652     }
0653 }
0654 
0655 void Core::onAcAdapterStateChanged(BatteryController::AcAdapterState state)
0656 {
0657     qCDebug(POWERDEVIL);
0658     // Post request for faking an activity event - usually adapters don't plug themselves out :)
0659     m_pendingWakeupEvent = true;
0660     loadProfile();
0661 
0662     if (state == BatteryController::Plugged) {
0663         // If the AC Adaptor has been plugged in, let's clear some pending suspend actions
0664         if (m_lowBatteryNotification) {
0665             m_lowBatteryNotification->close();
0666         }
0667 
0668         if (m_criticalBatteryNotification) {
0669             m_criticalBatteryNotification->close();
0670         }
0671 
0672         if (m_criticalBatteryTimer->isActive()) {
0673             m_criticalBatteryTimer->stop();
0674             emitRichNotification(QStringLiteral("pluggedin"), //
0675                                  i18n("AC Adapter Plugged In"),
0676                                  i18n("The computer will no longer go to sleep."));
0677         } else {
0678             emitRichNotification(QStringLiteral("pluggedin"), i18n("Running on AC power"), i18n("The power adapter has been plugged in."));
0679         }
0680     } else if (state == BatteryController::Unplugged) {
0681         emitRichNotification(QStringLiteral("unplugged"), i18n("Running on Battery Power"), i18n("The power adapter has been unplugged."));
0682     }
0683 }
0684 
0685 void Core::onBatteryChargePercentChanged(int percent, const QString &udi)
0686 {
0687     if (m_peripheralBatteriesPercent.contains(udi)) {
0688         const int previousPercent = m_peripheralBatteriesPercent.value(udi);
0689         m_peripheralBatteriesPercent[udi] = percent;
0690 
0691         if (percent < previousPercent) {
0692             emitBatteryChargePercentNotification(percent, previousPercent, udi);
0693         }
0694         return;
0695     }
0696 
0697     // Compute the previous and current global percentage
0698     const int previousPercent = currentChargePercent();
0699     const int currentPercent = previousPercent - (m_batteriesPercent[udi] - percent);
0700 
0701     // Update the battery percentage
0702     m_batteriesPercent[udi] = percent;
0703 
0704     if (currentPercent < previousPercent) {
0705         // When battery drains while still plugged in, warn nevertheless.
0706         if (emitBatteryChargePercentNotification(currentPercent, previousPercent, udi, ChargeNotificationFlag::NotifyWhenAcPluggedIn)) {
0707             // Only refresh status if a notification has actually been emitted
0708             loadProfile();
0709         }
0710     }
0711 }
0712 
0713 void Core::onBatteryChargeStateChanged(int state, const QString &udi)
0714 {
0715     if (!m_batteriesCharged.contains(udi)) {
0716         return;
0717     }
0718 
0719     bool previousCharged = true;
0720     for (auto i = m_batteriesCharged.constBegin(); i != m_batteriesCharged.constEnd(); ++i) {
0721         if (!i.value()) {
0722             previousCharged = false;
0723             break;
0724         }
0725     }
0726 
0727     m_batteriesCharged[udi] = (state == Solid::Battery::FullyCharged);
0728 
0729     if (m_batteryController->acAdapterState() != BatteryController::Plugged) {
0730         return;
0731     }
0732 
0733     bool currentCharged = true;
0734     for (auto i = m_batteriesCharged.constBegin(); i != m_batteriesCharged.constEnd(); ++i) {
0735         if (!i.value()) {
0736             currentCharged = false;
0737             break;
0738         }
0739     }
0740 
0741     if (!previousCharged && currentCharged) {
0742         emitRichNotification(QStringLiteral("fullbattery"), i18n("Charging Complete"), i18n("Battery now fully charged."));
0743         loadProfile();
0744     }
0745 }
0746 
0747 void Core::onCriticalBatteryTimerExpired()
0748 {
0749     if (m_criticalBatteryNotification) {
0750         m_criticalBatteryNotification->close();
0751     }
0752 
0753     // Do that only if we're not on AC
0754     if (m_batteryController->acAdapterState() == BatteryController::Unplugged) {
0755         triggerCriticalBatteryAction();
0756     }
0757 }
0758 
0759 void Core::triggerCriticalBatteryAction()
0760 {
0761     PowerDevil::Action *helperAction = action(QStringLiteral("SuspendSession"));
0762     if (helperAction) {
0763         QVariantMap args;
0764         args[QStringLiteral("Type")] = QVariant::fromValue<uint>(m_globalSettings->batteryCriticalAction());
0765         args[QStringLiteral("Explicit")] = true;
0766         helperAction->trigger(args);
0767     }
0768 }
0769 
0770 void Core::onBatteryRemainingTimeChanged(qulonglong time)
0771 {
0772     Q_EMIT batteryRemainingTimeChanged(time);
0773 }
0774 
0775 void Core::onSmoothedBatteryRemainingTimeChanged(qulonglong time)
0776 {
0777     Q_EMIT smoothedBatteryRemainingTimeChanged(time);
0778 }
0779 
0780 void Core::onKIdleTimeoutReached(int identifier, int msec)
0781 {
0782     // Find which action(s) requested this idle timeout
0783     for (auto i = m_registeredActionTimeouts.constBegin(), end = m_registeredActionTimeouts.constEnd(); i != end; ++i) {
0784         if (i.value().contains(identifier)) {
0785             i.key()->onIdleTimeout(std::chrono::milliseconds(msec));
0786 
0787             // And it will need to be awaken
0788             m_pendingResumeFromIdleActions.insert(i.key());
0789             break;
0790         }
0791     }
0792 
0793     // Catch the next resume event if some actions require it
0794     if (!m_pendingResumeFromIdleActions.isEmpty()) {
0795         KIdleTime::instance()->catchNextResumeEvent();
0796     }
0797 }
0798 
0799 void Core::onLidClosedChanged(bool closed)
0800 {
0801     Q_EMIT lidClosedChanged(closed);
0802 }
0803 
0804 void Core::onAboutToSuspend()
0805 {
0806     if (m_globalSettings->pausePlayersOnSuspend()) {
0807         qCDebug(POWERDEVIL) << "Pausing all media players before sleep";
0808 
0809         QDBusPendingCall listNamesCall = QDBusConnection::sessionBus().interface()->asyncCall(QStringLiteral("ListNames"));
0810         QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(listNamesCall, this);
0811         connect(callWatcher, &QDBusPendingCallWatcher::finished, this, [](QDBusPendingCallWatcher *watcher) {
0812             QDBusPendingReply<QStringList> reply = *watcher;
0813             watcher->deleteLater();
0814 
0815             if (reply.isError()) {
0816                 qCWarning(POWERDEVIL) << "Failed to fetch list of DBus service names for pausing players on entering sleep" << reply.error().message();
0817                 return;
0818             }
0819 
0820             const QStringList &services = reply.value();
0821             for (const QString &serviceName : services) {
0822                 if (!serviceName.startsWith(QLatin1String("org.mpris.MediaPlayer2."))) {
0823                     continue;
0824                 }
0825 
0826                 if (serviceName.startsWith(QLatin1String("org.mpris.MediaPlayer2.kdeconnect.mpris_"))) {
0827                     // This is actually a player on another device exposed by KDE Connect
0828                     // We don't want to pause it
0829                     // See https://bugs.kde.org/show_bug.cgi?id=427209
0830                     continue;
0831                 }
0832 
0833                 qCDebug(POWERDEVIL) << "Pausing media player with service name" << serviceName;
0834 
0835                 QDBusMessage pauseMsg = QDBusMessage::createMethodCall(serviceName,
0836                                                                        QStringLiteral("/org/mpris/MediaPlayer2"),
0837                                                                        QStringLiteral("org.mpris.MediaPlayer2.Player"),
0838                                                                        QStringLiteral("Pause"));
0839                 QDBusConnection::sessionBus().asyncCall(pauseMsg);
0840             }
0841         });
0842     }
0843 }
0844 
0845 void Core::registerActionTimeout(Action *action, std::chrono::milliseconds timeout)
0846 {
0847     // Register the timeout with KIdleTime
0848     int identifier = KIdleTime::instance()->addIdleTimeout(timeout);
0849 
0850     // Add the identifier to the action hash
0851     QList<int> timeouts = m_registeredActionTimeouts[action];
0852     timeouts.append(identifier);
0853     m_registeredActionTimeouts[action] = timeouts;
0854 }
0855 
0856 void Core::unregisterActionTimeouts(Action *action)
0857 {
0858     // Clear all timeouts from the action
0859     const QList<int> timeoutsToClean = m_registeredActionTimeouts[action];
0860 
0861     for (int id : timeoutsToClean) {
0862         KIdleTime::instance()->removeIdleTimeout(id);
0863     }
0864 
0865     m_registeredActionTimeouts.remove(action);
0866 }
0867 
0868 int Core::currentChargePercent() const
0869 {
0870     int chargePercent = 0;
0871     for (auto it = m_batteriesPercent.constBegin(); it != m_batteriesPercent.constEnd(); ++it) {
0872         chargePercent += it.value();
0873     }
0874     return chargePercent;
0875 }
0876 
0877 void Core::onResumingFromIdle()
0878 {
0879     KIdleTime::instance()->simulateUserActivity();
0880     // Wake up the actions in which an idle action was triggered
0881     std::for_each(m_pendingResumeFromIdleActions.cbegin(), m_pendingResumeFromIdleActions.cend(), std::mem_fn(&PowerDevil::Action::onWakeupFromIdle));
0882 
0883     m_pendingResumeFromIdleActions.clear();
0884 }
0885 
0886 void Core::onNotificationTimeout()
0887 {
0888     // cannot connect QTimer::singleShot directly to the other method
0889     onServiceRegistered(QString());
0890 }
0891 
0892 void Core::onServiceRegistered(const QString &service)
0893 {
0894     Q_UNUSED(service);
0895 
0896     if (m_notificationsReady) {
0897         return;
0898     }
0899 
0900     bool needsRefresh = false;
0901 
0902     // show warning about low batteries right on session startup, force it to show
0903     // by making sure the "old" percentage (that magic number) is always higher than the current one
0904     if (emitBatteryChargePercentNotification(currentChargePercent(), 1000)) {
0905         needsRefresh = true;
0906     }
0907 
0908     // now also emit notifications for all peripheral batteries
0909     for (auto it = m_peripheralBatteriesPercent.constBegin(), end = m_peripheralBatteriesPercent.constEnd(); it != end; ++it) {
0910         if (emitBatteryChargePercentNotification(it.value() /*currentPercent*/, 1000, it.key() /*udi*/)) {
0911             needsRefresh = true;
0912         }
0913     }
0914 
0915     // need to refresh status to prevent the notification from showing again when charge percentage changes
0916     if (needsRefresh) {
0917         refreshStatus();
0918     }
0919 
0920     m_notificationsReady = true;
0921 
0922     if (m_notificationsWatcher) {
0923         delete m_notificationsWatcher;
0924         m_notificationsWatcher = nullptr;
0925     }
0926 }
0927 
0928 void Core::readChargeThreshold()
0929 {
0930     KAuth::Action action(QStringLiteral("org.kde.powerdevil.chargethresholdhelper.getthreshold"));
0931     action.setHelperId(QStringLiteral("org.kde.powerdevil.chargethresholdhelper"));
0932     KAuth::ExecuteJob *job = action.execute();
0933     connect(job, &KJob::result, this, [this, job] {
0934         if (job->error()) {
0935             qCWarning(POWERDEVIL) << "org.kde.powerdevil.chargethresholdhelper.getthreshold failed" << job->errorText();
0936             return;
0937         }
0938 
0939         const auto data = job->data();
0940 
0941         const int chargeStartThreshold = data.value(QStringLiteral("chargeStartThreshold")).toInt();
0942         if (chargeStartThreshold != -1 && m_chargeStartThreshold != chargeStartThreshold) {
0943             m_chargeStartThreshold = chargeStartThreshold;
0944             Q_EMIT chargeStartThresholdChanged(chargeStartThreshold);
0945         }
0946 
0947         const int chargeStopThreshold = data.value(QStringLiteral("chargeStopThreshold")).toInt();
0948         if (chargeStopThreshold != -1 && m_chargeStopThreshold != chargeStopThreshold) {
0949             m_chargeStopThreshold = chargeStopThreshold;
0950             Q_EMIT chargeStopThresholdChanged(chargeStopThreshold);
0951         }
0952 
0953         qCDebug(POWERDEVIL) << "Charge thresholds: start at" << chargeStartThreshold << "- stop at" << chargeStopThreshold;
0954     });
0955     job->start();
0956 }
0957 
0958 BackendInterface *Core::backend()
0959 {
0960     return m_backend;
0961 }
0962 
0963 SuspendController *Core::suspendController()
0964 {
0965     return m_suspendController.get();
0966 }
0967 
0968 BatteryController *Core::batteryController()
0969 {
0970     return m_batteryController.get();
0971 }
0972 
0973 Action *Core::action(const QString actionId)
0974 {
0975     return m_actionPool.value(actionId, nullptr);
0976 }
0977 
0978 void Core::unloadAllActiveActions()
0979 {
0980     for (const QString &action : std::as_const(m_activeActions)) {
0981         m_actionPool[action]->onProfileUnload();
0982         m_actionPool[action]->unloadAction();
0983     }
0984     m_activeActions.clear();
0985 }
0986 
0987 LidController *Core::lidController()
0988 {
0989     return m_lidController.get();
0990 }
0991 
0992 bool Core::isLidClosed() const
0993 {
0994     return m_lidController->isLidClosed();
0995 }
0996 
0997 bool Core::isLidPresent() const
0998 {
0999     return m_lidController->isLidPresent();
1000 }
1001 
1002 bool Core::hasDualGpu() const
1003 {
1004     return m_hasDualGpu;
1005 }
1006 
1007 int Core::chargeStartThreshold() const
1008 {
1009     return m_chargeStartThreshold;
1010 }
1011 
1012 int Core::chargeStopThreshold() const
1013 {
1014     return m_chargeStopThreshold;
1015 }
1016 
1017 uint Core::scheduleWakeup(const QString &service, const QDBusObjectPath &path, qint64 timeout)
1018 {
1019     ++m_lastWakeupCookie;
1020 
1021     int cookie = m_lastWakeupCookie;
1022     // if some one is trying to time travel, deny them
1023     if (timeout < QDateTime::currentSecsSinceEpoch()) {
1024         sendErrorReply(QDBusError::InvalidArgs, "You can not schedule wakeup in past");
1025     } else {
1026 #ifndef Q_OS_LINUX
1027         sendErrorReply(QDBusError::NotSupported, "Scheduled wakeups are available only on Linux platforms");
1028 #else
1029         WakeupInfo wakeup{service, path, cookie, timeout};
1030         m_scheduledWakeups << wakeup;
1031         qCDebug(POWERDEVIL) << "Received request to wakeup at " << QDateTime::fromSecsSinceEpoch(timeout);
1032         resetAndScheduleNextWakeup();
1033 #endif
1034     }
1035     return cookie;
1036 }
1037 
1038 void Core::wakeup()
1039 {
1040     onResumingFromIdle();
1041     PowerDevil::Action *helperAction = action(QStringLiteral("DPMSControl"));
1042     if (helperAction) {
1043         QVariantMap args;
1044         // we pass empty string as type because when empty type is passed,
1045         // it turns screen on.
1046         args[QStringLiteral("Type")] = "";
1047         helperAction->trigger(args);
1048     }
1049 }
1050 
1051 void Core::clearWakeup(int cookie)
1052 {
1053     // if we do not have any timeouts return from here
1054     if (m_scheduledWakeups.isEmpty()) {
1055         return;
1056     }
1057 
1058     int oldListSize = m_scheduledWakeups.size();
1059 
1060     // depending on cookie, remove it from scheduled wakeups
1061     m_scheduledWakeups.erase(std::remove_if(m_scheduledWakeups.begin(),
1062                                             m_scheduledWakeups.end(),
1063                                             [cookie](WakeupInfo wakeup) {
1064                                                 return wakeup.cookie == cookie;
1065                                             }),
1066                              m_scheduledWakeups.end());
1067 
1068     if (oldListSize == m_scheduledWakeups.size()) {
1069         sendErrorReply(QDBusError::InvalidArgs, "Can not clear the invalid wakeup");
1070         return;
1071     }
1072 
1073     // reset timerfd
1074     resetAndScheduleNextWakeup();
1075 }
1076 
1077 qulonglong Core::batteryRemainingTime() const
1078 {
1079     return m_batteryController->batteryRemainingTime();
1080 }
1081 
1082 qulonglong Core::smoothedBatteryRemainingTime() const
1083 {
1084     return m_batteryController->smoothedBatteryRemainingTime();
1085 }
1086 
1087 uint Core::backendCapabilities()
1088 {
1089     return 1; // SignalResumeFromSuspend;
1090 }
1091 
1092 void Core::resetAndScheduleNextWakeup()
1093 {
1094 #ifdef Q_OS_LINUX
1095     // first we sort the wakeup list
1096     std::sort(m_scheduledWakeups.begin(), m_scheduledWakeups.end(), [](const WakeupInfo &lhs, const WakeupInfo &rhs) {
1097         return lhs.timeout < rhs.timeout;
1098     });
1099 
1100     // we don't want any of our wakeups to repeat'
1101     timespec interval = {0, 0};
1102     timespec nextWakeup;
1103     bool enableNotifier = false;
1104     // if we don't have any wakeups left, we call it a day and stop timer_fd
1105     if (m_scheduledWakeups.isEmpty()) {
1106         nextWakeup = {0, 0};
1107     } else {
1108         // now pick the first timeout from the list
1109         WakeupInfo wakeup = m_scheduledWakeups.first();
1110         nextWakeup = {wakeup.timeout, 0};
1111         enableNotifier = true;
1112     }
1113     if (m_timerFd != -1) {
1114         const itimerspec spec = {interval, nextWakeup};
1115         timerfd_settime(m_timerFd, TFD_TIMER_ABSTIME, &spec, nullptr);
1116     }
1117     m_timerFdSocketNotifier->setEnabled(enableNotifier);
1118 #endif
1119 }
1120 
1121 void Core::timerfdEventHandler()
1122 {
1123     // wakeup from the linux/rtc
1124 
1125     // Disable reading events from the timer_fd
1126     m_timerFdSocketNotifier->setEnabled(false);
1127 
1128     // At this point scheduled wakeup list should not be empty, but just in case
1129     if (m_scheduledWakeups.isEmpty()) {
1130         qWarning(POWERDEVIL) << "Wakeup was recieved but list is now empty! This should not happen!";
1131         return;
1132     }
1133 
1134     // first thing to do is, we pick the first wakeup from list
1135     WakeupInfo currentWakeup = m_scheduledWakeups.takeFirst();
1136 
1137     // Before doing anything further, lets set the next set of wakeup alarm
1138     resetAndScheduleNextWakeup();
1139 
1140     // Now current wakeup needs to be processed
1141     // prepare message for sending back to the consumer
1142     QDBusMessage msg = QDBusMessage::createMethodCall(currentWakeup.service,
1143                                                       currentWakeup.path.path(),
1144                                                       QStringLiteral("org.kde.PowerManagement"),
1145                                                       QStringLiteral("wakeupCallback"));
1146     msg << currentWakeup.cookie;
1147     // send it away
1148     QDBusConnection::sessionBus().call(msg, QDBus::NoBlock);
1149 }
1150 }
1151 
1152 #include "moc_powerdevilcore.cpp"