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