1 /***************************************************************************
2 * Copyright (C) 2010 by Dario Freddi <drf@kde.org> *
3 * *
4 * This program is free software; you can redistribute it and/or modify *
5 * it under the terms of the GNU General Public License as published by *
6 * the Free Software Foundation; either version 2 of the License, or *
7 * (at your option) any later version. *
8 * *
9 * This program is distributed in the hope that it will be useful, *
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
12 * GNU General Public License for more details. *
13 * *
14 * You should have received a copy of the GNU General Public License *
15 * along with this program; if not, write to the *
16 * Free Software Foundation, Inc., *
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . *
18 ***************************************************************************/
19
20 #include "powerdevilcore.h"
21
22 #include "PowerDevilSettings.h"
23
24 #include "powerdevilaction.h"
25 #include "powerdevilactionpool.h"
26 #include "powerdevilpolicyagent.h"
27 #include "powerdevilprofilegenerator.h"
28 #include "powerdevil_debug.h"
29
30 #include "actions/bundled/suspendsession.h"
31
32 #include <Solid/Battery>
33 #include <Solid/Device>
34 #include <Solid/DeviceNotifier>
35
36 #include <KAuthExecuteJob>
37 #include <KAuthAction>
38 #include <KIdleTime>
39 #include <KLocalizedString>
40 #include <KNotification>
41
42 #include <KActivities/Consumer>
43
44 #include <QTimer>
45 #include <QDBusConnection>
46 #include <QDBusConnectionInterface>
47 #include <QDBusServiceWatcher>
48
49 #include <QDebug>
50
51 #include <algorithm>
52
53 #ifdef Q_OS_LINUX
54 #include <sys/timerfd.h>
55 #endif
56
57 namespace PowerDevil
58 {
59
Core(QObject * parent)60 Core::Core(QObject* parent)
61 : QObject(parent)
62 , m_hasDualGpu(false)
63 , m_backend(nullptr)
64 , m_notificationsWatcher(nullptr)
65 , m_criticalBatteryTimer(new QTimer(this))
66 , m_activityConsumer(new KActivities::Consumer(this))
67 , m_pendingWakeupEvent(true)
68 {
69 KAuth::Action discreteGpuAction(QStringLiteral("org.kde.powerdevil.discretegpuhelper.hasdualgpu"));
70 discreteGpuAction.setHelperId(QStringLiteral("org.kde.powerdevil.discretegpuhelper"));
71 KAuth::ExecuteJob *discreteGpuJob = discreteGpuAction.execute();
72 connect(discreteGpuJob, &KJob::result, this, [this, discreteGpuJob] {
73 if (discreteGpuJob->error()) {
74 qCWarning(POWERDEVIL) << "org.kde.powerdevil.discretegpuhelper.hasdualgpu failed";
75 qCDebug(POWERDEVIL) << discreteGpuJob->errorText();
76 return;
77 }
78 m_hasDualGpu = discreteGpuJob->data()[QStringLiteral("hasdualgpu")].toBool();
79 });
80 discreteGpuJob->start();
81
82 readChargeThreshold();
83 }
84
~Core()85 Core::~Core()
86 {
87 qCDebug(POWERDEVIL) << "Core unloading";
88 // Unload all actions before exiting, and clear the cache
89 ActionPool::instance()->unloadAllActiveActions();
90 ActionPool::instance()->clearCache();
91 }
92
loadCore(BackendInterface * backend)93 void Core::loadCore(BackendInterface* backend)
94 {
95 if (!backend) {
96 return;
97 }
98
99 m_backend = backend;
100
101 // Async backend init - so that KDED gets a bit of a speed up
102 qCDebug(POWERDEVIL) << "Core loaded, initializing backend";
103 connect(m_backend, &BackendInterface::backendReady, this, &Core::onBackendReady);
104 m_backend->init();
105 }
106
onBackendReady()107 void Core::onBackendReady()
108 {
109 qCDebug(POWERDEVIL) << "Backend ready, KDE Power Management system initialized";
110
111 m_profilesConfig = KSharedConfig::openConfig(QStringLiteral("powermanagementprofilesrc"), KConfig::CascadeConfig);
112
113 QStringList groups = m_profilesConfig->groupList();
114 // the "migration" key is for shortcuts migration in added by migratePre512KeyboardShortcuts
115 // and as such our configuration would never be considered empty, ignore it!
116 groups.removeOne(QStringLiteral("migration"));
117
118 // Is it brand new?
119 if (groups.isEmpty()) {
120 // Generate defaults
121 qCDebug(POWERDEVIL) << "Generating a default configuration";
122 bool toRam = m_backend->supportedSuspendMethods() & PowerDevil::BackendInterface::ToRam;
123 bool toDisk = m_backend->supportedSuspendMethods() & PowerDevil::BackendInterface::ToDisk;
124 ProfileGenerator::generateProfiles(toRam, toDisk);
125 m_profilesConfig->reparseConfiguration();
126 }
127
128 // Get the battery devices ready
129 {
130 using namespace Solid;
131 connect(DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceAdded,
132 this, &Core::onDeviceAdded);
133 connect(DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceRemoved,
134 this, &Core::onDeviceRemoved);
135
136 // Force the addition of already existent batteries
137 const auto devices = Device::listFromType(DeviceInterface::Battery, QString());
138 for (const Device &device : devices) {
139 onDeviceAdded(device.udi());
140 }
141 }
142
143 connect(m_backend, &BackendInterface::acAdapterStateChanged,
144 this, &Core::onAcAdapterStateChanged);
145 connect(m_backend, &BackendInterface::batteryRemainingTimeChanged,
146 this, &Core::onBatteryRemainingTimeChanged);
147 connect(m_backend, &BackendInterface::lidClosedChanged,
148 this, &Core::onLidClosedChanged);
149 connect(m_backend, &BackendInterface::aboutToSuspend,
150 this, &Core::onAboutToSuspend);
151 connect(KIdleTime::instance(), SIGNAL(timeoutReached(int,int)),
152 this, SLOT(onKIdleTimeoutReached(int,int)));
153 connect(KIdleTime::instance(), &KIdleTime::resumingFromIdle,
154 this, &Core::onResumingFromIdle);
155 connect(m_activityConsumer, &KActivities::Consumer::currentActivityChanged, this, [this]() {
156 loadProfile();
157 });
158
159 // Set up the policy agent
160 PowerDevil::PolicyAgent::instance()->init();
161 // When inhibitions change, simulate user activity, see Bug 315438
162 connect(PowerDevil::PolicyAgent::instance(), &PowerDevil::PolicyAgent::unavailablePoliciesChanged, this,
163 [](PowerDevil::PolicyAgent::RequiredPolicies newPolicies) {
164 Q_UNUSED(newPolicies);
165 KIdleTime::instance()->simulateUserActivity();
166 });
167
168 // Bug 354250: Simulate user activity when session becomes inactive,
169 // this keeps us from sending the computer to sleep when switching to an idle session.
170 // (this is just being lazy as it will result in us clearing everything
171 connect(PowerDevil::PolicyAgent::instance(), &PowerDevil::PolicyAgent::sessionActiveChanged, this, [this](bool active) {
172 if (active) {
173 // force reload profile so all actions re-register their idle timeouts
174 loadProfile(true /*force*/);
175 } else {
176 // Bug 354250: Keep us from sending the computer to sleep when switching
177 // to an idle session by removing all idle timeouts
178 KIdleTime::instance()->removeAllIdleTimeouts();
179 m_registeredActionTimeouts.clear();
180 }
181 });
182
183 // Initialize the action pool, which will also load the needed startup actions.
184 PowerDevil::ActionPool::instance()->init(this);
185
186 // Set up the critical battery timer
187 m_criticalBatteryTimer->setSingleShot(true);
188 m_criticalBatteryTimer->setInterval(60000);
189 connect(m_criticalBatteryTimer, &QTimer::timeout, this, &Core::onCriticalBatteryTimerExpired);
190
191 // wait until the notification system is set up before firing notifications
192 // to avoid them showing ontop of ksplash...
193 if (QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.freedesktop.Notifications"))) {
194 onServiceRegistered(QString());
195 } else {
196 m_notificationsWatcher = new QDBusServiceWatcher(QStringLiteral("org.freedesktop.Notifications"),
197 QDBusConnection::sessionBus(),
198 QDBusServiceWatcher::WatchForRegistration,
199 this);
200 connect(m_notificationsWatcher, &QDBusServiceWatcher::serviceRegistered, this, &Core::onServiceRegistered);
201
202 // ...but fire them after 30s nonetheless to ensure they've been shown
203 QTimer::singleShot(30000, this, &Core::onNotificationTimeout);
204 }
205
206 #ifdef Q_OS_LINUX
207
208 // try creating a timerfd which can wake system from suspend
209 m_timerFd = timerfd_create(CLOCK_REALTIME_ALARM, TFD_CLOEXEC);
210
211 // if that fails due to privilges maybe, try normal timerfd
212 if (m_timerFd == -1) {
213 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";
214 m_timerFd = timerfd_create(CLOCK_REALTIME, TFD_CLOEXEC);
215 }
216
217 if (m_timerFd != -1) {
218 m_timerFdSocketNotifier = new QSocketNotifier(m_timerFd, QSocketNotifier::Read);
219 connect(m_timerFdSocketNotifier, &QSocketNotifier::activated, this, &Core::timerfdEventHandler);
220 // we disable events reading for now
221 m_timerFdSocketNotifier->setEnabled(false);
222 } else {
223 qCDebug(POWERDEVIL) << "Unable to create a CLOCK_REALTIME timer, scheduled wakeups won't be available";
224 }
225
226 #endif
227
228 // All systems up Houston, let's go!
229 Q_EMIT coreReady();
230 refreshStatus();
231 }
232
isActionSupported(const QString & actionName)233 bool Core::isActionSupported(const QString& actionName)
234 {
235 Action *action = ActionPool::instance()->loadAction(actionName, KConfigGroup(), this);
236 if (!action) {
237 return false;
238 } else {
239 return action->isSupported();
240 }
241 }
242
refreshStatus()243 void Core::refreshStatus()
244 {
245 /* The configuration could have changed if this function was called, so
246 * let's resync it.
247 */
248 reparseConfiguration();
249
250 loadProfile(true);
251 }
252
reparseConfiguration()253 void Core::reparseConfiguration()
254 {
255 PowerDevilSettings::self()->load();
256 m_profilesConfig->reparseConfiguration();
257
258 // Config reloaded
259 Q_EMIT configurationReloaded();
260
261 // Check if critical threshold might have changed and cancel the timer if necessary.
262 if (currentChargePercent() > PowerDevilSettings::batteryCriticalLevel()) {
263 m_criticalBatteryTimer->stop();
264 if (m_criticalBatteryNotification) {
265 m_criticalBatteryNotification->close();
266 }
267 }
268
269 if (m_lowBatteryNotification && currentChargePercent() > PowerDevilSettings::batteryLowLevel()) {
270 m_lowBatteryNotification->close();
271 }
272
273 readChargeThreshold();
274 }
275
currentProfile() const276 QString Core::currentProfile() const
277 {
278 return m_currentProfile;
279 }
280
loadProfile(bool force)281 void Core::loadProfile(bool force)
282 {
283 QString profileId;
284
285 // Policy check
286 if (PolicyAgent::instance()->requirePolicyCheck(PolicyAgent::ChangeProfile) != PolicyAgent::None) {
287 qCDebug(POWERDEVIL) << "Policy Agent prevention: on";
288 return;
289 }
290
291 KConfigGroup config;
292
293 // Check the activity in which we are in
294 QString activity = m_activityConsumer->currentActivity();
295 qCDebug(POWERDEVIL) << "Currently using activity " << activity;
296 KConfigGroup activitiesConfig(m_profilesConfig, "Activities");
297 qCDebug(POWERDEVIL) << activitiesConfig.groupList() << activitiesConfig.keyList();
298
299 // Are we mirroring an activity?
300 if (activitiesConfig.group(activity).readEntry("mode", "None") == QStringLiteral("ActLike") &&
301 activitiesConfig.group(activity).readEntry("actLike", QString()) != QStringLiteral("AC") &&
302 activitiesConfig.group(activity).readEntry("actLike", QString()) != QStringLiteral("Battery") &&
303 activitiesConfig.group(activity).readEntry("actLike", QString()) != QStringLiteral("LowBattery")) {
304 // Yes, let's use that then
305 activity = activitiesConfig.group(activity).readEntry("actLike", QString());
306 qCDebug(POWERDEVIL) << "Activity is a mirror";
307 }
308
309 KConfigGroup activityConfig = activitiesConfig.group(activity);
310 qCDebug(POWERDEVIL) << activityConfig.groupList() << activityConfig.keyList();
311
312 // See if this activity has priority
313 if (activityConfig.readEntry("mode", "None") == QStringLiteral("SeparateSettings")) {
314 // Prioritize this profile over anything
315 config = activityConfig.group("SeparateSettings");
316 qCDebug(POWERDEVIL) << "Activity is enforcing a different profile";
317 profileId = activity;
318 } else {
319 // It doesn't, let's load the current state's profile
320 if (m_batteriesPercent.isEmpty()) {
321 qCDebug(POWERDEVIL) << "No batteries found, loading AC";
322 profileId = QStringLiteral("AC");
323 } else if (activityConfig.readEntry("mode", "None") == QStringLiteral("ActLike")) {
324 if (activityConfig.readEntry("actLike", QString()) == QStringLiteral("AC") ||
325 activityConfig.readEntry("actLike", QString()) == QStringLiteral("Battery") ||
326 activityConfig.readEntry("actLike", QString()) == QStringLiteral("LowBattery")) {
327 // Same as above, but with an existing profile
328 config = m_profilesConfig.data()->group(activityConfig.readEntry("actLike", QString()));
329 profileId = activityConfig.readEntry("actLike", QString());
330 qCDebug(POWERDEVIL) << "Activity is mirroring a different profile";
331 }
332 } else {
333 // Compute the previous and current global percentage
334 const int percent = currentChargePercent();
335
336 if (backend()->acAdapterState() == BackendInterface::Plugged) {
337 profileId = QStringLiteral("AC");
338 qCDebug(POWERDEVIL) << "Loading profile for plugged AC";
339 } else if (percent <= PowerDevilSettings::batteryLowLevel()) {
340 profileId = QStringLiteral("LowBattery");
341 qCDebug(POWERDEVIL) << "Loading profile for low battery";
342 } else {
343 profileId = QStringLiteral("Battery");
344 qCDebug(POWERDEVIL) << "Loading profile for unplugged AC";
345 }
346 }
347
348 config = m_profilesConfig.data()->group(profileId);
349 qCDebug(POWERDEVIL) << "Activity is not forcing a profile";
350 }
351
352 // Release any special inhibitions
353 {
354 QHash<QString,int>::iterator i = m_sessionActivityInhibit.begin();
355 while (i != m_sessionActivityInhibit.end()) {
356 PolicyAgent::instance()->ReleaseInhibition(i.value());
357 i = m_sessionActivityInhibit.erase(i);
358 }
359
360 i = m_screenActivityInhibit.begin();
361 while (i != m_screenActivityInhibit.end()) {
362 PolicyAgent::instance()->ReleaseInhibition(i.value());
363 i = m_screenActivityInhibit.erase(i);
364 }
365 }
366
367 if (!config.isValid()) {
368 qCWarning(POWERDEVIL) << "Profile " << profileId << "has been selected but does not exist.";
369 return;
370 }
371
372 // Check: do we need to change profile at all?
373 if (m_currentProfile == profileId && !force) {
374 // No, let's leave things as they are
375 qCDebug(POWERDEVIL) << "Skipping action reload routine as profile has not changed";
376
377 // Do we need to force a wakeup?
378 if (m_pendingWakeupEvent) {
379 // Fake activity at this stage, when no timeouts are registered
380 onResumingFromIdle();
381 m_pendingWakeupEvent = false;
382 }
383 } else {
384 // First of all, let's clean the old actions. This will also call the onProfileUnload callback
385 ActionPool::instance()->unloadAllActiveActions();
386
387 // Do we need to force a wakeup?
388 if (m_pendingWakeupEvent) {
389 // Fake activity at this stage, when no timeouts are registered
390 onResumingFromIdle();
391 m_pendingWakeupEvent = false;
392 }
393
394 // Cool, now let's load the needed actions
395 const auto groupList = config.groupList();
396 for (const QString &actionName : groupList) {
397 Action *action = ActionPool::instance()->loadAction(actionName, config.group(actionName), this);
398 if (action) {
399 action->onProfileLoad();
400 } else {
401 // Ouch, error. But let's just warn and move on anyway
402 //TODO Maybe Remove from the configuration if unsupported
403 qCWarning(POWERDEVIL) << "The profile " << profileId << "tried to activate"
404 << actionName << "a non-existent action. This is usually due to an installation problem,"
405 " a configuration problem, or because the action is not supported";
406 }
407 }
408
409 // We are now on a different profile
410 m_currentProfile = profileId;
411 Q_EMIT profileChanged(m_currentProfile);
412 }
413
414 // Now... any special behaviors we'd like to consider?
415 if (activityConfig.readEntry("mode", "None") == QStringLiteral("SpecialBehavior")) {
416 qCDebug(POWERDEVIL) << "Activity has special behaviors";
417 KConfigGroup behaviorGroup = activityConfig.group("SpecialBehavior");
418 if (behaviorGroup.readEntry("performAction", false)) {
419 // Let's override the configuration for this action at all times
420 ActionPool::instance()->loadAction(QStringLiteral("SuspendSession"), behaviorGroup.group("ActionConfig"), this);
421 qCDebug(POWERDEVIL) << "Activity overrides suspend session action"; // debug hence not sleep
422 }
423
424 if (behaviorGroup.readEntry("noSuspend", false)) {
425 qCDebug(POWERDEVIL) << "Activity triggers a suspend inhibition"; // debug hence not sleep
426 // Trigger a special inhibition - if we don't have one yet
427 if (!m_sessionActivityInhibit.contains(activity)) {
428 int cookie =
429 PolicyAgent::instance()->AddInhibition(PolicyAgent::InterruptSession, i18n("Activity Manager"),
430 i18n("This activity's policies prevent the system from going to sleep"));
431
432 m_sessionActivityInhibit.insert(activity, cookie);
433 }
434 }
435
436 if (behaviorGroup.readEntry("noScreenManagement", false)) {
437 qCDebug(POWERDEVIL) << "Activity triggers a screen management inhibition";
438 // Trigger a special inhibition - if we don't have one yet
439 if (!m_screenActivityInhibit.contains(activity)) {
440 int cookie =
441 PolicyAgent::instance()->AddInhibition(PolicyAgent::ChangeScreenSettings, i18n("Activity Manager"),
442 i18n("This activity's policies prevent screen power management"));
443
444 m_screenActivityInhibit.insert(activity, cookie);
445 }
446 }
447 }
448
449 // If the lid is closed, retrigger the lid close signal
450 // so that "switching profile then closing the lid" has the same result as
451 // "closing lid then switching profile".
452 if (m_backend->isLidClosed()) {
453 Q_EMIT m_backend->buttonPressed(PowerDevil::BackendInterface::LidClose);
454 }
455 }
456
onDeviceAdded(const QString & udi)457 void Core::onDeviceAdded(const QString &udi)
458 {
459 if (m_batteriesPercent.contains(udi) || m_peripheralBatteriesPercent.contains(udi)) {
460 // We already know about this device
461 return;
462 }
463
464 using namespace Solid;
465 Device device(udi);
466 Battery *b = qobject_cast<Battery *>(device.asDeviceInterface(DeviceInterface::Battery));
467
468 if (!b) {
469 return;
470 }
471
472 connect(b, &Battery::chargePercentChanged, this, &Core::onBatteryChargePercentChanged);
473 connect(b, &Battery::chargeStateChanged, this, &Core::onBatteryChargeStateChanged);
474
475 qCDebug(POWERDEVIL) << "Battery with UDI" << udi << "was detected";
476
477 if (b->isPowerSupply()) {
478 m_batteriesPercent[udi] = b->chargePercent();
479 m_batteriesCharged[udi] = (b->chargeState() == Solid::Battery::FullyCharged);
480 } else { // non-power supply batteries are treated separately
481 m_peripheralBatteriesPercent[udi] = b->chargePercent();
482
483 // notify the user about the empty mouse/keyboard when plugging it in; don't when
484 // notifications aren't ready yet so we avoid showing them ontop of ksplash;
485 // also we'll notify about all devices when notifications are ready anyway
486 if (m_notificationsReady) {
487 emitBatteryChargePercentNotification(b->chargePercent(), 1000 /* so current is always lower than previous */, udi);
488 }
489 }
490
491 // If a new battery has been added, let's clear some pending suspend actions if the new global batteries percentage is
492 // higher than the battery critical level. (See bug 329537)
493 if (m_lowBatteryNotification && currentChargePercent() > PowerDevilSettings::batteryLowLevel()) {
494 m_lowBatteryNotification->close();
495 }
496
497 if (currentChargePercent() > PowerDevilSettings::batteryCriticalLevel()) {
498 if (m_criticalBatteryNotification) {
499 m_criticalBatteryNotification->close();
500 }
501
502 if (m_criticalBatteryTimer->isActive()) {
503 m_criticalBatteryTimer->stop();
504 emitRichNotification(QStringLiteral("pluggedin"),
505 i18n("Extra Battery Added"),
506 i18n("The computer will no longer go to sleep."));
507 }
508 }
509 }
510
onDeviceRemoved(const QString & udi)511 void Core::onDeviceRemoved(const QString &udi)
512 {
513 if (!m_batteriesPercent.contains(udi) && !m_peripheralBatteriesPercent.contains(udi)) {
514 // We don't know about this device
515 return;
516 }
517
518 using namespace Solid;
519 Device device(udi);
520 Battery *b = qobject_cast<Battery *>(device.asDeviceInterface(DeviceInterface::Battery));
521
522 disconnect(b, &Battery::chargePercentChanged, this, &Core::onBatteryChargePercentChanged);
523 disconnect(b, &Battery::chargeStateChanged, this, &Core::onBatteryChargeStateChanged);
524
525 qCDebug(POWERDEVIL) << "Battery with UDI" << udi << "has been removed";
526
527 m_batteriesPercent.remove(udi);
528 m_peripheralBatteriesPercent.remove(udi);
529 m_batteriesCharged.remove(udi);
530 }
531
emitNotification(const QString & eventId,const QString & title,const QString & message,const QString & iconName)532 void Core::emitNotification(const QString &eventId, const QString &title, const QString &message, const QString &iconName)
533 {
534 KNotification::event(eventId, title, message, iconName, nullptr, KNotification::CloseOnTimeout, QStringLiteral("powerdevil"));
535 }
536
emitRichNotification(const QString & evid,const QString & title,const QString & message)537 void Core::emitRichNotification(const QString &evid, const QString &title, const QString &message)
538 {
539 KNotification::event(evid, title, message, QPixmap(),
540 nullptr, KNotification::CloseOnTimeout, QStringLiteral("powerdevil"));
541 }
542
emitBatteryChargePercentNotification(int currentPercent,int previousPercent,const QString & udi)543 bool Core::emitBatteryChargePercentNotification(int currentPercent, int previousPercent, const QString &udi)
544 {
545 using namespace Solid;
546 Device device(udi);
547 Battery *b = qobject_cast<Battery *>(device.asDeviceInterface(DeviceInterface::Battery));
548
549 if (b && !b->isPowerSupply()) {
550 // if you leave the device out of reach or it has not been initialized yet
551 // it won't be "there" and report 0%, don't show anything in this case
552 if (!b->isPresent() || b->chargePercent() == 0) {
553 return false;
554 }
555
556 // Bluetooth devices don't report charge state, so it's "NoCharge" in all cases for them
557 if (b->chargeState() != Battery::Discharging && b->chargeState() != Battery::NoCharge) {
558 return false;
559 }
560
561 if (currentPercent <= PowerDevilSettings::peripheralBatteryLowLevel() &&
562 previousPercent > PowerDevilSettings::peripheralBatteryLowLevel()) {
563
564 QString name = device.product();
565 if (!device.vendor().isEmpty()) {
566 name = i18nc("%1 is vendor name, %2 is product name", "%1 %2", device.vendor(), device.product());
567 }
568
569 QString title;
570 QString msg;
571 QString icon;
572
573 switch(b->type()) {
574 case Battery::MouseBattery:
575 title = i18n("Mouse Battery Low (%1% Remaining)", currentPercent);
576 msg = i18nc("Placeholder is device name",
577 "The battery in (\"%1\") is running low, and the device may turn off at any time. "
578 "Please recharge or replace the battery.", name);
579 icon = QStringLiteral("input-mouse");
580 break;
581 case Battery::KeyboardBattery:
582 title = i18n("Keyboard Battery Low (%1% Remaining)", currentPercent);
583 msg = i18nc("Placeholder is device name",
584 "The battery in (\"%1\") is running low, and the device may turn off at any time. "
585 "Please recharge or replace the battery.", name);
586 icon = QStringLiteral("input-keyboard");
587 break;
588 case Battery::BluetoothBattery:
589 title = i18n("Bluetooth Device Battery Low (%1% Remaining)", currentPercent);
590 msg = i18nc("Placeholder is device name",
591 "The battery in Bluetooth device \"%1\" is running low, and the device may turn off at any time. "
592 "Please recharge or replace the battery.", name);
593 icon = QStringLiteral("preferences-system-bluetooth");
594 break;
595 default:
596 title = i18nc("The battery in an external device", "Device Battery Low (%1% Remaining)", currentPercent);
597 msg = i18nc("Placeholder is device name",
598 "The battery in (\"%1\") is running low, and the device may turn off at any time. "
599 "Please recharge or replace the battery.", name);
600 icon = QStringLiteral("battery-caution");
601 break;
602 }
603
604 emitNotification(QStringLiteral("lowperipheralbattery"), title, msg, icon);
605
606 return true;
607 }
608
609 return false;
610 }
611
612 if (m_backend->acAdapterState() == BackendInterface::Plugged) {
613 return false;
614 }
615
616 if (currentPercent <= PowerDevilSettings::batteryCriticalLevel() &&
617 previousPercent > PowerDevilSettings::batteryCriticalLevel()) {
618 handleCriticalBattery(currentPercent);
619 return true;
620 } else if (currentPercent <= PowerDevilSettings::batteryLowLevel() &&
621 previousPercent > PowerDevilSettings::batteryLowLevel()) {
622 handleLowBattery(currentPercent);
623 return true;
624 }
625 return false;
626 }
627
handleLowBattery(int percent)628 void Core::handleLowBattery(int percent)
629 {
630 if (m_lowBatteryNotification) {
631 return;
632 }
633
634 m_lowBatteryNotification = new KNotification(QStringLiteral("lowbattery"), KNotification::Persistent, nullptr);
635 m_lowBatteryNotification->setComponentName(QStringLiteral("powerdevil"));
636 m_lowBatteryNotification->setTitle(i18n("Battery Low (%1% Remaining)", percent));
637 m_lowBatteryNotification->setText(i18n("Battery running low - to continue using your computer, plug it in or shut it down and change the battery."));
638 m_lowBatteryNotification->setUrgency(KNotification::CriticalUrgency);
639 m_lowBatteryNotification->sendEvent();
640 }
641
handleCriticalBattery(int percent)642 void Core::handleCriticalBattery(int percent)
643 {
644 if (m_lowBatteryNotification) {
645 m_lowBatteryNotification->close();
646 }
647
648 // no parent, but it won't leak, since it will be closed both in case of timeout or direct action
649 m_criticalBatteryNotification = new KNotification(QStringLiteral("criticalbattery"), KNotification::Persistent, nullptr);
650 m_criticalBatteryNotification->setComponentName(QStringLiteral("powerdevil"));
651 m_criticalBatteryNotification->setTitle(i18n("Battery Critical (%1% Remaining)", percent));
652
653 const QStringList actions = {i18nc("Cancel timeout that will automatically put system to sleep because of low battery", "Cancel")};
654
655 connect(m_criticalBatteryNotification.data(), &KNotification::action1Activated, this, [this] {
656 m_criticalBatteryTimer->stop();
657 m_criticalBatteryNotification->close();
658 });
659
660 switch (PowerDevilSettings::batteryCriticalAction()) {
661 case PowerDevil::BundledActions::SuspendSession::ShutdownMode:
662 m_criticalBatteryNotification->setText(i18n("Battery level critical. Your computer will shut down in 60 seconds."));
663 m_criticalBatteryNotification->setActions(actions);
664 m_criticalBatteryTimer->start();
665 break;
666 case PowerDevil::BundledActions::SuspendSession::ToDiskMode:
667 m_criticalBatteryNotification->setText(i18n("Battery level critical. Your computer will enter hibernation mode in 60 seconds."));
668 m_criticalBatteryNotification->setActions(actions);
669 m_criticalBatteryTimer->start();
670 break;
671 case PowerDevil::BundledActions::SuspendSession::ToRamMode:
672 m_criticalBatteryNotification->setText(i18n("Battery level critical. Your computer will go to sleep in 60 seconds."));
673 m_criticalBatteryNotification->setActions(actions);
674 m_criticalBatteryTimer->start();
675 break;
676 default:
677 m_criticalBatteryNotification->setText(i18n("Battery level critical. Please save your work."));
678 // no timer, no actions
679 break;
680 }
681
682 m_criticalBatteryNotification->sendEvent();
683 }
684
onAcAdapterStateChanged(PowerDevil::BackendInterface::AcAdapterState state)685 void Core::onAcAdapterStateChanged(PowerDevil::BackendInterface::AcAdapterState state)
686 {
687 qCDebug(POWERDEVIL);
688 // Post request for faking an activity event - usually adapters don't plug themselves out :)
689 m_pendingWakeupEvent = true;
690 loadProfile();
691
692 if (state == BackendInterface::Plugged) {
693 // If the AC Adaptor has been plugged in, let's clear some pending suspend actions
694 if (m_lowBatteryNotification) {
695 m_lowBatteryNotification->close();
696 }
697
698 if (m_criticalBatteryNotification) {
699 m_criticalBatteryNotification->close();
700 }
701
702 if (m_criticalBatteryTimer->isActive()) {
703 m_criticalBatteryTimer->stop();
704 emitRichNotification(QStringLiteral("pluggedin"),
705 i18n("AC Adapter Plugged In"),
706 i18n("The computer will no longer go to sleep."));
707 } else {
708 emitRichNotification(QStringLiteral("pluggedin"), i18n("Running on AC power"), i18n("The power adapter has been plugged in."));
709 }
710 } else if (state == BackendInterface::Unplugged) {
711 emitRichNotification(QStringLiteral("unplugged"), i18n("Running on Battery Power"), i18n("The power adapter has been unplugged."));
712 }
713 }
714
onBatteryChargePercentChanged(int percent,const QString & udi)715 void Core::onBatteryChargePercentChanged(int percent, const QString &udi)
716 {
717 if (m_peripheralBatteriesPercent.contains(udi)) {
718 const int previousPercent = m_peripheralBatteriesPercent.value(udi);
719 m_peripheralBatteriesPercent[udi] = percent;
720
721 if (percent < previousPercent) {
722 emitBatteryChargePercentNotification(percent, previousPercent, udi);
723 }
724 return;
725 }
726
727 // Compute the previous and current global percentage
728 const int previousPercent = currentChargePercent();
729 const int currentPercent = previousPercent - (m_batteriesPercent[udi] - percent);
730
731 // Update the battery percentage
732 m_batteriesPercent[udi] = percent;
733
734 if (currentPercent < previousPercent) {
735 if (emitBatteryChargePercentNotification(currentPercent, previousPercent, udi)) {
736 // Only refresh status if a notification has actually been emitted
737 loadProfile();
738 }
739 }
740 }
741
onBatteryChargeStateChanged(int state,const QString & udi)742 void Core::onBatteryChargeStateChanged(int state, const QString &udi)
743 {
744 if (!m_batteriesCharged.contains(udi)) {
745 return;
746 }
747
748 bool previousCharged = true;
749 for (auto i = m_batteriesCharged.constBegin(); i != m_batteriesCharged.constEnd(); ++i) {
750 if (!i.value()) {
751 previousCharged = false;
752 break;
753 }
754 }
755
756 m_batteriesCharged[udi] = (state == Solid::Battery::FullyCharged);
757
758 if (m_backend->acAdapterState() != BackendInterface::Plugged) {
759 return;
760 }
761
762 bool currentCharged = true;
763 for (auto i = m_batteriesCharged.constBegin(); i != m_batteriesCharged.constEnd(); ++i) {
764 if (!i.value()) {
765 currentCharged = false;
766 break;
767 }
768 }
769
770 if (!previousCharged && currentCharged) {
771 emitRichNotification(QStringLiteral("fullbattery"), i18n("Charging Complete"), i18n("Battery now fully charged."));
772 loadProfile();
773 }
774 }
775
onCriticalBatteryTimerExpired()776 void Core::onCriticalBatteryTimerExpired()
777 {
778 if (m_criticalBatteryNotification) {
779 m_criticalBatteryNotification->close();
780 }
781
782 // Do that only if we're not on AC
783 if (m_backend->acAdapterState() == BackendInterface::Unplugged) {
784 // We consider this as a very special button
785 PowerDevil::Action *helperAction = ActionPool::instance()->loadAction(QStringLiteral("HandleButtonEvents"), KConfigGroup(), this);
786 if (helperAction) {
787 QVariantMap args;
788 args[QStringLiteral("Button")] = 32;
789 args[QStringLiteral("Type")] = QVariant::fromValue<uint>(PowerDevilSettings::batteryCriticalAction());
790 args[QStringLiteral("Explicit")] = true;
791 helperAction->trigger(args);
792 }
793 }
794 }
795
onBatteryRemainingTimeChanged(qulonglong time)796 void Core::onBatteryRemainingTimeChanged(qulonglong time)
797 {
798 Q_EMIT batteryRemainingTimeChanged(time);
799 }
800
onKIdleTimeoutReached(int identifier,int msec)801 void Core::onKIdleTimeoutReached(int identifier, int msec)
802 {
803 // Find which action(s) requested this idle timeout
804 for (auto i = m_registeredActionTimeouts.constBegin(), end = m_registeredActionTimeouts.constEnd(); i != end; ++i) {
805 if (i.value().contains(identifier)) {
806 i.key()->onIdleTimeout(msec);
807
808 // And it will need to be awaken
809 m_pendingResumeFromIdleActions.insert(i.key());
810 break;
811 }
812 }
813
814 // Catch the next resume event if some actions require it
815 if (!m_pendingResumeFromIdleActions.isEmpty()) {
816 KIdleTime::instance()->catchNextResumeEvent();
817 }
818 }
819
onLidClosedChanged(bool closed)820 void Core::onLidClosedChanged(bool closed)
821 {
822 Q_EMIT lidClosedChanged(closed);
823 }
824
onAboutToSuspend()825 void Core::onAboutToSuspend()
826 {
827 if (PowerDevilSettings::pausePlayersOnSuspend()) {
828 qCDebug(POWERDEVIL) << "Pausing all media players before sleep";
829
830 QDBusPendingCall listNamesCall = QDBusConnection::sessionBus().interface()->asyncCall(QStringLiteral("ListNames"));
831 QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(listNamesCall, this);
832 connect(callWatcher, &QDBusPendingCallWatcher::finished, this, [](QDBusPendingCallWatcher *watcher) {
833 QDBusPendingReply<QStringList> reply = *watcher;
834 watcher->deleteLater();
835
836 if (reply.isError()) {
837 qCWarning(POWERDEVIL) << "Failed to fetch list of DBus service names for pausing players on entering sleep" << reply.error().message();
838 return;
839 }
840
841 const QStringList &services = reply.value();
842 for (const QString &serviceName : services) {
843 if (!serviceName.startsWith(QLatin1String("org.mpris.MediaPlayer2."))) {
844 continue;
845 }
846
847 if (serviceName.startsWith(QLatin1String("org.mpris.MediaPlayer2.kdeconnect.mpris_"))) {
848 // This is actually a player on another device exposed by KDE Connect
849 // We don't want to pause it
850 // See https://bugs.kde.org/show_bug.cgi?id=427209
851 continue;
852 }
853
854 qCDebug(POWERDEVIL) << "Pausing media player with service name" << serviceName;
855
856 QDBusMessage pauseMsg = QDBusMessage::createMethodCall(serviceName,
857 QStringLiteral("/org/mpris/MediaPlayer2"),
858 QStringLiteral("org.mpris.MediaPlayer2.Player"),
859 QStringLiteral("Pause"));
860 QDBusConnection::sessionBus().asyncCall(pauseMsg);
861 }
862 });
863 }
864 }
865
registerActionTimeout(Action * action,int timeout)866 void Core::registerActionTimeout(Action* action, int timeout)
867 {
868 // Register the timeout with KIdleTime
869 int identifier = KIdleTime::instance()->addIdleTimeout(timeout);
870
871 // Add the identifier to the action hash
872 QList< int > timeouts = m_registeredActionTimeouts[action];
873 timeouts.append(identifier);
874 m_registeredActionTimeouts[action] = timeouts;
875 }
876
unregisterActionTimeouts(Action * action)877 void Core::unregisterActionTimeouts(Action* action)
878 {
879 // Clear all timeouts from the action
880 const QList< int > timeoutsToClean = m_registeredActionTimeouts[action];
881
882 for (int id : timeoutsToClean) {
883 KIdleTime::instance()->removeIdleTimeout(id);
884 }
885
886 m_registeredActionTimeouts.remove(action);
887 }
888
currentChargePercent() const889 int Core::currentChargePercent() const
890 {
891 int chargePercent = 0;
892 for (auto it = m_batteriesPercent.constBegin(); it != m_batteriesPercent.constEnd(); ++it) {
893 chargePercent += it.value();
894 }
895 return chargePercent;
896 }
897
onResumingFromIdle()898 void Core::onResumingFromIdle()
899 {
900 KIdleTime::instance()->simulateUserActivity();
901 // Wake up the actions in which an idle action was triggered
902 std::for_each(m_pendingResumeFromIdleActions.cbegin(), m_pendingResumeFromIdleActions.cend(),
903 std::mem_fn(&PowerDevil::Action::onWakeupFromIdle));
904
905 m_pendingResumeFromIdleActions.clear();
906 }
907
onNotificationTimeout()908 void Core::onNotificationTimeout()
909 {
910 // cannot connect QTimer::singleShot directly to the other method
911 onServiceRegistered(QString());
912 }
913
onServiceRegistered(const QString & service)914 void Core::onServiceRegistered(const QString &service)
915 {
916 Q_UNUSED(service);
917
918 if (m_notificationsReady) {
919 return;
920 }
921
922 bool needsRefresh = false;
923
924 // show warning about low batteries right on session startup, force it to show
925 // by making sure the "old" percentage (that magic number) is always higher than the current one
926 if (emitBatteryChargePercentNotification(currentChargePercent(), 1000)) {
927 needsRefresh = true;
928 }
929
930 // now also emit notifications for all peripheral batteries
931 for (auto it = m_peripheralBatteriesPercent.constBegin(), end = m_peripheralBatteriesPercent.constEnd(); it != end; ++it) {
932 if (emitBatteryChargePercentNotification(it.value() /*currentPercent*/, 1000, it.key() /*udi*/)) {
933 needsRefresh = true;
934 }
935 }
936
937 // need to refresh status to prevent the notification from showing again when charge percentage changes
938 if (needsRefresh) {
939 refreshStatus();
940 }
941
942 m_notificationsReady = true;
943
944 if (m_notificationsWatcher) {
945 delete m_notificationsWatcher;
946 m_notificationsWatcher = nullptr;
947 }
948 }
949
readChargeThreshold()950 void Core::readChargeThreshold()
951 {
952 KAuth::Action action(QStringLiteral("org.kde.powerdevil.chargethresholdhelper.getthreshold"));
953 action.setHelperId(QStringLiteral("org.kde.powerdevil.chargethresholdhelper"));
954 KAuth::ExecuteJob *job = action.execute();
955 connect(job, &KJob::result, this, [this, job] {
956 if (job->error()) {
957 qCWarning(POWERDEVIL) << "org.kde.powerdevil.chargethresholdhelper.getthreshold failed" << job->errorText();
958 return;
959 }
960
961 const auto data = job->data();
962
963 const int chargeStartThreshold = data.value(QStringLiteral("chargeStartThreshold")).toInt();
964 if (m_chargeStartThreshold != chargeStartThreshold) {
965 m_chargeStartThreshold = chargeStartThreshold;
966 Q_EMIT chargeStartThresholdChanged(chargeStartThreshold);
967 }
968
969 const int chargeStopThreshold = data.value(QStringLiteral("chargeStopThreshold")).toInt();
970 if (m_chargeStopThreshold != chargeStopThreshold) {
971 m_chargeStopThreshold = chargeStopThreshold;
972 Q_EMIT chargeStopThresholdChanged(chargeStopThreshold);
973 }
974
975 qCDebug(POWERDEVIL) << "Charge thresholds: start at" << chargeStartThreshold << "- stop at" << chargeStopThreshold;
976 });
977 job->start();
978 }
979
backend()980 BackendInterface* Core::backend()
981 {
982 return m_backend;
983 }
984
isLidClosed() const985 bool Core::isLidClosed() const
986 {
987 return m_backend->isLidClosed();
988 }
989
isLidPresent() const990 bool Core::isLidPresent() const
991 {
992 return m_backend->isLidPresent();
993 }
994
hasDualGpu() const995 bool Core::hasDualGpu() const
996 {
997 return m_hasDualGpu;
998 }
999
chargeStartThreshold() const1000 int Core::chargeStartThreshold() const
1001 {
1002 return m_chargeStartThreshold;
1003 }
1004
chargeStopThreshold() const1005 int Core::chargeStopThreshold() const
1006 {
1007 return m_chargeStopThreshold;
1008 }
1009
scheduleWakeup(const QString & service,const QDBusObjectPath & path,qint64 timeout)1010 uint Core::scheduleWakeup(const QString &service, const QDBusObjectPath &path, qint64 timeout)
1011 {
1012 ++m_lastWakeupCookie;
1013
1014 int cookie = m_lastWakeupCookie;
1015 // if some one is trying to time travel, deny them
1016 if (timeout < QDateTime::currentSecsSinceEpoch()) {
1017 sendErrorReply(QDBusError::InvalidArgs, "You can not schedule wakeup in past");
1018 } else {
1019 #ifndef Q_OS_LINUX
1020 sendErrorReply(QDBusError::NotSupported, "Scheduled wakeups are available only on Linux platforms");
1021 #else
1022 WakeupInfo wakeup{ service, path, cookie, timeout };
1023 m_scheduledWakeups << wakeup;
1024 qCDebug(POWERDEVIL) << "Received request to wakeup at " << QDateTime::fromSecsSinceEpoch(timeout);
1025 resetAndScheduleNextWakeup();
1026 #endif
1027 }
1028 return cookie;
1029 }
1030
wakeup()1031 void Core::wakeup()
1032 {
1033 onResumingFromIdle();
1034 PowerDevil::Action *helperAction = ActionPool::instance()->loadAction(QStringLiteral("DPMSControl"), KConfigGroup(), this);
1035 if (helperAction) {
1036 QVariantMap args;
1037 // we pass empty string as type because when empty type is passed,
1038 // it turns screen on.
1039 args[QStringLiteral("Type")] = "";
1040 helperAction->trigger(args);
1041 }
1042 }
1043
clearWakeup(int cookie)1044 void Core::clearWakeup(int cookie)
1045 {
1046 // if we do not have any timeouts return from here
1047 if (m_scheduledWakeups.isEmpty()) {
1048 return;
1049 }
1050
1051 // depending on cookie, remove it from scheduled wakeups
1052 auto erased = m_scheduledWakeups.erase(std::remove_if(m_scheduledWakeups.begin(), m_scheduledWakeups.end(), [cookie](WakeupInfo wakeup) {
1053 return wakeup.cookie == cookie;
1054 }));
1055
1056 if (erased == m_scheduledWakeups.end()) {
1057 sendErrorReply(QDBusError::InvalidArgs, "Can not clear the invalid wakeup");
1058 return;
1059 }
1060
1061 // reset timerfd
1062 resetAndScheduleNextWakeup();
1063 }
1064
batteryRemainingTime() const1065 qulonglong Core::batteryRemainingTime() const
1066 {
1067 return m_backend->batteryRemainingTime();
1068 }
1069
backendCapabilities()1070 uint Core::backendCapabilities()
1071 {
1072 return m_backend->capabilities();
1073 }
1074
resetAndScheduleNextWakeup()1075 void Core::resetAndScheduleNextWakeup()
1076 {
1077
1078 #ifdef Q_OS_LINUX
1079 // first we sort the wakeup list
1080 std::sort(m_scheduledWakeups.begin(), m_scheduledWakeups.end(), [](const WakeupInfo& lhs, const WakeupInfo& rhs)
1081 {
1082 return lhs.timeout < rhs.timeout;
1083 });
1084
1085 // we don't want any of our wakeups to repeat'
1086 timespec interval = {0, 0};
1087 timespec nextWakeup;
1088 bool enableNotifier = false;
1089 // if we don't have any wakeups left, we call it a day and stop timer_fd
1090 if(m_scheduledWakeups.isEmpty()) {
1091 nextWakeup = {0, 0};
1092 } else {
1093 // now pick the first timeout from the list
1094 WakeupInfo wakeup = m_scheduledWakeups.first();
1095 nextWakeup = {wakeup.timeout, 0};
1096 enableNotifier = true;
1097 }
1098 if (m_timerFd != -1) {
1099 const itimerspec spec = {interval, nextWakeup};
1100 timerfd_settime(m_timerFd, TFD_TIMER_ABSTIME, &spec, nullptr);
1101 }
1102 m_timerFdSocketNotifier->setEnabled(enableNotifier);
1103 #endif
1104 }
1105
timerfdEventHandler()1106 void Core::timerfdEventHandler()
1107 {
1108 // wakeup from the linux/rtc
1109
1110 // Disable reading events from the timer_fd
1111 m_timerFdSocketNotifier->setEnabled(false);
1112
1113 // At this point scheduled wakeup list should not be empty, but just in case
1114 if (m_scheduledWakeups.isEmpty()) {
1115 qWarning(POWERDEVIL) << "Wakeup was recieved but list is now empty! This should not happen!";
1116 return;
1117 }
1118
1119 // first thing to do is, we pick the first wakeup from list
1120 WakeupInfo currentWakeup = m_scheduledWakeups.takeFirst();
1121
1122 // Before doing anything further, lets set the next set of wakeup alarm
1123 resetAndScheduleNextWakeup();
1124
1125 // Now current wakeup needs to be processed
1126 // prepare message for sending back to the consumer
1127 QDBusMessage msg = QDBusMessage::createMethodCall(currentWakeup.service, currentWakeup.path.path(),
1128 QStringLiteral("org.kde.PowerManagement"), QStringLiteral("wakeupCallback"));
1129 msg << currentWakeup.cookie;
1130 // send it away
1131 QDBusConnection::sessionBus().call(msg, QDBus::NoBlock);
1132 }
1133
1134 }
1135