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"