File indexing completed on 2024-04-14 15:31:34

0001 /*
0002  *   SPDX-FileCopyrightText: 2022 Bart Ribbers <bribbers@disroot.org>
0003  *
0004  *   SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005  */
0006 
0007 #include "controllermanager.h"
0008 #include "notificationsmanager.h"
0009 #include "uinputsystem.h"
0010 #include "kwinfakeinputsystem.h"
0011 
0012 #include <KSharedConfig>
0013 #include <KConfigGroup>
0014 #include <KLocalizedString>
0015 #include <KStatusNotifierItem>
0016 #include <KWindowSystem>
0017 #include <QAction>
0018 #include <QMenu>
0019 
0020 #include <taskmanager/tasksmodel.h>
0021 #include <taskmanager/abstracttasksmodel.h>
0022 
0023 #include "remotecontrollers.h"
0024 #include "screensaver_interface.h"
0025 #include "plasmarc-debug.h"
0026 
0027 class NoOpInputSystem : public AbstractSystem
0028 {
0029 public:
0030     bool init() override { return true; }
0031     void emitKey(int key, bool pressed) override {
0032         Q_UNUSED(key)
0033         Q_UNUSED(pressed)
0034     }
0035 };
0036 
0037 ControllerManager::ControllerManager(QObject *parent)
0038     : QObject(parent)
0039     , m_dbusInterface(new ControllerManagerDBusInterface(this))
0040     , m_settings(new RemoteControllersSettings)
0041 {
0042     // Setup notifications
0043     QObject::connect(this, &ControllerManager::deviceConnected,
0044                      &NotificationsManager::instance(), &NotificationsManager::notifyNewDevice);
0045     QObject::connect(this, &ControllerManager::deviceDisconnected,
0046                      &NotificationsManager::instance(), &NotificationsManager::notifyDisconnectedDevice);
0047 
0048     resetInputSystem();
0049 
0050     m_sni = new KStatusNotifierItem(QStringLiteral("org.kde.plasma.remotecontrollers"), this);
0051     m_sni->setIconByName("input-gamepad");
0052     m_sni->setStandardActionsEnabled(false);
0053     m_sni->setContextMenu(new QMenu);
0054     m_sni->setTitle(i18n("Remote Controllers"));
0055     m_sni->setToolTipTitle(m_sni->title());
0056     m_sni->setToolTipSubTitle(i18n("Configure Inhibited Apps"));
0057 
0058     auto section = new QAction(i18n("Inhibitions"));
0059     section->setEnabled(false);
0060     m_sni->contextMenu()->addAction(section);
0061 
0062     m_appsModel = new TaskManager::TasksModel(this);
0063     connect(m_appsModel, &TaskManager::TasksModel::activeTaskChanged, this, [this] {
0064         const QString appId = m_appsModel->activeTask().data(TaskManager::AbstractTasksModel::AppId).toString();
0065         m_enabled = !appInhibited(appId);
0066     });
0067     connect(m_appsModel, &TaskManager::TasksModel::rowsAboutToBeRemoved, this, &ControllerManager::refreshApps);
0068     connect(m_appsModel, &TaskManager::TasksModel::rowsInserted, this, &ControllerManager::refreshApps);
0069     connect(m_appsModel, &TaskManager::TasksModel::modelReset, this, &ControllerManager::refreshApps);
0070 
0071     // Mark the SNI as passive after 10' without usage
0072     m_lastUsed.setInterval(10 * 60 * 1000);
0073     m_lastUsed.setSingleShot(true);
0074     connect(&m_lastUsed, &QTimer::timeout, this, [this] {
0075         m_sni->setStatus(KStatusNotifierItem::Passive);
0076     });
0077     
0078     refreshApps();
0079 }
0080 
0081 ControllerManager &ControllerManager::instance()
0082 {
0083     static ControllerManager _instance;
0084     return _instance;
0085 }
0086 
0087 void ControllerManager::newDevice(Device *device)
0088 {
0089     qInfo() << "New device connected:" << device->getName();
0090 
0091     m_usedKeys += device->usedKeys();
0092     m_inputSystem->setSupportedKeys(m_usedKeys);
0093 
0094     device->setIndex(m_connectedDevices.size());
0095 
0096     connect(device, &Device::deviceDisconnected, this, &ControllerManager::removeDevice);
0097 
0098     m_connectedDevices.append(device);
0099 
0100     // Don't send notifications for CEC devices, since we expect them to always be available
0101     if (device->getDeviceType() != DeviceCEC) {
0102         emit deviceConnected(device);
0103         emit m_dbusInterface->deviceConnected(device->getUniqueIdentifier());
0104     }
0105     m_lastUsed.start();
0106     m_sni->setStatus(KStatusNotifierItem::Active);
0107 }
0108 
0109 void ControllerManager::deviceRemoved(Device *device)
0110 {
0111     qInfo() << "Device disconnected:" << device->getName();
0112     emit deviceDisconnected(device);
0113     m_connectedDevices.removeOne(device);
0114     for (int i = 0; i < m_connectedDevices.size(); i++) {
0115         m_connectedDevices[i]->setIndex(i);
0116     }
0117     emit m_dbusInterface->deviceDisconnected(device->getUniqueIdentifier());
0118 
0119     m_sni->setStatus(m_connectedDevices.count() > 0 ? KStatusNotifierItem::Active : KStatusNotifierItem::Passive);
0120     m_lastUsed.start();
0121 }
0122 
0123 void ControllerManager::removeDevice(int deviceIndex)
0124 {
0125     Device *removedDevice = m_connectedDevices.at(deviceIndex);
0126     m_connectedDevices.remove(deviceIndex);
0127 
0128     qInfo() << "Device disconnected:" << removedDevice->getName();
0129 
0130     emit deviceDisconnected(removedDevice);
0131     delete removedDevice;
0132     
0133     // Reset indexes
0134     for (int i = 0; i < m_connectedDevices.size(); i++)
0135         m_connectedDevices.at(i)->setIndex(i);
0136 }
0137 
0138 bool ControllerManager::isConnected(QString uniqueIdentifier)
0139 {
0140     if (m_connectedDevices.size() < 1)
0141         return false;
0142 
0143     return std::find_if(m_connectedDevices.begin(), m_connectedDevices.end(), [&uniqueIdentifier](Device *other) {
0144         return other->getUniqueIdentifier() == uniqueIdentifier;
0145     }) != m_connectedDevices.end();
0146 }
0147 
0148 QVector<Device*> ControllerManager::getDevicesByType(DeviceType deviceType)
0149 {
0150     QVector<Device*> devices;
0151 
0152     for (int i = 0; i < m_connectedDevices.size(); i++)
0153         if (m_connectedDevices.at(i)->getDeviceType() == deviceType)
0154             devices.append(m_connectedDevices.at(i));
0155 
0156     return devices;
0157 }
0158 
0159 QVector<Device*> ControllerManager::connectedDevices()
0160 {
0161     return m_connectedDevices;
0162 }
0163 
0164 void ControllerManager::emitKey(int key, bool pressed)
0165 {
0166     m_sni->setStatus(KStatusNotifierItem::Active);
0167     m_lastUsed.start();
0168     if (!m_enabled) {
0169         // Make sure the system knows that it's being interacted with
0170         simulateUserActivity();
0171         return;
0172     }
0173 
0174     m_inputSystem->emitKey(key, pressed);
0175 }
0176 
0177 ControllerManager::~ControllerManager()
0178 {
0179     m_connectedDevices.clear();
0180 }
0181 
0182 void ControllerManager::noopInput()
0183 {
0184     m_inputSystem.reset(new NoOpInputSystem);
0185 }
0186 
0187 void ControllerManager::resetInputSystem()
0188 {
0189     m_inputSystem.reset();
0190     std::unique_ptr<AbstractSystem> inputSystem(new UInputSystem);
0191     if (inputSystem->init()) {
0192         m_inputSystem.reset(inputSystem.release());
0193     }
0194 
0195     if (!m_inputSystem && KWindowSystem::isPlatformWayland()) {
0196         std::unique_ptr<AbstractSystem> inputSystem(new KWinFakeInputSystem);
0197         if (inputSystem->init()) {
0198             m_inputSystem.reset(inputSystem.release());
0199         }
0200     }
0201     if (!m_inputSystem) {
0202         m_inputSystem.reset(new NoOpInputSystem);
0203         qCWarning(PLASMARC) << "Could not find an input system, plasma-remotecontrollers will not be able to send events";
0204     }
0205 }
0206 
0207 void ControllerManager::refreshApps()
0208 {
0209     auto menu = m_sni->contextMenu();
0210     auto actions = menu->actions();
0211     for (int i = 0, c = m_appsModel->rowCount(); i < c; ++i) {
0212         const auto task = m_appsModel->index(i, 0);
0213         auto appId = task.data(TaskManager::AbstractTasksModel::AppId).toString();
0214         auto &action = m_appActions[appId];
0215         if (action) {
0216             actions.removeAll(action);
0217         } else {
0218             QString appName = task.data(TaskManager::AbstractTasksModel::AppName).toString();
0219             if (appName.isNull()) {
0220                 appName = task.data(Qt::DisplayRole).toString();
0221             }
0222             action = new QAction(appName);
0223             action->setCheckable(true);
0224             action->setChecked(appInhibited(appId));
0225             action->setProperty("appId", appId);
0226             connect(action, &QAction::toggled, this, [this, action] (bool checked) {
0227                 if (checked) {
0228                     auto apps = m_settings->applications();
0229                     apps += action->property("appId").toString();
0230                     m_settings->setApplications(apps);
0231                 } else {
0232                     auto apps = m_settings->applications();
0233                     apps.removeAll(action->property("appId").toString());
0234                     m_settings->setApplications(apps);
0235                 }
0236                 m_settings->save();
0237             });
0238             m_appActions[appId] = action;
0239             menu->addAction(action);
0240         }
0241     }
0242     for (auto action : actions) {
0243         menu->removeAction(action);
0244         delete m_appActions.take(action->property("appId").toString());
0245     }
0246 }
0247 
0248 bool ControllerManager::appInhibited(const QString &appId) const
0249 {
0250     const auto inhibitedApps = m_settings->applications();
0251     for (const auto &inhibitedApp : inhibitedApps) {
0252         const QRegularExpression rx(QRegularExpression::wildcardToRegularExpression(inhibitedApp));
0253         const auto match = rx.match(appId);
0254         if (match.hasMatch()) {
0255             return true;
0256         }
0257     }
0258     return false;
0259 }
0260 
0261 void ControllerManager::simulateUserActivity()
0262 {
0263     const auto current = QDateTime::currentDateTimeUtc();
0264     if (!m_lastSimulated.isNull() && m_lastSimulated.secsTo(current) < 60) {
0265         return;
0266     }
0267 
0268     m_lastSimulated = current;
0269     OrgFreedesktopScreenSaverInterface iface(QStringLiteral("org.freedesktop.ScreenSaver"), QStringLiteral("/ScreenSaver"), QDBusConnection::sessionBus());
0270     iface.SimulateUserActivity();
0271 }