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"