Add remotekeyboard plugin

BUG: 370919
REVIEW: 129727
This commit is contained in:
Holger Kaelberer 2017-01-23 09:08:27 +01:00
parent f935af6903
commit 30cffbd96e
13 changed files with 511 additions and 0 deletions

View file

@ -24,6 +24,7 @@
#include <QDBusConnection>
#include <QCoreApplication>
#include <QTextStream>
#include <QFile>
#include <KAboutData>
#include <KLocalizedString>
@ -66,6 +67,7 @@ int main(int argc, char** argv)
parser.addOption(QCommandLineOption(QStringLiteral("encryption-info"), i18n("Get encryption info about said device")));
parser.addOption(QCommandLineOption(QStringLiteral("list-commands"), i18n("Lists remote commands and their ids")));
parser.addOption(QCommandLineOption(QStringLiteral("execute-command"), i18n("Executes a remote command by id"), QStringLiteral("id")));
parser.addOption(QCommandLineOption(QStringList{QStringLiteral("k"), QStringLiteral("send-keys")}, i18n("Sends keys to a said device")));
about.setupCommandLine(&parser);
parser.addHelpOption();
@ -199,6 +201,24 @@ int main(int argc, char** argv)
} else if(parser.isSet(QStringLiteral("ring"))) {
QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kdeconnect"), "/modules/kdeconnect/devices/"+device+"/findmyphone", QStringLiteral("org.kde.kdeconnect.device.findmyphone"), QStringLiteral("ring"));
QDBusConnection::sessionBus().call(msg);
} else if(parser.isSet("send-keys")) {
QString seq = parser.value("send-keys");
QDBusMessage msg = QDBusMessage::createMethodCall("org.kde.kdeconnect", "/modules/kdeconnect/devices/"+device+"/remotekeyboard", "org.kde.kdeconnect.device.remotekeyboard", "sendKeyPress");
if (seq.trimmed() == QLatin1String("-")) {
// from file
QFile in;
if(in.open(stdin,QIODevice::ReadOnly | QIODevice::Unbuffered)) {
while (!in.atEnd()) {
QByteArray line = in.readLine(); // sanitize to ASCII-codes > 31?
msg.setArguments({QString(line), -1, false, false, false});
QDBusConnection::sessionBus().call(msg);
}
in.close();
}
} else {
msg.setArguments({seq, -1, false, false, false});
QDBusConnection::sessionBus().call(msg);
}
} else if(parser.isSet(QStringLiteral("list-notifications"))) {
NotificationsModel notifications;
notifications.setDeviceId(device);

View file

@ -43,6 +43,7 @@ geninterface(${CMAKE_SOURCE_DIR}/plugins/mprisremote/mprisremoteplugin.h mprisre
geninterface(${CMAKE_SOURCE_DIR}/plugins/remotecontrol/remotecontrolplugin.h remotecontrolinterface)
geninterface(${CMAKE_SOURCE_DIR}/plugins/lockdevice/lockdeviceplugin.h lockdeviceinterface)
geninterface(${CMAKE_SOURCE_DIR}/plugins/remotecommands/remotecommandsplugin.h remotecommandsinterface)
geninterface(${CMAKE_SOURCE_DIR}/plugins/remotekeyboard/remotekeyboardplugin.h remotekeyboardinterface)
add_library(kdeconnectinterfaces SHARED ${libkdeconnect_SRC})

View file

@ -156,4 +156,12 @@ RemoteCommandsDbusInterface::RemoteCommandsDbusInterface(const QString& deviceId
RemoteCommandsDbusInterface::~RemoteCommandsDbusInterface() = default;
RemoteKeyboardDbusInterface::RemoteKeyboardDbusInterface(const QString& deviceId, QObject* parent):
OrgKdeKdeconnectDeviceRemotekeyboardInterface(DaemonDbusInterface::activatedService(), "/modules/kdeconnect/devices/" + deviceId + "/remotekeyboard", QDBusConnection::sessionBus(), parent)
{
connect(this, &OrgKdeKdeconnectDeviceRemotekeyboardInterface::remoteStateChanged, this, &RemoteKeyboardDbusInterface::remoteStateChanged);
}
RemoteKeyboardDbusInterface::~RemoteKeyboardDbusInterface() = default;
#include "dbusinterfaces.moc"

View file

@ -34,6 +34,7 @@
#include "interfaces/remotecontrolinterface.h"
#include "interfaces/lockdeviceinterface.h"
#include "interfaces/remotecommandsinterface.h"
#include "interfaces/remotekeyboardinterface.h"
/**
* Using these "proxy" classes just in case we need to rename the
@ -180,6 +181,18 @@ public:
~RemoteCommandsDbusInterface() override;
};
class KDECONNECTINTERFACES_EXPORT RemoteKeyboardDbusInterface
: public OrgKdeKdeconnectDeviceRemotekeyboardInterface
{
Q_OBJECT
Q_PROPERTY(bool remoteState READ remoteState NOTIFY remoteStateChanged)
public:
explicit RemoteKeyboardDbusInterface(const QString& deviceId, QObject* parent = nullptr);
~RemoteKeyboardDbusInterface() override;
Q_SIGNALS:
void remoteStateChanged(bool state);
};
template <typename T, typename W>
static void setWhenAvailable(const QDBusPendingReply<T> &pending, W func, QObject* parent)
{

View file

@ -49,6 +49,11 @@ QObject* createFindMyPhoneInterface(const QVariant &deviceId)
return new FindMyPhoneDeviceDbusInterface(deviceId.toString());
}
QObject* createRemoteKeyboardInterface(const QVariant &deviceId)
{
return new RemoteKeyboardDbusInterface(deviceId.toString());
}
QObject* createSftpInterface(const QVariant &deviceId)
{
return new SftpDbusInterface(deviceId.toString());
@ -85,6 +90,7 @@ void KdeConnectDeclarativePlugin::registerTypes(const char* uri)
qmlRegisterUncreatableType<MprisDbusInterface>(uri, 1, 0, "MprisDbusInterface", QStringLiteral("You're not supposed to instantiate interfacess"));
qmlRegisterUncreatableType<LockDeviceDbusInterface>(uri, 1, 0, "LockDeviceDbusInterface", QStringLiteral("You're not supposed to instantiate interfacess"));
qmlRegisterUncreatableType<FindMyPhoneDeviceDbusInterface>(uri, 1, 0, "FindMyPhoneDbusInterface", QStringLiteral("You're not supposed to instantiate interfacess"));
qmlRegisterUncreatableType<RemoteKeyboardDbusInterface>(uri, 1, 0, "RemoteKeyboardDbusInterface", QStringLiteral("You're not supposed to instantiate interfacess"));
qmlRegisterUncreatableType<DeviceDbusInterface>(uri, 1, 0, "DeviceDbusInterface", QStringLiteral("You're not supposed to instantiate interfacess"));
qmlRegisterSingletonType<DaemonDbusInterface>(uri, 1, 0, "DaemonDbusInterface",
[](QQmlEngine*, QJSEngine*) -> QObject* {
@ -109,6 +115,9 @@ void KdeConnectDeclarativePlugin::initializeEngine(QQmlEngine* engine, const cha
engine->rootContext()->setContextProperty(QStringLiteral("SftpDbusInterfaceFactory")
, new ObjectFactory(engine, createSftpInterface));
engine->rootContext()->setContextProperty(QStringLiteral("RemoteKeyboardDbusInterfaceFactory")
, new ObjectFactory(engine, createRemoteKeyboardInterface));
engine->rootContext()->setContextProperty(QStringLiteral("MprisDbusInterfaceFactory")
, new ObjectFactory(engine, createMprisInterface));

View file

@ -23,12 +23,70 @@ import QtQuick.Layouts 1.1
import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.components 2.0 as PlasmaComponents
import org.kde.kdeconnect 1.0
import QtQuick.Controls.Styles 1.4
PlasmaComponents.ListItem
{
id: root
readonly property QtObject device: DeviceDbusInterfaceFactory.create(model.deviceId)
RemoteKeyboard {
id: remoteKeyboard
device: root.device
onRemoteStateChanged: {
remoteKeyboardInput.available = remoteKeyboard.remoteState;
}
onKeyPressReceived: {
// console.log("XXX received keypress key=" + key + " special=" + specialKey + " shift=" + shift + " ctrl=" + ctrl + " text=" + remoteKeyboardInput.text + " cursorPos=" + remoteKeyboardInput.cursorPosition);
// interpret some special keys:
if (specialKey == 12 || specialKey == 14) // Return/Esc -> clear
remoteKeyboardInput.text = "";
else if (specialKey == 4 // Left
&& remoteKeyboardInput.cursorPosition > 0)
--remoteKeyboardInput.cursorPosition;
else if (specialKey == 6 // Right
&& remoteKeyboardInput.cursorPosition < remoteKeyboardInput.text.length)
++remoteKeyboardInput.cursorPosition;
else if (specialKey == 1) { // Backspace -> delete left
var pos = remoteKeyboardInput.cursorPosition;
if (pos > 0) {
remoteKeyboardInput.text = remoteKeyboardInput.text.substring(0, pos-1)
+ remoteKeyboardInput.text.substring(pos, remoteKeyboardInput.text.length);
remoteKeyboardInput.cursorPosition = pos - 1;
}
} else if (specialKey == 13) { // Delete -> delete right
var pos = remoteKeyboardInput.cursorPosition;
if (pos < remoteKeyboardInput.text.length) {
remoteKeyboardInput.text = remoteKeyboardInput.text.substring(0, pos)
+ remoteKeyboardInput.text.substring(pos+1, remoteKeyboardInput.text.length);
remoteKeyboardInput.cursorPosition = pos; // seems to be set to text.length automatically!
}
} else if (specialKey == 10) // Home
remoteKeyboardInput.cursorPosition = 0;
else if (specialKey == 11) // End
remoteKeyboardInput.cursorPosition = remoteKeyboardInput.text.length;
else {
// echo visible keys
var sanitized = "";
for (var i = 0; i < key.length; i++) {
if (key.charCodeAt(i) > 31)
sanitized += key.charAt(i);
}
if (sanitized.length > 0 && !ctrl && !alt) {
// insert sanitized at current pos:
var pos = remoteKeyboardInput.cursorPosition;
remoteKeyboardInput.text = remoteKeyboardInput.text.substring(0, pos)
+ sanitized
+ remoteKeyboardInput.text.substring(pos, remoteKeyboardInput.text.length);
remoteKeyboardInput.cursorPosition = pos + 1; // seems to be set to text.length automatically!
}
}
// console.log("XXX After received keypress key=" + key + " special=" + specialKey + " shift=" + shift + " ctrl=" + ctrl + " text=" + remoteKeyboardInput.text + " cursorPos=" + remoteKeyboardInput.cursorPosition);
}
}
Column {
width: parent.width
@ -86,6 +144,52 @@ PlasmaComponents.ListItem
width: parent.width
}
//RemoteKeyboard
PlasmaComponents.ListItem {
sectionDelegate: true
visible: remoteKeyboard.available
width: parent.width
Row {
width: parent.width
spacing: 5
PlasmaComponents.Label {
id: remoteKeyboardLabel
//font.bold: true
text: i18n("Remote Keyboard")
}
PlasmaComponents.TextField {
id: remoteKeyboardInput
property bool available: remoteKeyboard.remoteState
textColor: "black"
height: parent.height
width: parent.width - 5 - remoteKeyboardLabel.width
verticalAlignment: TextInput.AlignVCenter
readOnly: !available
enabled: available
style: TextFieldStyle {
textColor: "black"
background: Rectangle {
radius: 2
border.color: "gray"
border.width: 1
color: remoteKeyboardInput.available ? "white" : "lightgray"
}
}
Keys.onPressed: {
if (remoteKeyboard.available)
remoteKeyboard.sendEvent(event);
event.accepted = true;
}
}
}
}
//Battery
PlasmaComponents.ListItem {

View file

@ -0,0 +1,73 @@
/**
* Copyright 2017 Holger Kaelberer <holger.k@elberer.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.1
import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.components 2.0 as PlasmaComponents
import org.kde.kdeconnect 1.0
QtObject {
id: root
property alias device: checker.device
readonly property alias available: checker.available
readonly property PluginChecker pluginChecker: PluginChecker {
id: checker
pluginName: "remotekeyboard"
}
property variant remoteKeyboard: null
readonly property bool remoteState: available ? remoteKeyboard.remoteState : false
signal keyPressReceived(string key, int specialKey, bool shift, bool ctrl, bool alt)
function sendEvent(event) {
if (remoteKeyboard) {
var transEvent = JSON.parse(JSON.stringify(event)); // transform to anonymous object
if (transEvent.modifiers & Qt.ControlModifier) {
// special handling for ctrl+c/v/x/a, for which only 'key' gets
// set, but no visbile 'text', which is expected by the remoteKeyboard
// wire-format:
if (transEvent.key === Qt.Key_C)
transEvent.text = 'c';
if (transEvent.key === Qt.Key_V)
transEvent.text = 'v';
if (transEvent.key === Qt.Key_A)
transEvent.text = 'a';
if (transEvent.key === Qt.Key_X)
transEvent.text = 'x';
}
remoteKeyboard.sendQKeyEvent(transEvent);
}
}
onAvailableChanged: {
if (available) {
remoteKeyboard = RemoteKeyboardDbusInterfaceFactory.create(device.id());
remoteKeyboard.keyPressReceived.connect(keyPressReceived);
remoteKeyboard.remoteStateChanged.connect(remoteStateChanged);
} else {
remoteKeyboard = null
}
}
}

View file

@ -10,6 +10,7 @@ add_subdirectory(notifications)
add_subdirectory(battery)
add_subdirectory(remotecommands)
add_subdirectory(findmyphone)
add_subdirectory(remotekeyboard)
if(NOT WIN32)
add_subdirectory(runcommand)
add_subdirectory(sendnotifications)

View file

@ -0,0 +1,8 @@
kdeconnect_add_plugin(kdeconnect_remotekeyboard JSON kdeconnect_remotekeyboard.json
SOURCES remotekeyboardplugin.cpp)
target_link_libraries(kdeconnect_remotekeyboard
kdeconnectcore
KF5::I18n
Qt5::DBus
)

View file

@ -0,0 +1,23 @@
Sends key-events to remote devices. The payload structure corresponds basically
to that of remote key-presses in the mousepad-plugin (with the exception of the
"sendAck"-flag) , e.g.:
{
"key": "a",
"specialKey": 12,
"shift": false,
"ctrl": false,
"alt": false,
"sendAck": true
}
If "specialKey" is a valid keycode according to the internal map (1 <= x <= 32),
the event is interpreted as a special event and the contents of "key" are not
considered.
"key" may contain multi-char strings for performance reasons. In that case,
the peer is expected to print the whole string.
If "sendAck" is set to true, the device expects the remote peer to echo the
event in case it could be handled. This can be used to determine whether the
remote device is ready to accept remote keypresses.

View file

@ -0,0 +1,29 @@
{
"Encoding": "UTF-8",
"KPlugin": {
"Authors": [
{
"Email": "holger.k@elberer.de",
"Name": "Holger Kaelberer"
}
],
"Description": "Use your keyboard to send key-events to your paired device",
"Description[x-test]": "xxUse your keyboard to send key-events to your paired devicexx",
"EnabledByDefault": true,
"Icon": "edit-select",
"Id": "kdeconnect_remotekeyboard",
"License": "GPL",
"Name": "Remote keyboard from the desktop",
"ServiceTypes": [
"KdeConnect/Plugin"
],
"Version": "0.1"
},
"X-KdeConnect-OutgoingPackageType": [
"kdeconnect.mousepad.request"
],
"X-KdeConnect-SupportedPackageType": [
"kdeconnect.mousepad.echo",
"kdeconnect.mousepad.keyboardstate"
]
}

View file

@ -0,0 +1,149 @@
/**
* Copyright 2017 Holger Kaelberer <holger.k@elberer.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 "remotekeyboardplugin.h"
#include <KPluginFactory>
#include <KLocalizedString>
#include <QDebug>
#include <QString>
#include <QVariantMap>
K_PLUGIN_FACTORY_WITH_JSON( KdeConnectPluginFactory, "kdeconnect_remotekeyboard.json", registerPlugin< RemoteKeyboardPlugin >(); )
Q_LOGGING_CATEGORY(KDECONNECT_PLUGIN_REMOTEKEYBOARD, "kdeconnect.plugin.remotekeyboard");
// Mapping of Qt::Key to internal codes, corresponds to the mapping in mousepadplugin
QMap<int, int> specialKeysMap = {
//0, // Invalid
{Qt::Key_Backspace, 1},
{Qt::Key_Tab, 2},
//XK_Linefeed, // 3
{Qt::Key_Left, 4},
{Qt::Key_Up, 5},
{Qt::Key_Right, 6},
{Qt::Key_Down, 7},
{Qt::Key_PageUp, 8},
{Qt::Key_PageDown, 9},
{Qt::Key_Home, 10},
{Qt::Key_End, 11},
{Qt::Key_Return, 12},
{Qt::Key_Enter, 12},
{Qt::Key_Delete, 13},
{Qt::Key_Escape, 14},
{Qt::Key_SysReq, 15},
{Qt::Key_ScrollLock, 16},
//0, // 17
//0, // 18
//0, // 19
//0, // 20
{Qt::Key_F1, 21},
{Qt::Key_F2, 22},
{Qt::Key_F3, 23},
{Qt::Key_F4, 24},
{Qt::Key_F5, 25},
{Qt::Key_F6, 26},
{Qt::Key_F7, 27},
{Qt::Key_F8, 28},
{Qt::Key_F9, 29},
{Qt::Key_F10, 30},
{Qt::Key_F11, 31},
{Qt::Key_F12, 32},
};
RemoteKeyboardPlugin::RemoteKeyboardPlugin(QObject* parent, const QVariantList& args)
: KdeConnectPlugin(parent, args)
, m_remoteState(false)
{
}
RemoteKeyboardPlugin::~RemoteKeyboardPlugin()
{
}
bool RemoteKeyboardPlugin::receivePackage(const NetworkPackage& np)
{
if (np.type() == PACKAGE_TYPE_MOUSEPAD_ECHO) {
if (!np.has("isAck") || !np.has("key")) {
qCWarning(KDECONNECT_PLUGIN_REMOTEKEYBOARD) << "Invalid packet of type"
<< PACKAGE_TYPE_MOUSEPAD_ECHO;
return false;
}
// qCWarning(KDECONNECT_PLUGIN_REMOTEKEYBOARD) << "Received keypress" << np;
Q_EMIT keyPressReceived(np.get<QString>("key"),
np.get<int>("specialKey", 0),
np.get<int>("shift", false),
np.get<int>("ctrl", false),
np.get<int>("alt", false));
return true;
} else if (np.type() == PACKAGE_TYPE_MOUSEPAD_KEYBOARDSTATE) {
// qCWarning(KDECONNECT_PLUGIN_REMOTEKEYBOARD) << "Received keyboardstate" << np;
if (m_remoteState != np.get<bool>("state")) {
m_remoteState = np.get<bool>("state");
Q_EMIT remoteStateChanged(m_remoteState);
}
return true;
}
return false;
}
void RemoteKeyboardPlugin::sendKeyPress(const QString& key, int specialKey,
bool shift, bool ctrl,
bool alt, bool sendAck) const
{
NetworkPackage np(PACKAGE_TYPE_MOUSEPAD_REQUEST, {
{"key", key},
{"specialKey", specialKey},
{"shift", shift},
{"ctrl", ctrl},
{"alt", alt},
{"sendAck", sendAck}
});
sendPackage(np);
}
void RemoteKeyboardPlugin::sendQKeyEvent(const QVariantMap& keyEvent, bool sendAck) const
{
if (!keyEvent.contains("key"))
return;
int k = translateQtKey(keyEvent.value("key").toInt());
int modifiers = keyEvent.value("modifiers").toInt();
sendKeyPress(keyEvent.value("text").toString(), k,
modifiers & Qt::ShiftModifier,
modifiers & Qt::ControlModifier,
modifiers & Qt::AltModifier,
sendAck);
}
int RemoteKeyboardPlugin::translateQtKey(int qtKey) const
{
return specialKeysMap.value(qtKey, 0);
}
void RemoteKeyboardPlugin::connected()
{
}
QString RemoteKeyboardPlugin::dbusPath() const
{
return "/modules/kdeconnect/devices/" + device()->id() + "/remotekeyboard";
}
#include "remotekeyboardplugin.moc"

View file

@ -0,0 +1,73 @@
/**
* Copyright 2017 Holger Kaelberer <holger.k@elberer.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 REMOTEKEYBOARDPLUGIN_H
#define REMOTEKEYBOARDPLUGIN_H
#include <core/kdeconnectplugin.h>
#include <QDBusInterface>
#include <QLoggingCategory>
#include <QVariantMap>
struct FakeKey;
Q_DECLARE_LOGGING_CATEGORY(KDECONNECT_PLUGIN_REMOTEKEYBOARD);
#define PACKAGE_TYPE_MOUSEPAD_REQUEST QLatin1String("kdeconnect.mousepad.request")
#define PACKAGE_TYPE_MOUSEPAD_ECHO QLatin1String("kdeconnect.mousepad.echo")
#define PACKAGE_TYPE_MOUSEPAD_KEYBOARDSTATE QLatin1String("kdeconnect.mousepad.keyboardstate")
class RemoteKeyboardPlugin
: public KdeConnectPlugin
{
Q_OBJECT
Q_CLASSINFO("D-Bus Interface", "org.kde.kdeconnect.device.remotekeyboard")
Q_PROPERTY(bool remoteState READ remoteState NOTIFY remoteStateChanged)
private:
bool m_remoteState;
public:
explicit RemoteKeyboardPlugin(QObject *parent, const QVariantList &args);
~RemoteKeyboardPlugin() override;
bool receivePackage(const NetworkPackage& np) override;
QString dbusPath() const override;
void connected() override;
bool remoteState() const {
return m_remoteState;
}
Q_SCRIPTABLE void sendKeyPress(const QString& key, int specialKey = 0,
bool shift = false, bool ctrl = false,
bool alt = false, bool sendAck = true) const;
Q_SCRIPTABLE void sendQKeyEvent(const QVariantMap& keyEvent,
bool sendAck = true) const;
Q_SCRIPTABLE int translateQtKey(int qtKey) const;
Q_SIGNALS:
Q_SCRIPTABLE void keyPressReceived(const QString& key, int specialKey = 0,
bool shift = false, bool ctrl = false,
bool alt = false) const;
Q_SCRIPTABLE void remoteStateChanged(bool state) const;
};
#endif