File indexing completed on 2024-05-19 05:37:58

0001 /*
0002     SPDX-FileCopyrightText: 2022 Kai Uwe Broulik <kde@broulik.de>
0003 
0004     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 #include "devicenotifications.h"
0008 
0009 #include <QSocketNotifier>
0010 
0011 #include <KLocalizedString>
0012 #include <KNotification>
0013 #include <KPluginFactory>
0014 
0015 K_PLUGIN_CLASS_WITH_JSON(KdedDeviceNotifications, "devicenotifications.json")
0016 
0017 // TODO Can we put this in KStringHandler?
0018 static QString decodePropertyValue(const QByteArray &encoded)
0019 {
0020     const int len = encoded.length();
0021     QByteArray decoded;
0022     // The resulting string is definitely same length or shorter.
0023     decoded.reserve(len);
0024 
0025     for (int i = 0; i < len; ++i) {
0026         const auto ch = encoded.at(i);
0027 
0028         if (ch == '\\') {
0029             if (i + 1 < len && encoded.at(i + 1) == '\\') {
0030                 decoded.append('\\');
0031                 ++i;
0032             } else if (i + 3 < len && encoded.at(i + 1) == 'x') {
0033                 QByteArray hex = encoded.mid(i + 2, 2);
0034                 bool ok;
0035                 const int code = hex.toInt(&ok, 16);
0036                 if (ok) {
0037                     decoded.append(char(code));
0038                 }
0039                 i += 3;
0040             }
0041         } else {
0042             decoded.append(ch);
0043         }
0044     }
0045     return QString::fromUtf8(decoded);
0046 }
0047 
0048 UdevDevice::UdevDevice(struct udev_device *device)
0049     : UdevDevice(device, true /*ref*/)
0050 {
0051 }
0052 
0053 UdevDevice::UdevDevice(struct udev_device *device, bool ref)
0054     : m_device(device)
0055 {
0056     if (ref) {
0057         udev_device_ref(m_device);
0058     }
0059 }
0060 
0061 UdevDevice UdevDevice::fromDevice(struct udev_device *device)
0062 {
0063     return UdevDevice(device, false /*ref*/);
0064 }
0065 
0066 UdevDevice::~UdevDevice()
0067 {
0068     if (m_device) {
0069         udev_device_unref(m_device);
0070     }
0071 }
0072 
0073 UdevDevice &UdevDevice::operator=(const UdevDevice &other)
0074 {
0075     udev_device_unref(m_device);
0076     m_device = udev_device_ref(other.m_device);
0077     return *this;
0078 }
0079 
0080 struct udev_device *UdevDevice::handle() const
0081 {
0082     return m_device;
0083 }
0084 
0085 QString UdevDevice::action() const
0086 {
0087     return getDeviceString(udev_device_get_action);
0088 }
0089 
0090 QString UdevDevice::name() const
0091 {
0092     return getDeviceString(udev_device_get_sysname);
0093 }
0094 
0095 QString UdevDevice::sysfsPath() const
0096 {
0097     return getDeviceString(udev_device_get_syspath);
0098 }
0099 
0100 QString UdevDevice::subsystem() const
0101 {
0102     return getDeviceString(udev_device_get_subsystem);
0103 }
0104 
0105 QString UdevDevice::type() const
0106 {
0107     return getDeviceString(udev_device_get_devtype);
0108 }
0109 
0110 QString UdevDevice::deviceProperty(const char *name) const
0111 {
0112     if (m_device) {
0113         const auto *value = udev_device_get_property_value(m_device, name);
0114         if (value) {
0115             return QString::fromLatin1(value);
0116         }
0117     }
0118     return {};
0119 }
0120 
0121 QString UdevDevice::sysfsProperty(const char *name) const
0122 {
0123     if (m_device) {
0124         const auto *value = udev_device_get_sysattr_value(m_device, name);
0125         if (value) {
0126             return QString::fromLatin1(value);
0127         }
0128     }
0129     return {};
0130 }
0131 
0132 QString UdevDevice::getDeviceString(const char *(*getter)(udev_device *)) const
0133 {
0134     if (m_device) {
0135         return QString::fromLatin1((*getter)(m_device));
0136     }
0137     return QString();
0138 }
0139 
0140 QString UdevDevice::model() const
0141 {
0142     QString name = sysfsProperty("product");
0143     if (name.isEmpty()) {
0144         name = deviceProperty("ID_MODEL_FROM_DATABASE");
0145     }
0146     if (name.isEmpty()) {
0147         name = decodePropertyValue(deviceProperty("ID_MODEL_ENC").toLatin1());
0148     }
0149     if (name.isEmpty()) {
0150         name = deviceProperty("ID_MODEL");
0151     }
0152     return name;
0153 }
0154 
0155 QString UdevDevice::vendor() const
0156 {
0157     QString vendor = sysfsProperty("manufacturer");
0158     if (vendor.isEmpty()) {
0159         vendor = deviceProperty("ID_VENDOR_FROM_DATABASE");
0160     }
0161     if (vendor.isEmpty()) {
0162         vendor = decodePropertyValue(deviceProperty("ID_VENDOR_ENC").toLatin1());
0163     }
0164     if (vendor.isEmpty()) {
0165         vendor = deviceProperty("ID_VENDOR");
0166     }
0167     return vendor;
0168 }
0169 
0170 QString UdevDevice::displayName() const
0171 {
0172     const QString generic = QStringLiteral("Generic");
0173 
0174     QStringList displayNameSegments;
0175 
0176     QString vendor = this->vendor();
0177     if (vendor == generic) {
0178         vendor.clear();
0179     }
0180     if (!vendor.isEmpty()) {
0181         displayNameSegments.append(vendor);
0182     }
0183 
0184     QString model = this->model();
0185     if (model == generic) {
0186         model.clear();
0187     }
0188     if (!model.isEmpty()) {
0189         displayNameSegments.append(model);
0190     }
0191 
0192     return displayNameSegments.join(QLatin1Char(' '));
0193 }
0194 
0195 bool UdevDevice::isRemovable() const
0196 {
0197     // "removable" is a device that can be removed from the platform by the user.
0198     // See https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-devices-removable
0199     return sysfsProperty("removable") == QLatin1String("removable");
0200 }
0201 
0202 Udev::Udev(QObject *parent)
0203     : QObject(parent)
0204     , m_udev(udev_new())
0205 {
0206     if (!m_udev) {
0207         return;
0208     }
0209 
0210     m_monitor = udev_monitor_new_from_netlink(m_udev, "udev");
0211     if (!m_monitor) {
0212         return;
0213     }
0214 
0215     // For now we're only interested in devices on the "usb" subsystem.
0216     udev_monitor_filter_add_match_subsystem_devtype(m_monitor, "usb", nullptr);
0217 
0218     m_notifier = new QSocketNotifier(udev_monitor_get_fd(m_monitor), QSocketNotifier::Read, this);
0219     connect(m_notifier, &QSocketNotifier::activated, this, &Udev::onSocketActivated);
0220 
0221     udev_monitor_enable_receiving(m_monitor);
0222 }
0223 
0224 Udev::~Udev()
0225 {
0226     if (m_monitor) {
0227         udev_monitor_unref(m_monitor);
0228     }
0229     if (m_udev) {
0230         udev_unref(m_udev);
0231     }
0232 }
0233 
0234 void Udev::onSocketActivated()
0235 {
0236     m_notifier->setEnabled(false);
0237     UdevDevice device = UdevDevice::fromDevice(udev_monitor_receive_device(m_monitor));
0238     m_notifier->setEnabled(true);
0239 
0240     const QString action = device.action();
0241 
0242     if (action == QLatin1String("add")) {
0243         Q_EMIT deviceAdded(device);
0244     } else if (action == QLatin1String("remove")) {
0245         Q_EMIT deviceRemoved(device);
0246     }
0247 }
0248 
0249 KdedDeviceNotifications::KdedDeviceNotifications(QObject *parent, const QList<QVariant> &)
0250     : KDEDModule(parent)
0251 {
0252     connect(&m_udev, &Udev::deviceAdded, this, &KdedDeviceNotifications::onDeviceAdded);
0253     connect(&m_udev, &Udev::deviceRemoved, this, &KdedDeviceNotifications::onDeviceRemoved);
0254 }
0255 
0256 KdedDeviceNotifications::~KdedDeviceNotifications() = default;
0257 
0258 void KdedDeviceNotifications::onDeviceAdded(const UdevDevice &device)
0259 {
0260     // We only care about actual USB devices, no interfaces, controllers, etc.
0261     if (device.type() != QLatin1String("usb_device")) {
0262         return;
0263     }
0264 
0265     if (!device.isRemovable()) {
0266         return;
0267     }
0268 
0269     // By the time we receive the "removed" signal, the device's properties
0270     // are already discarded, so we need to remember whether it is removable
0271     // here, so we know that when the device is removed.
0272     m_removableDevices.append(device.sysfsPath());
0273 
0274     const QString displayName = device.displayName();
0275     const QString text = !displayName.isEmpty() ? i18n("%1 has been plugged in.", displayName.toHtmlEscaped()) : i18n("A USB device has been plugged in.");
0276 
0277     KNotification::event(QStringLiteral("deviceAdded"),
0278                          i18nc("@title:notifications", "USB Device Detected"),
0279                          text,
0280                          QStringLiteral("drive-removable-media-usb"),
0281                          KNotification::DefaultEvent);
0282 }
0283 
0284 void KdedDeviceNotifications::onDeviceRemoved(const UdevDevice &device)
0285 {
0286     if (device.type() != QLatin1String("usb_device")) {
0287         return;
0288     }
0289 
0290     if (!m_removableDevices.removeOne(device.sysfsPath()) && !device.isRemovable()) {
0291         return;
0292     }
0293 
0294     const QString displayName = m_displayNames.take(device.sysfsPath());
0295     const QString text = !displayName.isEmpty() ? i18n("%1 has been unplugged.", displayName.toHtmlEscaped()) : i18n("A USB device has been unplugged.");
0296 
0297     KNotification::event(QStringLiteral("deviceRemoved"),
0298                          i18nc("@title:notifications", "USB Device Removed"),
0299                          text,
0300                          QStringLiteral("drive-removable-media-usb"),
0301                          KNotification::DefaultEvent);
0302 }
0303 
0304 #include "devicenotifications.moc"