[app] Add system volume control

Summary: Add a remote systemvolume plugin.

Test Plan:
Change volume via slider -> Volume on target changes
Change volume on target -> Slider changes

Mute via button -> Mute on target changes
Mute on target -> Button changes icon

Add/remove sink on host -> Sink is add/removed in list

Reviewers: #kde_connect, apol

Reviewed By: #kde_connect, apol

Subscribers: apol, kdeconnect

Tags: #kde_connect

Differential Revision: https://phabricator.kde.org/D16667
This commit is contained in:
Nicolas Fella 2018-11-07 20:54:00 +01:00
parent 061d415f83
commit 2f76d2143f
14 changed files with 562 additions and 2 deletions

View file

@ -114,6 +114,12 @@ Kirigami.Page
shareIface.shareUrl(fileDialog.fileUrl)
}
}
PluginItem {
label: i18n("Volume control")
interfaceFactory: RemoteSystemVolumeDbusInterfaceFactory
component: "qrc:/qml/volume.qml"
pluginName: "remotesystemvolume"
}
Item { Layout.fillHeight: true }
}

68
app/qml/volume.qml Normal file
View file

@ -0,0 +1,68 @@
/*
* Copyright 2018 Nicolas Fella <nicolas.fella@gmx.de>
*
* This program 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 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
import QtQuick 2.2
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.1
import org.kde.kirigami 2.0 as Kirigami
import org.kde.kdeconnect 1.0
Kirigami.Page
{
id: root
title: i18n("Volume control")
property QtObject pluginInterface
ListView {
id: sinkList
anchors.fill: parent
spacing: Kirigami.Units.largeSpacing
model: RemoteSinksModel {
deviceId: pluginInterface.deviceId
}
delegate: ColumnLayout {
width: parent.width
Label {
text: description
width: parent.width
}
RowLayout {
Button {
icon.name: muted ? "player-volume-muted" : "player-volume"
onClicked: pluginInterface.sendMuted(name, !muted)
}
Slider {
Layout.fillWidth: true
from: 0
value: volume
to: maxVolume
onMoved: pluginInterface.sendVolume(name, value)
}
}
}
}
}

View file

@ -9,5 +9,6 @@
<file>qml/DevicePage.qml</file>
<file>qml/FindDevicesPage.qml</file>
<file>qml/runcommand.qml</file>
<file>qml/volume.qml</file>
</qresource>
</RCC>

View file

@ -33,6 +33,7 @@
#include "interfaces/devicesmodel.h"
#include "interfaces/notificationsmodel.h"
#include <remotecommandsmodel.h>
#include <remotesinksmodel.h>
QObject* createDeviceDbusInterface(const QVariant& deviceId)
{
@ -95,6 +96,12 @@ QObject* createShareInterface(const QVariant& deviceId)
return new ShareDbusInterface(deviceId.toString());
}
QObject* createRemoteSystemVolumeInterface(const QVariant& deviceId)
{
return new RemoteSystemVolumeDbusInterface(deviceId.toString());
}
void KdeConnectDeclarativePlugin::registerTypes(const char* uri)
{
qmlRegisterType<DevicesModel>(uri, 1, 0, "DevicesModel");
@ -102,13 +109,15 @@ void KdeConnectDeclarativePlugin::registerTypes(const char* uri)
qmlRegisterType<RemoteCommandsModel>(uri, 1, 0, "RemoteCommandsModel");
qmlRegisterType<DBusAsyncResponse>(uri, 1, 0, "DBusAsyncResponse");
qmlRegisterType<DevicesSortProxyModel>(uri, 1, 0, "DevicesSortProxyModel");
qmlRegisterType<RemoteSinksModel>(uri, 1, 0, "RemoteSinksModel");
qmlRegisterUncreatableType<MprisDbusInterface>(uri, 1, 0, "MprisDbusInterface", QStringLiteral("You're not supposed to instantiate interfaces"));
qmlRegisterUncreatableType<LockDeviceDbusInterface>(uri, 1, 0, "LockDeviceDbusInterface", QStringLiteral("You're not supposed to instantiate interfaces"));
qmlRegisterUncreatableType<FindMyPhoneDeviceDbusInterface>(uri, 1, 0, "FindMyPhoneDbusInterface", QStringLiteral("You're not supposed to instantiate interfaces"));
qmlRegisterUncreatableType<RemoteKeyboardDbusInterface>(uri, 1, 0, "RemoteKeyboardDbusInterface", QStringLiteral("You're not supposed to instantiate interfaces"));
qmlRegisterUncreatableType<DeviceDbusInterface>(uri, 1, 0, "DeviceDbusInterface", QStringLiteral("You're not supposed to instantiate interfaces"));
qmlRegisterUncreatableType<DeviceDbusInterface>(uri, 1, 0, "RemoteCommandsDbusInterface", QStringLiteral("You're not supposed to instantiate interfaces"));
qmlRegisterUncreatableType<DeviceDbusInterface>(uri, 1, 0, "ShareDbusInterface", QStringLiteral("You're not supposed to instantiate interfaces"));
qmlRegisterUncreatableType<RemoteCommandsDbusInterface>(uri, 1, 0, "RemoteCommandsDbusInterface", QStringLiteral("You're not supposed to instantiate interfaces"));
qmlRegisterUncreatableType<RemoteSystemVolumeDbusInterface>(uri, 1, 0, "RemoteSystemVolumeInterface", QStringLiteral("You're not supposed to instantiate interfaces"));
qmlRegisterUncreatableType<ShareDbusInterface>(uri, 1, 0, "ShareDbusInterface", QStringLiteral("You're not supposed to instantiate interfaces"));
qmlRegisterSingletonType<DaemonDbusInterface>(uri, 1, 0, "DaemonDbusInterface",
[](QQmlEngine*, QJSEngine*) -> QObject* {
return new DaemonDbusInterface;
@ -158,4 +167,8 @@ void KdeConnectDeclarativePlugin::initializeEngine(QQmlEngine* engine, const cha
engine->rootContext()->setContextProperty(QStringLiteral("ShareDbusInterfaceFactory")
, new ObjectFactory(engine, createShareInterface));
engine->rootContext()->setContextProperty(QStringLiteral("RemoteSystemVolumeDbusInterfaceFactory")
, new ObjectFactory(engine, createRemoteSystemVolumeInterface));
}

View file

@ -19,6 +19,7 @@ set(libkdeconnect_SRC
devicessortproxymodel.cpp
conversationmessage.cpp
remotecommandsmodel.cpp
remotesinksmodel.cpp
# modeltest.cpp
)
@ -51,6 +52,8 @@ geninterface(${CMAKE_SOURCE_DIR}/plugins/remotekeyboard/remotekeyboardplugin.h r
geninterface(${CMAKE_SOURCE_DIR}/plugins/sms/smsplugin.h smsinterface)
geninterface(${CMAKE_SOURCE_DIR}/plugins/sms/conversationsdbusinterface.h conversationsinterface)
geninterface(${CMAKE_SOURCE_DIR}/plugins/share/shareplugin.h shareinterface)
geninterface(${CMAKE_SOURCE_DIR}/plugins/remotesystemvolume/remotesystemvolumeplugin.h remotesystemvolumeinterface)
add_library(kdeconnectinterfaces SHARED ${libkdeconnect_SRC})

View file

@ -189,3 +189,8 @@ ShareDbusInterface::ShareDbusInterface(const QString& deviceId, QObject* parent)
}
ShareDbusInterface::~ShareDbusInterface() = default;
RemoteSystemVolumeDbusInterface::RemoteSystemVolumeDbusInterface(const QString& deviceId, QObject* parent):
OrgKdeKdeconnectDeviceRemotesystemvolumeInterface(DaemonDbusInterface::activatedService(), "/modules/kdeconnect/devices/" + deviceId + "/remotesystemvolume", QDBusConnection::sessionBus(), parent)
{
}

View file

@ -38,6 +38,7 @@
#include "interfaces/smsinterface.h"
#include "interfaces/conversationsinterface.h"
#include "interfaces/shareinterface.h"
#include "interfaces/remotesystemvolumeinterface.h"
/**
* Using these "proxy" classes just in case we need to rename the
@ -229,6 +230,15 @@ public:
~ShareDbusInterface() override;
};
class KDECONNECTINTERFACES_EXPORT RemoteSystemVolumeDbusInterface
: public OrgKdeKdeconnectDeviceRemotesystemvolumeInterface
{
Q_OBJECT
public:
explicit RemoteSystemVolumeDbusInterface(const QString& deviceId, QObject* parent = nullptr);
~RemoteSystemVolumeDbusInterface() = default;
};
template <typename T, typename W>
static void setWhenAvailable(const QDBusPendingReply<T>& pending, W func, QObject* parent)
{

View file

@ -0,0 +1,173 @@
/**
* Copyright 2018 Nicolas Fella <nicolas.fella@gmx.de>
*
* This program 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 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "remotesinksmodel.h"
#include "interfaces_debug.h"
#include <QDebug>
#include <QDBusInterface>
RemoteSinksModel::RemoteSinksModel(QObject* parent)
: QAbstractListModel(parent)
, m_dbusInterface(nullptr)
{
connect(this, &QAbstractItemModel::rowsInserted,
this, &RemoteSinksModel::rowsChanged);
connect(this, &QAbstractItemModel::rowsRemoved,
this, &RemoteSinksModel::rowsChanged);
QDBusServiceWatcher* watcher = new QDBusServiceWatcher(DaemonDbusInterface::activatedService(),
QDBusConnection::sessionBus(), QDBusServiceWatcher::WatchForOwnerChange, this);
connect(watcher, &QDBusServiceWatcher::serviceRegistered, this, &RemoteSinksModel::refreshSinkList);
connect(watcher, &QDBusServiceWatcher::serviceUnregistered, this, &RemoteSinksModel::refreshSinkList);
}
QHash<int, QByteArray> RemoteSinksModel::roleNames() const
{
//Role names for QML
QHash<int, QByteArray> names = QAbstractItemModel::roleNames();
names.insert(NameRole, "name");
names.insert(DescriptionRole, "description");
names.insert(MaxVolumeRole, "maxVolume");
names.insert(VolumeRole, "volume");
names.insert(MutedRole, "muted");
return names;
}
RemoteSinksModel::~RemoteSinksModel()
{
}
QString RemoteSinksModel::deviceId() const
{
return m_deviceId;
}
void RemoteSinksModel::setDeviceId(const QString& deviceId)
{
m_deviceId = deviceId;
if (m_dbusInterface) {
delete m_dbusInterface;
}
m_dbusInterface = new RemoteSystemVolumeDbusInterface(deviceId, this);
connect(m_dbusInterface, &OrgKdeKdeconnectDeviceRemotesystemvolumeInterface::sinksChanged,
this, &RemoteSinksModel::refreshSinkList);
connect(m_dbusInterface, &OrgKdeKdeconnectDeviceRemotesystemvolumeInterface::volumeChanged, this, [this](const QString& name, int volume) {
for (Sink* s: m_sinkList) {
if (s->name == name) {
s->volume = volume;
Q_EMIT dataChanged(index(0,0), index(m_sinkList.size() - 1, 0));
}
}
});
connect(m_dbusInterface, &OrgKdeKdeconnectDeviceRemotesystemvolumeInterface::mutedChanged, this, [this](const QString& name, bool muted) {
for (Sink* s: m_sinkList) {
if (s->name == name) {
s->muted = muted;
Q_EMIT dataChanged(index(0,0), index(m_sinkList.size() - 1, 0));
}
}
});
refreshSinkList();
Q_EMIT deviceIdChanged(deviceId);
}
void RemoteSinksModel::refreshSinkList()
{
if (!m_dbusInterface) {
return;
}
if (!m_dbusInterface->isValid()) {
qCWarning(KDECONNECT_INTERFACES) << "dbus interface not valid";
return;
}
const auto cmds = QJsonDocument::fromJson(m_dbusInterface->sinks()).array();
beginResetModel();
qDeleteAll(m_sinkList);
m_sinkList.clear();
for (auto it = cmds.constBegin(), itEnd = cmds.constEnd(); it!=itEnd; ++it) {
const QJsonObject cont = it->toObject();
Sink* sink = new Sink();
sink->name = cont.value(QStringLiteral("name")).toString();
sink->description = cont.value(QStringLiteral("description")).toString();
sink->maxVolume = cont.value(QStringLiteral("maxVolume")).toInt();
sink->volume = cont.value(QStringLiteral("volume")).toInt();
sink->muted = cont.value(QStringLiteral("muted")).toBool();
m_sinkList.append(sink);
}
endResetModel();
}
QVariant RemoteSinksModel::data(const QModelIndex& index, int role) const
{
if (!index.isValid()
|| index.row() < 0
|| index.row() >= m_sinkList.count())
{
return QVariant();
}
if (!m_dbusInterface || !m_dbusInterface->isValid()) {
return QVariant();
}
Sink* sink = m_sinkList[index.row()];
switch (role) {
case NameRole:
return sink->name;
case DescriptionRole:
return sink->description;
case MaxVolumeRole:
return sink->maxVolume;
case VolumeRole:
return sink->volume;
case MutedRole:
return sink->muted;
default:
return QVariant();
}
}
int RemoteSinksModel::rowCount(const QModelIndex& parent) const
{
if (parent.isValid()) {
//Return size 0 if we are a child because this is not a tree
return 0;
}
return m_sinkList.count();
}

View file

@ -0,0 +1,75 @@
/**
* Copyright 2018 Nicolas Fella <nicolas.fella@gmx.de>
*
* This program 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 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef REMOTESINKSMODEL_H
#define REMOTESINKSMODEL_H
#include <QAbstractListModel>
#include "interfaces/dbusinterfaces.h"
struct Sink {
QString name;
QString description;
int maxVolume;
int volume;
bool muted;
};
class KDECONNECTINTERFACES_EXPORT RemoteSinksModel
: public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(QString deviceId READ deviceId WRITE setDeviceId NOTIFY deviceIdChanged)
public:
enum ModelRoles {
NameRole,
DescriptionRole,
MaxVolumeRole,
VolumeRole,
MutedRole
};
explicit RemoteSinksModel(QObject* parent = nullptr);
~RemoteSinksModel() override;
QString deviceId() const;
void setDeviceId(const QString& deviceId);
QVariant data(const QModelIndex& index, int role) const override;
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
QHash<int, QByteArray> roleNames() const override;
private Q_SLOTS:
void refreshSinkList();
Q_SIGNALS:
void deviceIdChanged(const QString& value);
void rowsChanged();
private:
RemoteSystemVolumeDbusInterface* m_dbusInterface;
QVector<Sink*> m_sinkList;
QString m_deviceId;
};
#endif // DEVICESMODEL_H

View file

@ -37,6 +37,7 @@ if(SAILFISHOS OR EXPERIMENTALAPP_ENABLED)
add_subdirectory(mprisremote)
add_subdirectory(remotecontrol)
add_subdirectory(lockdevice)
add_subdirectory(remotesystemvolume)
endif()
if(KF5PulseAudioQt_FOUND)

View file

@ -0,0 +1,11 @@
set(kdeconnect_remotesystemvolume_SRCS
remotesystemvolumeplugin.cpp
)
kdeconnect_add_plugin(kdeconnect_remotesystemvolume JSON kdeconnect_remotesystemvolume.json SOURCES ${kdeconnect_remotesystemvolume_SRCS})
target_link_libraries(kdeconnect_remotesystemvolume
kdeconnectcore
Qt5::DBus
KF5::I18n
)

View file

@ -0,0 +1,27 @@
{
"KPlugin": {
"Authors": [
{
"Email": "nicolas.fella@gmx.de",
"Name": "Nicolas Fella"
}
],
"Description": "Control the volume of the connected device",
"EnabledByDefault": true,
"Icon": "player-volume",
"Id": "kdeconnect_remotesystemvolume",
"License": "GPL",
"Name": "Remote system volume",
"ServiceTypes": [
"KdeConnect/Plugin"
],
"Version": "0.1",
"Website": "https://nicolasfella.wordpress.com"
},
"X-KdeConnect-OutgoingPacketType": [
"kdeconnect.systemvolume.request"
],
"X-KdeConnect-SupportedPacketType": [
"kdeconnect.systemvolume"
]
}

View file

@ -0,0 +1,105 @@
/**
* Copyright 2018 Nicolas Fella <nicolas.fella@gmx.de>
*
* This program 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 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "remotesystemvolumeplugin.h"
#include <KLocalizedString>
#include <KPluginFactory>
#include <QDebug>
#include <QDBusConnection>
#include <QLoggingCategory>
#include <QJsonArray>
#include <QJsonDocument>
#include <core/device.h>
#include <core/daemon.h>
K_PLUGIN_FACTORY_WITH_JSON( KdeConnectPluginFactory, "kdeconnect_remotesystemvolume.json", registerPlugin< RemoteSystemVolumePlugin >(); )
Q_LOGGING_CATEGORY(KDECONNECT_PLUGIN_PING, "kdeconnect.plugin.remotesystemvolume")
RemoteSystemVolumePlugin::RemoteSystemVolumePlugin(QObject* parent, const QVariantList& args)
: KdeConnectPlugin(parent, args)
{
}
RemoteSystemVolumePlugin::~RemoteSystemVolumePlugin()
{
}
bool RemoteSystemVolumePlugin::receivePacket(const NetworkPacket& np)
{
if (np.has(QStringLiteral("sinkList"))) {
QJsonDocument document(np.get<QJsonArray>(QStringLiteral("sinkList")));
m_sinks = document.toJson();
Q_EMIT sinksChanged();
} else {
QString name = np.get<QString>(QStringLiteral("name"));
if (np.has(QStringLiteral("volume"))) {
Q_EMIT volumeChanged(name, np.get<int>(QStringLiteral("volume")));
}
if (np.has(QStringLiteral("muted"))) {
Q_EMIT mutedChanged(name, np.get<int>(QStringLiteral("muted")));
}
}
return true;
}
void RemoteSystemVolumePlugin::sendVolume(const QString& name, int volume)
{
NetworkPacket np(PACKET_TYPE_SYSTEMVOLUME_REQUEST);
np.set<QString>(QStringLiteral("name"), name);
np.set<int>(QStringLiteral("volume"), volume);
sendPacket(np);
}
void RemoteSystemVolumePlugin::sendMuted(const QString& name, bool muted)
{
NetworkPacket np(PACKET_TYPE_SYSTEMVOLUME_REQUEST);
np.set<QString>(QStringLiteral("name"), name);
np.set<bool>(QStringLiteral("muted"), muted);
sendPacket(np);
}
void RemoteSystemVolumePlugin::connected()
{
NetworkPacket np(PACKET_TYPE_SYSTEMVOLUME_REQUEST);
np.set<bool>(QStringLiteral("requestSinks"), true);
sendPacket(np);
}
QByteArray RemoteSystemVolumePlugin::sinks()
{
return m_sinks;
}
QString RemoteSystemVolumePlugin::dbusPath() const
{
return "/modules/kdeconnect/devices/" + device()->id() + "/remotesystemvolume";
}
#include "remotesystemvolumeplugin.moc"

View file

@ -0,0 +1,62 @@
/**
* Copyright 2018 Nicolas Fella <nicolas.fella@gmx.de>
*
* This program 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 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef REMOTESYSTEMVOLUMEPLUGIN_H
#define REMOTESYSTEMVOLUMEPLUGIN_H
#include <QObject>
#include <core/kdeconnectplugin.h>
#define PACKET_TYPE_SYSTEMVOLUME QStringLiteral("kdeconnect.systemvolume")
#define PACKET_TYPE_SYSTEMVOLUME_REQUEST QStringLiteral("kdeconnect.systemvolume.request")
class Q_DECL_EXPORT RemoteSystemVolumePlugin
: public KdeConnectPlugin
{
Q_OBJECT
Q_CLASSINFO("D-Bus Interface", "org.kde.kdeconnect.device.remotesystemvolume")
Q_PROPERTY(QByteArray sinks READ sinks NOTIFY sinksChanged)
Q_PROPERTY(QString deviceId READ deviceId CONSTANT)
public:
explicit RemoteSystemVolumePlugin(QObject* parent, const QVariantList& args);
~RemoteSystemVolumePlugin() override;
bool receivePacket(const NetworkPacket& np) override;
void connected() override;
QString dbusPath() const override;
QString deviceId() const { return device()->id(); }
QByteArray sinks();
Q_SCRIPTABLE void sendVolume(const QString& name, int volume);
Q_SCRIPTABLE void sendMuted(const QString& name, bool muted);
Q_SIGNALS:
Q_SCRIPTABLE void sinksChanged();
Q_SCRIPTABLE void volumeChanged(const QString& name, int volume);
Q_SCRIPTABLE void mutedChanged(const QString& name, bool muted);
private:
QByteArray m_sinks;
};
#endif // REMOTESYSTEMVOLUMEPLUGIN_H