Clementine-audio-player-Mac.../src/devices/devicemanager.cpp

790 lines
24 KiB
C++

/* This file is part of Clementine.
Copyright 2010, David Sansome <me@davidsansome.com>
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 <http://www.gnu.org/licenses/>.
*/
#include "devicemanager.h"
#include <QApplication>
#include <QDir>
#include <QIcon>
#include <QMessageBox>
#include <QPainter>
#include <QPushButton>
#include <QSortFilterProxyModel>
#include <QUrl>
#include <memory>
#include "config.h"
#include "core/application.h"
#include "core/concurrentrun.h"
#include "core/database.h"
#include "core/logging.h"
#include "core/musicstorage.h"
#include "core/simpletreemodel.h"
#include "core/taskmanager.h"
#include "core/utilities.h"
#include "devicedatabasebackend.h"
#include "deviceinfo.h"
#include "devicestatefiltermodel.h"
#include "filesystemdevice.h"
#include "ui/iconloader.h"
#ifdef HAVE_AUDIOCD
#include "cddadevice.h"
#include "cddalister.h"
#endif
#if defined(Q_OS_DARWIN) and defined(HAVE_LIBMTP)
#include "macdevicelister.h"
#endif
#ifdef HAVE_LIBGPOD
#include "gpoddevice.h"
#endif
#ifdef HAVE_GIO
#include "giolister.h"
#endif
#ifdef HAVE_LIBMTP
#include "mtpdevice.h"
#endif
#ifdef HAVE_UDISKS2
#include "udisks2lister.h"
#endif
using std::bind;
const int DeviceManager::kDeviceIconSize = 32;
const int DeviceManager::kDeviceIconOverlaySize = 16;
DeviceManager::DeviceManager(Application* app, QObject* parent)
: SimpleTreeModel<DeviceInfo>(new DeviceInfo(this), parent),
app_(app),
not_connected_overlay_(
IconLoader::Load("edit-delete", IconLoader::Base)) {
thread_pool_.setMaxThreadCount(1);
connect(app_->task_manager(), SIGNAL(TasksChanged()), SLOT(TasksChanged()));
// Create the backend in the database thread
backend_ = new DeviceDatabaseBackend;
backend_->moveToThread(app_->database()->thread());
backend_->Init(app_->database());
connect(this, SIGNAL(DeviceCreatedFromDb(DeviceInfo*)),
SLOT(AddDeviceFromDb(DeviceInfo*)));
// This reads from the database and contends on the database mutex, which can
// be very slow on startup.
ConcurrentRun::Run<void>(&thread_pool_,
bind(&DeviceManager::LoadAllDevices, this));
// This proxy model only shows connected devices
connected_devices_model_ = new DeviceStateFilterModel(this);
connected_devices_model_->setSourceModel(this);
// CD devices are detected via the DiskArbitration framework instead on Darwin.
#if defined(HAVE_AUDIOCD) && !defined(Q_OS_DARWIN)
AddLister(new CddaLister);
#endif
#ifdef HAVE_UDISKS2
AddLister(new Udisks2Lister);
#endif
#ifdef HAVE_GIO
AddLister(new GioLister);
#endif
#if defined(Q_OS_DARWIN) and defined(HAVE_LIBMTP)
AddLister(new MacDeviceLister);
#endif
AddDeviceClass<FilesystemDevice>();
#ifdef HAVE_AUDIOCD
AddDeviceClass<CddaDevice>();
#endif
#ifdef HAVE_LIBGPOD
AddDeviceClass<GPodDevice>();
#endif
#ifdef HAVE_LIBMTP
AddDeviceClass<MtpDevice>();
#endif
}
DeviceManager::~DeviceManager() {
for (DeviceLister* lister : listers_) {
lister->ShutDown();
delete lister;
}
backend_->deleteLater();
delete root_;
}
// This runs concurrently.
void DeviceManager::LoadAllDevices() {
Q_ASSERT(QThread::currentThread() != qApp->thread());
DeviceDatabaseBackend::DeviceList devices = backend_->GetAllDevices();
for (const DeviceDatabaseBackend::Device& device : devices) {
DeviceInfo* info = new DeviceInfo(DeviceInfo::Type_Device, root_);
info->InitFromDb(device);
// Use of QPixMap and device insertion should only be done on the main
// thread. Send a signal to finish the device addition.
emit DeviceCreatedFromDb(info);
}
}
void DeviceManager::AddDeviceFromDb(DeviceInfo* info) {
// At this point, icon_name_ contains the value from the database where the
// value is allowed to be a comma delimited list.
QStringList icon_names = info->icon_name_.split(',');
QVariantList icons;
for (const QString& icon_name : icon_names) {
icons << icon_name;
}
info->LoadIcon(icons, info->friendly_name_);
DeviceInfo* existing = FindEquivalentDevice(info);
if (existing) {
qLog(Info) << "Found existing device: " << info->friendly_name_;
// Update user configuration from the database.
existing->icon_name_ = info->icon_name_;
existing->icon_ = info->icon_;
QModelIndex idx = ItemToIndex(existing);
if (idx.isValid()) emit dataChanged(idx, idx);
// Discard the info loaded from the database. This doesn't use
// begin/endRemoveRows, but beginInsertRows has not yet been called to
// notify listeners of the existence.
root_->Delete(info->row);
} else {
qLog(Info) << "Device added from database: " << info->friendly_name_;
beginInsertRows(ItemToIndex(root_), devices_.count(), devices_.count());
devices_ << info;
endInsertRows();
}
}
QVariant DeviceManager::data(const QModelIndex& idx, int role) const {
if (!idx.isValid() || idx.column() != 0) return QVariant();
DeviceInfo* info = IndexToItem(idx);
if (!info) return QVariant();
switch (role) {
case Qt::DisplayRole: {
QString text;
if (!info->friendly_name_.isEmpty())
text = info->friendly_name_;
else if (info->BestBackend())
text = info->BestBackend()->unique_id_;
if (info->size_)
text = text + QString(" (%1)").arg(Utilities::PrettySize(info->size_));
return text;
}
case Qt::DecorationRole: {
QPixmap pixmap = info->icon_.pixmap(kDeviceIconSize);
if (info->backends_.isEmpty() || !info->BestBackend()->lister_) {
// Disconnected but remembered
QPainter p(&pixmap);
p.drawPixmap(kDeviceIconSize - kDeviceIconOverlaySize,
kDeviceIconSize - kDeviceIconOverlaySize,
not_connected_overlay_.pixmap(kDeviceIconOverlaySize));
}
return pixmap;
}
case Role_FriendlyName:
return info->friendly_name_;
case Role_UniqueId:
if (!info->BestBackend()) return QString();
return info->BestBackend()->unique_id_;
case Role_IconName:
return info->icon_name_;
case Role_Capacity:
case MusicStorage::Role_Capacity:
return info->size_;
case Role_FreeSpace:
case MusicStorage::Role_FreeSpace:
return info->BestBackend() && info->BestBackend()->lister_
? info->BestBackend()->lister_->DeviceFreeSpace(
info->BestBackend()->unique_id_)
: QVariant();
case Role_State:
if (info->device_) return State_Connected;
if (info->BestBackend() && info->BestBackend()->lister_) {
if (info->BestBackend()->lister_->DeviceNeedsMount(
info->BestBackend()->unique_id_))
return State_NotMounted;
return State_NotConnected;
}
return State_Remembered;
case Role_UpdatingPercentage:
if (info->task_percentage_ == -1) return QVariant();
return info->task_percentage_;
case MusicStorage::Role_Storage:
if (!info->device_ && info->database_id_ != -1)
const_cast<DeviceManager*>(this)->Connect(info);
if (!info->device_) return QVariant();
return QVariant::fromValue<std::shared_ptr<MusicStorage>>(info->device_);
case MusicStorage::Role_StorageForceConnect:
if (!info->BestBackend()) return QVariant();
if (!info->device_) {
if (info->database_id_ == -1 &&
!info->BestBackend()->lister_->DeviceNeedsMount(
info->BestBackend()->unique_id_)) {
if (info->BestBackend()->lister_->AskForScan(
info->BestBackend()->unique_id_)) {
std::unique_ptr<QMessageBox> dialog(new QMessageBox(
QMessageBox::Information, tr("Connect device"),
tr("This is the first time you have connected this device. "
"Clementine will now scan the device to find music files - "
"this may take some time."),
QMessageBox::Cancel));
QPushButton* connect = dialog->addButton(tr("Connect device"),
QMessageBox::AcceptRole);
dialog->exec();
if (dialog->clickedButton() != connect) return QVariant();
}
}
const_cast<DeviceManager*>(this)->Connect(info);
}
if (!info->device_) return QVariant();
return QVariant::fromValue<std::shared_ptr<MusicStorage>>(info->device_);
case Role_MountPath: {
if (!info->device_) return QVariant();
QString ret = info->device_->url().path();
#ifdef Q_OS_WIN32
if (ret.startsWith('/')) ret.remove(0, 1);
#endif
return QDir::toNativeSeparators(ret);
}
case Role_TranscodeMode:
return info->transcode_mode_;
case Role_TranscodeFormat:
return info->transcode_format_;
case Role_SongCount:
if (!info->device_) return QVariant();
return info->device_->song_count();
default:
return QVariant();
}
}
void DeviceManager::AddLister(DeviceLister* lister) {
listers_ << lister;
connect(lister, SIGNAL(DeviceAdded(QString)),
SLOT(PhysicalDeviceAdded(QString)));
connect(lister, SIGNAL(DeviceRemoved(QString)),
SLOT(PhysicalDeviceRemoved(QString)));
connect(lister, SIGNAL(DeviceChanged(QString)),
SLOT(PhysicalDeviceChanged(QString)));
lister->Start();
}
DeviceInfo* DeviceManager::FindDeviceById(const QString& id) const {
for (int i = 0; i < devices_.count(); ++i) {
for (const DeviceInfo::Backend& backend : devices_[i]->backends_) {
if (backend.unique_id_ == id) return devices_[i];
}
}
return nullptr;
}
DeviceInfo* DeviceManager::FindDeviceByUrl(const QList<QUrl>& urls) const {
if (urls.isEmpty()) return nullptr;
for (int i = 0; i < devices_.count(); ++i) {
for (const DeviceInfo::Backend& backend : devices_[i]->backends_) {
if (!backend.lister_) continue;
QList<QUrl> device_urls =
backend.lister_->MakeDeviceUrls(backend.unique_id_);
for (const QUrl& url : device_urls) {
if (urls.contains(url)) return devices_[i];
}
}
}
return nullptr;
}
DeviceInfo* DeviceManager::FindEquivalentDevice(DeviceInfo* info) const {
for (const DeviceInfo::Backend& backend : info->backends_) {
DeviceInfo* match = FindDeviceById(backend.unique_id_);
if (match) return match;
}
return nullptr;
}
QList<DeviceInfo*> DeviceManager::FindDevicesByUrlSchemes(
QStringList url_schemes) const {
QList<DeviceInfo*> matches;
for (DeviceInfo* device_info : devices_) {
for (const DeviceInfo::Backend& backend : device_info->backends_) {
if (!backend.lister_) continue;
QList<QUrl> device_urls =
backend.lister_->MakeDeviceUrls(backend.unique_id_);
for (const QUrl& url : device_urls) {
if (url_schemes.contains(url.scheme())) {
matches << device_info;
break;
}
}
}
}
return matches;
}
void DeviceManager::PhysicalDeviceAdded(const QString& id) {
DeviceLister* lister = qobject_cast<DeviceLister*>(sender());
qLog(Info) << "Device added:" << id;
// Do we have this device already?
DeviceInfo* info = FindDeviceById(id);
if (info) {
for (int backend_index = 0; backend_index < info->backends_.count();
++backend_index) {
if (info->backends_[backend_index].unique_id_ == id) {
info->backends_[backend_index].lister_ = lister;
break;
}
}
QModelIndex idx = ItemToIndex(info);
if (idx.isValid()) emit dataChanged(idx, idx);
} else {
// Check if we have another device with the same URL
info = FindDeviceByUrl(lister->MakeDeviceUrls(id));
if (info) {
// Add this device's lister to the existing device
info->backends_ << DeviceInfo::Backend(lister, id);
// If the user hasn't saved the device in the DB yet then overwrite the
// device's name and icon etc.
if (info->database_id_ == -1 && info->BestBackend()->lister_ == lister) {
info->friendly_name_ = lister->MakeFriendlyName(id);
info->size_ = lister->DeviceCapacity(id);
info->LoadIcon(lister->DeviceIcons(id), info->friendly_name_);
}
QModelIndex idx = ItemToIndex(info);
if (idx.isValid()) emit dataChanged(idx, idx);
} else {
// It's a completely new device
DeviceInfo* info = new DeviceInfo(DeviceInfo::Type_Device, root_);
info->backends_ << DeviceInfo::Backend(lister, id);
info->friendly_name_ = lister->MakeFriendlyName(id);
info->size_ = lister->DeviceCapacity(id);
info->LoadIcon(lister->DeviceIcons(id), info->friendly_name_);
beginInsertRows(ItemToIndex(root_), devices_.count(), devices_.count());
devices_ << info;
endInsertRows();
}
}
}
void DeviceManager::PhysicalDeviceRemoved(const QString& id) {
DeviceLister* lister = qobject_cast<DeviceLister*>(sender());
qLog(Info) << "Device removed:" << id;
DeviceInfo* info = FindDeviceById(id);
if (!info) {
// Shouldn't happen
return;
}
QModelIndex idx = ItemToIndex(info);
if (!idx.isValid()) return;
if (info->database_id_ != -1) {
// Keep the structure around, but just "disconnect" it
for (int backend_index = 0; backend_index < info->backends_.count();
++backend_index) {
if (info->backends_[backend_index].unique_id_ == id) {
info->backends_[backend_index].lister_ = nullptr;
break;
}
}
if (info->device_ && info->device_->lister() == lister)
info->device_.reset();
if (!info->device_) emit DeviceDisconnected(idx);
emit dataChanged(idx, idx);
} else {
// If this was the last lister for the device then remove it from the model
for (int backend_index = 0; backend_index < info->backends_.count();
++backend_index) {
if (info->backends_[backend_index].unique_id_ == id) {
info->backends_.removeAt(backend_index);
break;
}
}
if (info->backends_.isEmpty()) {
beginRemoveRows(ItemToIndex(root_), idx.row(), idx.row());
devices_.removeAll(info);
root_->Delete(info->row);
endRemoveRows();
}
}
}
void DeviceManager::PhysicalDeviceChanged(const QString& id) {
DeviceLister* lister = qobject_cast<DeviceLister*>(sender());
Q_UNUSED(lister);
DeviceInfo* info = FindDeviceById(id);
if (!info) {
// Shouldn't happen
return;
}
// TODO
}
std::shared_ptr<ConnectedDevice> DeviceManager::Connect(QModelIndex idx) {
DeviceInfo* info = IndexToItem(idx);
if (!info) return std::shared_ptr<ConnectedDevice>();
return Connect(info);
}
std::shared_ptr<ConnectedDevice> DeviceManager::Connect(DeviceInfo* info) {
std::shared_ptr<ConnectedDevice> ret;
if (!info) return ret;
if (info->device_) // Already connected
return info->device_;
if (!info->BestBackend() ||
!info->BestBackend()->lister_) // Not physically connected
return ret;
if (info->BestBackend()->lister_->DeviceNeedsMount(
info->BestBackend()->unique_id_)) {
// Mount the device
info->BestBackend()->lister_->MountDevice(info->BestBackend()->unique_id_);
return ret;
}
bool first_time = (info->database_id_ == -1);
if (first_time) {
// We haven't stored this device in the database before
info->database_id_ = backend_->AddDevice(info->SaveToDb());
}
// Get the device URLs
QList<QUrl> urls = info->BestBackend()->lister_->MakeDeviceUrls(
info->BestBackend()->unique_id_);
if (urls.isEmpty()) return ret;
// Take the first URL that we have a handler for
QUrl device_url;
for (const QUrl& url : urls) {
qLog(Info) << "Connecting" << url;
// Find a device class for this URL's scheme
if (device_classes_.contains(url.scheme())) {
device_url = url;
break;
}
// If we get here it means that this URL scheme wasn't supported. If it
// was "ipod" or "mtp" then the user compiled out support and the device
// won't work properly.
if (url.scheme() == "mtp" || url.scheme() == "gphoto2") {
if (QMessageBox::critical(
nullptr, tr("This device will not work properly"),
tr("This is an MTP device, but you compiled Clementine without "
"libmtp support.") +
" " +
tr("If you continue, this device will work slowly and "
"songs copied to it may not work."),
QMessageBox::Abort, QMessageBox::Ignore) == QMessageBox::Abort)
return ret;
}
if (url.scheme() == "ipod") {
if (QMessageBox::critical(
nullptr, tr("This device will not work properly"),
tr("This is an iPod, but you compiled Clementine without libgpod "
"support.") +
" " +
tr("If you continue, this device will work slowly and "
"songs copied to it may not work."),
QMessageBox::Abort, QMessageBox::Ignore) == QMessageBox::Abort)
return ret;
}
}
if (device_url.isEmpty()) {
// Munge the URL list into a string list
QStringList url_strings;
for (const QUrl& url : urls) {
url_strings << url.toString();
}
app_->AddError(tr("This type of device is not supported: %1")
.arg(url_strings.join(", ")));
return ret;
}
QMetaObject meta_object = device_classes_.value(device_url.scheme());
QObject* instance = meta_object.newInstance(
Q_ARG(QUrl, device_url),
Q_ARG(DeviceLister*, info->BestBackend()->lister_),
Q_ARG(QString, info->BestBackend()->unique_id_),
Q_ARG(DeviceManager*, this), Q_ARG(Application*, app_),
Q_ARG(int, info->database_id_), Q_ARG(bool, first_time));
ret.reset(static_cast<ConnectedDevice*>(instance));
if (!ret) {
qLog(Warning) << "Could not create device for" << device_url.toString();
} else {
if (!ret->Init()) {
qLog(Warning) << "Could not initialize device for "
<< device_url.toString();
ret.reset();
return ret;
}
info->device_ = ret;
QModelIndex idx = ItemToIndex(info);
if (!idx.isValid()) return ret;
emit dataChanged(idx, idx);
connect(info->device_.get(), SIGNAL(TaskStarted(int)),
SLOT(DeviceTaskStarted(int)));
connect(info->device_.get(), SIGNAL(SongCountUpdated(int)),
SLOT(DeviceSongCountUpdated(int)));
connect(info->device_.get(), SIGNAL(ConnectFinished(const QString&, bool)),
SLOT(DeviceConnectFinished(const QString&, bool)));
ret->ConnectAsync();
}
return ret;
}
void DeviceManager::DeviceConnectFinished(const QString& id, bool success) {
DeviceInfo* info = FindDeviceById(id);
if (!info) return;
QModelIndex idx = ItemToIndex(info);
if (!idx.isValid()) return;
if (success) {
emit DeviceConnected(idx);
} else {
info->device_.reset();
emit dataChanged(idx, idx);
}
}
std::shared_ptr<ConnectedDevice> DeviceManager::GetConnectedDevice(
QModelIndex idx) const {
DeviceInfo* info = IndexToItem(idx);
if (!info) return std::shared_ptr<ConnectedDevice>();
return info->device_;
}
std::shared_ptr<ConnectedDevice> DeviceManager::GetConnectedDevice(
DeviceInfo* info) const {
if (!info) return std::shared_ptr<ConnectedDevice>();
return info->device_;
}
int DeviceManager::GetDatabaseId(const QModelIndex& idx) const {
if (!idx.isValid()) return -1;
DeviceInfo* info = IndexToItem(idx);
if (!info) return -1;
return info->database_id_;
}
DeviceLister* DeviceManager::GetLister(QModelIndex idx) const {
if (!idx.isValid()) return nullptr;
DeviceInfo* info = IndexToItem(idx);
if (!info || !info->BestBackend()) return nullptr;
return info->BestBackend()->lister_;
}
void DeviceManager::Disconnect(QModelIndex idx) {
if (!idx.isValid()) return;
DeviceInfo* info = IndexToItem(idx);
if (!info || !info->device_) // Already disconnected
return;
info->device_.reset();
emit DeviceDisconnected(idx);
emit dataChanged(idx, idx);
}
void DeviceManager::Forget(QModelIndex idx) {
if (!idx.isValid()) return;
DeviceInfo* info = IndexToItem(idx);
if (!info) return;
if (info->database_id_ == -1) return;
if (info->device_) Disconnect(idx);
backend_->RemoveDevice(info->database_id_);
info->database_id_ = -1;
if (!info->BestBackend() ||
(info->BestBackend() && !info->BestBackend()->lister_)) {
// It's not attached any more so remove it from the list
beginRemoveRows(ItemToIndex(root_), idx.row(), idx.row());
devices_.removeAll(info);
root_->Delete(info->row);
endRemoveRows();
} else {
// It's still attached, set the name and icon back to what they were
// originally
const QString id = info->BestBackend()->unique_id_;
info->friendly_name_ = info->BestBackend()->lister_->MakeFriendlyName(id);
info->LoadIcon(info->BestBackend()->lister_->DeviceIcons(id),
info->friendly_name_);
emit dataChanged(idx, idx);
}
}
void DeviceManager::SetDeviceOptions(QModelIndex idx,
const QString& friendly_name,
const QString& icon_name,
MusicStorage::TranscodeMode mode,
Song::FileType format) {
if (!idx.isValid()) return;
DeviceInfo* info = IndexToItem(idx);
if (!info) return;
info->friendly_name_ = friendly_name;
info->LoadIcon(QVariantList() << icon_name, friendly_name);
info->transcode_mode_ = mode;
info->transcode_format_ = format;
emit dataChanged(idx, idx);
if (info->database_id_ != -1)
backend_->SetDeviceOptions(info->database_id_, friendly_name, icon_name,
mode, format);
}
void DeviceManager::DeviceTaskStarted(int id) {
ConnectedDevice* device = qobject_cast<ConnectedDevice*>(sender());
if (!device) return;
for (int i = 0; i < devices_.count(); ++i) {
DeviceInfo* info = devices_[i];
if (info->device_.get() == device) {
QModelIndex index = ItemToIndex(info);
if (!index.isValid()) continue;
active_tasks_[id] = index;
info->task_percentage_ = 0;
emit dataChanged(index, index);
return;
}
}
}
void DeviceManager::TasksChanged() {
QList<TaskManager::Task> tasks = app_->task_manager()->GetTasks();
QList<QPersistentModelIndex> finished_tasks = active_tasks_.values();
for (const TaskManager::Task& task : tasks) {
if (!active_tasks_.contains(task.id)) continue;
QPersistentModelIndex idx = active_tasks_[task.id];
if (!idx.isValid()) continue;
DeviceInfo* info = IndexToItem(idx);
if (task.progress_max)
info->task_percentage_ = float(task.progress) / task.progress_max * 100;
else
info->task_percentage_ = 0;
emit dataChanged(idx, idx);
finished_tasks.removeAll(idx);
}
for (const QPersistentModelIndex& idx : finished_tasks) {
if (!idx.isValid()) continue;
DeviceInfo* info = IndexToItem(idx);
if (!info) continue;
info->task_percentage_ = -1;
emit dataChanged(idx, idx);
active_tasks_.remove(active_tasks_.key(idx));
}
}
void DeviceManager::UnmountAsync(QModelIndex idx) {
Q_ASSERT(QMetaObject::invokeMethod(this, "Unmount", Q_ARG(QModelIndex, idx)));
}
void DeviceManager::Unmount(QModelIndex idx) {
if (!idx.isValid()) return;
DeviceInfo* info = IndexToItem(idx);
if (!info) return;
if (info->database_id_ != -1 && !info->device_) return;
if (info->device_) Disconnect(idx);
if (info->BestBackend()->lister_)
info->BestBackend()->lister_->UnmountDevice(
info->BestBackend()->unique_id_);
}
void DeviceManager::DeviceSongCountUpdated(int count) {
ConnectedDevice* device = qobject_cast<ConnectedDevice*>(sender());
if (!device) return;
DeviceInfo* info = FindDeviceById(device->unique_id());
if (!info) return;
QModelIndex idx = ItemToIndex(info);
if (!idx.isValid()) return;
emit dataChanged(idx, idx);
}
void DeviceManager::LazyPopulate(DeviceInfo* parent, bool signal) {
if (parent->lazy_loaded) return;
parent->lazy_loaded = true;
}