diff --git a/PowerDevilSettings.kcfg b/PowerDevilSettings.kcfg index b35bfb03..0b82211d 100644 --- a/PowerDevilSettings.kcfg +++ b/PowerDevilSettings.kcfg @@ -1,38 +1,41 @@ true + + true + 250 100 10 5 2 10 diff --git a/daemon/powerdevilcore.cpp b/daemon/powerdevilcore.cpp index e9e91bc2..a69eee51 100644 --- a/daemon/powerdevilcore.cpp +++ b/daemon/powerdevilcore.cpp @@ -1,874 +1,910 @@ /*************************************************************************** * Copyright (C) 2010 by Dario Freddi * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * ***************************************************************************/ #include "powerdevilcore.h" #include "PowerDevilSettings.h" #include "powerdevilaction.h" #include "powerdevilactionpool.h" #include "powerdevilbackendinterface.h" #include "powerdevilpolicyagent.h" #include "powerdevilprofilegenerator.h" #include "powerdevil_debug.h" #include "actions/bundled/suspendsession.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace PowerDevil { Core::Core(QObject* parent) : QObject(parent) , m_hasDualGpu(false) , m_backend(nullptr) , m_notificationsWatcher(nullptr) , m_criticalBatteryTimer(new QTimer(this)) , m_activityConsumer(new KActivities::Consumer(this)) , m_pendingWakeupEvent(true) { KAuth::Action discreteGpuAction("org.kde.powerdevil.discretegpuhelper.hasdualgpu"); discreteGpuAction.setHelperId("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()["hasdualgpu"].toBool(); }); discreteGpuJob->start(); } Core::~Core() { qCDebug(POWERDEVIL) << "Core unloading"; // Unload all actions before exiting, and clear the cache ActionPool::instance()->unloadAllActiveActions(); ActionPool::instance()->clearCache(); } void Core::loadCore(BackendInterface* backend) { if (!backend) { onBackendError(i18n("No valid Power Management backend plugins are available. " "A new installation might solve this problem.")); return; } m_backend = backend; // Async backend init - so that KDED gets a bit of a speed up qCDebug(POWERDEVIL) << "Core loaded, initializing backend"; connect(m_backend, SIGNAL(backendReady()), this, SLOT(onBackendReady())); connect(m_backend, SIGNAL(backendError(QString)), this, SLOT(onBackendError(QString))); m_backend->init(); } void Core::onBackendReady() { qCDebug(POWERDEVIL) << "Backend is ready, KDE Power Management system initialized"; m_profilesConfig = KSharedConfig::openConfig("powermanagementprofilesrc", KConfig::CascadeConfig); // Is it brand new? if (m_profilesConfig->groupList().isEmpty()) { // Generate defaults bool toRam = m_backend->supportedSuspendMethods() & PowerDevil::BackendInterface::ToRam; bool toDisk = m_backend->supportedSuspendMethods() & PowerDevil::BackendInterface::ToDisk; ProfileGenerator::generateProfiles(toRam, toDisk); m_profilesConfig->reparseConfiguration(); } // Get the battery devices ready { using namespace Solid; connect(DeviceNotifier::instance(), SIGNAL(deviceAdded(QString)), this, SLOT(onDeviceAdded(QString))); connect(DeviceNotifier::instance(), SIGNAL(deviceRemoved(QString)), this, SLOT(onDeviceRemoved(QString))); // Force the addition of already existent batteries Q_FOREACH (const Device &device, Device::listFromType(DeviceInterface::Battery, QString())) { onDeviceAdded(device.udi()); } } connect(m_backend, SIGNAL(acAdapterStateChanged(PowerDevil::BackendInterface::AcAdapterState)), this, SLOT(onAcAdapterStateChanged(PowerDevil::BackendInterface::AcAdapterState))); connect(m_backend, SIGNAL(batteryRemainingTimeChanged(qulonglong)), this, SLOT(onBatteryRemainingTimeChanged(qulonglong))); connect(m_backend, SIGNAL(lidClosedChanged(bool)), this, SLOT(onLidClosedChanged(bool))); + connect(m_backend, &BackendInterface::aboutToSuspend, + this, &Core::onAboutToSuspend); connect(KIdleTime::instance(), SIGNAL(timeoutReached(int,int)), this, SLOT(onKIdleTimeoutReached(int,int))); connect(KIdleTime::instance(), SIGNAL(resumingFromIdle()), this, SLOT(onResumingFromIdle())); connect(m_activityConsumer, &KActivities::Consumer::currentActivityChanged, this, [this]() { loadProfile(); }); // Set up the policy agent PowerDevil::PolicyAgent::instance()->init(); // 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. PowerDevil::ActionPool::instance()->init(this); // Set up the critical battery timer m_criticalBatteryTimer->setSingleShot(true); m_criticalBatteryTimer->setInterval(60000); connect(m_criticalBatteryTimer, SIGNAL(timeout()), this, SLOT(onCriticalBatteryTimerExpired())); // wait until the notification system is set up before firing notifications // to avoid them showing ontop of ksplash... if (QDBusConnection::sessionBus().interface()->isServiceRegistered("org.freedesktop.Notifications")) { onServiceRegistered(QString()); } else { m_notificationsWatcher = new QDBusServiceWatcher("org.freedesktop.Notifications", QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForRegistration, this); connect(m_notificationsWatcher, SIGNAL(serviceRegistered(QString)), this, SLOT(onServiceRegistered(QString))); // ...but fire them after 30s nonetheless to ensure they've been shown QTimer::singleShot(30000, this, SLOT(onNotificationTimeout())); } // All systems up Houston, let's go! Q_EMIT coreReady(); refreshStatus(); } bool Core::isActionSupported(const QString& actionName) { Action *action = ActionPool::instance()->loadAction(actionName, KConfigGroup(), this); if (!action) { return false; } else { return action->isSupported(); } } void Core::refreshStatus() { /* The configuration could have changed if this function was called, so * let's resync it. */ reparseConfiguration(); loadProfile(true); } void Core::reparseConfiguration() { PowerDevilSettings::self()->load(); m_profilesConfig->reparseConfiguration(); // Config reloaded Q_EMIT configurationReloaded(); } QString Core::currentProfile() const { return m_currentProfile; } void Core::loadProfile(bool force) { QString profileId; // Policy check if (PolicyAgent::instance()->requirePolicyCheck(PolicyAgent::ChangeProfile) != PolicyAgent::None) { qCDebug(POWERDEVIL) << "Policy Agent prevention: on"; return; } KConfigGroup config; // Check the activity in which we are in QString activity = m_activityConsumer->currentActivity(); qCDebug(POWERDEVIL) << "We are now into activity " << activity; KConfigGroup activitiesConfig(m_profilesConfig, "Activities"); qCDebug(POWERDEVIL) << activitiesConfig.groupList() << activitiesConfig.keyList(); // Are we mirroring an activity? if (activitiesConfig.group(activity).readEntry("mode", "None") == "ActLike" && activitiesConfig.group(activity).readEntry("actLike", QString()) != "AC" && activitiesConfig.group(activity).readEntry("actLike", QString()) != "Battery" && activitiesConfig.group(activity).readEntry("actLike", QString()) != "LowBattery") { // Yes, let's use that then activity = activitiesConfig.group(activity).readEntry("actLike", QString()); qCDebug(POWERDEVIL) << "Activity is a mirror"; } KConfigGroup activityConfig = activitiesConfig.group(activity); qCDebug(POWERDEVIL) << activityConfig.groupList() << activityConfig.keyList(); // See if this activity has priority if (activityConfig.readEntry("mode", "None") == "SeparateSettings") { // Prioritize this profile over anything config = activityConfig.group("SeparateSettings"); qCDebug(POWERDEVIL) << "Activity is enforcing a different profile"; profileId = activity; } else { // It doesn't, let's load the current state's profile if (m_batteriesPercent.isEmpty()) { qCDebug(POWERDEVIL) << "No batteries found, loading AC"; profileId = "AC"; } else if (activityConfig.readEntry("mode", "None") == "ActLike") { if (activityConfig.readEntry("actLike", QString()) == "AC" || activityConfig.readEntry("actLike", QString()) == "Battery" || activityConfig.readEntry("actLike", QString()) == "LowBattery") { // Same as above, but with an existing profile config = m_profilesConfig.data()->group(activityConfig.readEntry("actLike", QString())); profileId = activityConfig.readEntry("actLike", QString()); qCDebug(POWERDEVIL) << "Activity is mirroring a different profile"; } } else { // Compute the previous and current global percentage const int percent = currentChargePercent(); if (backend()->acAdapterState() == BackendInterface::Plugged) { profileId = "AC"; qCDebug(POWERDEVIL) << "Loading profile for plugged AC"; } else if (percent <= PowerDevilSettings::batteryLowLevel()) { profileId = "LowBattery"; qCDebug(POWERDEVIL) << "Loading profile for low battery"; } else { profileId = "Battery"; qCDebug(POWERDEVIL) << "Loading profile for unplugged AC"; } } config = m_profilesConfig.data()->group(profileId); qCDebug(POWERDEVIL) << "Activity is not forcing a profile"; } // 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); } } if (!config.isValid()) { emitNotification("powerdevilerror", i18n("The profile \"%1\" has been selected, " "but it does not exist.\nPlease check your PowerDevil configuration.", profileId)); return; } // 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(); KIdleTime::instance()->simulateUserActivity(); m_pendingWakeupEvent = false; } } else { // First of all, let's clean the old actions. This will also call the onProfileUnload callback ActionPool::instance()->unloadAllActiveActions(); // Do we need to force a wakeup? if (m_pendingWakeupEvent) { // Fake activity at this stage, when no timeouts are registered onResumingFromIdle(); KIdleTime::instance()->simulateUserActivity(); m_pendingWakeupEvent = false; } // Cool, now let's load the needed actions Q_FOREACH (const QString &actionName, config.groupList()) { Action *action = ActionPool::instance()->loadAction(actionName, config.group(actionName), this); if (action) { action->onProfileLoad(); } else { // Ouch, error. But let's just warn and move on anyway //TODO Maybe Remove from the configuration if unsupported qCWarning(POWERDEVIL) << "The profile " << profileId << "tried to activate" << actionName << "a non-existent action. This is usually due to an installation problem," " or to a configuration problem, or simply the action is not supported"; } } // 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 (activityConfig.readEntry("mode", "None") == "SpecialBehavior") { qCDebug(POWERDEVIL) << "Activity has special behaviors"; KConfigGroup behaviorGroup = activityConfig.group("SpecialBehavior"); if (behaviorGroup.readEntry("performAction", false)) { // Let's override the configuration for this action at all times ActionPool::instance()->loadAction("SuspendSession", behaviorGroup.group("ActionConfig"), this); qCDebug(POWERDEVIL) << "Activity overrides suspend session action"; } if (behaviorGroup.readEntry("noSuspend", false)) { qCDebug(POWERDEVIL) << "Activity triggers a suspend inhibition"; // 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 suspending")); m_sessionActivityInhibit.insert(activity, cookie); } } if (behaviorGroup.readEntry("noScreenManagement", false)) { 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_backend->isLidClosed()) { Q_EMIT m_backend->buttonPressed(PowerDevil::BackendInterface::LidClose); } } 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; } if (!connect(b, &Battery::chargePercentChanged, this, &Core::onBatteryChargePercentChanged) || !connect(b, &Battery::chargeStateChanged, this, &Core::onBatteryChargeStateChanged)) { emitNotification("powerdevilerror", i18n("Could not connect to battery interface.\n" "Please check your system configuration")); } 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_criticalBatteryTimer->isActive() && currentChargePercent() > PowerDevilSettings::batteryCriticalLevel()) { m_criticalBatteryTimer->stop(); if (m_criticalBatteryNotification) { m_criticalBatteryNotification->close(); } emitRichNotification("pluggedin", i18n("Extra Battery Added"), i18n("All pending suspend actions have been canceled.")); // FIXME This wording is too technical } } 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); Battery *b = qobject_cast(device.asDeviceInterface(DeviceInterface::Battery)); disconnect(b, &Battery::chargePercentChanged, this, &Core::onBatteryChargePercentChanged); disconnect(b, &Battery::chargeStateChanged, this, &Core::onBatteryChargeStateChanged); 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 &evid, const QString &message, const QString &iconname) { if (!iconname.isEmpty()) { KNotification::event(evid, message, QIcon::fromTheme(iconname).pixmap(48,48), 0, KNotification::CloseOnTimeout, "powerdevil"); } else { KNotification::event(evid, message, QPixmap(), 0, KNotification::CloseOnTimeout, "powerdevil"); } } void Core::emitNotification(const QString &eventId, const QString &title, const QString &message, const QString &iconName) { KNotification::event(eventId, title, message, iconName, 0, KNotification::CloseOnTimeout, "powerdevil"); } void Core::emitRichNotification(const QString &evid, const QString &title, const QString &message) { KNotification::event(evid, title, message, QPixmap(), 0, KNotification::CloseOnTimeout, "powerdevil"); } bool Core::emitBatteryChargePercentNotification(int currentPercent, int previousPercent, const QString &udi) { using namespace Solid; Device device(udi); Battery *b = qobject_cast(device.asDeviceInterface(DeviceInterface::Battery)); if (b && !b->isPowerSupply()) { // 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()) { return false; } if (currentPercent <= PowerDevilSettings::peripheralBatteryLowLevel() && previousPercent > PowerDevilSettings::peripheralBatteryLowLevel()) { 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; QString msg; QString icon; switch(b->type()) { case Battery::MouseBattery: title = i18n("Mouse Battery Low (%1% Remaining)", currentPercent); msg = i18nc("Placeholder is device name", "The battery in your mouse (\"%1\") is low, and the device may turn itself off at any time. " "Please replace or charge the battery as soon as possible.", name); icon = QStringLiteral("input-mouse"); break; case Battery::KeyboardBattery: title = i18n("Keyboard Battery Low (%1% Remaining)", currentPercent); msg = i18nc("Placeholder is device name", "The battery in your keyboard (\"%1\") is low, and the device may turn itself off at any time. " "Please replace or charge the battery as soon as possible.", name); icon = QStringLiteral("input-keyboard"); break; default: title = i18nc("The battery in an external device", "Device Battery Low (%1% Remaining)", currentPercent); msg = i18nc("Placeholder is device name", "The battery in a connected device (\"%1\") is low, and the device may turn itself off at any time. " "Please replace or charge the battery as soon as possible.", name); icon = QStringLiteral("battery-caution"); break; } emitNotification("lowperipheralbattery", title, msg, icon); return true; } return false; } if (m_backend->acAdapterState() == BackendInterface::Plugged) { return false; } if (currentPercent <= PowerDevilSettings::batteryCriticalLevel() && previousPercent > PowerDevilSettings::batteryCriticalLevel()) { handleCriticalBattery(currentPercent); return true; } else if (currentPercent <= PowerDevilSettings::batteryLowLevel() && previousPercent > PowerDevilSettings::batteryLowLevel()) { emitRichNotification("lowbattery", i18n("Battery Low (%1% Remaining)", currentPercent), i18n("Your battery is low. If you need to continue using your computer, either plug in your computer, or shut it down and then change the battery.")); return true; } return false; } void Core::handleCriticalBattery(int percent) { // no parent, but it won't leak, since it will be closed both in case of timeout or direct action m_criticalBatteryNotification = new KNotification("criticalbattery", KNotification::Persistent, nullptr); m_criticalBatteryNotification->setComponentName(QStringLiteral("powerdevil")); m_criticalBatteryNotification->setTitle(i18n("Battery Critical (%1% Remaining)", percent)); const QStringList actions = {i18nc("Cancel timeout that will automatically suspend system because of low battery", "Cancel")}; connect(m_criticalBatteryNotification.data(), &KNotification::action1Activated, this, [this] { m_criticalBatteryTimer->stop(); m_criticalBatteryNotification->close(); }); switch (PowerDevilSettings::batteryCriticalAction()) { case PowerDevil::BundledActions::SuspendSession::ShutdownMode: m_criticalBatteryNotification->setText(i18n("Your battery level is critical, the computer will be halted in 60 seconds.")); m_criticalBatteryNotification->setActions(actions); m_criticalBatteryTimer->start(); break; case PowerDevil::BundledActions::SuspendSession::ToDiskMode: m_criticalBatteryNotification->setText(i18n("Your battery level is critical, the computer will be hibernated in 60 seconds.")); m_criticalBatteryNotification->setActions(actions); m_criticalBatteryTimer->start(); break; case PowerDevil::BundledActions::SuspendSession::ToRamMode: m_criticalBatteryNotification->setText(i18n("Your battery level is critical, the computer will be suspended in 60 seconds.")); m_criticalBatteryNotification->setActions(actions); m_criticalBatteryTimer->start(); break; default: m_criticalBatteryNotification->setText(i18n("Your battery level is critical, save your work as soon as possible.")); // no timer, no actions break; } m_criticalBatteryNotification->sendEvent(); } void Core::onAcAdapterStateChanged(PowerDevil::BackendInterface::AcAdapterState state) { qCDebug(POWERDEVIL); // Post request for faking an activity event - usually adapters don't plug themselves out :) m_pendingWakeupEvent = true; loadProfile(); if (state == BackendInterface::Plugged) { // If the AC Adaptor has been plugged in, let's clear some pending suspend actions if (m_criticalBatteryTimer->isActive()) { m_criticalBatteryTimer->stop(); if (m_criticalBatteryNotification) { m_criticalBatteryNotification->close(); } emitRichNotification("pluggedin", i18n("AC Adapter Plugged In"), i18n("All pending suspend actions have been canceled.")); } else { emitRichNotification("pluggedin", i18n("Running on AC power"), i18n("The power adaptor has been plugged in.")); } } else if (state == BackendInterface::Unplugged) { emitRichNotification("unplugged", i18n("Running on Battery Power"), i18n("The power adaptor has been unplugged.")); } } void Core::onBackendError(const QString& error) { emitNotification("powerdevilerror", i18n("KDE Power Management System could not be initialized. " "The backend reported the following error: %1\n" "Please check your system configuration", error)); } 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) { if (emitBatteryChargePercentNotification(currentPercent, previousPercent, udi)) { // 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_backend->acAdapterState() != BackendInterface::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("fullbattery", i18n("Charge Complete"), i18n("Your battery is now fully charged.")); loadProfile(); } } void Core::onCriticalBatteryTimerExpired() { if (m_criticalBatteryNotification) { m_criticalBatteryNotification->close(); } // Do that only if we're not on AC if (m_backend->acAdapterState() == BackendInterface::Unplugged) { // We consider this as a very special button PowerDevil::Action *helperAction = ActionPool::instance()->loadAction("HandleButtonEvents", KConfigGroup(), this); if (helperAction) { QVariantMap args; args["Button"] = 32; args["Type"] = QVariant::fromValue(PowerDevilSettings::batteryCriticalAction()); helperAction->trigger(args); } } } void Core::onBatteryRemainingTimeChanged(qulonglong time) { Q_EMIT batteryRemainingTimeChanged(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(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 (PowerDevilSettings::pausePlayersOnSuspend()) { + qCDebug(POWERDEVIL) << "Pausing all media players before suspending"; + + QDBusPendingCall listNamesCall = QDBusConnection::sessionBus().interface()->asyncCall(QStringLiteral("ListNames")); + QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(listNamesCall, this); + connect(callWatcher, &QDBusPendingCallWatcher::finished, this, [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 suspend" << reply.error().message(); + return; + } + + const QStringList &services = reply.value(); + for (const QString &serviceName : services) { + if (!serviceName.startsWith(QLatin1String("org.mpris.MediaPlayer2."))) { + 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, int timeout) { // Register the timeout with KIdleTime int identifier = KIdleTime::instance()->addIdleTimeout(timeout); // Add the identifier to the action hash QList< int > timeouts = m_registeredActionTimeouts[action]; timeouts.append(identifier); m_registeredActionTimeouts[action] = timeouts; } void Core::unregisterActionTimeouts(Action* action) { // Clear all timeouts from the action QList< int > timeoutsToClean = m_registeredActionTimeouts[action]; Q_FOREACH (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() { // Wake up the actions in which an idle action was triggered auto i = m_pendingResumeFromIdleActions.begin(); while (i != m_pendingResumeFromIdleActions.end()) { (*i)->onWakeupFromIdle(); i = m_pendingResumeFromIdleActions.erase(i); } } 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; } } BackendInterface* Core::backend() { return m_backend; } bool Core::isLidClosed() const { return m_backend->isLidClosed(); } bool Core::isLidPresent() const { return m_backend->isLidPresent(); } bool Core::hasDualGpu() const { return m_hasDualGpu; } qulonglong Core::batteryRemainingTime() const { return m_backend->batteryRemainingTime(); } uint Core::backendCapabilities() { return m_backend->capabilities(); } } diff --git a/daemon/powerdevilcore.h b/daemon/powerdevilcore.h index cbe04ac0..2c1f9d0f 100644 --- a/daemon/powerdevilcore.h +++ b/daemon/powerdevilcore.h @@ -1,161 +1,162 @@ /*************************************************************************** * Copyright (C) 2010 by Dario Freddi * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * ***************************************************************************/ #ifndef POWERDEVILCORE_H #define POWERDEVILCORE_H #include "powerdevilbackendinterface.h" #include #include #include #include namespace KActivities { class Consumer; } // namespace KActivities class KDirWatch; class QDBusServiceWatcher; class QTimer; class KNotification; namespace Solid { class Battery; } namespace PowerDevil { class BackendInterface; class Action; class Q_DECL_EXPORT Core : public QObject { Q_OBJECT Q_DISABLE_COPY(Core) Q_CLASSINFO("D-Bus Interface", "org.kde.Solid.PowerManagement") public: explicit Core(QObject* parent); virtual ~Core(); void reloadProfile(int state); void emitNotification(const QString &evid, const QString &message = QString(), const QString &iconname = QString()); void emitRichNotification(const QString &evid, const QString &title, const QString &message = QString()); void emitNotification(const QString &eventId, const QString &title, const QString &message, const QString &iconName); bool emitBatteryChargePercentNotification(int currentPercent, int previousPercent, const QString &udi = QString()); BackendInterface *backend(); // More... public Q_SLOTS: void loadCore(PowerDevil::BackendInterface *backend); // Set of common action - useful for the DBus interface uint backendCapabilities(); void refreshStatus(); void reparseConfiguration(); QString currentProfile() const; void loadProfile(bool force = false); qulonglong batteryRemainingTime() const; bool isLidClosed() const; bool isLidPresent() const; bool isActionSupported(const QString &actionName); bool hasDualGpu() const; Q_SIGNALS: void coreReady(); void profileChanged(const QString &newProfile); void configurationReloaded(); void batteryRemainingTimeChanged(qulonglong time); void lidClosedChanged(bool closed); private: void registerActionTimeout(Action *action, int timeout); void unregisterActionTimeouts(Action *action); void handleCriticalBattery(int percent); /** * Computes the current global charge percentage. * Sum of all battery charges. */ int currentChargePercent() const; friend class Action; bool m_hasDualGpu; BackendInterface *m_backend; QDBusServiceWatcher *m_notificationsWatcher; bool m_notificationsReady = false; KSharedConfigPtr m_profilesConfig; QString m_currentProfile; QHash m_batteriesPercent; QHash m_peripheralBatteriesPercent; QHash m_batteriesCharged; QTimer *m_criticalBatteryTimer; QPointer m_criticalBatteryNotification; KActivities::Consumer *m_activityConsumer; // Idle time management QHash< Action*, QList< int > > m_registeredActionTimeouts; QSet m_pendingResumeFromIdleActions; bool m_pendingWakeupEvent; // Activity inhibition management QHash< QString, int > m_sessionActivityInhibit; QHash< QString, int > m_screenActivityInhibit; private Q_SLOTS: void onBackendReady(); void onBackendError(const QString &error); void onAcAdapterStateChanged(PowerDevil::BackendInterface::AcAdapterState); void onBatteryChargePercentChanged(int,const QString&); void onBatteryChargeStateChanged(int,const QString&); void onBatteryRemainingTimeChanged(qulonglong); void onKIdleTimeoutReached(int,int); void onResumingFromIdle(); void onDeviceAdded(const QString &udi); void onDeviceRemoved(const QString &udi); void onCriticalBatteryTimerExpired(); void onNotificationTimeout(); void onServiceRegistered(const QString &service); void onLidClosedChanged(bool closed); + void onAboutToSuspend(); }; } #endif // POWERDEVILCORE_H diff --git a/kcmodule/global/GeneralPage.cpp b/kcmodule/global/GeneralPage.cpp index 78a6fe20..565e01f1 100644 --- a/kcmodule/global/GeneralPage.cpp +++ b/kcmodule/global/GeneralPage.cpp @@ -1,209 +1,215 @@ /*************************************************************************** * Copyright (C) 2008 by Dario Freddi * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * ***************************************************************************/ #include "GeneralPage.h" #include "ErrorOverlay.h" #include "PowerDevilSettings.h" #include "actions/bundled/suspendsession.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include K_PLUGIN_FACTORY(PowerDevilGeneralKCMFactory, registerPlugin(); ) GeneralPage::GeneralPage(QWidget *parent, const QVariantList &args) : KCModule(0, parent, args) { setButtons(Apply | Help); // KAboutData *about = // new KAboutData("powerdevilglobalconfig", "powerdevilglobalconfig", ki18n("Global Power Management Configuration"), // "", ki18n("A global power management configurator for KDE Power Management System"), // KAboutData::License_GPL, ki18n("(c), 2010 Dario Freddi"), // ki18n("From this module, you can configure the main Power Management daemon, assign profiles to " // "states, and do some advanced fine tuning on battery handling")); // // about->addAuthor(ki18n("Dario Freddi"), ki18n("Maintainer") , "drf@kde.org", // "http://drfav.wordpress.com"); // // setAboutData(about); setupUi(this); fillUi(); QDBusServiceWatcher *watcher = new QDBusServiceWatcher("org.kde.Solid.PowerManagement", QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForRegistration | QDBusServiceWatcher::WatchForUnregistration, this); connect(watcher, SIGNAL(serviceRegistered(QString)), this, SLOT(onServiceRegistered(QString))); connect(watcher, SIGNAL(serviceUnregistered(QString)), this, SLOT(onServiceUnregistered(QString))); if (QDBusConnection::sessionBus().interface()->isServiceRegistered("org.kde.Solid.PowerManagement")) { onServiceRegistered("org.kde.Solid.PowerManagement"); } else { onServiceUnregistered("org.kde.Solid.PowerManagement"); } } GeneralPage::~GeneralPage() { } void GeneralPage::fillUi() { bool hasPowerSupplyBattery = false; bool hasPeripheralBattery = false; Q_FOREACH (const Solid::Device &device, Solid::Device::listFromType(Solid::DeviceInterface::Battery, QString())) { const Solid::Battery *b = qobject_cast (device.asDeviceInterface(Solid::DeviceInterface::Battery)); if (b->isPowerSupply()) { hasPowerSupplyBattery = true; } else { hasPeripheralBattery = true; } } QSet< Solid::PowerManagement::SleepState > methods = Solid::PowerManagement::supportedSleepStates(); BatteryCriticalCombo->addItem(QIcon::fromTheme("dialog-cancel"), i18n("Do nothing"), PowerDevil::BundledActions::SuspendSession::None); if (methods.contains(Solid::PowerManagement::SuspendState)) { BatteryCriticalCombo->addItem(QIcon::fromTheme("system-suspend"), i18n("Suspend"), PowerDevil::BundledActions::SuspendSession::ToRamMode); } if (methods.contains(Solid::PowerManagement::HibernateState)) { BatteryCriticalCombo->addItem(QIcon::fromTheme("system-suspend-hibernate"), i18n("Hibernate"), PowerDevil::BundledActions::SuspendSession::ToDiskMode); } BatteryCriticalCombo->addItem(QIcon::fromTheme("system-shutdown"), i18n("Shut down"), PowerDevil::BundledActions::SuspendSession::ShutdownMode); notificationsButton->setIcon(QIcon::fromTheme("preferences-desktop-notification")); // modified fields... connect(notificationsButton, SIGNAL(clicked()), SLOT(configureNotifications())); connect(lowSpin, SIGNAL(valueChanged(int)), SLOT(changed())); connect(criticalSpin, SIGNAL(valueChanged(int)), SLOT(changed())); connect(lowPeripheralSpin, SIGNAL(valueChanged(int)), SLOT(changed())); connect(BatteryCriticalCombo, SIGNAL(currentIndexChanged(int)), SLOT(changed())); + connect(pausePlayersCheckBox, &QCheckBox::stateChanged, this, static_cast(&KCModule::changed)); + if (!hasPowerSupplyBattery) { BatteryCriticalLabel->hide(); BatteryCriticalCombo->hide(); lowLabel->hide(); lowSpin->hide(); criticalLabel->hide(); criticalSpin->hide(); } if (!hasPeripheralBattery) { lowPeripheralLabel->hide(); lowPeripheralSpin->hide(); } if (!hasPeripheralBattery && !hasPeripheralBattery) { batteryLevelsLabel->hide(); } } void GeneralPage::load() { lowSpin->setValue(PowerDevilSettings::batteryLowLevel()); criticalSpin->setValue(PowerDevilSettings::batteryCriticalLevel()); lowPeripheralSpin->setValue(PowerDevilSettings::peripheralBatteryLowLevel()); BatteryCriticalCombo->setCurrentIndex(BatteryCriticalCombo->findData(PowerDevilSettings::batteryCriticalAction())); + + pausePlayersCheckBox->setChecked(PowerDevilSettings::pausePlayersOnSuspend()); } void GeneralPage::configureNotifications() { KNotifyConfigWidget::configure(this, "powerdevil"); } void GeneralPage::save() { PowerDevilSettings::setBatteryLowLevel(lowSpin->value()); PowerDevilSettings::setBatteryCriticalLevel(criticalSpin->value()); PowerDevilSettings::setPeripheralBatteryLowLevel(lowPeripheralSpin->value()); PowerDevilSettings::setBatteryCriticalAction(BatteryCriticalCombo->itemData(BatteryCriticalCombo->currentIndex()).toInt()); + PowerDevilSettings::setPausePlayersOnSuspend(pausePlayersCheckBox->checkState() == Qt::Checked); + PowerDevilSettings::self()->save(); // Notify Daemon QDBusMessage call = QDBusMessage::createMethodCall("org.kde.Solid.PowerManagement", "/org/kde/Solid/PowerManagement", "org.kde.Solid.PowerManagement", "refreshStatus"); // Perform call QDBusConnection::sessionBus().asyncCall(call); // And now we are set with no change Q_EMIT changed(false); } void GeneralPage::defaults() { KCModule::defaults(); } void GeneralPage::onServiceRegistered(const QString& service) { Q_UNUSED(service); if (m_errorOverlay) { m_errorOverlay->deleteLater(); m_errorOverlay = nullptr; } } void GeneralPage::onServiceUnregistered(const QString& service) { Q_UNUSED(service); if (m_errorOverlay) { m_errorOverlay->deleteLater(); } m_errorOverlay = new ErrorOverlay(this, i18n("The Power Management Service appears not to be running.\n" "This can be solved by starting or scheduling it inside \"Startup and Shutdown\""), this); } #include "GeneralPage.moc" diff --git a/kcmodule/global/generalPage.ui b/kcmodule/global/generalPage.ui index 52985af9..8d6b466a 100644 --- a/kcmodule/global/generalPage.ui +++ b/kcmodule/global/generalPage.ui @@ -1,194 +1,208 @@ generalPage 0 0 - 494 + 510 276 QFormLayout::ExpandingFieldsGrow <b>Battery Levels </b> &Low level: lowSpin 100 16777215 Low battery level Battery will be considered low when it reaches this level % 100 &Critical level: criticalSpin 100 16777215 Critical battery level Battery will be considered critical when it reaches this level % 100 A&t critical level: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter false BatteryCriticalCombo 300 16777215 - + Qt::Vertical 20 16 - + 0 0 16777215 16777215 Configure Notifications... Low level for peripheral devices: % 100 + + + + Enabled + + + + + + + Pause media players when suspending: + + + Qt::Horizontal QSizePolicy::Fixed 8 20 KComboBox QComboBox
kcombobox.h