diff --git a/CMakeLists.txt b/CMakeLists.txt index 563100425..6f21ce11a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -249,6 +249,10 @@ optional_component(DEVICEKIT ON "Devices: DeviceKit backend" DEPENDS "D-Bus support" HAVE_DBUS ) +optional_component(UDISKS2 ON "Devices: UDisks2 backend" + DEPENDS "D-Bus support" HAVE_DBUS +) + optional_component(SPOTIFY_BLOB ON "Spotify support: non-GPL binary helper" DEPENDS "protobuf" PROTOBUF_FOUND PROTOBUF_PROTOC_EXECUTABLE DEPENDS "libspotify" SPOTIFY_FOUND diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 91ca559f8..91a83335b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -964,6 +964,34 @@ if(HAVE_DBUS) dbus/udisksdevice) endif(HAVE_DEVICEKIT) + if(HAVE_UDISKS2) + set_source_files_properties(dbus/org.freedesktop.DBus.ObjectManager.xml + PROPERTIES NO_NAMESPACE dbus/objectmanager INCLUDE dbus/metatypes.h) + set_source_files_properties(dbus/org.freedesktop.UDisks2.Filesystem.xml + PROPERTIES NO_NAMESPACE dbus/udisks2filesystem INCLUDE dbus/metatypes.h) + set_source_files_properties(dbus/org.freedesktop.UDisks2.Block.xml + PROPERTIES NO_NAMESPACE dbus/udisks2block INCLUDE dbus/metatypes.h) + set_source_files_properties(dbus/org.freedesktop.UDisks2.Drive.xml + PROPERTIES NO_NAMESPACE dbus/udisks2drive INCLUDE dbus/metatypes.h) + set_source_files_properties(dbus/org.freedesktop.UDisks2.Job.xml + PROPERTIES NO_NAMESPACE dbus/udisks2job INCLUDE dbus/metatypes.h) + qt4_add_dbus_interface(SOURCES + dbus/org.freedesktop.DBus.ObjectManager.xml + dbus/objectmanager) + qt4_add_dbus_interface(SOURCES + dbus/org.freedesktop.UDisks2.Filesystem.xml + dbus/udisks2filesystem) + qt4_add_dbus_interface(SOURCES + dbus/org.freedesktop.UDisks2.Block.xml + dbus/udisks2block) + qt4_add_dbus_interface(SOURCES + dbus/org.freedesktop.UDisks2.Drive.xml + dbus/udisks2drive) + qt4_add_dbus_interface(SOURCES + dbus/org.freedesktop.UDisks2.Job.xml + dbus/udisks2job) + endif(HAVE_UDISKS2) + # Wiimotedev interface classes if(ENABLE_WIIMOTEDEV) qt4_add_dbus_interface(SOURCES @@ -999,6 +1027,11 @@ optional_source(HAVE_DEVICEKIT HEADERS devices/devicekitlister.h ) +optional_source(HAVE_UDISKS2 + SOURCES devices/udisks2lister.cpp + HEADERS devices/udisks2lister.h +) + # Libgpod device backend optional_source(HAVE_LIBGPOD INCLUDE_DIRECTORIES ${LIBGPOD_INCLUDE_DIRS} diff --git a/src/config.h.in b/src/config.h.in index f22fe9bc8..2bd31b705 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -41,6 +41,7 @@ #cmakedefine HAVE_SKYDRIVE #cmakedefine HAVE_SPARKLE #cmakedefine HAVE_SPOTIFY_DOWNLOADER +#cmakedefine HAVE_UDISKS2 #cmakedefine HAVE_VK #cmakedefine HAVE_WIIMOTEDEV #cmakedefine TAGLIB_HAS_OPUS diff --git a/src/core/metatypes.cpp b/src/core/metatypes.cpp index 62e246012..00b3553d9 100644 --- a/src/core/metatypes.cpp +++ b/src/core/metatypes.cpp @@ -125,5 +125,8 @@ void RegisterMetaTypes() { qDBusRegisterMetaType(); qDBusRegisterMetaType(); qDBusRegisterMetaType(); + + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); #endif } diff --git a/src/dbus/metatypes.h b/src/dbus/metatypes.h index 4e0024574..3ec6de1d3 100644 --- a/src/dbus/metatypes.h +++ b/src/dbus/metatypes.h @@ -19,7 +19,14 @@ #define DBUS_METATYPES_H_ #include +#include -Q_DECLARE_METATYPE(QList); +Q_DECLARE_METATYPE(QList) + +typedef QMap InterfacesAndProperties; +typedef QMap ManagedObjectList; + +Q_DECLARE_METATYPE(InterfacesAndProperties) +Q_DECLARE_METATYPE(ManagedObjectList) #endif // DBUS_METATYPES_H_ diff --git a/src/dbus/org.freedesktop.DBus.ObjectManager.xml b/src/dbus/org.freedesktop.DBus.ObjectManager.xml new file mode 100644 index 000000000..efc389dd2 --- /dev/null +++ b/src/dbus/org.freedesktop.DBus.ObjectManager.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/dbus/org.freedesktop.UDisks2.Block.xml b/src/dbus/org.freedesktop.UDisks2.Block.xml new file mode 100644 index 000000000..f0e3a06c4 --- /dev/null +++ b/src/dbus/org.freedesktop.UDisks2.Block.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/dbus/org.freedesktop.UDisks2.Drive.xml b/src/dbus/org.freedesktop.UDisks2.Drive.xml new file mode 100644 index 000000000..5312b2250 --- /dev/null +++ b/src/dbus/org.freedesktop.UDisks2.Drive.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/dbus/org.freedesktop.UDisks2.Filesystem.xml b/src/dbus/org.freedesktop.UDisks2.Filesystem.xml new file mode 100644 index 000000000..1781919ab --- /dev/null +++ b/src/dbus/org.freedesktop.UDisks2.Filesystem.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/dbus/org.freedesktop.UDisks2.Job.xml b/src/dbus/org.freedesktop.UDisks2.Job.xml new file mode 100644 index 000000000..2cd42533d --- /dev/null +++ b/src/dbus/org.freedesktop.UDisks2.Job.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/devices/devicemanager.cpp b/src/devices/devicemanager.cpp index 6396b56cb..32ba33657 100644 --- a/src/devices/devicemanager.cpp +++ b/src/devices/devicemanager.cpp @@ -59,6 +59,9 @@ #ifdef HAVE_LIBMTP #include "mtpdevice.h" #endif +#ifdef HAVE_UDISKS2 +#include "udisks2lister.h" +#endif using std::bind; @@ -191,6 +194,9 @@ DeviceManager::DeviceManager(Application* app, QObject* parent) #ifdef HAVE_DEVICEKIT AddLister(new DeviceKitLister); #endif +#ifdef HAVE_UDISKS2 + AddLister(new Udisks2Lister); +#endif #ifdef HAVE_GIO AddLister(new GioLister); #endif @@ -228,7 +234,10 @@ void DeviceManager::LoadAllDevices() { for (const DeviceDatabaseBackend::Device& device : devices) { DeviceInfo info; info.InitFromDb(device); + + beginInsertRows(QModelIndex(), devices_.count(), devices_.count()); devices_ << info; + endInsertRows(); } } diff --git a/src/devices/udisks2lister.cpp b/src/devices/udisks2lister.cpp new file mode 100644 index 000000000..c5c72ef1c --- /dev/null +++ b/src/devices/udisks2lister.cpp @@ -0,0 +1,368 @@ +/* This file is part of Clementine. + Copyright 2016, Valeriy Malov + + Clementine is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Clementine is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Clementine. If not, see . +*/ + +#include "udisks2lister.h" + +#include + +#include "core/logging.h" +#include "core/utilities.h" +#include "dbus/objectmanager.h" +#include "dbus/udisks2block.h" +#include "dbus/udisks2drive.h" +#include "dbus/udisks2filesystem.h" +#include "dbus/udisks2job.h" + +constexpr char Udisks2Lister::udisks2_service_[]; + +Udisks2Lister::Udisks2Lister() {} + +Udisks2Lister::~Udisks2Lister() {} + +QStringList Udisks2Lister::DeviceUniqueIDs() { + QReadLocker locker(&device_data_lock_); + return device_data_.keys(); +} + +QVariantList Udisks2Lister::DeviceIcons(const QString& id) { + return QVariantList(); +} + +QString Udisks2Lister::DeviceManufacturer(const QString& id) { + QReadLocker locker(&device_data_lock_); + if (!device_data_.contains(id)) return ""; + return device_data_[id].vendor; +} + +QString Udisks2Lister::DeviceModel(const QString& id) { + QReadLocker locker(&device_data_lock_); + if (!device_data_.contains(id)) return ""; + return device_data_[id].model; +} + +quint64 Udisks2Lister::DeviceCapacity(const QString& id) { + QReadLocker locker(&device_data_lock_); + if (!device_data_.contains(id)) return 0; + return device_data_[id].capacity; +} + +quint64 Udisks2Lister::DeviceFreeSpace(const QString& id) { + QReadLocker locker(&device_data_lock_); + if (!device_data_.contains(id)) return 0; + return device_data_[id].free_space; +} + +QVariantMap Udisks2Lister::DeviceHardwareInfo(const QString& id) { + QReadLocker locker(&device_data_lock_); + if (!device_data_.contains(id)) return QVariantMap(); + + QVariantMap result; + + const auto& data = device_data_[id]; + result[QT_TR_NOOP("DBus path")] = data.dbus_path; + result[QT_TR_NOOP("Serial number")] = data.serial; + result[QT_TR_NOOP("Mount points")] = data.mount_paths.join(", "); + result[QT_TR_NOOP("Parition label")] = data.label; + result[QT_TR_NOOP("UUID")] = data.uuid; + + return result; +} + +QString Udisks2Lister::MakeFriendlyName(const QString& id) { + QReadLocker locker(&device_data_lock_); + if (!device_data_.contains(id)) return ""; + return device_data_[id].friendly_name; +} + +QList Udisks2Lister::MakeDeviceUrls(const QString& id) { + QReadLocker locker(&device_data_lock_); + if (!device_data_.contains(id)) return QList(); + return QList() << QUrl::fromLocalFile( + device_data_[id].mount_paths.at(0)); +} + +void Udisks2Lister::UnmountDevice(const QString& id) { + QReadLocker locker(&device_data_lock_); + if (!device_data_.contains(id)) return; + + OrgFreedesktopUDisks2FilesystemInterface filesystem( + udisks2_service_, device_data_[id].dbus_path, + QDBusConnection::systemBus()); + + if (filesystem.isValid()) { + auto unmount_result = filesystem.Unmount(QVariantMap()); + unmount_result.waitForFinished(); + + if (unmount_result.isError()) { + qLog(Warning) << "Failed to unmount " << id << ": " + << unmount_result.error(); + return; + } + + OrgFreedesktopUDisks2DriveInterface drive(udisks2_service_, + device_data_[id].dbus_drive_path, + QDBusConnection::systemBus()); + + if (drive.isValid()) { + auto eject_result = drive.Eject(QVariantMap()); + eject_result.waitForFinished(); + + if (eject_result.isError()) + qLog(Warning) << "Failed to eject " << id << ": " + << eject_result.error(); + } + + device_data_.remove(id); + DeviceRemoved(id); + } +} + +void Udisks2Lister::UpdateDeviceFreeSpace(const QString& id) { + QWriteLocker locker(&device_data_lock_); + device_data_[id].free_space = + Utilities::FileSystemFreeSpace(device_data_[id].mount_paths.at(0)); + + emit DeviceChanged(id); +} + +void Udisks2Lister::Init() { + udisks2_interface_.reset(new OrgFreedesktopDBusObjectManagerInterface( + udisks2_service_, "/org/freedesktop/UDisks2", + QDBusConnection::systemBus())); + + QDBusPendingReply reply = + udisks2_interface_->GetManagedObjects(); + reply.waitForFinished(); + + if (!reply.isValid()) { + qLog(Warning) << "Error enumerating udisks2 devices:" + << reply.error().name() << reply.error().message(); + udisks2_interface_.reset(); + return; + } + + for (const QDBusObjectPath& path : reply.value().keys()) { + auto partition_data = ReadPartitionData(path); + + if (!partition_data.dbus_path.isEmpty()) { + QWriteLocker locker(&device_data_lock_); + device_data_[partition_data.unique_id()] = partition_data; + } + } + + for (const auto& id : device_data_.keys()) { + emit DeviceAdded(id); + } + + connect(udisks2_interface_.get(), + SIGNAL(InterfacesAdded(QDBusObjectPath, InterfacesAndProperties)), + SLOT(DBusInterfaceAdded(QDBusObjectPath, InterfacesAndProperties))); + connect(udisks2_interface_.get(), + SIGNAL(InterfacesRemoved(QDBusObjectPath, QStringList)), + SLOT(DBusInterfaceRemoved(QDBusObjectPath, QStringList))); +} + +void Udisks2Lister::DBusInterfaceAdded( + const QDBusObjectPath& path, const InterfacesAndProperties& interfaces) { + for (auto interface = interfaces.constBegin(); + interface != interfaces.constEnd(); ++interface) { + if (interface.key() != "org.freedesktop.UDisks2.Job") continue; + + std::shared_ptr job = + std::make_shared( + udisks2_service_, path.path(), QDBusConnection::systemBus()); + + if (!job->isValid()) continue; + + bool is_mount_job = false; + if (job->operation() == "filesystem-mount") { + is_mount_job = true; + } else if (job->operation() == "filesystem-unmount") { + is_mount_job = false; + } else { + continue; + } + + auto mounted_partitions = job->objects(); + + if (mounted_partitions.isEmpty()) { + qLog(Warning) << "Empty Udisks2 mount/umount job " << path.path(); + continue; + } + + { + QMutexLocker locker(&jobs_lock_); + qLog(Debug) << "Adding pending job | DBus Path = " << job->path() + << " | IsMountJob = " << is_mount_job + << " | First partition = " << mounted_partitions.at(0).path(); + mounting_jobs_[path].dbus_interface = job; + mounting_jobs_[path].is_mount = is_mount_job; + mounting_jobs_[path].mounted_partitions = mounted_partitions; + connect(job.get(), SIGNAL(Completed(bool, const QString&)), + SLOT(JobCompleted(bool, const QString&))); + } + } +} + +void Udisks2Lister::DBusInterfaceRemoved(const QDBusObjectPath& path, + const QStringList& ifaces) { + if (!isPendingJob(path)) RemoveDevice(path); +} + +bool Udisks2Lister::isPendingJob(const QDBusObjectPath& job_path) { + QMutexLocker locker(&jobs_lock_); + + if (!mounting_jobs_.contains(job_path)) return false; + + mounting_jobs_.remove(job_path); + return true; +} + +void Udisks2Lister::RemoveDevice(const QDBusObjectPath& device_path) { + QWriteLocker locker(&device_data_lock_); + QString id; + for (const auto& data : device_data_) { + if (data.dbus_path == device_path.path()) { + id = data.unique_id(); + break; + } + } + + if (id.isEmpty()) return; + + qLog(Debug) << "UDisks2 device removed: " << device_path.path(); + device_data_.remove(id); + DeviceRemoved(id); +} + +QList Udisks2Lister::GetMountedPartitionsFromDBusArgument( + const QDBusArgument& input) { + QList result; + + input.beginArray(); + while (!input.atEnd()) { + QDBusObjectPath extractedPath; + input >> extractedPath; + result.push_back(extractedPath); + } + input.endArray(); + + return result; +} + +void Udisks2Lister::JobCompleted(bool success, const QString& message) { + auto job = qobject_cast(sender()); + QDBusObjectPath jobPath(job->path()); + + if (!job->isValid() || !success || !mounting_jobs_.contains(jobPath)) return; + + qLog(Debug) << "Pending Job Completed | Path = " << job->path() + << " | Mount? = " << mounting_jobs_[jobPath].is_mount + << " | Success = " << success; + + for (const auto& mounted_object : + mounting_jobs_[jobPath].mounted_partitions) { + auto partition_data = ReadPartitionData(mounted_object); + if (partition_data.dbus_path.isEmpty()) continue; + + mounting_jobs_[jobPath].is_mount + ? HandleFinishedMountJob(partition_data) + : HandleFinishedUnmountJob(partition_data, mounted_object); + } +} + +void Udisks2Lister::HandleFinishedMountJob( + const Udisks2Lister::PartitionData& partition_data) { + qLog(Debug) << "UDisks2 mount job finished: Drive = " + << partition_data.dbus_drive_path + << " | Partition = " << partition_data.dbus_path; + QWriteLocker locker(&device_data_lock_); + device_data_[partition_data.unique_id()] = partition_data; + DeviceAdded(partition_data.unique_id()); +} + +void Udisks2Lister::HandleFinishedUnmountJob( + const Udisks2Lister::PartitionData& partition_data, + const QDBusObjectPath& mounted_object) { + QWriteLocker locker(&device_data_lock_); + QString id; + for (auto& data : device_data_) { + if (data.mount_paths.contains(mounted_object.path())) { + qLog(Debug) + << "UDisks2 umount job finished, found corresponding device: Drive = " + << data.dbus_drive_path << " | Partition = " << data.dbus_path; + data.mount_paths.removeOne(mounted_object.path()); + if (data.mount_paths.empty()) id = data.unique_id(); + break; + } + } + + if (!id.isEmpty()) { + qLog(Debug) << "Partition " << partition_data.dbus_path + << " has no more mount points, removing it from device list"; + device_data_.remove(id); + DeviceRemoved(id); + } +} + +Udisks2Lister::PartitionData Udisks2Lister::ReadPartitionData( + const QDBusObjectPath& path) { + PartitionData result; + OrgFreedesktopUDisks2FilesystemInterface filesystem( + udisks2_service_, path.path(), QDBusConnection::systemBus()); + OrgFreedesktopUDisks2BlockInterface block(udisks2_service_, path.path(), + QDBusConnection::systemBus()); + + if (filesystem.isValid() && block.isValid() && + !filesystem.mountPoints().empty()) { + OrgFreedesktopUDisks2DriveInterface drive( + udisks2_service_, block.drive().path(), QDBusConnection::systemBus()); + + if (drive.isValid() && drive.mediaRemovable()) { + result.dbus_path = path.path(); + result.dbus_drive_path = block.drive().path(); + + result.serial = drive.serial(); + result.vendor = drive.vendor(); + result.model = drive.model(); + + result.label = block.idLabel(); + result.uuid = block.idUUID(); + result.capacity = drive.size(); + + if (!result.label.isEmpty()) + result.friendly_name = result.label; + else + result.friendly_name = result.model + " " + result.uuid; + + for (const auto& path : filesystem.mountPoints()) + result.mount_paths.push_back(path); + + result.free_space = + Utilities::FileSystemFreeSpace(result.mount_paths.at(0)); + } + } + + return result; +} + +QString Udisks2Lister::PartitionData::unique_id() const { + return QString("Udisks2/%1/%2/%3/%4/%5") + .arg(serial, vendor, model) + .arg(capacity) + .arg(uuid); +} diff --git a/src/devices/udisks2lister.h b/src/devices/udisks2lister.h new file mode 100644 index 000000000..3a2a01207 --- /dev/null +++ b/src/devices/udisks2lister.h @@ -0,0 +1,119 @@ +/* This file is part of Clementine. + Copyright 2016, Valeriy Malov + + Clementine is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Clementine is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Clementine. If not, see . +*/ + +#ifndef UDISKS2LISTER_H +#define UDISKS2LISTER_H + +#include + +#include +#include +#include +#include + +#include "devicelister.h" +#include "dbus/metatypes.h" + +class OrgFreedesktopDBusObjectManagerInterface; +class OrgFreedesktopUDisks2JobInterface; + +class Udisks2Lister : public DeviceLister { + Q_OBJECT + + public: + Udisks2Lister(); + ~Udisks2Lister(); + + QStringList DeviceUniqueIDs() override; + QVariantList DeviceIcons(const QString& id) override; + QString DeviceManufacturer(const QString& id) override; + QString DeviceModel(const QString& id) override; + quint64 DeviceCapacity(const QString& id) override; + quint64 DeviceFreeSpace(const QString& id) override; + QVariantMap DeviceHardwareInfo(const QString& id) override; + + QString MakeFriendlyName(const QString& id) override; + QList MakeDeviceUrls(const QString& id) override; + + void UnmountDevice(const QString& id) override; + + public slots: + void UpdateDeviceFreeSpace(const QString& id) override; + + protected: + void Init() override; + + private slots: + void DBusInterfaceAdded(const QDBusObjectPath& path, + const InterfacesAndProperties& ifaces); + void DBusInterfaceRemoved(const QDBusObjectPath& path, + const QStringList& ifaces); + void JobCompleted(bool success, const QString& message); + + private: + bool isPendingJob(const QDBusObjectPath& job_path); + void RemoveDevice(const QDBusObjectPath& device_path); + QList GetMountedPartitionsFromDBusArgument( + const QDBusArgument& input); + + struct Udisks2Job { + bool is_mount = true; + QList mounted_partitions; + std::shared_ptr dbus_interface; + }; + + QMutex jobs_lock_; + QMap mounting_jobs_; + + private: + struct PartitionData { + QString unique_id() const; + + QString dbus_path; + QString friendly_name; + + // Device + QString serial; + QString vendor; + QString model; + quint64 capacity = 0; + QString dbus_drive_path; + + // Paritition + QString label; + QString uuid; + quint64 free_space = 0; + QStringList mount_paths; + }; + + PartitionData ReadPartitionData(const QDBusObjectPath& path); + void HandleFinishedMountJob( + const Udisks2Lister::PartitionData& partition_data); + void HandleFinishedUnmountJob( + const Udisks2Lister::PartitionData& partition_data, + const QDBusObjectPath& mounted_object); + + QReadWriteLock device_data_lock_; + QMap device_data_; + + private: + std::unique_ptr udisks2_interface_; + + static constexpr char udisks2_service_[] = "org.freedesktop.UDisks2"; +}; + +#endif // UDISKS2LISTER_H