/* * SPDX-FileCopyrightText: 2024 Bohdan Onofriichuk * * SPDX-License-Identifier: GPL-2.0-or-later */ #include "devicecontrol.h" #include "devicenotifier_debug.h" // solid specific includes #include #include #include #include #include #include #include #include #include DeviceControl::DeviceControl(QObject *parent) : QAbstractListModel(parent) , m_types({ Solid::DeviceInterface::PortableMediaPlayer, Solid::DeviceInterface::Camera, Solid::DeviceInterface::OpticalDisc, Solid::DeviceInterface::StorageVolume, Solid::DeviceInterface::OpticalDrive, Solid::DeviceInterface::StorageDrive, Solid::DeviceInterface::NetworkShare, Solid::DeviceInterface::StorageAccess, }) , m_encryptedPredicate(Solid::Predicate(QStringLiteral("StorageVolume"), QStringLiteral("usage"), QLatin1String("Encrypted"))) , m_spaceMonitor(SpaceMonitor::instance()) , m_stateMonitor(DevicesStateMonitor::instance()) , m_errorMonitor(DeviceErrorMonitor::instance()) { qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: Begin initializing"; for (auto type : m_types) { m_predicateDeviceMatch |= Solid::Predicate(type); } QList devices = Solid::Device::listFromQuery(m_predicateDeviceMatch); for (Solid::Device &device : devices) { onDeviceAdded(device.udi()); } connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceAdded, this, &DeviceControl::onDeviceAdded); connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceRemoved, this, &DeviceControl::onDeviceRemoved); connect(m_spaceMonitor.get(), &SpaceMonitor::sizeChanged, this, &DeviceControl::onDeviceSizeChanged); connect(m_stateMonitor.get(), &DevicesStateMonitor::stateChanged, this, &DeviceControl::onDeviceStatusChanged); connect(m_errorMonitor.get(), &DeviceErrorMonitor::errorDataChanged, this, &DeviceControl::onDeviceErrorChanged); qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: Initialized"; } DeviceControl::~DeviceControl() { } int DeviceControl::rowCount(const QModelIndex &parent) const { return parent.isValid() ? 0 : m_devices.size(); } QVariant DeviceControl::data(const QModelIndex &index, int role) const { if (!index.isValid()) { qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: Model : Index is not valid. Role : " << role; return {}; } switch (role) { case Udi: return m_devices[index.row()].udi(); case Icon: return m_deviceTypes[m_devices[index.row()].udi()].second.first; case Emblems: return m_devices[index.row()].emblems(); case Description: return m_deviceTypes[m_devices[index.row()].udi()].second.second; case IsRemovable: { return m_stateMonitor->isRemovable(m_devices[index.row()].udi()); } case Size: return m_spaceMonitor->getFullSize(m_devices[index.row()].udi()); case FreeSpace: return m_spaceMonitor->getFreeSize(m_devices[index.row()].udi()); case SizeText: { double size = m_spaceMonitor->getFullSize(m_devices[index.row()].udi()); return size != -1 ? KFormat().formatByteSize(size) : QString(); } case FreeSpaceText: { double freeSpace = m_spaceMonitor->getFreeSize(m_devices[index.row()].udi()); return freeSpace != -1 ? KFormat().formatByteSize(freeSpace) : QString(); } case Mounted: { return m_stateMonitor->isMounted(m_devices[index.row()].udi()); } case OperationResult: { return m_stateMonitor->getOperationResult(m_devices[index.row()].udi()); } case Timestamp: { return m_stateMonitor->getDeviceTimeStamp(m_devices[index.row()].udi()); } case Type: { return m_deviceTypes[m_devices[index.row()].udi()].first; } case Error: return m_errorMonitor->getError(m_devices[index.row()].udi()); case ErrorMessage: return m_errorMonitor->getErrorMassage(m_devices[index.row()].udi()); case Actions: { if (auto it = m_actions.constFind(m_devices[index.row()].udi()); it != m_actions.end()) { return QVariant::fromValue(*it); } return {}; } } return {}; } QHash DeviceControl::roleNames() const { QHash roles; roles[Udi] = "deviceUdi"; roles[Description] = "deviceDescription"; roles[Type] = "deviceType"; roles[Icon] = "deviceIcon"; roles[Emblems] = "deviceEmblems"; roles[IsRemovable] = "deviceIsRemovable"; roles[FreeSpace] = "deviceFreeSpace"; roles[Size] = "deviceSize"; roles[FreeSpaceText] = "deviceFreeSpaceText"; roles[SizeText] = "deviceSizeText"; roles[Mounted] = "deviceMounted"; roles[OperationResult] = "deviceOperationResult"; roles[Timestamp] = "deviceTimestamp"; roles[Error] = "deviceError"; roles[ErrorMessage] = "deviceErrorMessage"; roles[Actions] = "deviceActions"; return roles; } void DeviceControl::onDeviceAdded(const QString &udi) { qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: Added device signal arrived : " << udi; Solid::Device device(udi); if (!device.isValid()) { qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: Attempt to add invalid device "; return; } if (!m_predicateDeviceMatch.matches(device)) { qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: device : " << udi << "not in our interest"; return; } qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: New device : " << udi << " begin initializing"; // Skip things we know we don't care about if (device.is()) { qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: device : " << udi << " is storage drive"; const Solid::StorageDrive *drive = device.as(); if (!drive->isHotpluggable()) { qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: device : " << udi << " is not in our interest. Skipping"; return; } } else if (device.is()) { qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: device : " << udi << " is storage volume"; const Solid::StorageVolume *volume = device.as(); if (!volume) { qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: device : " << udi << " is not in our interest. Skipping"; return; } Solid::StorageVolume::UsageType type = volume->usage(); if ((type == Solid::StorageVolume::Unused || type == Solid::StorageVolume::PartitionTable) && !device.is()) { qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: device : " << udi << " is not in our interest. Skipping"; return; } } auto actions = new ActionsControl(udi, this); if (!m_encryptedPredicate.matches(device) && actions->isEmpty()) { qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: device : " << udi << " is not in our interest. Skipping"; actions->deleteLater(); return; } if (auto it = m_removeTimers.constFind(udi); it != m_removeTimers.cend()) { deviceDelayRemove(it->udi, it->parentUdi); // A device is removed and added back immediately, can happen during formatting } m_actions[udi] = actions; qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: New device added : " << udi; int position = m_devices.size(); for (auto type : m_types) { const Solid::DeviceInterface *interface = device.asDeviceInterface(type); if (interface) { m_deviceTypes[udi].first = Solid::DeviceInterface::typeDescription(type); break; } } m_deviceTypes[udi].second.first = device.icon(); m_deviceTypes[udi].second.second = device.description(); beginInsertRows(QModelIndex(), position, position); qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: Add device: " << udi << " to the model at position : " << position; m_stateMonitor->addMonitoringDevice(udi); m_spaceMonitor->addMonitoringDevice(udi); m_errorMonitor->addMonitoringDevice(udi); m_devices.append(device); endInsertRows(); // Save storage drive parent for storage volumes to delay remove it and to properly remove it from device model // if device was physically removed from the computer. Storage volume with storage drive parent need to // be delay removed to show last message from deviceerrormonitor. Other devices don't have such message // so don't need to delay remove them. if (m_stateMonitor->isRemovable(udi) && device.is()) { qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: Save parent device: " << m_devices[position].parent().udi() << "for device: " << udi; if (auto it = m_parentDevices.find(udi); it != m_parentDevices.end()) { qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: Parent already present: append to parent`s list"; it->append(m_devices[position]); } else { if (m_devices[position].parent().is()) { qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: Creating a new parent list"; m_parentDevices.insert(m_devices[position].parent().udi(), {m_devices[position]}); } else { qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: Parent device is not valid. Don't add one"; } } } qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: device: " << udi << " successfully added to the model"; } void DeviceControl::onDeviceRemoved(const QString &udi) { qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: Removed device signal arrived : " << udi; // No need to keep device anymore because it was physically removed. if (auto it = m_parentDevices.constFind(udi); it != m_parentDevices.cend()) { int size = it->size(); qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: Parent was removed for : " << udi; for (int device = 0; device < size; ++device) { qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: Remove child : " << it->at(device).udi(); if (auto childIt = m_actions.find(it->at(device).udi()); childIt != m_actions.end()) { qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: Remove actions for : " << it->at(device).udi(); childIt.value()->deleteLater(); m_actions.erase(childIt); m_spaceMonitor->removeMonitoringDevice(udi); } deviceDelayRemove(it->at(device).udi(), udi); } return; } if (!m_actions.contains(udi)) { qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: Removed device not exist. Skipping : " << udi; return; } for (int position = 0; position < m_devices.size(); ++position) { if (m_devices[position].udi() == udi) { qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: Begin remove device: " << udi << " from the model at position : " << position; m_actions[udi]->deleteLater(); m_actions.remove(udi); QModelIndex index = DeviceControl::index(position); Q_EMIT dataChanged(index, index, {Actions}); // remove space monitoring because device not mounted m_spaceMonitor->removeMonitoringDevice(udi); for (auto it = m_parentDevices.begin(); it != m_parentDevices.end(); ++it) { for (int position = 0; position < it->size(); ++position) { if (udi == it->at(position).udi()) { auto timer = new QTimer(this); timer->setSingleShot(true); timer->setInterval(std::chrono::seconds(5)); // this keeps the delegate around for 5 seconds after the device has been // removed in case there was a message, such as "you can now safely remove this" connect(timer, &QTimer::timeout, this, [this, udi] { const RemoveTimerData &data = m_removeTimers[udi]; qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: Timer activated for " << udi; Q_ASSERT(udi == data.udi); deviceDelayRemove(udi, data.parentUdi); }); timer->start(); m_removeTimers.insert(udi, {timer, udi, it.key()}); return; } } } deviceDelayRemove(udi, QString()); } } } void DeviceControl::deviceDelayRemove(const QString &udi, const QString &parentUdi) { qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: device " << udi << " : start delay remove"; if (!parentUdi.isEmpty() && m_stateMonitor->isRemovable(udi)) { auto it = m_parentDevices.find(parentUdi); if (it != m_parentDevices.end()) { // PLASMA-WORKSPACE-146Y: If a parent device is not of StorageDrive type for (int position = 0; position < it->size(); ++position) { if (udi == it->at(position).udi()) { qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: device " << udi << " : found parent device. Removing"; it->removeAt(position); if (it->isEmpty()) { qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: parent don't have any child devices. Erase parent"; m_parentDevices.erase(it); } break; } } } } for (int position = 0; position < m_devices.size(); ++position) { if (m_devices[position].udi() == udi) { beginRemoveRows(QModelIndex(), position, position); m_deviceTypes.remove(udi); m_stateMonitor->removeMonitoringDevice(m_devices[position].udi()); m_errorMonitor->removeMonitoringDevice(m_devices[position].udi()); qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: device: " << m_devices[position].udi() << " successfully removed from the model"; m_devices.removeAt(position); endRemoveRows(); break; } } if (auto it = m_removeTimers.find(udi); it != m_removeTimers.end()) { if (it->timer->isActive()) { qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: device " << udi << " Timer was active: stop"; it->timer->stop(); } qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: device " << udi << " Remove timer"; it->timer->deleteLater(); m_removeTimers.erase(it); // PLASMA-WORKSPACE-15SV: This must be removed at the end to ensure udi and parentUdi are not dangling } } void DeviceControl::onDeviceChanged(const QMap &props) { auto iface = qobject_cast(sender()); if (iface && iface->isValid() && props.contains(QLatin1String("Size")) && iface->property(QStringLiteral("Size")).toInt() > 0) { const QString udi = qobject_cast(iface)->property("udi").toString(); m_spaceMonitor->forceUpdateSize(udi); qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: 2-stage device successfully initialized : " << udi; } } void DeviceControl::onDeviceSizeChanged(const QString &udi) { // update the volume in case of 2-stage devices Solid::Device device(udi); if (device.is()) { bool isDeviceValid = false; for (const auto &findingDevice : m_devices) { if (findingDevice.udi() == udi) { isDeviceValid = true; } } if (isDeviceValid && m_spaceMonitor->getFullSize(udi) == 0) { qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: 2-stage device arrived : " << udi; Solid::GenericInterface *iface = device.as(); if (iface) { iface->setProperty("udi", device.udi()); connect(iface, &Solid::GenericInterface::propertyChanged, this, &DeviceControl::onDeviceChanged); return; } } } for (int position = 0; position < m_devices.size(); ++position) { if (m_devices[position].udi() == udi) { qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: Size for device : " << udi << " changed"; QModelIndex index = DeviceControl::index(position); Q_EMIT dataChanged(index, index, {Size, SizeText, FreeSpace, FreeSpaceText}); return; } } } void DeviceControl::onDeviceStatusChanged(const QString &udi) { qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: Status for device : " << udi << " changed"; for (int position = 0; position < m_devices.size(); ++position) { if (m_devices[position].udi() == udi) { QModelIndex index = DeviceControl::index(position); Q_EMIT dataChanged(index, index, {Mounted, OperationResult, Emblems}); return; } } } void DeviceControl::onDeviceErrorChanged(const QString &udi) { qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: Error for device : " << udi << " changed"; for (int position = 0; position < m_devices.size(); ++position) { if (m_devices[position].udi() == udi) { QModelIndex index = DeviceControl::index(position); Q_EMIT dataChanged(index, index, {Error, ErrorMessage}); return; } } qCDebug(APPLETS::DEVICENOTIFIER) << "Device Controller: Error for device : " << udi << " Fail to update. Device not exists"; } #include "moc_devicecontrol.cpp"