/* * SPDX-FileCopyrightText: 2010 Dario Freddi * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "powerdevilcore.h" #include "powerdevil_debug.h" #include "powerdevilaction.h" #include "powerdevilenums.h" #include "powerdevilmigrateconfig.h" #include "powerdevilpolicyagent.h" #include "powerdevilpowermanagement.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_LINUX #include #endif using namespace Qt::StringLiterals; namespace PowerDevil { Core::Core(QObject *parent) : QObject(parent) , m_hasDualGpu(false) , m_criticalBatteryTimer(new QTimer(this)) , m_activityConsumer(new KActivities::Consumer(this)) , m_pendingWakeupEvent(true) { KAuth::Action discreteGpuAction(QStringLiteral("org.kde.powerdevil.discretegpuhelper.hasdualgpu")); discreteGpuAction.setHelperId(QStringLiteral("org.kde.powerdevil.discretegpuhelper")); KAuth::ExecuteJob *discreteGpuJob = discreteGpuAction.execute(); connect(discreteGpuJob, &KJob::result, this, [this, discreteGpuJob] { if (discreteGpuJob->error()) { qCWarning(POWERDEVIL) << "org.kde.powerdevil.discretegpuhelper.hasdualgpu failed"; qCDebug(POWERDEVIL) << discreteGpuJob->errorText(); return; } m_hasDualGpu = discreteGpuJob->data()[QStringLiteral("hasdualgpu")].toBool(); }); discreteGpuJob->start(); readChargeThreshold(); readBatteryConservationMode(); } Core::~Core() { qCDebug(POWERDEVIL) << "Core unloading"; // Unload all actions before exiting unloadAllActiveActions(); } void Core::loadCore() { m_suspendController = std::make_unique(); m_batteryController = std::make_unique(); m_lidController = std::make_unique(); m_keyboardBrightnessController = std::make_unique(); m_screenBrightnessController = std::make_unique(); connect(m_screenBrightnessController.get(), &ScreenBrightnessController::detectionFinished, this, &Core::onControllersReady); connect(m_screenBrightnessController.get(), &ScreenBrightnessController::displayAdded, this, &Core::refreshActions); connect(m_screenBrightnessController.get(), &ScreenBrightnessController::displayRemoved, this, &Core::refreshActions); m_screenBrightnessController->detectDisplays(); } void Core::onControllersReady() { qCDebug(POWERDEVIL) << "Controllers ready, KDE Power Management system initialized"; const bool isMobile = Kirigami::Platform::TabletModeWatcher::self()->isTabletMode(); const bool isVM = PowerDevil::PowerManagement::instance()->isVirtualMachine(); const bool canSuspend = m_suspendController->canSuspend(); const bool canHibernate = m_suspendController->canHibernate(); PowerDevil::migrateConfig(isMobile, isVM, canSuspend); m_globalSettings = new PowerDevil::GlobalSettings(canSuspend, canHibernate, this); // Get the battery devices ready { using namespace Solid; connect(DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceAdded, this, &Core::onDeviceAdded); connect(DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceRemoved, this, &Core::onDeviceRemoved); // Force the addition of already existent batteries const auto devices = Device::listFromType(DeviceInterface::Battery, QString()); for (const Device &device : devices) { onDeviceAdded(device.udi()); } } connect(m_batteryController.get(), &BatteryController::acAdapterStateChanged, this, &Core::onAcAdapterStateChanged); connect(m_batteryController.get(), &BatteryController::batteryRemainingTimeChanged, this, &Core::onBatteryRemainingTimeChanged); connect(m_batteryController.get(), &BatteryController::smoothedBatteryRemainingTimeChanged, this, &Core::onSmoothedBatteryRemainingTimeChanged); connect(m_lidController.get(), &LidController::lidClosedChanged, this, &Core::onLidClosedChanged); connect(m_suspendController.get(), &SuspendController::aboutToSuspend, this, &Core::onAboutToSuspend); connect(KIdleTime::instance(), &KIdleTime::timeoutReached, this, &Core::onKIdleTimeoutReached); connect(KIdleTime::instance(), &KIdleTime::resumingFromIdle, this, &Core::onResumingFromIdle); connect(m_activityConsumer, &KActivities::Consumer::currentActivityChanged, this, [this]() { loadProfile(); }); // Set up the policy agent PowerDevil::PolicyAgent::instance()->init(m_globalSettings); // When inhibitions change, simulate user activity, see Bug 315438 connect(PowerDevil::PolicyAgent::instance(), &PowerDevil::PolicyAgent::unavailablePoliciesChanged, this, [](PowerDevil::PolicyAgent::RequiredPolicies newPolicies) { Q_UNUSED(newPolicies); KIdleTime::instance()->simulateUserActivity(); }); // Bug 354250: Simulate user activity when session becomes inactive, // this keeps us from sending the computer to sleep when switching to an idle session. // (this is just being lazy as it will result in us clearing everything connect(PowerDevil::PolicyAgent::instance(), &PowerDevil::PolicyAgent::sessionActiveChanged, this, [this](bool active) { if (active) { // force reload profile so all actions re-register their idle timeouts loadProfile(true /*force*/); } else { // Bug 354250: Keep us from sending the computer to sleep when switching // to an idle session by removing all idle timeouts KIdleTime::instance()->removeAllIdleTimeouts(); m_registeredActionTimeouts.clear(); } }); // Initialize the action pool, which will also load the needed startup actions. refreshActions(); // Set up the critical battery timer m_criticalBatteryTimer->setSingleShot(true); m_criticalBatteryTimer->setInterval(60000); connect(m_criticalBatteryTimer, &QTimer::timeout, this, &Core::onCriticalBatteryTimerExpired); // wait until the notification system is set up before firing notifications // to avoid them showing ontop of ksplash... if (QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.freedesktop.Notifications"))) { onServiceRegistered(QString()); } else { m_notificationsWatcher = new QDBusServiceWatcher(QStringLiteral("org.freedesktop.Notifications"), QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForRegistration, this); connect(m_notificationsWatcher, &QDBusServiceWatcher::serviceRegistered, this, &Core::onServiceRegistered); // ...but fire them after 30s nonetheless to ensure they've been shown QTimer::singleShot(30000, this, &Core::onNotificationTimeout); } #ifdef Q_OS_LINUX // try creating a timerfd which can wake system from suspend m_timerFd = timerfd_create(CLOCK_REALTIME_ALARM, TFD_CLOEXEC); // if that fails due to privilges maybe, try normal timerfd if (m_timerFd == -1) { qCDebug(POWERDEVIL) << "Unable to create a CLOCK_REALTIME_ALARM timer, trying CLOCK_REALTIME\n This would mean that wakeup requests won't wake device from suspend"; m_timerFd = timerfd_create(CLOCK_REALTIME, TFD_CLOEXEC); } if (m_timerFd != -1) { m_timerFdSocketNotifier = new QSocketNotifier(m_timerFd, QSocketNotifier::Read, this); connect(m_timerFdSocketNotifier, &QSocketNotifier::activated, this, &Core::timerfdEventHandler); // we disable events reading for now m_timerFdSocketNotifier->setEnabled(false); } else { qCDebug(POWERDEVIL) << "Unable to create a CLOCK_REALTIME timer, scheduled wakeups won't be available"; } #endif // All systems up Houston, let's go! Q_EMIT coreReady(); refreshStatus(); } void Core::refreshActions() { const QList offers = KPluginMetaData::findPlugins(QStringLiteral("powerdevil/action")); for (const KPluginMetaData &data : offers) { const QString actionId = data.value(QStringLiteral("X-KDE-PowerDevil-Action-ID")); if (m_actionPool.contains(actionId)) { continue; } std::unique_ptr action{KPluginFactory::instantiatePlugin(data, this).plugin}; if (action && action->isSupported()) { if (data.value(QStringLiteral("X-KDE-PowerDevil-Action-RegistersDBusInterface"), false)) { QDBusConnection::sessionBus().registerObject(QStringLiteral("/org/kde/Solid/PowerManagement/Actions/") + actionId, action.get()); } m_actionPool.emplace(actionId, std::move(action)); } } // Remove now-unsupported actions for (auto it = m_actionPool.begin(); it != m_actionPool.end();) { Action *action = it->second.get(); if (!action->isSupported()) { m_registeredActionTimeouts.remove(action); m_pendingResumeFromIdleActions.remove(action); it = m_actionPool.erase(it); } else { ++it; } } } bool Core::isActionSupported(const QString &actionName) { Action *act = action(actionName); return act ? act->isSupported() : false; } void Core::refreshStatus() { /* The configuration could have changed if this function was called, so * let's resync it. */ reparseConfiguration(); loadProfile(true); } void Core::reparseConfiguration() { m_globalSettings->load(); // Config reloaded Q_EMIT configurationReloaded(); // Check if critical threshold might have changed and cancel the timer if necessary. if (currentChargePercent() > m_globalSettings->batteryCriticalLevel()) { m_criticalBatteryTimer->stop(); if (m_criticalBatteryNotification) { m_criticalBatteryNotification->close(); } } if (m_lowBatteryNotification && currentChargePercent() > m_globalSettings->batteryLowLevel()) { m_lowBatteryNotification->close(); } readChargeThreshold(); readBatteryConservationMode(); } QString Core::currentProfile() const { return m_currentProfile; } void Core::loadProfile(bool force) { QString profileId; // Check the activity in which we are in QString activity = m_activityConsumer->currentActivity(); qCDebug(POWERDEVIL) << "Currently using activity " << activity; PowerDevil::ActivitySettings activitySettings(activity); qCDebug(POWERDEVIL) << "Settings for loaded activity:"; for (KConfigSkeletonItem *item : activitySettings.items()) { qCDebug(POWERDEVIL) << item->key() << "=" << item->property(); } // let's load the current state's profile if (m_batteriesPercent.isEmpty()) { qCDebug(POWERDEVIL) << "No batteries found, loading AC"; profileId = QStringLiteral("AC"); } else { // Compute the previous and current global percentage const int percent = currentChargePercent(); if (m_batteryController->acAdapterState() == BatteryController::Plugged) { profileId = QStringLiteral("AC"); qCDebug(POWERDEVIL) << "Loading profile for plugged AC"; } else if (percent <= m_globalSettings->batteryLowLevel()) { profileId = QStringLiteral("LowBattery"); qCDebug(POWERDEVIL) << "Loading profile for low battery"; } else { profileId = QStringLiteral("Battery"); qCDebug(POWERDEVIL) << "Loading profile for unplugged AC"; } } // Load settings for the current profile const bool isMobile = Kirigami::Platform::TabletModeWatcher::self()->isTabletMode(); const bool isVM = PowerDevil::PowerManagement::instance()->isVirtualMachine(); bool canSuspend = m_suspendController->canSuspend(); PowerDevil::ProfileSettings profileSettings(profileId, isMobile, isVM, canSuspend); // Release any special inhibitions { QHash::iterator i = m_sessionActivityInhibit.begin(); while (i != m_sessionActivityInhibit.end()) { PolicyAgent::instance()->ReleaseInhibition(i.value()); i = m_sessionActivityInhibit.erase(i); } i = m_screenActivityInhibit.begin(); while (i != m_screenActivityInhibit.end()) { PolicyAgent::instance()->ReleaseInhibition(i.value()); i = m_screenActivityInhibit.erase(i); } } // Check: do we need to change profile at all? if (m_currentProfile == profileId && !force) { // No, let's leave things as they are qCDebug(POWERDEVIL) << "Skipping action reload routine as profile has not changed"; // Do we need to force a wakeup? if (m_pendingWakeupEvent) { // Fake activity at this stage, when no timeouts are registered onResumingFromIdle(); m_pendingWakeupEvent = false; } } else { // First of all, let's clean the old actions. This will also call the onProfileUnload callback unloadAllActiveActions(); // Do we need to force a wakeup? if (m_pendingWakeupEvent) { // Fake activity at this stage, when no timeouts are registered onResumingFromIdle(); m_pendingWakeupEvent = false; } // Cool, now let's load the needed actions. Mark the ones as active that want to be loaded for (const auto &[id, action] : m_actionPool) { if (action->loadAction(profileSettings)) { m_activeActions.append(id); action->onProfileLoad(m_currentProfile, profileId); } } // We are now on a different profile m_currentProfile = profileId; Q_EMIT profileChanged(m_currentProfile); } // Now... any special behaviors we'd like to consider? if (activitySettings.inhibitSuspend()) { qCDebug(POWERDEVIL) << "Activity triggers a suspend inhibition"; // debug hence not sleep // Trigger a special inhibition - if we don't have one yet if (!m_sessionActivityInhibit.contains(activity)) { int cookie = PolicyAgent::instance()->AddInhibition(PolicyAgent::InterruptSession, i18n("Activity Manager"), i18n("This activity's policies prevent the system from going to sleep")); m_sessionActivityInhibit.insert(activity, cookie); } } if (activitySettings.inhibitScreenManagement()) { qCDebug(POWERDEVIL) << "Activity triggers a screen management inhibition"; // Trigger a special inhibition - if we don't have one yet if (!m_screenActivityInhibit.contains(activity)) { int cookie = PolicyAgent::instance()->AddInhibition(PolicyAgent::ChangeScreenSettings, i18n("Activity Manager"), i18n("This activity's policies prevent screen power management")); m_screenActivityInhibit.insert(activity, cookie); } } // If the lid is closed, retrigger the lid close signal // so that "switching profile then closing the lid" has the same result as // "closing lid then switching profile". if (m_lidController->isLidClosed()) { Q_EMIT m_lidController->lidClosedChanged(true); } } void Core::onDeviceAdded(const QString &udi) { if (m_batteriesPercent.contains(udi) || m_peripheralBatteriesPercent.contains(udi)) { // We already know about this device return; } using namespace Solid; Device device(udi); Battery *b = qobject_cast(device.asDeviceInterface(DeviceInterface::Battery)); if (!b) { return; } connect(b, &Battery::chargePercentChanged, this, &Core::onBatteryChargePercentChanged); connect(b, &Battery::chargeStateChanged, this, &Core::onBatteryChargeStateChanged); qCDebug(POWERDEVIL) << "Battery with UDI" << udi << "was detected"; if (b->isPowerSupply()) { m_batteriesPercent[udi] = b->chargePercent(); m_batteriesCharged[udi] = (b->chargeState() == Solid::Battery::FullyCharged); } else { // non-power supply batteries are treated separately m_peripheralBatteriesPercent[udi] = b->chargePercent(); // notify the user about the empty mouse/keyboard when plugging it in; don't when // notifications aren't ready yet so we avoid showing them ontop of ksplash; // also we'll notify about all devices when notifications are ready anyway if (m_notificationsReady) { emitBatteryChargePercentNotification(b->chargePercent(), 1000 /* so current is always lower than previous */, udi); } } // If a new battery has been added, let's clear some pending suspend actions if the new global batteries percentage is // higher than the battery critical level. (See bug 329537) if (m_lowBatteryNotification && currentChargePercent() > m_globalSettings->batteryLowLevel()) { m_lowBatteryNotification->close(); } if (currentChargePercent() > m_globalSettings->batteryCriticalLevel()) { if (m_criticalBatteryNotification) { m_criticalBatteryNotification->close(); } if (m_criticalBatteryTimer->isActive()) { m_criticalBatteryTimer->stop(); emitRichNotification(QStringLiteral("pluggedin"), // i18n("Extra Battery Added"), i18n("The system will no longer go to sleep.")); } } } void Core::onDeviceRemoved(const QString &udi) { if (!m_batteriesPercent.contains(udi) && !m_peripheralBatteriesPercent.contains(udi)) { // We don't know about this device return; } using namespace Solid; Device device(udi); // No need to undo the connections from onDeviceAdded, // because the backend object has already been deleted. Q_ASSERT(!device.isValid()); qCDebug(POWERDEVIL) << "Battery with UDI" << udi << "has been removed"; m_batteriesPercent.remove(udi); m_peripheralBatteriesPercent.remove(udi); m_batteriesCharged.remove(udi); } void Core::emitNotification(const QString &eventId, const QString &title, const QString &message, const QString &iconName) { KNotification::event(eventId, title, message, iconName, KNotification::CloseOnTimeout, QStringLiteral("powerdevil")); } void Core::emitRichNotification(const QString &evid, const QString &title, const QString &message) { KNotification::event(evid, title, message, QPixmap(), KNotification::CloseOnTimeout, QStringLiteral("powerdevil")); } bool Core::emitBatteryChargePercentNotification(int currentPercent, int previousPercent, const QString &udi, Core::ChargeNotificationFlags flags) { if (m_peripheralBatteriesPercent.contains(udi)) { // Show the notification just once on each normal->low transition if (currentPercent > m_globalSettings->peripheralBatteryLowLevel() || previousPercent <= m_globalSettings->peripheralBatteryLowLevel()) { return false; } using namespace Solid; Device device(udi); Battery *b = qobject_cast(device.asDeviceInterface(DeviceInterface::Battery)); if (!b) { return false; } // if you leave the device out of reach or it has not been initialized yet // it won't be "there" and report 0%, don't show anything in this case if (!b->isPresent() || b->chargePercent() == 0) { return false; } // Bluetooth devices don't report charge state, so it's "NoCharge" in all cases for them if (b->chargeState() != Battery::Discharging && b->chargeState() != Battery::NoCharge) { return false; } { QString name = device.product(); if (!device.vendor().isEmpty()) { name = i18nc("%1 is vendor name, %2 is product name", "%1 %2", device.vendor(), device.product()); } QString title = i18nc("The battery in an external device", "Device Battery Low (%1% Remaining)", currentPercent); QString msg = i18nc("Placeholder is device name", "The battery in \"%1\" is running low, and the device may turn off at any time. " "Please recharge or replace the battery.", name); QString icon = QStringLiteral("battery-caution"); switch (b->type()) { case Battery::MouseBattery: title = i18n("Mouse Battery Low (%1% Remaining)", currentPercent); icon = QStringLiteral("input-mouse"); break; case Battery::KeyboardBattery: title = i18n("Keyboard Battery Low (%1% Remaining)", currentPercent); icon = QStringLiteral("input-keyboard"); break; case Battery::BluetoothBattery: title = i18n("Bluetooth Device Battery Low (%1% Remaining)", currentPercent); msg = i18nc("Placeholder is device name", "The battery in Bluetooth device \"%1\" is running low, and the device may turn off at any time. " "Please recharge or replace the battery.", name); icon = QStringLiteral("preferences-system-bluetooth"); break; default: break; } emitNotification(QStringLiteral("lowperipheralbattery"), title, msg, icon); } return true; } // Make sure a notificaton that's kept open updates its percentage live. updateBatteryNotifications(currentPercent); if (m_batteryController->acAdapterState() == BatteryController::Plugged && !flags.testFlag(ChargeNotificationFlag::NotifyWhenAcPluggedIn)) { return false; } if (currentPercent <= m_globalSettings->batteryCriticalLevel() && previousPercent > m_globalSettings->batteryCriticalLevel()) { handleCriticalBattery(currentPercent); return true; } else if (currentPercent <= m_globalSettings->batteryLowLevel() && previousPercent > m_globalSettings->batteryLowLevel()) { handleLowBattery(currentPercent); return true; } return false; } void Core::handleLowBattery(int percent) { if (m_lowBatteryNotification) { return; } m_lowBatteryNotification = new KNotification(QStringLiteral("lowbattery"), KNotification::Persistent, nullptr); m_lowBatteryNotification->setComponentName(QStringLiteral("powerdevil")); updateBatteryNotifications(percent); // sets title if (m_batteryController->acAdapterState() == BatteryController::Plugged) { m_lowBatteryNotification->setText(i18n("Ensure that the power cord is plugged in all the way and provides enough power.")); } else { m_lowBatteryNotification->setText(i18n("Plug in the power cord.")); } m_lowBatteryNotification->setUrgency(KNotification::CriticalUrgency); m_lowBatteryNotification->sendEvent(); } void Core::handleCriticalBattery(int percent) { if (m_lowBatteryNotification) { m_lowBatteryNotification->close(); } // no parent, but it won't leak, since it will be closed both in case of timeout or direct action m_criticalBatteryNotification = new KNotification(QStringLiteral("criticalbattery"), KNotification::Persistent, nullptr); m_criticalBatteryNotification->setComponentName(QStringLiteral("powerdevil")); updateBatteryNotifications(percent); // sets title switch (static_cast(m_globalSettings->batteryCriticalAction())) { case PowerButtonAction::Shutdown: { m_criticalBatteryNotification->setText(i18n("The system will shut down in 60 seconds.")); auto action = m_criticalBatteryNotification->addAction(i18nc("@action:button Shut down without waiting for the battery critical timer", "Shut Down Now")); connect(action, &KNotificationAction::activated, this, &Core::triggerCriticalBatteryAction); m_criticalBatteryTimer->start(); break; } case PowerButtonAction::Hibernate: { m_criticalBatteryNotification->setText(i18n("The system will hibernate in 60 seconds.")); auto action = m_criticalBatteryNotification->addAction( i18nc("@action:button Enter hibernation mode without waiting for the battery critical timer", "Hibernate Now")); connect(action, &KNotificationAction::activated, this, &Core::triggerCriticalBatteryAction); m_criticalBatteryTimer->start(); break; } case PowerButtonAction::Sleep: { m_criticalBatteryNotification->setText(i18n("The system will go to sleep in 60 seconds.")); auto action = m_criticalBatteryNotification->addAction(i18nc("@action:button Suspend to ram without waiting for the battery critical timer", "Sleep Now")); connect(action, &KNotificationAction::activated, this, &Core::triggerCriticalBatteryAction); m_criticalBatteryTimer->start(); break; } default: m_criticalBatteryNotification->setText(i18n("Please save your work.")); // no timer, no actions break; } auto cancelAction = m_criticalBatteryNotification->addAction(i18nc("Cancel timeout that will automatically put system to sleep because of low battery", "Cancel")); connect(cancelAction, &KNotificationAction::activated, this, [this] { m_criticalBatteryTimer->stop(); m_criticalBatteryNotification->close(); }); m_criticalBatteryNotification->sendEvent(); } void Core::updateBatteryNotifications(int percent) { if (m_lowBatteryNotification) { m_lowBatteryNotification->setTitle(i18n("Battery Low (%1% Remaining)", percent)); } if (m_criticalBatteryNotification) { m_criticalBatteryNotification->setTitle(i18n("Battery Critical (%1% Remaining)", percent)); } } void Core::onAcAdapterStateChanged(BatteryController::AcAdapterState state) { qCDebug(POWERDEVIL); // Post request for faking an activity event - usually adapters don't plug themselves out :) m_pendingWakeupEvent = true; loadProfile(); if (state == BatteryController::Plugged) { // If the AC Adaptor has been plugged in, let's clear some pending suspend actions if (m_lowBatteryNotification) { m_lowBatteryNotification->close(); } if (m_criticalBatteryNotification) { m_criticalBatteryNotification->close(); } if (m_criticalBatteryTimer->isActive()) { m_criticalBatteryTimer->stop(); emitRichNotification(QStringLiteral("pluggedin"), // i18n("Power Cord Plugged In"), i18n("The computer will no longer go to sleep.")); } else { emitRichNotification(QStringLiteral("pluggedin"), i18n("Running on AC power"), i18n("The power cord has been plugged in.")); } } else if (state == BatteryController::Unplugged) { emitRichNotification(QStringLiteral("unplugged"), i18n("Running on Battery Power"), i18n("The power cord has been unplugged.")); } } void Core::onBatteryChargePercentChanged(int percent, const QString &udi) { if (m_peripheralBatteriesPercent.contains(udi)) { const int previousPercent = m_peripheralBatteriesPercent.value(udi); m_peripheralBatteriesPercent[udi] = percent; if (percent < previousPercent) { emitBatteryChargePercentNotification(percent, previousPercent, udi); } return; } // Compute the previous and current global percentage const int previousPercent = currentChargePercent(); const int currentPercent = previousPercent - (m_batteriesPercent[udi] - percent); // Update the battery percentage m_batteriesPercent[udi] = percent; if (currentPercent < previousPercent) { // When battery drains while still plugged in, warn nevertheless. if (emitBatteryChargePercentNotification(currentPercent, previousPercent, udi, ChargeNotificationFlag::NotifyWhenAcPluggedIn)) { // Only refresh status if a notification has actually been emitted loadProfile(); } } } void Core::onBatteryChargeStateChanged(int state, const QString &udi) { if (!m_batteriesCharged.contains(udi)) { return; } bool previousCharged = true; for (auto i = m_batteriesCharged.constBegin(); i != m_batteriesCharged.constEnd(); ++i) { if (!i.value()) { previousCharged = false; break; } } m_batteriesCharged[udi] = (state == Solid::Battery::FullyCharged); if (m_batteryController->acAdapterState() != BatteryController::Plugged) { return; } bool currentCharged = true; for (auto i = m_batteriesCharged.constBegin(); i != m_batteriesCharged.constEnd(); ++i) { if (!i.value()) { currentCharged = false; break; } } if (!previousCharged && currentCharged) { emitRichNotification(QStringLiteral("fullbattery"), i18n("Charging Complete"), i18n("Battery now fully charged.")); loadProfile(); } } void Core::onCriticalBatteryTimerExpired() { if (m_criticalBatteryNotification) { m_criticalBatteryNotification->close(); } // Do that only if we're not on AC if (m_batteryController->acAdapterState() == BatteryController::Unplugged) { triggerCriticalBatteryAction(); } } void Core::triggerCriticalBatteryAction() { PowerDevil::Action *helperAction = action(QStringLiteral("SuspendSession")); if (helperAction) { QVariantMap args; args[QStringLiteral("Type")] = QVariant::fromValue(m_globalSettings->batteryCriticalAction()); args[QStringLiteral("Explicit")] = true; helperAction->trigger(args); } } void Core::onBatteryRemainingTimeChanged(qulonglong time) { Q_EMIT batteryRemainingTimeChanged(time); } void Core::onSmoothedBatteryRemainingTimeChanged(qulonglong time) { Q_EMIT smoothedBatteryRemainingTimeChanged(time); } void Core::onKIdleTimeoutReached(int identifier, int msec) { // Find which action(s) requested this idle timeout for (auto i = m_registeredActionTimeouts.constBegin(), end = m_registeredActionTimeouts.constEnd(); i != end; ++i) { if (i.value().contains(identifier)) { i.key()->onIdleTimeout(std::chrono::milliseconds(msec)); // And it will need to be awaken m_pendingResumeFromIdleActions.insert(i.key()); break; } } // Catch the next resume event if some actions require it if (!m_pendingResumeFromIdleActions.isEmpty()) { KIdleTime::instance()->catchNextResumeEvent(); } } void Core::onLidClosedChanged(bool closed) { Q_EMIT lidClosedChanged(closed); } void Core::onAboutToSuspend() { if (m_globalSettings->pausePlayersOnSuspend()) { qCDebug(POWERDEVIL) << "Pausing all media players before sleep"; QDBusPendingCall listNamesCall = QDBusConnection::sessionBus().interface()->asyncCall(QStringLiteral("ListNames")); QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(listNamesCall, this); connect(callWatcher, &QDBusPendingCallWatcher::finished, this, [](QDBusPendingCallWatcher *watcher) { QDBusPendingReply reply = *watcher; watcher->deleteLater(); if (reply.isError()) { qCWarning(POWERDEVIL) << "Failed to fetch list of DBus service names for pausing players on entering sleep" << reply.error().message(); return; } const QStringList &services = reply.value(); for (const QString &serviceName : services) { if (!serviceName.startsWith(QLatin1String("org.mpris.MediaPlayer2."))) { continue; } if (serviceName.startsWith(QLatin1String("org.mpris.MediaPlayer2.kdeconnect.mpris_"))) { // This is actually a player on another device exposed by KDE Connect // We don't want to pause it // See https://bugs.kde.org/show_bug.cgi?id=427209 continue; } qCDebug(POWERDEVIL) << "Pausing media player with service name" << serviceName; QDBusMessage pauseMsg = QDBusMessage::createMethodCall(serviceName, QStringLiteral("/org/mpris/MediaPlayer2"), QStringLiteral("org.mpris.MediaPlayer2.Player"), QStringLiteral("Pause")); QDBusConnection::sessionBus().asyncCall(pauseMsg); } }); } } void Core::registerActionTimeout(Action *action, std::chrono::milliseconds timeout) { // Register the timeout with KIdleTime int identifier = KIdleTime::instance()->addIdleTimeout(timeout); // Add the identifier to the action hash QList timeouts = m_registeredActionTimeouts[action]; timeouts.append(identifier); m_registeredActionTimeouts[action] = timeouts; } void Core::unregisterActionTimeouts(Action *action) { // Clear all timeouts from the action const QList timeoutsToClean = m_registeredActionTimeouts[action]; for (int id : timeoutsToClean) { KIdleTime::instance()->removeIdleTimeout(id); } m_registeredActionTimeouts.remove(action); } int Core::currentChargePercent() const { int chargePercent = 0; for (auto it = m_batteriesPercent.constBegin(); it != m_batteriesPercent.constEnd(); ++it) { chargePercent += it.value(); } return chargePercent; } void Core::onResumingFromIdle() { KIdleTime::instance()->simulateUserActivity(); // Wake up the actions in which an idle action was triggered std::for_each(m_pendingResumeFromIdleActions.cbegin(), m_pendingResumeFromIdleActions.cend(), std::mem_fn(&PowerDevil::Action::onWakeupFromIdle)); m_pendingResumeFromIdleActions.clear(); } void Core::onNotificationTimeout() { // cannot connect QTimer::singleShot directly to the other method onServiceRegistered(QString()); } void Core::onServiceRegistered(const QString &service) { Q_UNUSED(service); if (m_notificationsReady) { return; } bool needsRefresh = false; // show warning about low batteries right on session startup, force it to show // by making sure the "old" percentage (that magic number) is always higher than the current one if (emitBatteryChargePercentNotification(currentChargePercent(), 1000)) { needsRefresh = true; } // now also emit notifications for all peripheral batteries for (auto it = m_peripheralBatteriesPercent.constBegin(), end = m_peripheralBatteriesPercent.constEnd(); it != end; ++it) { if (emitBatteryChargePercentNotification(it.value() /*currentPercent*/, 1000, it.key() /*udi*/)) { needsRefresh = true; } } // need to refresh status to prevent the notification from showing again when charge percentage changes if (needsRefresh) { refreshStatus(); } m_notificationsReady = true; if (m_notificationsWatcher) { delete m_notificationsWatcher; m_notificationsWatcher = nullptr; } } void Core::readChargeThreshold() { KAuth::Action action(QStringLiteral("org.kde.powerdevil.chargethresholdhelper.getthreshold")); action.setHelperId(QStringLiteral("org.kde.powerdevil.chargethresholdhelper")); KAuth::ExecuteJob *job = action.execute(); connect(job, &KJob::result, this, [this, job] { if (job->error()) { qCWarning(POWERDEVIL) << "org.kde.powerdevil.chargethresholdhelper.getthreshold failed" << job->errorText(); return; } const auto data = job->data(); const int chargeStartThreshold = data.value(QStringLiteral("chargeStartThreshold")).toInt(); if (chargeStartThreshold != -1 && m_chargeStartThreshold != chargeStartThreshold) { m_chargeStartThreshold = chargeStartThreshold; Q_EMIT chargeStartThresholdChanged(chargeStartThreshold); } const int chargeStopThreshold = data.value(QStringLiteral("chargeStopThreshold")).toInt(); if (chargeStopThreshold != -1 && m_chargeStopThreshold != chargeStopThreshold) { m_chargeStopThreshold = chargeStopThreshold; Q_EMIT chargeStopThresholdChanged(chargeStopThreshold); } qCDebug(POWERDEVIL) << "Charge thresholds: start at" << chargeStartThreshold << "- stop at" << chargeStopThreshold; }); job->start(); } void Core::readBatteryConservationMode() { KAuth::Action action(QStringLiteral("org.kde.powerdevil.chargethresholdhelper.getconservationmode")); action.setHelperId(QStringLiteral("org.kde.powerdevil.chargethresholdhelper")); KAuth::ExecuteJob *job = action.execute(); connect(job, &KJob::result, this, [this, job] { if (job->error()) { qCWarning(POWERDEVIL) << "org.kde.powerdevil.chargethresholdhelper.getconservationmode failed" << job->errorText(); return; } const auto data = job->data(); const bool enabled = data.value(QStringLiteral("batteryConservationModeEnabled")).toBool(); if (m_batteryConservationModeEnabled != enabled) { m_batteryConservationModeEnabled = enabled; Q_EMIT batteryConservationModeChanged(enabled); } qCDebug(POWERDEVIL) << "Battery conservation mode is" << (enabled ? "enabled" : "disabled"); }); job->start(); } SuspendController *Core::suspendController() { return m_suspendController.get(); } BatteryController *Core::batteryController() { return m_batteryController.get(); } Action *Core::action(const QString actionId) { const auto it = m_actionPool.find(actionId); return it == m_actionPool.end() ? nullptr : it->second.get(); } void Core::unloadAllActiveActions() { for (const QString &action : std::as_const(m_activeActions)) { m_actionPool[action]->onProfileUnload(); m_actionPool[action]->unloadAction(); } m_activeActions.clear(); } LidController *Core::lidController() { return m_lidController.get(); } bool Core::isLidClosed() const { return m_lidController->isLidClosed(); } bool Core::isLidPresent() const { return m_lidController->isLidPresent(); } bool Core::hasDualGpu() const { return m_hasDualGpu; } bool Core::isBatteryConservationModeEnabled() const { return m_batteryConservationModeEnabled; } int Core::chargeStartThreshold() const { return m_chargeStartThreshold; } int Core::chargeStopThreshold() const { return m_chargeStopThreshold; } uint Core::scheduleWakeup(const QString &service, const QDBusObjectPath &path, qint64 timeout) { ++m_lastWakeupCookie; int cookie = m_lastWakeupCookie; // if some one is trying to time travel, deny them if (timeout < QDateTime::currentSecsSinceEpoch()) { sendErrorReply(QDBusError::InvalidArgs, u"You can not schedule wakeup in past"_s); } else { #ifndef Q_OS_LINUX sendErrorReply(QDBusError::NotSupported, u"Scheduled wakeups are available only on Linux platforms"_s); #else WakeupInfo wakeup{service, path, cookie, timeout}; m_scheduledWakeups << wakeup; qCDebug(POWERDEVIL) << "Received request to wakeup at " << QDateTime::fromSecsSinceEpoch(timeout); resetAndScheduleNextWakeup(); #endif } return cookie; } void Core::wakeup() { onResumingFromIdle(); PowerDevil::Action *helperAction = action(QStringLiteral("DPMSControl")); if (helperAction) { QVariantMap args; // we pass empty string as type because when empty type is passed, // it turns screen on. args[u"Type"_s] = QString(); helperAction->trigger(args); } } void Core::clearWakeup(int cookie) { // if we do not have any timeouts return from here if (m_scheduledWakeups.isEmpty()) { return; } int oldListSize = m_scheduledWakeups.size(); // depending on cookie, remove it from scheduled wakeups m_scheduledWakeups.erase(std::remove_if(m_scheduledWakeups.begin(), m_scheduledWakeups.end(), [cookie](WakeupInfo wakeup) { return wakeup.cookie == cookie; }), m_scheduledWakeups.end()); if (oldListSize == m_scheduledWakeups.size()) { sendErrorReply(QDBusError::InvalidArgs, u"Can not clear the invalid wakeup"_s); return; } // reset timerfd resetAndScheduleNextWakeup(); } qulonglong Core::batteryRemainingTime() const { return m_batteryController->batteryRemainingTime(); } qulonglong Core::smoothedBatteryRemainingTime() const { return m_batteryController->smoothedBatteryRemainingTime(); } uint Core::backendCapabilities() { return 1; // SignalResumeFromSuspend; } void Core::resetAndScheduleNextWakeup() { #ifdef Q_OS_LINUX // first we sort the wakeup list std::sort(m_scheduledWakeups.begin(), m_scheduledWakeups.end(), [](const WakeupInfo &lhs, const WakeupInfo &rhs) { return lhs.timeout < rhs.timeout; }); // we don't want any of our wakeups to repeat' timespec interval = {0, 0}; timespec nextWakeup; bool enableNotifier = false; // if we don't have any wakeups left, we call it a day and stop timer_fd if (m_scheduledWakeups.isEmpty()) { nextWakeup = {0, 0}; } else { // now pick the first timeout from the list WakeupInfo wakeup = m_scheduledWakeups.first(); nextWakeup = {wakeup.timeout, 0}; enableNotifier = true; } if (m_timerFd != -1) { const itimerspec spec = {interval, nextWakeup}; timerfd_settime(m_timerFd, TFD_TIMER_ABSTIME, &spec, nullptr); } m_timerFdSocketNotifier->setEnabled(enableNotifier); #endif } void Core::timerfdEventHandler() { // wakeup from the linux/rtc // Disable reading events from the timer_fd m_timerFdSocketNotifier->setEnabled(false); // At this point scheduled wakeup list should not be empty, but just in case if (m_scheduledWakeups.isEmpty()) { qWarning(POWERDEVIL) << "Wakeup was recieved but list is now empty! This should not happen!"; return; } // first thing to do is, we pick the first wakeup from list WakeupInfo currentWakeup = m_scheduledWakeups.takeFirst(); // Before doing anything further, lets set the next set of wakeup alarm resetAndScheduleNextWakeup(); // Now current wakeup needs to be processed // prepare message for sending back to the consumer QDBusMessage msg = QDBusMessage::createMethodCall(currentWakeup.service, currentWakeup.path.path(), QStringLiteral("org.kde.PowerManagement"), QStringLiteral("wakeupCallback")); msg << currentWakeup.cookie; // send it away QDBusConnection::sessionBus().call(msg, QDBus::NoBlock); } KeyboardBrightnessController *Core::keyboardBrightnessController() { return m_keyboardBrightnessController.get(); } ScreenBrightnessController *Core::screenBrightnessController() { return m_screenBrightnessController.get(); } } #include "moc_powerdevilcore.cpp"